/* global fetch */ const $ = (id) => document.getElementById(id); const state = { eventMap: new Map(), lastIntent: null, lastQuote: null, lastCheckoutQuote: null, lastStatus: null, channelReady: false, walletSessionToken: "", walletSessionExpiresAt: "", walletSessionRefreshInFlight: null, walletBalanceNative: "", walletBalanceUSDC: "", }; function nowISO() { return new Date().toISOString(); } function baseURL() { return $("apiBase").value.trim().replace(/\/+$/, ""); } function wallet() { return $("walletAddress").value.trim(); } function chainID() { return Number($("chainId").value || 0); } function normalizedAddress(value) { return String(value || "").trim().toLowerCase(); } function normalizeAssurance(value) { return String(value || "none").trim().toLowerCase() || "none"; } function assuranceDisplay(value) { const assurance = normalizeAssurance(value); if (assurance === "onramp_attested") { return "onramp_attested"; } if (assurance === "crypto_direct_unattested") { return "crypto_direct_unattested"; } if (assurance === "sponsored_unattested") { return "sponsored_unattested"; } return "none"; } function isOnrampAttested(value) { 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() { return globalThis.ethereum || null; } async function requireProvider() { const provider = injectedProvider(); if (!provider) { throw new Error("no injected wallet provider found (expected window.ethereum)"); } return provider; } function utf8ToHex(value) { return `0x${Array.from(new TextEncoder().encode(String(value))) .map((b) => b.toString(16).padStart(2, "0")) .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) { const log = $("log"); const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`; log.textContent = line + log.textContent; } const errorCodeHelp = { wallet_session_required: "Session required. Reconnect wallet or refresh session.", wallet_session_invalid: "Session invalid. Reconnect wallet to continue.", wallet_session_expired: "Session expired. Refresh session or reconnect wallet.", wallet_session_revoked: "Session revoked. Reconnect wallet to continue.", wallet_session_mismatch: "Session does not match the active wallet.", membership_required: "EDUT ID must be active before this action.", membership_inactive: "EDUT ID is not active for this wallet.", identity_assurance_insufficient: "This admin action requires on-ramp attestation.", role_insufficient: "Action blocked: org root owner role required.", boundary_mismatch: "Boundary mismatch. Verify org root, principal, and workspace.", entitlement_inactive: "Entitlement is inactive for this action.", entitlement_contract_unconfigured: "Checkout is temporarily unavailable (entitlement contract not configured).", tx_verification_failed: "Transaction not yet verifiable. Wait for chain confirmation and retry.", membership_verification_failed: "Membership proof not yet verifiable. Wait for chain confirmation and retry.", quote_expired: "Quote expired. Request a new quote.", quote_not_found: "Quote not found. Start a new quote flow.", install_token_expired: "Install token expired. Request a new install token.", install_token_not_found: "Install token not found. Request a new install token.", install_token_consumed: "Install token already used. Request a new install token.", chain_verification_unavailable: "Chain verification unavailable. Retry when provider is reachable.", }; function friendlyError(err) { const code = String(err?.code || "").trim(); if (code && errorCodeHelp[code]) { return { code, message: errorCodeHelp[code], }; } const payloadMessage = String(err?.payload?.error || "").trim(); const message = payloadMessage || String(err?.message || err || "unknown error"); return { code, message, }; } function reportActionError(actionID, err) { const normalized = friendlyError(err); setFlowStatus(normalized.message); logLine(`${actionID} error`, { code: normalized.code, message: normalized.message, raw_error: String(err), payload: err?.payload || null, }); } function setSummary(id, value) { const el = $(id); if (!el) return; 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) { const currentWallet = wallet(); setSummary("summaryWallet", currentWallet || "not connected"); setSummary("summaryFunds", walletBalanceSummary()); setSummary("summarySession", sessionSummary()); refreshModeUI(); if (statusPayload && typeof statusPayload === "object") { setSummary("summaryMembership", statusPayload.status || "unknown"); setSummary("summaryDesignation", statusPayload.designation_code || "-"); const assurance = assuranceDisplay(statusPayload.identity_assurance_level); setSummary("summaryAssurance", assurance); setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked"); setSummary("summaryLastSync", nowISO()); refreshActionLocks(statusPayload); return; } const designation = $("designationCode")?.value?.trim() || "-"; setSummary("summaryDesignation", designation); const assurance = assuranceDisplay(state.lastStatus?.identity_assurance_level); setSummary("summaryAssurance", assurance); setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked"); refreshActionLocks(); } function setFlowStatus(message) { const el = $("flowStatus"); if (el) { el.textContent = message; } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } 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 = { method, headers, }; if (body !== undefined) { opts.body = JSON.stringify(body); } const res = await fetch(`${baseURL()}${path}`, opts); captureSessionHeaders(res); const text = await res.text(); let json = {}; if (text.trim() !== "") { try { json = JSON.parse(text); } catch { json = { raw: text }; } } if (!res.ok) { 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; } function requireWallet() { const value = wallet(); if (!value) { throw new Error("wallet is required"); } return value; } function deviceID() { return $("deviceId").value.trim(); } function orgRootID() { return $("orgRootId").value.trim(); } function principalID() { return $("principalId").value.trim(); } function principalRole() { 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) { const list = $("eventList"); list.innerHTML = ""; for (const evt of events) { state.eventMap.set(evt.event_id, evt); const card = document.createElement("article"); card.className = "event"; const title = document.createElement("h3"); title.textContent = `${evt.class} · ${evt.event_id}`; card.appendChild(title); const body = document.createElement("p"); body.textContent = evt.body || evt.title || ""; card.appendChild(body); const meta = document.createElement("div"); meta.className = "meta"; meta.textContent = `${evt.created_at} · scope=${evt.visibility_scope} · ack=${evt.requires_ack}`; card.appendChild(meta); if (evt.requires_ack) { const actions = document.createElement("div"); actions.className = "actions"; const button = document.createElement("button"); button.textContent = "Ack"; button.addEventListener("click", async () => { try { const out = await request("POST", `/member/channel/events/${evt.event_id}/ack`, { wallet: requireWallet(), device_id: deviceID(), acknowledged_at: nowISO(), }); logLine(`event ack ${evt.event_id}`, out); } catch (err) { logLine(`event ack ${evt.event_id} error`, { error: String(err) }); } }); actions.appendChild(button); card.appendChild(actions); } list.appendChild(card); } } function buildIntentTypedData(intent, origin) { return { types: { EIP712Domain: [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" }, ], DesignationIntent: [ { name: "designationCode", type: "string" }, { name: "designationToken", type: "string" }, { name: "nonce", type: "string" }, { name: "issuedAt", type: "string" }, { name: "origin", type: "string" }, ], }, primaryType: "DesignationIntent", domain: { name: intent.domain_name, version: "1", chainId: Number(intent.chain_id), verifyingContract: intent.verifying_contract, }, message: { designationCode: intent.designation_code, designationToken: intent.display_token, nonce: intent.nonce, issuedAt: intent.issued_at, origin, }, }; } async function onConnectWallet() { const previousWallet = normalizedAddress($("walletAddress").value); const provider = await requireProvider(); const accounts = await provider.request({ method: "eth_requestAccounts" }); if (!accounts || accounts.length === 0) { throw new Error("wallet provider returned no accounts"); } 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 providerChainID = Number.parseInt(chainHex, 16); if (Number.isFinite(providerChainID) && providerChainID !== chainID()) { logLine("wallet chain mismatch", { configured_chain_id: chainID(), provider_chain_id: providerChainID, provider_chain_hex: chainHex, }); } else { logLine("wallet connected", { wallet: $("walletAddress").value, provider_chain_id: providerChainID, }); } try { await onRefreshBalances(); } catch (err) { logLine("wallet balance refresh warning", { error: String(err) }); } 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() { if (!state.lastIntent) { throw new Error("create intent before signing"); } const provider = await requireProvider(); const owner = requireWallet(); const typedData = buildIntentTypedData(state.lastIntent, $("walletOrigin").value.trim() || "https://edut.ai"); const typedDataJSON = JSON.stringify(typedData); let signature = ""; try { signature = await provider.request({ method: "eth_signTypedData_v4", params: [owner, typedDataJSON], }); } catch (primaryErr) { signature = await provider.request({ method: "eth_signTypedData", params: [owner, typedData], }); logLine("wallet sign intent fallback", { warning: String(primaryErr) }); } $("walletSignature").value = signature; logLine("wallet sign intent", { wallet: owner, designation_code: state.lastIntent.designation_code, signature, }); } async function onSignPayerProof() { const provider = await requireProvider(); const owner = requireWallet(); const payer = normalizedAddress($("payerWallet").value); if (!payer) { throw new Error("payer wallet is required to sign payer proof"); } const designationCode = $("designationCode").value.trim(); if (!designationCode) { throw new Error("designation code is required"); } const message = `EDUT-PAYER-AUTH:${designationCode}:${normalizedAddress(owner)}:${payer}:${chainID()}`; const messageHex = utf8ToHex(message); let signature = ""; try { signature = await provider.request({ method: "personal_sign", params: [messageHex, owner], }); } catch (primaryErr) { signature = await provider.request({ method: "personal_sign", params: [owner, messageHex], }); logLine("wallet payer proof fallback", { warning: String(primaryErr) }); } $("payerProof").value = signature; logLine("wallet signed payer proof", { wallet: owner, payer_wallet: payer, designation_code: designationCode, signature, }); } async function onSendMembershipTx() { if (!state.lastQuote || !state.lastQuote.tx) { throw new Error("request quote before sending transaction"); } const provider = await requireProvider(); const from = normalizedAddress(state.lastQuote.tx.from || requireWallet()); if (from !== normalizedAddress(requireWallet())) { throw new Error(`active wallet ${requireWallet()} does not match quote payer ${from}`); } const txRequest = { from, to: state.lastQuote.tx.to, data: state.lastQuote.tx.data, value: state.lastQuote.tx.value || "0x0", }; const txHash = await provider.request({ method: "eth_sendTransaction", params: [txRequest], }); $("confirmTxHash").value = txHash; logLine("EDUT ID tx sent", { quote_id: state.lastQuote.quote_id, tx_hash: txHash, payer_wallet: from, }); } async function onIntent() { const payload = { address: requireWallet(), origin: $("walletOrigin").value.trim() || "https://edut.ai", locale: $("walletLocale").value.trim() || "en", chain_id: chainID(), }; const out = await request("POST", "/secret/wallet/intent", payload); state.lastIntent = out; $("intentId").value = out.intent_id || ""; $("designationCode").value = out.designation_code || ""; $("displayToken").value = out.display_token || ""; logLine("wallet intent", out); refreshOverview(); } async function onVerify() { const out = await request("POST", "/secret/wallet/verify", { intent_id: $("intentId").value.trim(), address: requireWallet(), chain_id: chainID(), signature: $("walletSignature").value.trim(), }); if (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); if (state.walletSessionToken) { logLine("wallet session active", { wallet: requireWallet(), session_expires_at: state.walletSessionExpiresAt || "unknown", }); } 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() { const out = await request( "GET", `/secret/membership/status?wallet=${encodeURIComponent(requireWallet())}`, ); state.lastStatus = out; if (out.designation_code) { $("designationCode").value = out.designation_code; } logLine("EDUT ID status", out); refreshOverview(out); return out; } async function onQuote() { const payload = { designation_code: $("designationCode").value.trim(), address: requireWallet(), chain_id: chainID(), }; const payerWallet = $("payerWallet").value.trim(); const payerProof = $("payerProof").value.trim(); const sponsorOrgRoot = $("sponsorOrgRoot").value.trim(); if (payerWallet) { payload.payer_wallet = payerWallet; } if (payerProof) { payload.payer_proof = payerProof; } if (sponsorOrgRoot) { payload.sponsor_org_root_id = sponsorOrgRoot; } const out = await request("POST", "/secret/membership/quote", payload); state.lastQuote = out; $("quoteId").value = out.quote_id || ""; $("quoteValue").value = out.value || ""; $("quotePayer").value = out.payer_wallet || ""; logLine("EDUT ID quote", out); return out; } async function onConfirmMembership() { const payload = { designation_code: $("designationCode").value.trim(), quote_id: $("quoteId").value.trim(), tx_hash: $("confirmTxHash").value.trim(), address: requireWallet(), chain_id: chainID(), }; const assurance = $("membershipIdentityAssurance").value.trim(); const attestedBy = $("membershipIdentityAttestedBy").value.trim(); const attestationID = $("membershipIdentityAttestationId").value.trim(); if (assurance) { payload.identity_assurance_level = assurance; } if (attestedBy) { payload.identity_attested_by = attestedBy; } if (attestationID) { payload.identity_attestation_id = attestationID; } const out = await request("POST", "/secret/membership/confirm", payload); state.lastStatus = { ...(state.lastStatus || {}), status: "active", wallet: requireWallet(), designation_code: out.designation_code || $("designationCode").value.trim(), identity_assurance_level: out.identity_assurance_level || state.lastStatus?.identity_assurance_level || "none", identity_attested_by: out.identity_attested_by || "", identity_attestation_id: out.identity_attestation_id || "", }; logLine("EDUT ID confirm", out); return out; } async function requireMembershipState(actionLabel, opts = {}) { const status = await onStatus(); if (String(status.status || "").toLowerCase() !== "active") { throw new Error(`${actionLabel} requires active EDUT ID`); } if (opts.requireOnramp && !isOnrampAttested(status.identity_assurance_level)) { throw new Error(`${actionLabel} requires onramp_attested identity assurance`); } return status; } async function waitForTxMined(txHash, timeoutMs = 120000, intervalMs = 3000) { const provider = await requireProvider(); const started = Date.now(); while (Date.now() - started < timeoutMs) { const receipt = await provider.request({ method: "eth_getTransactionReceipt", params: [txHash], }); if (receipt) { const statusHex = String(receipt.status || "").toLowerCase(); if (statusHex === "0x1" || statusHex === "1") { return receipt; } throw new Error(`transaction reverted: status=${receipt.status}`); } await sleep(intervalMs); } throw new Error(`transaction not mined within ${timeoutMs}ms`); } async function confirmMembershipWithRetry(maxAttempts = 8, intervalMs = 2500) { let lastErr = null; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { const out = await onConfirmMembership(); return out; } catch (err) { lastErr = err; const message = String(err || ""); if (!message.includes("tx verification pending/failed")) { throw err; } setFlowStatus(`confirm pending (${attempt}/${maxAttempts})`); await sleep(intervalMs); } } throw lastErr || new Error("EDUT ID confirmation failed"); } async function onRunMembershipFlow() { setFlowStatus("connecting wallet"); await onConnectWallet(); setFlowStatus("checking EDUT ID"); const status = await onStatus(); if (String(status.status || "").toLowerCase() === "active") { setFlowStatus("EDUT ID already active"); return; } setFlowStatus("creating intent"); await onIntent(); setFlowStatus("signing intent"); await onSignIntent(); setFlowStatus("verifying intent"); await onVerify(); setFlowStatus("quoting EDUT ID"); await onQuote(); setFlowStatus("sending EDUT ID transaction"); await onSendMembershipTx(); const txHash = $("confirmTxHash").value.trim(); if (!txHash) { throw new Error("missing tx hash after send"); } setFlowStatus("waiting for chain confirmation"); await waitForTxMined(txHash); setFlowStatus("confirming EDUT ID with API"); await confirmMembershipWithRetry(); setFlowStatus("refreshing status"); const refreshed = await onStatus(); if (isOnrampAttested(refreshed.identity_assurance_level)) { setFlowStatus("EDUT ID flow complete (attested)"); } else { setFlowStatus("EDUT ID active (unattested)"); } } async function onRegisterChannel() { const out = await request("POST", "/member/channel/device/register", { wallet: requireWallet(), chain_id: chainID(), device_id: deviceID(), platform: $("platform").value.trim(), org_root_id: orgRootID(), principal_id: principalID(), principal_role: principalRole(), app_version: $("appVersion").value.trim(), push_provider: "none", }); state.channelReady = true; logLine("channel register", out); return out; } async function onUnregisterChannel() { const out = await request("POST", "/member/channel/device/unregister", { wallet: requireWallet(), device_id: deviceID(), }); state.channelReady = false; logLine("channel unregister", out); return out; } async function onPollEvents() { const cursor = $("eventCursor").value.trim(); const limit = $("eventLimit").value.trim() || "25"; const query = new URLSearchParams({ wallet: requireWallet(), device_id: deviceID(), limit, }); if (cursor) { query.set("cursor", cursor); } const out = await request("GET", `/member/channel/events?${query.toString()}`); if (out.next_cursor) { $("eventCursor").value = out.next_cursor; } renderEvents(out.events || []); logLine("channel poll", out); return out; } async function onSupportTicket() { await requireMembershipState("owner support", { requireOnramp: true }); const out = await request("POST", "/member/channel/support/ticket", { wallet: requireWallet(), org_root_id: orgRootID(), principal_id: principalID(), category: "admin_support", summary: $("supportSummary").value.trim(), context: { source: "launcher-shell", requested_at: nowISO(), }, }); logLine("support ticket", out); } async function onInstallToken() { await requireMembershipState("governance install token", { requireOnramp: true }); const out = await request("POST", "/governance/install/token", { wallet: requireWallet(), org_root_id: orgRootID(), principal_id: principalID(), principal_role: principalRole(), device_id: deviceID(), launcher_version: $("appVersion").value.trim(), platform: $("platform").value.trim(), }); $("installToken").value = out.install_token || ""; $("entitlementId").value = out.entitlement_id || ""; $("runtimeVersion").value = out.package?.runtime_version || ""; $("packageHash").value = out.package?.package_hash || ""; logLine("install token", out); } async function onInstallConfirm() { const out = await request("POST", "/governance/install/confirm", { install_token: $("installToken").value.trim(), wallet: requireWallet(), device_id: deviceID(), entitlement_id: $("entitlementId").value.trim(), package_hash: $("packageHash").value.trim(), runtime_version: $("runtimeVersion").value.trim(), operation_mode: operationMode(), installed_at: nowISO(), launcher_receipt_hash: `receipt-${Date.now()}`, }); logLine("install confirm", out); } async function onInstallStatus() { const query = new URLSearchParams({ wallet: requireWallet(), device_id: deviceID(), }); const out = await request("GET", `/governance/install/status?${query.toString()}`); logLine("install status", out); return out; } async function onListOffers() { const out = await request("GET", "/marketplace/offers"); logLine("marketplace offers", out); return out; } async function onCheckoutQuote() { const payload = { wallet: requireWallet(), offer_id: $("checkoutOfferId").value.trim(), org_root_id: orgRootID(), principal_id: principalID(), principal_role: principalRole(), include_membership_if_missing: false, }; const payerWallet = $("payerWallet").value.trim(); const payerProof = $("payerProof").value.trim(); if (payerWallet) { payload.payer_wallet = payerWallet; } if (payerProof) { payload.ownership_proof = payerProof; } const out = await request("POST", "/marketplace/checkout/quote", payload); state.lastCheckoutQuote = out; $("checkoutQuoteId").value = out.quote_id || ""; $("checkoutTotal").value = out.total_amount || ""; logLine("marketplace checkout quote", out); return out; } async function onSendCheckoutTx() { if (!state.lastCheckoutQuote || !state.lastCheckoutQuote.tx) { throw new Error("request checkout quote before sending transaction"); } const provider = await requireProvider(); const from = normalizedAddress(state.lastCheckoutQuote.tx.from || requireWallet()); if (from !== normalizedAddress(requireWallet())) { throw new Error(`active wallet ${requireWallet()} does not match checkout payer ${from}`); } const txRequest = { from, to: state.lastCheckoutQuote.tx.to, data: state.lastCheckoutQuote.tx.data, value: state.lastCheckoutQuote.tx.value || "0x0", }; const txHash = await provider.request({ method: "eth_sendTransaction", params: [txRequest], }); $("checkoutTxHash").value = txHash; logLine("marketplace tx sent", { quote_id: state.lastCheckoutQuote.quote_id, tx_hash: txHash, payer_wallet: from, }); } async function onCheckoutConfirm() { const quoteID = $("checkoutQuoteId").value.trim(); if (!quoteID) { throw new Error("checkout quote id is required"); } const txHash = $("checkoutTxHash").value.trim(); if (!txHash) { throw new Error("checkout tx hash is required"); } const offerID = state.lastCheckoutQuote?.offer_id || $("checkoutOfferId").value.trim(); const out = await request("POST", "/marketplace/checkout/confirm", { quote_id: quoteID, wallet: requireWallet(), offer_id: offerID, org_root_id: orgRootID(), principal_id: principalID(), principal_role: principalRole(), tx_hash: txHash, chain_id: chainID(), }); $("checkoutEntitlementId").value = out.entitlement_id || ""; logLine("marketplace checkout confirm", out); return out; } async function confirmCheckoutWithRetry(maxAttempts = 8, intervalMs = 2500) { let lastErr = null; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { const out = await onCheckoutConfirm(); return out; } catch (err) { lastErr = err; const message = String(err || ""); if (!message.includes("tx verification pending/failed") && !message.includes("membership verification pending/failed")) { throw err; } setFlowStatus(`checkout confirm pending (${attempt}/${maxAttempts})`); await sleep(intervalMs); } } throw lastErr || new Error("checkout confirm failed"); } async function onRunCheckoutFlow() { setFlowStatus("quoting checkout"); await onCheckoutQuote(); setFlowStatus("sending checkout transaction"); await onSendCheckoutTx(); const txHash = $("checkoutTxHash").value.trim(); if (!txHash) { throw new Error("missing checkout tx hash after send"); } setFlowStatus("waiting for checkout chain confirmation"); await waitForTxMined(txHash); setFlowStatus("confirming checkout with API"); await confirmCheckoutWithRetry(); setFlowStatus("refreshing entitlements"); await onListEntitlements(); setFlowStatus("checkout flow complete"); } async function onListEntitlements() { const out = await request("GET", `/marketplace/entitlements?wallet=${encodeURIComponent(requireWallet())}`); logLine("marketplace entitlements", out); return out; } async function ensureChannelBinding() { if (state.channelReady) { return; } await onRegisterChannel(); } async function onQuickConnect() { await onConnectWallet(); try { await onStatus(); } catch (err) { logLine("quick connect status warning", { error: String(err) }); } } async function onQuickActivate() { await onRunMembershipFlow(); try { await ensureChannelBinding(); await onPollEvents(); } catch (err) { logLine("quick activate feed warning", { error: String(err) }); } } async function onQuickRefresh() { const status = await onStatus(); if (String(status.status || "").toLowerCase() !== "active") { setFlowStatus("EDUT ID inactive"); return; } try { await ensureChannelBinding(); await onPollEvents(); if (isOnrampAttested(status.identity_assurance_level)) { setFlowStatus("status synced (admin-ready)"); } else { setFlowStatus("status synced (member mode)"); } } catch (err) { setFlowStatus("feed sync warning"); throw err; } } async function onQuickInstallStatus() { const out = await onInstallStatus(); const assurance = assuranceDisplay(out.identity_assurance_level); if (isOnrampAttested(assurance)) { setFlowStatus("governance status synced (attested)"); } else { setFlowStatus("governance status synced (attestation required)"); } } async function onLeaseHeartbeat() { const out = await request("POST", "/governance/lease/heartbeat", { wallet: requireWallet(), org_root_id: orgRootID(), principal_id: principalID(), device_id: deviceID(), }); logLine("lease heartbeat", out); } async function onOfflineRenew() { const out = await request("POST", "/governance/lease/offline-renew", { wallet: requireWallet(), org_root_id: orgRootID(), principal_id: principalID(), renewal_bundle: { bundle_id: `renew-${Date.now()}`, source: "launcher-shell", }, }); 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) { const el = $(id); if (!el) { return; } el.addEventListener("click", async () => { try { await handler(); } catch (err) { reportActionError(id, err); } }); } bind("btnQuickConnect", onQuickConnect); bind("btnQuickActivate", onQuickActivate); bind("btnQuickRefresh", onQuickRefresh); bind("btnQuickInstallStatus", onQuickInstallStatus); bind("btnRefreshBalances", onRefreshBalances); bind("btnCopyWallet", onCopyWallet); bind("btnModeHuman", onModeHuman); bind("btnModeAuto", onModeAuto); bind("btnConnectWallet", onConnectWallet); bind("btnRunMembershipFlow", onRunMembershipFlow); bind("btnIntent", onIntent); bind("btnSignIntent", onSignIntent); bind("btnVerify", onVerify); bind("btnRefreshSession", onRefreshSession); bind("btnRevokeSession", onRevokeSession); bind("btnStatus", onStatus); bind("btnQuote", onQuote); bind("btnSignPayerProof", onSignPayerProof); bind("btnSendMembershipTx", onSendMembershipTx); bind("btnConfirmMembership", onConfirmMembership); bind("btnRegisterChannel", onRegisterChannel); bind("btnUnregisterChannel", onUnregisterChannel); bind("btnPollEvents", onPollEvents); bind("btnSupportTicket", onSupportTicket); bind("btnInstallToken", onInstallToken); bind("btnInstallConfirm", onInstallConfirm); bind("btnInstallStatus", onInstallStatus); bind("btnLeaseHeartbeat", onLeaseHeartbeat); bind("btnOfflineRenew", onOfflineRenew); bind("btnListOffers", onListOffers); bind("btnCheckoutQuote", onCheckoutQuote); bind("btnSendCheckoutTx", onSendCheckoutTx); bind("btnCheckoutConfirm", onCheckoutConfirm); bind("btnRunCheckoutFlow", onRunCheckoutFlow); bind("btnListEntitlements", onListEntitlements); const operationModeSelect = $("operationMode"); if (operationModeSelect) { operationModeSelect.addEventListener("change", () => { setOperationMode(operationModeSelect.value, "advanced_select"); }); } refreshModeUI(); logLine("launcher shell ready", { api_base: baseURL(), chain_id: chainID(), }); refreshOverview();