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`
|
4. `Governance status`
|
||||||
5. Wallet/membership/designation/last-sync overview cards
|
5. Wallet/membership/designation/last-sync overview cards
|
||||||
6. Pull-first updates feed + support ticket action
|
6. Pull-first updates feed + support ticket action
|
||||||
|
7. Identity assurance visibility (`none` / `crypto_direct_unattested` / `sponsored_unattested` / `onramp_attested`)
|
||||||
|
|
||||||
Advanced integration controls (collapsible):
|
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`.
|
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`.
|
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`.
|
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:
|
Run locally:
|
||||||
|
|
||||||
|
|||||||
91
app/app.js
91
app/app.js
@ -7,6 +7,7 @@ const state = {
|
|||||||
lastIntent: null,
|
lastIntent: null,
|
||||||
lastQuote: null,
|
lastQuote: null,
|
||||||
lastCheckoutQuote: null,
|
lastCheckoutQuote: null,
|
||||||
|
lastStatus: null,
|
||||||
channelReady: false,
|
channelReady: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,6 +31,28 @@ function normalizedAddress(value) {
|
|||||||
return String(value || "").trim().toLowerCase();
|
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() {
|
function injectedProvider() {
|
||||||
return globalThis.ethereum || null;
|
return globalThis.ethereum || null;
|
||||||
}
|
}
|
||||||
@ -66,11 +89,17 @@ function refreshOverview(statusPayload) {
|
|||||||
if (statusPayload && typeof statusPayload === "object") {
|
if (statusPayload && typeof statusPayload === "object") {
|
||||||
setSummary("summaryMembership", statusPayload.status || "unknown");
|
setSummary("summaryMembership", statusPayload.status || "unknown");
|
||||||
setSummary("summaryDesignation", statusPayload.designation_code || "-");
|
setSummary("summaryDesignation", statusPayload.designation_code || "-");
|
||||||
|
const assurance = assuranceDisplay(statusPayload.identity_assurance_level);
|
||||||
|
setSummary("summaryAssurance", assurance);
|
||||||
|
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
||||||
setSummary("summaryLastSync", nowISO());
|
setSummary("summaryLastSync", nowISO());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const designation = $("designationCode")?.value?.trim() || "-";
|
const designation = $("designationCode")?.value?.trim() || "-";
|
||||||
setSummary("summaryDesignation", designation);
|
setSummary("summaryDesignation", designation);
|
||||||
|
const assurance = assuranceDisplay(state.lastStatus?.identity_assurance_level);
|
||||||
|
setSummary("summaryAssurance", assurance);
|
||||||
|
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFlowStatus(message) {
|
function setFlowStatus(message) {
|
||||||
@ -361,6 +390,7 @@ async function onStatus() {
|
|||||||
"GET",
|
"GET",
|
||||||
`/secret/membership/status?wallet=${encodeURIComponent(requireWallet())}`,
|
`/secret/membership/status?wallet=${encodeURIComponent(requireWallet())}`,
|
||||||
);
|
);
|
||||||
|
state.lastStatus = out;
|
||||||
if (out.designation_code) {
|
if (out.designation_code) {
|
||||||
$("designationCode").value = out.designation_code;
|
$("designationCode").value = out.designation_code;
|
||||||
}
|
}
|
||||||
@ -397,17 +427,50 @@ async function onQuote() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onConfirmMembership() {
|
async function onConfirmMembership() {
|
||||||
const out = await request("POST", "/secret/membership/confirm", {
|
const payload = {
|
||||||
designation_code: $("designationCode").value.trim(),
|
designation_code: $("designationCode").value.trim(),
|
||||||
quote_id: $("quoteId").value.trim(),
|
quote_id: $("quoteId").value.trim(),
|
||||||
tx_hash: $("confirmTxHash").value.trim(),
|
tx_hash: $("confirmTxHash").value.trim(),
|
||||||
address: requireWallet(),
|
address: requireWallet(),
|
||||||
chain_id: chainID(),
|
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);
|
logLine("membership confirm", out);
|
||||||
return 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) {
|
async function waitForTxMined(txHash, timeoutMs = 120000, intervalMs = 3000) {
|
||||||
const provider = await requireProvider();
|
const provider = await requireProvider();
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
@ -478,8 +541,12 @@ async function onRunMembershipFlow() {
|
|||||||
setFlowStatus("confirming membership with API");
|
setFlowStatus("confirming membership with API");
|
||||||
await confirmMembershipWithRetry();
|
await confirmMembershipWithRetry();
|
||||||
setFlowStatus("refreshing status");
|
setFlowStatus("refreshing status");
|
||||||
await onStatus();
|
const refreshed = await onStatus();
|
||||||
setFlowStatus("membership flow complete");
|
if (isOnrampAttested(refreshed.identity_assurance_level)) {
|
||||||
|
setFlowStatus("membership flow complete (attested)");
|
||||||
|
} else {
|
||||||
|
setFlowStatus("membership active (unattested)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRegisterChannel() {
|
async function onRegisterChannel() {
|
||||||
@ -530,6 +597,7 @@ async function onPollEvents() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onSupportTicket() {
|
async function onSupportTicket() {
|
||||||
|
await requireMembershipState("owner support", { requireOnramp: true });
|
||||||
const out = await request("POST", "/member/channel/support/ticket", {
|
const out = await request("POST", "/member/channel/support/ticket", {
|
||||||
wallet: requireWallet(),
|
wallet: requireWallet(),
|
||||||
org_root_id: orgRootID(),
|
org_root_id: orgRootID(),
|
||||||
@ -545,6 +613,7 @@ async function onSupportTicket() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onInstallToken() {
|
async function onInstallToken() {
|
||||||
|
await requireMembershipState("governance install token", { requireOnramp: true });
|
||||||
const out = await request("POST", "/governance/install/token", {
|
const out = await request("POST", "/governance/install/token", {
|
||||||
wallet: requireWallet(),
|
wallet: requireWallet(),
|
||||||
org_root_id: orgRootID(),
|
org_root_id: orgRootID(),
|
||||||
@ -747,7 +816,11 @@ async function onQuickRefresh() {
|
|||||||
try {
|
try {
|
||||||
await ensureChannelBinding();
|
await ensureChannelBinding();
|
||||||
await onPollEvents();
|
await onPollEvents();
|
||||||
setFlowStatus("status synced");
|
if (isOnrampAttested(status.identity_assurance_level)) {
|
||||||
|
setFlowStatus("status synced (admin-ready)");
|
||||||
|
} else {
|
||||||
|
setFlowStatus("status synced (member mode)");
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFlowStatus("feed sync warning");
|
setFlowStatus("feed sync warning");
|
||||||
throw err;
|
throw err;
|
||||||
@ -755,7 +828,13 @@ async function onQuickRefresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onQuickInstallStatus() {
|
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() {
|
async function onLeaseHeartbeat() {
|
||||||
|
|||||||
@ -35,6 +35,14 @@
|
|||||||
<h3>Designation</h3>
|
<h3>Designation</h3>
|
||||||
<p id="summaryDesignation">-</p>
|
<p id="summaryDesignation">-</p>
|
||||||
</article>
|
</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">
|
<article class="stat">
|
||||||
<h3>Last Sync</h3>
|
<h3>Last Sync</h3>
|
||||||
<p id="summaryLastSync">never</p>
|
<p id="summaryLastSync">never</p>
|
||||||
@ -165,6 +173,25 @@
|
|||||||
Confirm tx hash
|
Confirm tx hash
|
||||||
<input id="confirmTxHash" placeholder="0x..." />
|
<input id="confirmTxHash" placeholder="0x..." />
|
||||||
</label>
|
</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>
|
||||||
|
|
||||||
<section class="subpanel">
|
<section class="subpanel">
|
||||||
|
|||||||
@ -10,3 +10,5 @@
|
|||||||
8. `L-008` Wallet onboarding creates local wallet without forcing seed phrase display.
|
8. `L-008` Wallet onboarding creates local wallet without forcing seed phrase display.
|
||||||
9. `L-009` Outgoing sends require biometric/PIN confirmation.
|
9. `L-009` Outgoing sends require biometric/PIN confirmation.
|
||||||
10. `L-010` Primary wallet screens render USD-first balances and plain-language history.
|
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.
|
2. All install packages verified by hash and signature.
|
||||||
3. Membership and entitlement unknown state fails closed.
|
3. Membership and entitlement unknown state fails closed.
|
||||||
4. Event inbox polling remains canonical even if push unavailable.
|
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