From 2e5ba323baf9c110ffb5427aa357f1049f70339a Mon Sep 17 00:00:00 2001 From: Joshua Date: Wed, 18 Feb 2026 14:18:19 -0800 Subject: [PATCH] Wire launcher assurance-aware membership and admin gates --- README.md | 8 ++++ app/app.js | 91 +++++++++++++++++++++++++++++++++--- app/index.html | 27 +++++++++++ docs/conformance-vectors.md | 2 + docs/integration-contract.md | 2 + 5 files changed, 124 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fac0082..60752bb 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Top-level control surface: 4. `Governance status` 5. Wallet/membership/designation/last-sync overview cards 6. Pull-first updates feed + support ticket action +7. Identity assurance visibility (`none` / `crypto_direct_unattested` / `sponsored_unattested` / `onramp_attested`) Advanced integration controls (collapsible): @@ -52,6 +53,13 @@ Wallet automation helpers remain available in advanced controls: 2. `Sign intent (EIP-712)` signs the current intent payload and fills `walletSignature`. 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`. +5. Membership confirm can optionally attach on-ramp attestation fields (`identity_assurance_level`, `identity_attested_by`, `identity_attestation_id`) for provider-integrated flows. + +Policy behavior in launcher shell: + +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. +3. Assurance state is displayed independently from membership state in the top summary cards. Run locally: diff --git a/app/app.js b/app/app.js index a42c1fb..5ccb890 100644 --- a/app/app.js +++ b/app/app.js @@ -7,6 +7,7 @@ const state = { lastIntent: null, lastQuote: null, lastCheckoutQuote: null, + lastStatus: null, channelReady: false, }; @@ -30,6 +31,28 @@ 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 injectedProvider() { return globalThis.ethereum || null; } @@ -66,11 +89,17 @@ function refreshOverview(statusPayload) { 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()); 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"); } function setFlowStatus(message) { @@ -361,6 +390,7 @@ async function onStatus() { "GET", `/secret/membership/status?wallet=${encodeURIComponent(requireWallet())}`, ); + state.lastStatus = out; if (out.designation_code) { $("designationCode").value = out.designation_code; } @@ -397,17 +427,50 @@ async function onQuote() { } async function onConfirmMembership() { - const out = await request("POST", "/secret/membership/confirm", { + 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("membership 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 membership`); + } + 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(); @@ -478,8 +541,12 @@ async function onRunMembershipFlow() { setFlowStatus("confirming membership with API"); await confirmMembershipWithRetry(); setFlowStatus("refreshing status"); - await onStatus(); - setFlowStatus("membership flow complete"); + const refreshed = await onStatus(); + if (isOnrampAttested(refreshed.identity_assurance_level)) { + setFlowStatus("membership flow complete (attested)"); + } else { + setFlowStatus("membership active (unattested)"); + } } async function onRegisterChannel() { @@ -530,6 +597,7 @@ async function onPollEvents() { } async function onSupportTicket() { + await requireMembershipState("owner support", { requireOnramp: true }); const out = await request("POST", "/member/channel/support/ticket", { wallet: requireWallet(), org_root_id: orgRootID(), @@ -545,6 +613,7 @@ async function onSupportTicket() { } async function onInstallToken() { + await requireMembershipState("governance install token", { requireOnramp: true }); const out = await request("POST", "/governance/install/token", { wallet: requireWallet(), org_root_id: orgRootID(), @@ -747,7 +816,11 @@ async function onQuickRefresh() { try { await ensureChannelBinding(); await onPollEvents(); - setFlowStatus("status synced"); + 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; @@ -755,7 +828,13 @@ async function onQuickRefresh() { } async function onQuickInstallStatus() { - await onInstallStatus(); + 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() { diff --git a/app/index.html b/app/index.html index 4fda08e..a6445d9 100644 --- a/app/index.html +++ b/app/index.html @@ -35,6 +35,14 @@

Designation

-

+
+

Identity Assurance

+

unknown

+
+
+

Admin Policy

+

blocked

+

Last Sync

never

@@ -165,6 +173,25 @@ Confirm tx hash +
+ + + +
diff --git a/docs/conformance-vectors.md b/docs/conformance-vectors.md index 02791d8..756004b 100644 --- a/docs/conformance-vectors.md +++ b/docs/conformance-vectors.md @@ -10,3 +10,5 @@ 8. `L-008` Wallet onboarding creates local wallet without forcing seed phrase display. 9. `L-009` Outgoing sends require biometric/PIN confirmation. 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. +12. `L-012` Owner support and governance install actions are blocked when assurance is not `onramp_attested`. diff --git a/docs/integration-contract.md b/docs/integration-contract.md index a6c9bcf..b872558 100644 --- a/docs/integration-contract.md +++ b/docs/integration-contract.md @@ -24,3 +24,5 @@ Launcher integrates with EDUT web/backend contracts as follows: 2. All install packages verified by hash and signature. 3. Membership and entitlement unknown state fails closed. 4. Event inbox polling remains canonical even if push unavailable. +5. Identity assurance is evaluated independently from membership state. +6. Owner/admin launcher actions must require `identity_assurance_level=onramp_attested`.