Wire launcher assurance-aware membership and admin gates
This commit is contained in:
parent
787c056f6b
commit
2e5ba323ba
@ -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:
|
||||
|
||||
|
||||
91
app/app.js
91
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() {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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`.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user