Compare commits
10 Commits
2e5ba323ba
...
d9f963f1b0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9f963f1b0 | ||
|
|
96cbba87ca | ||
|
|
18a0d6fe29 | ||
|
|
87b6a321c4 | ||
|
|
c92676194f | ||
|
|
26cdbeae38 | ||
|
|
42bccf5ed2 | ||
|
|
b0c54660fb | ||
|
|
82141d8e22 | ||
|
|
f41b014ebb |
11
README.md
11
README.md
@ -33,9 +33,11 @@ Top-level control surface:
|
|||||||
2. `Activate membership`
|
2. `Activate membership`
|
||||||
3. `Refresh status + feed`
|
3. `Refresh status + feed`
|
||||||
4. `Governance status`
|
4. `Governance status`
|
||||||
5. Wallet/membership/designation/last-sync overview cards
|
5. Wallet/session/membership/designation/last-sync overview cards
|
||||||
6. Pull-first updates feed + support ticket action
|
6. Pull-first updates feed + support ticket action
|
||||||
7. Identity assurance visibility (`none` / `crypto_direct_unattested` / `sponsored_unattested` / `onramp_attested`)
|
7. Identity assurance visibility (`none` / `crypto_direct_unattested` / `sponsored_unattested` / `onramp_attested`)
|
||||||
|
8. Explicit operator-visible mode toggles (`Human mode` / `Auto mode`) synced to governance `operation_mode`
|
||||||
|
9. Wallet utility actions (`Refresh balances`, `Copy address`) with native + USDC balance visibility
|
||||||
|
|
||||||
Advanced integration controls (collapsible):
|
Advanced integration controls (collapsible):
|
||||||
|
|
||||||
@ -44,8 +46,9 @@ Advanced integration controls (collapsible):
|
|||||||
3. Membership quote + confirm primitives
|
3. Membership quote + confirm primitives
|
||||||
4. Marketplace offer list + checkout quote/send/confirm primitives
|
4. Marketplace offer list + checkout quote/send/confirm primitives
|
||||||
5. Member channel register/poll primitives
|
5. Member channel register/poll primitives
|
||||||
6. Governance install + lease primitives
|
6. Governance install + lease primitives (with explicit `operation_mode`)
|
||||||
7. Raw response log for deterministic troubleshooting
|
7. Raw response log for deterministic troubleshooting
|
||||||
|
8. Wallet session lifecycle controls (manual refresh/revoke + automatic pre-expiry refresh)
|
||||||
|
|
||||||
Wallet automation helpers remain available in advanced controls:
|
Wallet automation helpers remain available in advanced controls:
|
||||||
|
|
||||||
@ -54,12 +57,16 @@ Wallet automation helpers remain available in advanced controls:
|
|||||||
3. `Sign payer proof` signs distinct-payer ownership proof and fills `payerProof`.
|
3. `Sign payer proof` signs distinct-payer ownership proof and fills `payerProof`.
|
||||||
4. `Send membership tx` submits the quote transaction via `eth_sendTransaction` and fills `confirmTxHash`.
|
4. `Send membership tx` submits the quote transaction via `eth_sendTransaction` and fills `confirmTxHash`.
|
||||||
5. Membership confirm can optionally attach on-ramp attestation fields (`identity_assurance_level`, `identity_attested_by`, `identity_attestation_id`) for provider-integrated flows.
|
5. Membership confirm can optionally attach on-ramp attestation fields (`identity_assurance_level`, `identity_attested_by`, `identity_attestation_id`) for provider-integrated flows.
|
||||||
|
6. Wallet verify returns a session token; launcher forwards it on marketplace/member/governance API calls via bearer + `X-Edut-Session`.
|
||||||
|
7. Launcher proactively refreshes wallet sessions before expiry and clears local session state on terminal session errors (`invalid`, `expired`, `revoked`, `mismatch`).
|
||||||
|
|
||||||
Policy behavior in launcher shell:
|
Policy behavior in launcher shell:
|
||||||
|
|
||||||
1. Membership is required for all member-channel polling flows.
|
1. Membership is required for all member-channel polling flows.
|
||||||
2. `onramp_attested` identity assurance is required for owner support-ticket and governance install-token actions.
|
2. `onramp_attested` identity assurance is required for owner support-ticket and governance install-token actions.
|
||||||
3. Assurance state is displayed independently from membership state in the top summary cards.
|
3. Assurance state is displayed independently from membership state in the top summary cards.
|
||||||
|
4. Owner-only buttons are UI-disabled until both membership is active and assurance is `onramp_attested`.
|
||||||
|
5. Governance activation evidence must carry explicit signing authority class (`identity_human` or delegated).
|
||||||
|
|
||||||
Run locally:
|
Run locally:
|
||||||
|
|
||||||
|
|||||||
400
app/app.js
400
app/app.js
@ -9,6 +9,11 @@ const state = {
|
|||||||
lastCheckoutQuote: null,
|
lastCheckoutQuote: null,
|
||||||
lastStatus: null,
|
lastStatus: null,
|
||||||
channelReady: false,
|
channelReady: false,
|
||||||
|
walletSessionToken: "",
|
||||||
|
walletSessionExpiresAt: "",
|
||||||
|
walletSessionRefreshInFlight: null,
|
||||||
|
walletBalanceNative: "",
|
||||||
|
walletBalanceUSDC: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
function nowISO() {
|
function nowISO() {
|
||||||
@ -53,6 +58,122 @@ function isOnrampAttested(value) {
|
|||||||
return normalizeAssurance(value) === "onramp_attested";
|
return normalizeAssurance(value) === "onramp_attested";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sessionSummary() {
|
||||||
|
if (!state.walletSessionToken) {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
if (!state.walletSessionExpiresAt) {
|
||||||
|
return "active";
|
||||||
|
}
|
||||||
|
const expires = Date.parse(state.walletSessionExpiresAt);
|
||||||
|
if (!Number.isFinite(expires)) {
|
||||||
|
return `active (exp ${state.walletSessionExpiresAt})`;
|
||||||
|
}
|
||||||
|
const remainingMs = expires - Date.now();
|
||||||
|
if (remainingMs <= 0) {
|
||||||
|
return `expired (${state.walletSessionExpiresAt})`;
|
||||||
|
}
|
||||||
|
if (remainingMs <= 5 * 60 * 1000) {
|
||||||
|
return `expiring soon (${state.walletSessionExpiresAt})`;
|
||||||
|
}
|
||||||
|
return `active (exp ${state.walletSessionExpiresAt})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function walletBalanceSummary() {
|
||||||
|
const native = state.walletBalanceNative || "-- ETH";
|
||||||
|
const usdc = state.walletBalanceUSDC || "-- USDC";
|
||||||
|
return `${native} | ${usdc}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearWalletSession(reason, payload = {}) {
|
||||||
|
if (!state.walletSessionToken && !state.walletSessionExpiresAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previousToken = state.walletSessionToken;
|
||||||
|
const previousExpiry = state.walletSessionExpiresAt;
|
||||||
|
state.walletSessionToken = "";
|
||||||
|
state.walletSessionExpiresAt = "";
|
||||||
|
if (reason) {
|
||||||
|
logLine("wallet session cleared", {
|
||||||
|
reason,
|
||||||
|
previous_token_preview: previousToken ? `${previousToken.slice(0, 8)}...` : "",
|
||||||
|
previous_expiry: previousExpiry || "",
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
refreshOverview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureSessionHeaders(res) {
|
||||||
|
const token = String(res.headers.get("x-edut-session") || "").trim();
|
||||||
|
const expiresAt = String(res.headers.get("x-edut-session-expires-at") || "").trim();
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.walletSessionToken = token;
|
||||||
|
if (expiresAt) {
|
||||||
|
state.walletSessionExpiresAt = expiresAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTerminalSessionErrorCode(code) {
|
||||||
|
const normalized = String(code || "").trim().toLowerCase();
|
||||||
|
return normalized === "wallet_session_invalid"
|
||||||
|
|| normalized === "wallet_session_expired"
|
||||||
|
|| normalized === "wallet_session_revoked"
|
||||||
|
|| normalized === "wallet_session_mismatch";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybeRefreshSession(path) {
|
||||||
|
if (!state.walletSessionToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (String(path || "").startsWith("/secret/wallet/session/")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.walletSessionExpiresAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const expiresAt = Date.parse(state.walletSessionExpiresAt);
|
||||||
|
if (!Number.isFinite(expiresAt)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const refreshWindowMs = 5 * 60 * 1000;
|
||||||
|
if ((expiresAt - Date.now()) > refreshWindowMs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.walletSessionRefreshInFlight) {
|
||||||
|
await state.walletSessionRefreshInFlight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentWallet = normalizedAddress(wallet());
|
||||||
|
if (!currentWallet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.walletSessionRefreshInFlight = (async () => {
|
||||||
|
const out = await request(
|
||||||
|
"POST",
|
||||||
|
"/secret/wallet/session/refresh",
|
||||||
|
{ wallet: currentWallet },
|
||||||
|
{ skipSessionPreflight: true },
|
||||||
|
);
|
||||||
|
if (out.session_token) {
|
||||||
|
state.walletSessionToken = String(out.session_token).trim();
|
||||||
|
}
|
||||||
|
if (out.session_expires_at) {
|
||||||
|
state.walletSessionExpiresAt = String(out.session_expires_at).trim();
|
||||||
|
}
|
||||||
|
logLine("wallet session auto-refresh", {
|
||||||
|
wallet: currentWallet,
|
||||||
|
session_expires_at: state.walletSessionExpiresAt || "unknown",
|
||||||
|
});
|
||||||
|
refreshOverview();
|
||||||
|
})().finally(() => {
|
||||||
|
state.walletSessionRefreshInFlight = null;
|
||||||
|
});
|
||||||
|
await state.walletSessionRefreshInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
function injectedProvider() {
|
function injectedProvider() {
|
||||||
return globalThis.ethereum || null;
|
return globalThis.ethereum || null;
|
||||||
}
|
}
|
||||||
@ -71,6 +192,41 @@ function utf8ToHex(value) {
|
|||||||
.join("")}`;
|
.join("")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHexQuantity(value) {
|
||||||
|
const raw = String(value || "").trim().toLowerCase();
|
||||||
|
if (!raw.startsWith("0x")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUnitsHex(hexValue, decimals, precision = 6) {
|
||||||
|
const hex = normalizeHexQuantity(hexValue);
|
||||||
|
if (!hex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let bigint;
|
||||||
|
try {
|
||||||
|
bigint = BigInt(hex);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const unit = 10n ** BigInt(Math.max(0, Number(decimals) || 0));
|
||||||
|
const whole = bigint / unit;
|
||||||
|
const fraction = bigint % unit;
|
||||||
|
if (fraction === 0n) {
|
||||||
|
return whole.toString();
|
||||||
|
}
|
||||||
|
const padded = fraction.toString().padStart(Number(decimals), "0");
|
||||||
|
const trimmed = padded.slice(0, Math.max(1, precision)).replace(/0+$/, "");
|
||||||
|
return trimmed ? `${whole.toString()}.${trimmed}` : whole.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeAddressWord(address) {
|
||||||
|
const normalized = normalizedAddress(address).replace(/^0x/, "");
|
||||||
|
return normalized.padStart(64, "0");
|
||||||
|
}
|
||||||
|
|
||||||
function logLine(label, payload) {
|
function logLine(label, payload) {
|
||||||
const log = $("log");
|
const log = $("log");
|
||||||
const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`;
|
const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`;
|
||||||
@ -83,9 +239,29 @@ function setSummary(id, value) {
|
|||||||
el.textContent = value;
|
el.textContent = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setButtonDisabled(id, disabled) {
|
||||||
|
const el = $(id);
|
||||||
|
if (!el) return;
|
||||||
|
el.disabled = Boolean(disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshActionLocks(statusPayload) {
|
||||||
|
const effective = statusPayload && typeof statusPayload === "object" ? statusPayload : state.lastStatus;
|
||||||
|
const membershipActive = String(effective?.status || "").toLowerCase() === "active";
|
||||||
|
const attested = isOnrampAttested(effective?.identity_assurance_level);
|
||||||
|
const ownerActionReady = membershipActive && attested;
|
||||||
|
|
||||||
|
setButtonDisabled("btnSupportTicket", !ownerActionReady);
|
||||||
|
setButtonDisabled("btnInstallToken", !ownerActionReady);
|
||||||
|
setButtonDisabled("btnQuickInstallStatus", !membershipActive);
|
||||||
|
}
|
||||||
|
|
||||||
function refreshOverview(statusPayload) {
|
function refreshOverview(statusPayload) {
|
||||||
const currentWallet = wallet();
|
const currentWallet = wallet();
|
||||||
setSummary("summaryWallet", currentWallet || "not connected");
|
setSummary("summaryWallet", currentWallet || "not connected");
|
||||||
|
setSummary("summaryFunds", walletBalanceSummary());
|
||||||
|
setSummary("summarySession", sessionSummary());
|
||||||
|
refreshModeUI();
|
||||||
if (statusPayload && typeof statusPayload === "object") {
|
if (statusPayload && typeof statusPayload === "object") {
|
||||||
setSummary("summaryMembership", statusPayload.status || "unknown");
|
setSummary("summaryMembership", statusPayload.status || "unknown");
|
||||||
setSummary("summaryDesignation", statusPayload.designation_code || "-");
|
setSummary("summaryDesignation", statusPayload.designation_code || "-");
|
||||||
@ -93,6 +269,7 @@ function refreshOverview(statusPayload) {
|
|||||||
setSummary("summaryAssurance", assurance);
|
setSummary("summaryAssurance", assurance);
|
||||||
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
||||||
setSummary("summaryLastSync", nowISO());
|
setSummary("summaryLastSync", nowISO());
|
||||||
|
refreshActionLocks(statusPayload);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const designation = $("designationCode")?.value?.trim() || "-";
|
const designation = $("designationCode")?.value?.trim() || "-";
|
||||||
@ -100,6 +277,7 @@ function refreshOverview(statusPayload) {
|
|||||||
const assurance = assuranceDisplay(state.lastStatus?.identity_assurance_level);
|
const assurance = assuranceDisplay(state.lastStatus?.identity_assurance_level);
|
||||||
setSummary("summaryAssurance", assurance);
|
setSummary("summaryAssurance", assurance);
|
||||||
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
||||||
|
refreshActionLocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFlowStatus(message) {
|
function setFlowStatus(message) {
|
||||||
@ -113,15 +291,24 @@ function sleep(ms) {
|
|||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function request(method, path, body) {
|
async function request(method, path, body, options = {}) {
|
||||||
|
if (!options.skipSessionPreflight) {
|
||||||
|
await maybeRefreshSession(path);
|
||||||
|
}
|
||||||
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
if (state.walletSessionToken) {
|
||||||
|
headers["X-Edut-Session"] = state.walletSessionToken;
|
||||||
|
headers.Authorization = `Bearer ${state.walletSessionToken}`;
|
||||||
|
}
|
||||||
const opts = {
|
const opts = {
|
||||||
method,
|
method,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers,
|
||||||
};
|
};
|
||||||
if (body !== undefined) {
|
if (body !== undefined) {
|
||||||
opts.body = JSON.stringify(body);
|
opts.body = JSON.stringify(body);
|
||||||
}
|
}
|
||||||
const res = await fetch(`${baseURL()}${path}`, opts);
|
const res = await fetch(`${baseURL()}${path}`, opts);
|
||||||
|
captureSessionHeaders(res);
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
let json = {};
|
let json = {};
|
||||||
if (text.trim() !== "") {
|
if (text.trim() !== "") {
|
||||||
@ -132,7 +319,16 @@ async function request(method, path, body) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`${res.status} ${res.statusText}: ${JSON.stringify(json)}`);
|
const errorMessage = String(json?.error || res.statusText || "request failed");
|
||||||
|
const errorCode = String(json?.code || "");
|
||||||
|
if (isTerminalSessionErrorCode(errorCode)) {
|
||||||
|
clearWalletSession(errorCode, { path, message: errorMessage });
|
||||||
|
}
|
||||||
|
const err = new Error(`${res.status} ${res.statusText}: ${errorMessage}`);
|
||||||
|
err.status = res.status;
|
||||||
|
err.code = errorCode;
|
||||||
|
err.payload = json;
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@ -161,6 +357,38 @@ function principalRole() {
|
|||||||
return $("principalRole").value.trim();
|
return $("principalRole").value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function operationMode() {
|
||||||
|
return $("operationMode").value.trim() || "human_manual";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOperationMode(value) {
|
||||||
|
return String(value || "").trim().toLowerCase() === "worker_auto" ? "worker_auto" : "human_manual";
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshModeUI() {
|
||||||
|
const mode = normalizeOperationMode(operationMode());
|
||||||
|
setSummary("summaryMode", mode);
|
||||||
|
const humanBtn = $("btnModeHuman");
|
||||||
|
const autoBtn = $("btnModeAuto");
|
||||||
|
if (humanBtn) {
|
||||||
|
humanBtn.classList.toggle("mode-active", mode === "human_manual");
|
||||||
|
}
|
||||||
|
if (autoBtn) {
|
||||||
|
autoBtn.classList.toggle("mode-active", mode === "worker_auto");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOperationMode(mode, source = "ui") {
|
||||||
|
const normalized = normalizeOperationMode(mode);
|
||||||
|
const select = $("operationMode");
|
||||||
|
if (select) {
|
||||||
|
select.value = normalized;
|
||||||
|
}
|
||||||
|
refreshModeUI();
|
||||||
|
logLine("operation mode set", { mode: normalized, source });
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
function renderEvents(events) {
|
function renderEvents(events) {
|
||||||
const list = $("eventList");
|
const list = $("eventList");
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
@ -241,12 +469,20 @@ function buildIntentTypedData(intent, origin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onConnectWallet() {
|
async function onConnectWallet() {
|
||||||
|
const previousWallet = normalizedAddress($("walletAddress").value);
|
||||||
const provider = await requireProvider();
|
const provider = await requireProvider();
|
||||||
const accounts = await provider.request({ method: "eth_requestAccounts" });
|
const accounts = await provider.request({ method: "eth_requestAccounts" });
|
||||||
if (!accounts || accounts.length === 0) {
|
if (!accounts || accounts.length === 0) {
|
||||||
throw new Error("wallet provider returned no accounts");
|
throw new Error("wallet provider returned no accounts");
|
||||||
}
|
}
|
||||||
$("walletAddress").value = normalizedAddress(accounts[0]);
|
const nextWallet = normalizedAddress(accounts[0]);
|
||||||
|
$("walletAddress").value = nextWallet;
|
||||||
|
if (previousWallet && previousWallet !== nextWallet) {
|
||||||
|
clearWalletSession("wallet_changed", {
|
||||||
|
previous_wallet: previousWallet,
|
||||||
|
next_wallet: nextWallet,
|
||||||
|
});
|
||||||
|
}
|
||||||
const chainHex = await provider.request({ method: "eth_chainId" });
|
const chainHex = await provider.request({ method: "eth_chainId" });
|
||||||
const providerChainID = Number.parseInt(chainHex, 16);
|
const providerChainID = Number.parseInt(chainHex, 16);
|
||||||
if (Number.isFinite(providerChainID) && providerChainID !== chainID()) {
|
if (Number.isFinite(providerChainID) && providerChainID !== chainID()) {
|
||||||
@ -261,9 +497,74 @@ async function onConnectWallet() {
|
|||||||
provider_chain_id: providerChainID,
|
provider_chain_id: providerChainID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await onRefreshBalances();
|
||||||
|
} catch (err) {
|
||||||
|
logLine("wallet balance refresh warning", { error: String(err) });
|
||||||
|
}
|
||||||
refreshOverview();
|
refreshOverview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onRefreshBalances() {
|
||||||
|
const address = normalizedAddress(requireWallet());
|
||||||
|
const provider = await requireProvider();
|
||||||
|
const nativeHex = await provider.request({
|
||||||
|
method: "eth_getBalance",
|
||||||
|
params: [address, "latest"],
|
||||||
|
});
|
||||||
|
const nativeDisplay = formatUnitsHex(nativeHex, 18, 6);
|
||||||
|
if (nativeDisplay) {
|
||||||
|
state.walletBalanceNative = `${nativeDisplay} ETH`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usdcToken = normalizedAddress($("usdcTokenAddress")?.value || "");
|
||||||
|
if (usdcToken && /^0x[a-f0-9]{40}$/.test(usdcToken)) {
|
||||||
|
const balanceOfSelector = "0x70a08231";
|
||||||
|
const decimalsSelector = "0x313ce567";
|
||||||
|
const balanceCallData = `${balanceOfSelector}${encodeAddressWord(address)}`;
|
||||||
|
const usdcHex = await provider.request({
|
||||||
|
method: "eth_call",
|
||||||
|
params: [{ to: usdcToken, data: balanceCallData }, "latest"],
|
||||||
|
});
|
||||||
|
let decimals = 6;
|
||||||
|
try {
|
||||||
|
const decimalsHex = await provider.request({
|
||||||
|
method: "eth_call",
|
||||||
|
params: [{ to: usdcToken, data: decimalsSelector }, "latest"],
|
||||||
|
});
|
||||||
|
const parsed = Number(BigInt(String(decimalsHex || "0x6")));
|
||||||
|
if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 36) {
|
||||||
|
decimals = parsed;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// keep default 6 when token decimals call is unavailable
|
||||||
|
}
|
||||||
|
const usdcDisplay = formatUnitsHex(usdcHex, decimals, 2);
|
||||||
|
if (usdcDisplay) {
|
||||||
|
state.walletBalanceUSDC = `${usdcDisplay} USDC`;
|
||||||
|
}
|
||||||
|
} else if (usdcToken) {
|
||||||
|
state.walletBalanceUSDC = "invalid token";
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshOverview();
|
||||||
|
logLine("wallet balances refreshed", {
|
||||||
|
wallet: address,
|
||||||
|
native: state.walletBalanceNative || "",
|
||||||
|
usdc: state.walletBalanceUSDC || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCopyWallet() {
|
||||||
|
const address = requireWallet();
|
||||||
|
if (!navigator?.clipboard?.writeText) {
|
||||||
|
throw new Error("clipboard API unavailable");
|
||||||
|
}
|
||||||
|
await navigator.clipboard.writeText(address);
|
||||||
|
setFlowStatus("wallet address copied");
|
||||||
|
logLine("wallet copied", { wallet: address });
|
||||||
|
}
|
||||||
|
|
||||||
async function onSignIntent() {
|
async function onSignIntent() {
|
||||||
if (!state.lastIntent) {
|
if (!state.lastIntent) {
|
||||||
throw new Error("create intent before signing");
|
throw new Error("create intent before signing");
|
||||||
@ -348,7 +649,7 @@ async function onSendMembershipTx() {
|
|||||||
params: [txRequest],
|
params: [txRequest],
|
||||||
});
|
});
|
||||||
$("confirmTxHash").value = txHash;
|
$("confirmTxHash").value = txHash;
|
||||||
logLine("membership tx sent", {
|
logLine("EDUT ID tx sent", {
|
||||||
quote_id: state.lastQuote.quote_id,
|
quote_id: state.lastQuote.quote_id,
|
||||||
tx_hash: txHash,
|
tx_hash: txHash,
|
||||||
payer_wallet: from,
|
payer_wallet: from,
|
||||||
@ -381,10 +682,48 @@ async function onVerify() {
|
|||||||
if (out.designation_code) {
|
if (out.designation_code) {
|
||||||
$("designationCode").value = out.designation_code;
|
$("designationCode").value = out.designation_code;
|
||||||
}
|
}
|
||||||
|
state.walletSessionToken = String(out.session_token || "").trim();
|
||||||
|
state.walletSessionExpiresAt = String(out.session_expires_at || "").trim();
|
||||||
logLine("wallet verify", out);
|
logLine("wallet verify", out);
|
||||||
|
if (state.walletSessionToken) {
|
||||||
|
logLine("wallet session active", {
|
||||||
|
wallet: requireWallet(),
|
||||||
|
session_expires_at: state.walletSessionExpiresAt || "unknown",
|
||||||
|
});
|
||||||
|
}
|
||||||
refreshOverview();
|
refreshOverview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onRefreshSession() {
|
||||||
|
const out = await request(
|
||||||
|
"POST",
|
||||||
|
"/secret/wallet/session/refresh",
|
||||||
|
{ wallet: requireWallet() },
|
||||||
|
{ skipSessionPreflight: true },
|
||||||
|
);
|
||||||
|
if (out.session_token) {
|
||||||
|
state.walletSessionToken = String(out.session_token).trim();
|
||||||
|
}
|
||||||
|
if (out.session_expires_at) {
|
||||||
|
state.walletSessionExpiresAt = String(out.session_expires_at).trim();
|
||||||
|
}
|
||||||
|
logLine("wallet session refresh", out);
|
||||||
|
refreshOverview();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRevokeSession() {
|
||||||
|
const out = await request(
|
||||||
|
"POST",
|
||||||
|
"/secret/wallet/session/revoke",
|
||||||
|
{ wallet: requireWallet() },
|
||||||
|
{ skipSessionPreflight: true },
|
||||||
|
);
|
||||||
|
logLine("wallet session revoke", out);
|
||||||
|
clearWalletSession("manual_revoke", { wallet: requireWallet() });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
async function onStatus() {
|
async function onStatus() {
|
||||||
const out = await request(
|
const out = await request(
|
||||||
"GET",
|
"GET",
|
||||||
@ -394,7 +733,7 @@ async function onStatus() {
|
|||||||
if (out.designation_code) {
|
if (out.designation_code) {
|
||||||
$("designationCode").value = out.designation_code;
|
$("designationCode").value = out.designation_code;
|
||||||
}
|
}
|
||||||
logLine("membership status", out);
|
logLine("EDUT ID status", out);
|
||||||
refreshOverview(out);
|
refreshOverview(out);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@ -422,7 +761,7 @@ async function onQuote() {
|
|||||||
$("quoteId").value = out.quote_id || "";
|
$("quoteId").value = out.quote_id || "";
|
||||||
$("quoteValue").value = out.value || "";
|
$("quoteValue").value = out.value || "";
|
||||||
$("quotePayer").value = out.payer_wallet || "";
|
$("quotePayer").value = out.payer_wallet || "";
|
||||||
logLine("membership quote", out);
|
logLine("EDUT ID quote", out);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,14 +795,14 @@ async function onConfirmMembership() {
|
|||||||
identity_attested_by: out.identity_attested_by || "",
|
identity_attested_by: out.identity_attested_by || "",
|
||||||
identity_attestation_id: out.identity_attestation_id || "",
|
identity_attestation_id: out.identity_attestation_id || "",
|
||||||
};
|
};
|
||||||
logLine("membership confirm", out);
|
logLine("EDUT ID confirm", out);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requireMembershipState(actionLabel, opts = {}) {
|
async function requireMembershipState(actionLabel, opts = {}) {
|
||||||
const status = await onStatus();
|
const status = await onStatus();
|
||||||
if (String(status.status || "").toLowerCase() !== "active") {
|
if (String(status.status || "").toLowerCase() !== "active") {
|
||||||
throw new Error(`${actionLabel} requires active membership`);
|
throw new Error(`${actionLabel} requires active EDUT ID`);
|
||||||
}
|
}
|
||||||
if (opts.requireOnramp && !isOnrampAttested(status.identity_assurance_level)) {
|
if (opts.requireOnramp && !isOnrampAttested(status.identity_assurance_level)) {
|
||||||
throw new Error(`${actionLabel} requires onramp_attested identity assurance`);
|
throw new Error(`${actionLabel} requires onramp_attested identity assurance`);
|
||||||
@ -507,17 +846,17 @@ async function confirmMembershipWithRetry(maxAttempts = 8, intervalMs = 2500) {
|
|||||||
await sleep(intervalMs);
|
await sleep(intervalMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw lastErr || new Error("membership confirm failed");
|
throw lastErr || new Error("EDUT ID confirmation failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRunMembershipFlow() {
|
async function onRunMembershipFlow() {
|
||||||
setFlowStatus("connecting wallet");
|
setFlowStatus("connecting wallet");
|
||||||
await onConnectWallet();
|
await onConnectWallet();
|
||||||
|
|
||||||
setFlowStatus("checking membership");
|
setFlowStatus("checking EDUT ID");
|
||||||
const status = await onStatus();
|
const status = await onStatus();
|
||||||
if (String(status.status || "").toLowerCase() === "active") {
|
if (String(status.status || "").toLowerCase() === "active") {
|
||||||
setFlowStatus("membership already active");
|
setFlowStatus("EDUT ID already active");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -527,9 +866,9 @@ async function onRunMembershipFlow() {
|
|||||||
await onSignIntent();
|
await onSignIntent();
|
||||||
setFlowStatus("verifying intent");
|
setFlowStatus("verifying intent");
|
||||||
await onVerify();
|
await onVerify();
|
||||||
setFlowStatus("quoting membership");
|
setFlowStatus("quoting EDUT ID");
|
||||||
await onQuote();
|
await onQuote();
|
||||||
setFlowStatus("sending membership transaction");
|
setFlowStatus("sending EDUT ID transaction");
|
||||||
await onSendMembershipTx();
|
await onSendMembershipTx();
|
||||||
|
|
||||||
const txHash = $("confirmTxHash").value.trim();
|
const txHash = $("confirmTxHash").value.trim();
|
||||||
@ -538,14 +877,14 @@ async function onRunMembershipFlow() {
|
|||||||
}
|
}
|
||||||
setFlowStatus("waiting for chain confirmation");
|
setFlowStatus("waiting for chain confirmation");
|
||||||
await waitForTxMined(txHash);
|
await waitForTxMined(txHash);
|
||||||
setFlowStatus("confirming membership with API");
|
setFlowStatus("confirming EDUT ID with API");
|
||||||
await confirmMembershipWithRetry();
|
await confirmMembershipWithRetry();
|
||||||
setFlowStatus("refreshing status");
|
setFlowStatus("refreshing status");
|
||||||
const refreshed = await onStatus();
|
const refreshed = await onStatus();
|
||||||
if (isOnrampAttested(refreshed.identity_assurance_level)) {
|
if (isOnrampAttested(refreshed.identity_assurance_level)) {
|
||||||
setFlowStatus("membership flow complete (attested)");
|
setFlowStatus("EDUT ID flow complete (attested)");
|
||||||
} else {
|
} else {
|
||||||
setFlowStatus("membership active (unattested)");
|
setFlowStatus("EDUT ID active (unattested)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -638,6 +977,7 @@ async function onInstallConfirm() {
|
|||||||
entitlement_id: $("entitlementId").value.trim(),
|
entitlement_id: $("entitlementId").value.trim(),
|
||||||
package_hash: $("packageHash").value.trim(),
|
package_hash: $("packageHash").value.trim(),
|
||||||
runtime_version: $("runtimeVersion").value.trim(),
|
runtime_version: $("runtimeVersion").value.trim(),
|
||||||
|
operation_mode: operationMode(),
|
||||||
installed_at: nowISO(),
|
installed_at: nowISO(),
|
||||||
launcher_receipt_hash: `receipt-${Date.now()}`,
|
launcher_receipt_hash: `receipt-${Date.now()}`,
|
||||||
});
|
});
|
||||||
@ -810,7 +1150,7 @@ async function onQuickActivate() {
|
|||||||
async function onQuickRefresh() {
|
async function onQuickRefresh() {
|
||||||
const status = await onStatus();
|
const status = await onStatus();
|
||||||
if (String(status.status || "").toLowerCase() !== "active") {
|
if (String(status.status || "").toLowerCase() !== "active") {
|
||||||
setFlowStatus("membership inactive");
|
setFlowStatus("EDUT ID inactive");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -860,6 +1200,16 @@ async function onOfflineRenew() {
|
|||||||
logLine("offline renew", out);
|
logLine("offline renew", out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onModeHuman() {
|
||||||
|
setOperationMode("human_manual", "quick_toggle");
|
||||||
|
setFlowStatus("mode set: human_manual");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onModeAuto() {
|
||||||
|
setOperationMode("worker_auto", "quick_toggle");
|
||||||
|
setFlowStatus("mode set: worker_auto");
|
||||||
|
}
|
||||||
|
|
||||||
function bind(id, handler) {
|
function bind(id, handler) {
|
||||||
const el = $(id);
|
const el = $(id);
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@ -878,11 +1228,17 @@ bind("btnQuickConnect", onQuickConnect);
|
|||||||
bind("btnQuickActivate", onQuickActivate);
|
bind("btnQuickActivate", onQuickActivate);
|
||||||
bind("btnQuickRefresh", onQuickRefresh);
|
bind("btnQuickRefresh", onQuickRefresh);
|
||||||
bind("btnQuickInstallStatus", onQuickInstallStatus);
|
bind("btnQuickInstallStatus", onQuickInstallStatus);
|
||||||
|
bind("btnRefreshBalances", onRefreshBalances);
|
||||||
|
bind("btnCopyWallet", onCopyWallet);
|
||||||
|
bind("btnModeHuman", onModeHuman);
|
||||||
|
bind("btnModeAuto", onModeAuto);
|
||||||
bind("btnConnectWallet", onConnectWallet);
|
bind("btnConnectWallet", onConnectWallet);
|
||||||
bind("btnRunMembershipFlow", onRunMembershipFlow);
|
bind("btnRunMembershipFlow", onRunMembershipFlow);
|
||||||
bind("btnIntent", onIntent);
|
bind("btnIntent", onIntent);
|
||||||
bind("btnSignIntent", onSignIntent);
|
bind("btnSignIntent", onSignIntent);
|
||||||
bind("btnVerify", onVerify);
|
bind("btnVerify", onVerify);
|
||||||
|
bind("btnRefreshSession", onRefreshSession);
|
||||||
|
bind("btnRevokeSession", onRevokeSession);
|
||||||
bind("btnStatus", onStatus);
|
bind("btnStatus", onStatus);
|
||||||
bind("btnQuote", onQuote);
|
bind("btnQuote", onQuote);
|
||||||
bind("btnSignPayerProof", onSignPayerProof);
|
bind("btnSignPayerProof", onSignPayerProof);
|
||||||
@ -904,6 +1260,14 @@ bind("btnCheckoutConfirm", onCheckoutConfirm);
|
|||||||
bind("btnRunCheckoutFlow", onRunCheckoutFlow);
|
bind("btnRunCheckoutFlow", onRunCheckoutFlow);
|
||||||
bind("btnListEntitlements", onListEntitlements);
|
bind("btnListEntitlements", onListEntitlements);
|
||||||
|
|
||||||
|
const operationModeSelect = $("operationMode");
|
||||||
|
if (operationModeSelect) {
|
||||||
|
operationModeSelect.addEventListener("change", () => {
|
||||||
|
setOperationMode(operationModeSelect.value, "advanced_select");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshModeUI();
|
||||||
logLine("launcher shell ready", {
|
logLine("launcher shell ready", {
|
||||||
api_base: baseURL(),
|
api_base: baseURL(),
|
||||||
chain_id: chainID(),
|
chain_id: chainID(),
|
||||||
|
|||||||
@ -3,23 +3,27 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>EDUT Launcher</title>
|
<title>EDUT ID Manager</title>
|
||||||
<link rel="stylesheet" href="./style.css" />
|
<link rel="stylesheet" href="./style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="shell">
|
<main class="shell">
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<h1>EDUT Launcher</h1>
|
<h1>EDUT ID Manager</h1>
|
||||||
<p>Deterministic infrastructure control surface.</p>
|
<p>Deterministic identity, access, and control surface.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Control Surface</h2>
|
<h2>Control Surface</h2>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="btnQuickConnect">Connect wallet</button>
|
<button id="btnQuickConnect">Connect wallet</button>
|
||||||
<button id="btnQuickActivate">Activate membership</button>
|
<button id="btnQuickActivate">Activate EDUT ID</button>
|
||||||
<button id="btnQuickRefresh">Refresh status + feed</button>
|
<button id="btnQuickRefresh">Refresh status + feed</button>
|
||||||
<button id="btnQuickInstallStatus">Governance status</button>
|
<button id="btnQuickInstallStatus">Governance status</button>
|
||||||
|
<button id="btnRefreshBalances">Refresh balances</button>
|
||||||
|
<button id="btnCopyWallet">Copy address</button>
|
||||||
|
<button id="btnModeHuman" class="mode-active">Human mode</button>
|
||||||
|
<button id="btnModeAuto">Auto mode</button>
|
||||||
</div>
|
</div>
|
||||||
<p id="flowStatus" class="flow-status">ready</p>
|
<p id="flowStatus" class="flow-status">ready</p>
|
||||||
<div class="grid two">
|
<div class="grid two">
|
||||||
@ -28,15 +32,27 @@
|
|||||||
<p id="summaryWallet">not connected</p>
|
<p id="summaryWallet">not connected</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat">
|
<article class="stat">
|
||||||
<h3>Membership</h3>
|
<h3>Session</h3>
|
||||||
|
<p id="summarySession">none</p>
|
||||||
|
</article>
|
||||||
|
<article class="stat">
|
||||||
|
<h3>Wallet Funds</h3>
|
||||||
|
<p id="summaryFunds">-- ETH | -- USDC</p>
|
||||||
|
</article>
|
||||||
|
<article class="stat">
|
||||||
|
<h3><span class="term-inline">EDUT ID<details class="inline-help"><summary aria-label="What is EDUT ID?">?</summary><p>EDUT ID is your one-time identity credential used for checkout, activation, and ownership proof.</p></details></span></h3>
|
||||||
<p id="summaryMembership">unknown</p>
|
<p id="summaryMembership">unknown</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat">
|
<article class="stat">
|
||||||
<h3>Designation</h3>
|
<h3>Mode</h3>
|
||||||
|
<p id="summaryMode">human_manual</p>
|
||||||
|
</article>
|
||||||
|
<article class="stat">
|
||||||
|
<h3><span class="term-inline">Designation<details class="inline-help"><summary aria-label="What is designation?">?</summary><p>Designation is the protocol reference created during wallet intent and carried through activation evidence.</p></details></span></h3>
|
||||||
<p id="summaryDesignation">-</p>
|
<p id="summaryDesignation">-</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat">
|
<article class="stat">
|
||||||
<h3>Identity Assurance</h3>
|
<h3><span class="term-inline">Identity Assurance<details class="inline-help"><summary aria-label="What is identity assurance?">?</summary><p>Assurance level records how identity was attested, such as direct crypto flow or on-ramp attestation.</p></details></span></h3>
|
||||||
<p id="summaryAssurance">unknown</p>
|
<p id="summaryAssurance">unknown</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat">
|
<article class="stat">
|
||||||
@ -86,18 +102,24 @@
|
|||||||
Chain ID
|
Chain ID
|
||||||
<input id="chainId" type="number" value="84532" />
|
<input id="chainId" type="number" value="84532" />
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
USDC token address
|
||||||
|
<input id="usdcTokenAddress" value="0x036cbd53842c5426634e7929541ec2318f3dcf7e" />
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="subpanel">
|
<section class="subpanel">
|
||||||
<h2>Membership Flow Controls</h2>
|
<h2>EDUT ID Flow Controls</h2>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="btnRunMembershipFlow">Run membership flow</button>
|
<button id="btnRunMembershipFlow">Run EDUT ID flow</button>
|
||||||
<button id="btnConnectWallet">Connect wallet</button>
|
<button id="btnConnectWallet">Connect wallet</button>
|
||||||
<button id="btnIntent">Create intent</button>
|
<button id="btnIntent">Create intent</button>
|
||||||
<button id="btnSignIntent">Sign intent (EIP-712)</button>
|
<button id="btnSignIntent">Sign intent (EIP-712)</button>
|
||||||
<button id="btnVerify">Verify signature</button>
|
<button id="btnVerify">Verify signature</button>
|
||||||
<button id="btnStatus">Membership status</button>
|
<button id="btnRefreshSession">Refresh session</button>
|
||||||
|
<button id="btnRevokeSession">Revoke session</button>
|
||||||
|
<button id="btnStatus">EDUT ID status</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid three">
|
<div class="grid three">
|
||||||
<label>
|
<label>
|
||||||
@ -138,8 +160,8 @@
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="btnQuote">Get quote</button>
|
<button id="btnQuote">Get quote</button>
|
||||||
<button id="btnSignPayerProof">Sign payer proof</button>
|
<button id="btnSignPayerProof">Sign payer proof</button>
|
||||||
<button id="btnSendMembershipTx">Send membership tx</button>
|
<button id="btnSendMembershipTx">Send EDUT ID tx</button>
|
||||||
<button id="btnConfirmMembership">Confirm membership tx</button>
|
<button id="btnConfirmMembership">Confirm EDUT ID tx</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid three">
|
<div class="grid three">
|
||||||
<label>
|
<label>
|
||||||
@ -310,6 +332,13 @@
|
|||||||
<input id="runtimeVersion" />
|
<input id="runtimeVersion" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<label>
|
||||||
|
Operation mode
|
||||||
|
<select id="operationMode">
|
||||||
|
<option value="human_manual">human_manual</option>
|
||||||
|
<option value="worker_auto">worker_auto</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Package hash
|
Package hash
|
||||||
<input id="packageHash" />
|
<input id="packageHash" />
|
||||||
|
|||||||
@ -53,6 +53,51 @@ body {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.term-inline {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-help {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-help > summary {
|
||||||
|
list-style: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 14px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
background: #0f1828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-help > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-help > p {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
width: min(300px, 80vw);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0f1828;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.45;
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
|
||||||
.note {
|
.note {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@ -149,10 +194,22 @@ button:hover {
|
|||||||
filter: brightness(1.08);
|
filter: brightness(1.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.mode-active {
|
||||||
|
border-color: #4ed380;
|
||||||
|
background: #1f4a33;
|
||||||
|
color: #e0ffe9;
|
||||||
|
}
|
||||||
|
|
||||||
button:active {
|
button:active {
|
||||||
transform: translateY(1px);
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
filter: grayscale(0.5);
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
.event-list {
|
.event-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@ -12,3 +12,4 @@
|
|||||||
10. `L-010` Primary wallet screens render USD-first balances and plain-language history.
|
10. `L-010` Primary wallet screens render USD-first balances and plain-language history.
|
||||||
11. `L-011` Launcher must surface `identity_assurance_level` separately from membership state.
|
11. `L-011` Launcher must surface `identity_assurance_level` separately from membership state.
|
||||||
12. `L-012` Owner support and governance install actions are blocked when assurance is not `onramp_attested`.
|
12. `L-012` Owner support and governance install actions are blocked when assurance is not `onramp_attested`.
|
||||||
|
13. `L-013` Launcher emits signing authority class in governance activation evidence and defaults owner-driven activation to `identity_human`.
|
||||||
|
|||||||
@ -18,6 +18,24 @@ Launcher integrates with EDUT web/backend contracts as follows:
|
|||||||
12. `GET /governance/install/status`
|
12. `GET /governance/install/status`
|
||||||
13. `GET /member/channel/events`
|
13. `GET /member/channel/events`
|
||||||
|
|
||||||
|
## Wallet Session Contract
|
||||||
|
|
||||||
|
1. `POST /secret/wallet/verify` returns `session_token` and `session_expires_at`.
|
||||||
|
2. Launcher must attach session token on wallet-scoped calls using:
|
||||||
|
- `Authorization: Bearer <session_token>` (preferred)
|
||||||
|
- `X-Edut-Session: <session_token>` (compatibility)
|
||||||
|
3. Wallet change must clear cached session token before further calls.
|
||||||
|
4. Endpoints that require membership/admin authority can fail with:
|
||||||
|
- `wallet_session_required`
|
||||||
|
- `wallet_session_invalid`
|
||||||
|
- `wallet_session_expired`
|
||||||
|
- `wallet_session_mismatch`
|
||||||
|
|
||||||
|
## Runtime Mode Signal
|
||||||
|
|
||||||
|
1. Launcher install-confirm payload carries `operation_mode` (`human_manual` or `worker_auto`).
|
||||||
|
2. Mode signal is deterministic evidence input for governance activation policy and receipt hashing.
|
||||||
|
|
||||||
## Deterministic Requirements
|
## Deterministic Requirements
|
||||||
|
|
||||||
1. No runtime activation without entitlement proof.
|
1. No runtime activation without entitlement proof.
|
||||||
@ -26,3 +44,4 @@ Launcher integrates with EDUT web/backend contracts as follows:
|
|||||||
4. Event inbox polling remains canonical even if push unavailable.
|
4. Event inbox polling remains canonical even if push unavailable.
|
||||||
5. Identity assurance is evaluated independently from membership state.
|
5. Identity assurance is evaluated independently from membership state.
|
||||||
6. Owner/admin launcher actions must require `identity_assurance_level=onramp_attested`.
|
6. Owner/admin launcher actions must require `identity_assurance_level=onramp_attested`.
|
||||||
|
7. Governance activation evidence must include signing authority class (`identity_human` vs delegated).
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
3. Governance install path fails closed on invalid evidence.
|
3. Governance install path fails closed on invalid evidence.
|
||||||
4. Marketplace and status APIs are called with app-session auth.
|
4. Marketplace and status APIs are called with app-session auth.
|
||||||
5. Wallet v1 acceptance criteria pass (`docs/wallet-v1-product-spec.md`).
|
5. Wallet v1 acceptance criteria pass (`docs/wallet-v1-product-spec.md`).
|
||||||
|
6. Owner/admin actions block unless `identity_assurance_level=onramp_attested`.
|
||||||
|
|
||||||
## Blockers
|
## Blockers
|
||||||
|
|
||||||
@ -14,3 +15,4 @@
|
|||||||
2. Any path that leaks private key material.
|
2. Any path that leaks private key material.
|
||||||
3. Any path that bypasses entitlement checks for governance activation.
|
3. Any path that bypasses entitlement checks for governance activation.
|
||||||
4. Any launch flow that exposes seed phrase by default.
|
4. Any launch flow that exposes seed phrase by default.
|
||||||
|
5. Any owner support/install action that proceeds without required identity assurance.
|
||||||
|
|||||||
@ -127,6 +127,8 @@ Technical details are available only in expanded view:
|
|||||||
3. Recovery path must exist but remain opt-in in onboarding.
|
3. Recovery path must exist but remain opt-in in onboarding.
|
||||||
4. Sensitive operations fail closed on secure storage errors.
|
4. Sensitive operations fail closed on secure storage errors.
|
||||||
5. Wallet export (seed/private key) requires explicit authenticated flow.
|
5. Wallet export (seed/private key) requires explicit authenticated flow.
|
||||||
|
6. AI/delegated automation must never use the human identity signer key directly.
|
||||||
|
7. Any delegated signing authority must be explicit, scoped, and revocable.
|
||||||
|
|
||||||
## Asset/Display Model
|
## Asset/Display Model
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user