Wire launcher assurance-aware membership and admin gates

This commit is contained in:
Joshua 2026-02-18 14:18:19 -08:00
parent 787c056f6b
commit 2e5ba323ba
5 changed files with 124 additions and 6 deletions

View File

@ -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:

View File

@ -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() {

View File

@ -35,6 +35,14 @@
<h3>Designation</h3>
<p id="summaryDesignation">-</p>
</article>
<article class="stat">
<h3>Identity Assurance</h3>
<p id="summaryAssurance">unknown</p>
</article>
<article class="stat">
<h3>Admin Policy</h3>
<p id="summaryAdminPolicy">blocked</p>
</article>
<article class="stat">
<h3>Last Sync</h3>
<p id="summaryLastSync">never</p>
@ -165,6 +173,25 @@
Confirm tx hash
<input id="confirmTxHash" placeholder="0x..." />
</label>
<div class="grid three">
<label>
Identity assurance (optional)
<select id="membershipIdentityAssurance">
<option value="">(auto)</option>
<option value="onramp_attested">onramp_attested</option>
<option value="crypto_direct_unattested">crypto_direct_unattested</option>
<option value="sponsored_unattested">sponsored_unattested</option>
</select>
</label>
<label>
Attested by (optional)
<input id="membershipIdentityAttestedBy" placeholder="moonpay" />
</label>
<label>
Attestation id (optional)
<input id="membershipIdentityAttestationId" placeholder="provider-session-id" />
</label>
</div>
</section>
<section class="subpanel">

View File

@ -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`.

View File

@ -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`.