Identity Assurance
+unknown
+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 @@
-
+unknown
+blocked
+never
@@ -165,6 +173,25 @@ Confirm tx hash +