Compare commits
No commits in common. "d9f963f1b0d10abd11357d12cfa238237832bdcd" and "2e5ba323baf9c110ffb5427aa357f1049f70339a" have entirely different histories.
d9f963f1b0
...
2e5ba323ba
11
README.md
11
README.md
@ -33,11 +33,9 @@ Top-level control surface:
|
||||
2. `Activate membership`
|
||||
3. `Refresh status + feed`
|
||||
4. `Governance status`
|
||||
5. Wallet/session/membership/designation/last-sync overview cards
|
||||
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`)
|
||||
8. Explicit operator-visible mode toggles (`Human mode` / `Auto mode`) synced to governance `operation_mode`
|
||||
9. Wallet utility actions (`Refresh balances`, `Copy address`) with native + USDC balance visibility
|
||||
|
||||
Advanced integration controls (collapsible):
|
||||
|
||||
@ -46,9 +44,8 @@ Advanced integration controls (collapsible):
|
||||
3. Membership quote + confirm primitives
|
||||
4. Marketplace offer list + checkout quote/send/confirm primitives
|
||||
5. Member channel register/poll primitives
|
||||
6. Governance install + lease primitives (with explicit `operation_mode`)
|
||||
6. Governance install + lease primitives
|
||||
7. Raw response log for deterministic troubleshooting
|
||||
8. Wallet session lifecycle controls (manual refresh/revoke + automatic pre-expiry refresh)
|
||||
|
||||
Wallet automation helpers remain available in advanced controls:
|
||||
|
||||
@ -57,16 +54,12 @@ Wallet automation helpers remain available in advanced controls:
|
||||
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.
|
||||
6. Wallet verify returns a session token; launcher forwards it on marketplace/member/governance API calls via bearer + `X-Edut-Session`.
|
||||
7. Launcher proactively refreshes wallet sessions before expiry and clears local session state on terminal session errors (`invalid`, `expired`, `revoked`, `mismatch`).
|
||||
|
||||
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.
|
||||
4. Owner-only buttons are UI-disabled until both membership is active and assurance is `onramp_attested`.
|
||||
5. Governance activation evidence must carry explicit signing authority class (`identity_human` or delegated).
|
||||
|
||||
Run locally:
|
||||
|
||||
|
||||
400
app/app.js
400
app/app.js
@ -9,11 +9,6 @@ const state = {
|
||||
lastCheckoutQuote: null,
|
||||
lastStatus: null,
|
||||
channelReady: false,
|
||||
walletSessionToken: "",
|
||||
walletSessionExpiresAt: "",
|
||||
walletSessionRefreshInFlight: null,
|
||||
walletBalanceNative: "",
|
||||
walletBalanceUSDC: "",
|
||||
};
|
||||
|
||||
function nowISO() {
|
||||
@ -58,122 +53,6 @@ function isOnrampAttested(value) {
|
||||
return normalizeAssurance(value) === "onramp_attested";
|
||||
}
|
||||
|
||||
function sessionSummary() {
|
||||
if (!state.walletSessionToken) {
|
||||
return "none";
|
||||
}
|
||||
if (!state.walletSessionExpiresAt) {
|
||||
return "active";
|
||||
}
|
||||
const expires = Date.parse(state.walletSessionExpiresAt);
|
||||
if (!Number.isFinite(expires)) {
|
||||
return `active (exp ${state.walletSessionExpiresAt})`;
|
||||
}
|
||||
const remainingMs = expires - Date.now();
|
||||
if (remainingMs <= 0) {
|
||||
return `expired (${state.walletSessionExpiresAt})`;
|
||||
}
|
||||
if (remainingMs <= 5 * 60 * 1000) {
|
||||
return `expiring soon (${state.walletSessionExpiresAt})`;
|
||||
}
|
||||
return `active (exp ${state.walletSessionExpiresAt})`;
|
||||
}
|
||||
|
||||
function walletBalanceSummary() {
|
||||
const native = state.walletBalanceNative || "-- ETH";
|
||||
const usdc = state.walletBalanceUSDC || "-- USDC";
|
||||
return `${native} | ${usdc}`;
|
||||
}
|
||||
|
||||
function clearWalletSession(reason, payload = {}) {
|
||||
if (!state.walletSessionToken && !state.walletSessionExpiresAt) {
|
||||
return;
|
||||
}
|
||||
const previousToken = state.walletSessionToken;
|
||||
const previousExpiry = state.walletSessionExpiresAt;
|
||||
state.walletSessionToken = "";
|
||||
state.walletSessionExpiresAt = "";
|
||||
if (reason) {
|
||||
logLine("wallet session cleared", {
|
||||
reason,
|
||||
previous_token_preview: previousToken ? `${previousToken.slice(0, 8)}...` : "",
|
||||
previous_expiry: previousExpiry || "",
|
||||
...payload,
|
||||
});
|
||||
}
|
||||
refreshOverview();
|
||||
}
|
||||
|
||||
function captureSessionHeaders(res) {
|
||||
const token = String(res.headers.get("x-edut-session") || "").trim();
|
||||
const expiresAt = String(res.headers.get("x-edut-session-expires-at") || "").trim();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
state.walletSessionToken = token;
|
||||
if (expiresAt) {
|
||||
state.walletSessionExpiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
||||
function isTerminalSessionErrorCode(code) {
|
||||
const normalized = String(code || "").trim().toLowerCase();
|
||||
return normalized === "wallet_session_invalid"
|
||||
|| normalized === "wallet_session_expired"
|
||||
|| normalized === "wallet_session_revoked"
|
||||
|| normalized === "wallet_session_mismatch";
|
||||
}
|
||||
|
||||
async function maybeRefreshSession(path) {
|
||||
if (!state.walletSessionToken) {
|
||||
return;
|
||||
}
|
||||
if (String(path || "").startsWith("/secret/wallet/session/")) {
|
||||
return;
|
||||
}
|
||||
if (!state.walletSessionExpiresAt) {
|
||||
return;
|
||||
}
|
||||
const expiresAt = Date.parse(state.walletSessionExpiresAt);
|
||||
if (!Number.isFinite(expiresAt)) {
|
||||
return;
|
||||
}
|
||||
const refreshWindowMs = 5 * 60 * 1000;
|
||||
if ((expiresAt - Date.now()) > refreshWindowMs) {
|
||||
return;
|
||||
}
|
||||
if (state.walletSessionRefreshInFlight) {
|
||||
await state.walletSessionRefreshInFlight;
|
||||
return;
|
||||
}
|
||||
const currentWallet = normalizedAddress(wallet());
|
||||
if (!currentWallet) {
|
||||
return;
|
||||
}
|
||||
state.walletSessionRefreshInFlight = (async () => {
|
||||
const out = await request(
|
||||
"POST",
|
||||
"/secret/wallet/session/refresh",
|
||||
{ wallet: currentWallet },
|
||||
{ skipSessionPreflight: true },
|
||||
);
|
||||
if (out.session_token) {
|
||||
state.walletSessionToken = String(out.session_token).trim();
|
||||
}
|
||||
if (out.session_expires_at) {
|
||||
state.walletSessionExpiresAt = String(out.session_expires_at).trim();
|
||||
}
|
||||
logLine("wallet session auto-refresh", {
|
||||
wallet: currentWallet,
|
||||
session_expires_at: state.walletSessionExpiresAt || "unknown",
|
||||
});
|
||||
refreshOverview();
|
||||
})().finally(() => {
|
||||
state.walletSessionRefreshInFlight = null;
|
||||
});
|
||||
await state.walletSessionRefreshInFlight;
|
||||
}
|
||||
|
||||
function injectedProvider() {
|
||||
return globalThis.ethereum || null;
|
||||
}
|
||||
@ -192,41 +71,6 @@ function utf8ToHex(value) {
|
||||
.join("")}`;
|
||||
}
|
||||
|
||||
function normalizeHexQuantity(value) {
|
||||
const raw = String(value || "").trim().toLowerCase();
|
||||
if (!raw.startsWith("0x")) {
|
||||
return null;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function formatUnitsHex(hexValue, decimals, precision = 6) {
|
||||
const hex = normalizeHexQuantity(hexValue);
|
||||
if (!hex) {
|
||||
return null;
|
||||
}
|
||||
let bigint;
|
||||
try {
|
||||
bigint = BigInt(hex);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const unit = 10n ** BigInt(Math.max(0, Number(decimals) || 0));
|
||||
const whole = bigint / unit;
|
||||
const fraction = bigint % unit;
|
||||
if (fraction === 0n) {
|
||||
return whole.toString();
|
||||
}
|
||||
const padded = fraction.toString().padStart(Number(decimals), "0");
|
||||
const trimmed = padded.slice(0, Math.max(1, precision)).replace(/0+$/, "");
|
||||
return trimmed ? `${whole.toString()}.${trimmed}` : whole.toString();
|
||||
}
|
||||
|
||||
function encodeAddressWord(address) {
|
||||
const normalized = normalizedAddress(address).replace(/^0x/, "");
|
||||
return normalized.padStart(64, "0");
|
||||
}
|
||||
|
||||
function logLine(label, payload) {
|
||||
const log = $("log");
|
||||
const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`;
|
||||
@ -239,29 +83,9 @@ function setSummary(id, value) {
|
||||
el.textContent = value;
|
||||
}
|
||||
|
||||
function setButtonDisabled(id, disabled) {
|
||||
const el = $(id);
|
||||
if (!el) return;
|
||||
el.disabled = Boolean(disabled);
|
||||
}
|
||||
|
||||
function refreshActionLocks(statusPayload) {
|
||||
const effective = statusPayload && typeof statusPayload === "object" ? statusPayload : state.lastStatus;
|
||||
const membershipActive = String(effective?.status || "").toLowerCase() === "active";
|
||||
const attested = isOnrampAttested(effective?.identity_assurance_level);
|
||||
const ownerActionReady = membershipActive && attested;
|
||||
|
||||
setButtonDisabled("btnSupportTicket", !ownerActionReady);
|
||||
setButtonDisabled("btnInstallToken", !ownerActionReady);
|
||||
setButtonDisabled("btnQuickInstallStatus", !membershipActive);
|
||||
}
|
||||
|
||||
function refreshOverview(statusPayload) {
|
||||
const currentWallet = wallet();
|
||||
setSummary("summaryWallet", currentWallet || "not connected");
|
||||
setSummary("summaryFunds", walletBalanceSummary());
|
||||
setSummary("summarySession", sessionSummary());
|
||||
refreshModeUI();
|
||||
if (statusPayload && typeof statusPayload === "object") {
|
||||
setSummary("summaryMembership", statusPayload.status || "unknown");
|
||||
setSummary("summaryDesignation", statusPayload.designation_code || "-");
|
||||
@ -269,7 +93,6 @@ function refreshOverview(statusPayload) {
|
||||
setSummary("summaryAssurance", assurance);
|
||||
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
||||
setSummary("summaryLastSync", nowISO());
|
||||
refreshActionLocks(statusPayload);
|
||||
return;
|
||||
}
|
||||
const designation = $("designationCode")?.value?.trim() || "-";
|
||||
@ -277,7 +100,6 @@ function refreshOverview(statusPayload) {
|
||||
const assurance = assuranceDisplay(state.lastStatus?.identity_assurance_level);
|
||||
setSummary("summaryAssurance", assurance);
|
||||
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
||||
refreshActionLocks();
|
||||
}
|
||||
|
||||
function setFlowStatus(message) {
|
||||
@ -291,24 +113,15 @@ function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function request(method, path, body, options = {}) {
|
||||
if (!options.skipSessionPreflight) {
|
||||
await maybeRefreshSession(path);
|
||||
}
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (state.walletSessionToken) {
|
||||
headers["X-Edut-Session"] = state.walletSessionToken;
|
||||
headers.Authorization = `Bearer ${state.walletSessionToken}`;
|
||||
}
|
||||
async function request(method, path, body) {
|
||||
const opts = {
|
||||
method,
|
||||
headers,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
};
|
||||
if (body !== undefined) {
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
const res = await fetch(`${baseURL()}${path}`, opts);
|
||||
captureSessionHeaders(res);
|
||||
const text = await res.text();
|
||||
let json = {};
|
||||
if (text.trim() !== "") {
|
||||
@ -319,16 +132,7 @@ async function request(method, path, body, options = {}) {
|
||||
}
|
||||
}
|
||||
if (!res.ok) {
|
||||
const errorMessage = String(json?.error || res.statusText || "request failed");
|
||||
const errorCode = String(json?.code || "");
|
||||
if (isTerminalSessionErrorCode(errorCode)) {
|
||||
clearWalletSession(errorCode, { path, message: errorMessage });
|
||||
}
|
||||
const err = new Error(`${res.status} ${res.statusText}: ${errorMessage}`);
|
||||
err.status = res.status;
|
||||
err.code = errorCode;
|
||||
err.payload = json;
|
||||
throw err;
|
||||
throw new Error(`${res.status} ${res.statusText}: ${JSON.stringify(json)}`);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
@ -357,38 +161,6 @@ function principalRole() {
|
||||
return $("principalRole").value.trim();
|
||||
}
|
||||
|
||||
function operationMode() {
|
||||
return $("operationMode").value.trim() || "human_manual";
|
||||
}
|
||||
|
||||
function normalizeOperationMode(value) {
|
||||
return String(value || "").trim().toLowerCase() === "worker_auto" ? "worker_auto" : "human_manual";
|
||||
}
|
||||
|
||||
function refreshModeUI() {
|
||||
const mode = normalizeOperationMode(operationMode());
|
||||
setSummary("summaryMode", mode);
|
||||
const humanBtn = $("btnModeHuman");
|
||||
const autoBtn = $("btnModeAuto");
|
||||
if (humanBtn) {
|
||||
humanBtn.classList.toggle("mode-active", mode === "human_manual");
|
||||
}
|
||||
if (autoBtn) {
|
||||
autoBtn.classList.toggle("mode-active", mode === "worker_auto");
|
||||
}
|
||||
}
|
||||
|
||||
function setOperationMode(mode, source = "ui") {
|
||||
const normalized = normalizeOperationMode(mode);
|
||||
const select = $("operationMode");
|
||||
if (select) {
|
||||
select.value = normalized;
|
||||
}
|
||||
refreshModeUI();
|
||||
logLine("operation mode set", { mode: normalized, source });
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function renderEvents(events) {
|
||||
const list = $("eventList");
|
||||
list.innerHTML = "";
|
||||
@ -469,20 +241,12 @@ function buildIntentTypedData(intent, origin) {
|
||||
}
|
||||
|
||||
async function onConnectWallet() {
|
||||
const previousWallet = normalizedAddress($("walletAddress").value);
|
||||
const provider = await requireProvider();
|
||||
const accounts = await provider.request({ method: "eth_requestAccounts" });
|
||||
if (!accounts || accounts.length === 0) {
|
||||
throw new Error("wallet provider returned no accounts");
|
||||
}
|
||||
const nextWallet = normalizedAddress(accounts[0]);
|
||||
$("walletAddress").value = nextWallet;
|
||||
if (previousWallet && previousWallet !== nextWallet) {
|
||||
clearWalletSession("wallet_changed", {
|
||||
previous_wallet: previousWallet,
|
||||
next_wallet: nextWallet,
|
||||
});
|
||||
}
|
||||
$("walletAddress").value = normalizedAddress(accounts[0]);
|
||||
const chainHex = await provider.request({ method: "eth_chainId" });
|
||||
const providerChainID = Number.parseInt(chainHex, 16);
|
||||
if (Number.isFinite(providerChainID) && providerChainID !== chainID()) {
|
||||
@ -497,74 +261,9 @@ async function onConnectWallet() {
|
||||
provider_chain_id: providerChainID,
|
||||
});
|
||||
}
|
||||
try {
|
||||
await onRefreshBalances();
|
||||
} catch (err) {
|
||||
logLine("wallet balance refresh warning", { error: String(err) });
|
||||
}
|
||||
refreshOverview();
|
||||
}
|
||||
|
||||
async function onRefreshBalances() {
|
||||
const address = normalizedAddress(requireWallet());
|
||||
const provider = await requireProvider();
|
||||
const nativeHex = await provider.request({
|
||||
method: "eth_getBalance",
|
||||
params: [address, "latest"],
|
||||
});
|
||||
const nativeDisplay = formatUnitsHex(nativeHex, 18, 6);
|
||||
if (nativeDisplay) {
|
||||
state.walletBalanceNative = `${nativeDisplay} ETH`;
|
||||
}
|
||||
|
||||
const usdcToken = normalizedAddress($("usdcTokenAddress")?.value || "");
|
||||
if (usdcToken && /^0x[a-f0-9]{40}$/.test(usdcToken)) {
|
||||
const balanceOfSelector = "0x70a08231";
|
||||
const decimalsSelector = "0x313ce567";
|
||||
const balanceCallData = `${balanceOfSelector}${encodeAddressWord(address)}`;
|
||||
const usdcHex = await provider.request({
|
||||
method: "eth_call",
|
||||
params: [{ to: usdcToken, data: balanceCallData }, "latest"],
|
||||
});
|
||||
let decimals = 6;
|
||||
try {
|
||||
const decimalsHex = await provider.request({
|
||||
method: "eth_call",
|
||||
params: [{ to: usdcToken, data: decimalsSelector }, "latest"],
|
||||
});
|
||||
const parsed = Number(BigInt(String(decimalsHex || "0x6")));
|
||||
if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 36) {
|
||||
decimals = parsed;
|
||||
}
|
||||
} catch {
|
||||
// keep default 6 when token decimals call is unavailable
|
||||
}
|
||||
const usdcDisplay = formatUnitsHex(usdcHex, decimals, 2);
|
||||
if (usdcDisplay) {
|
||||
state.walletBalanceUSDC = `${usdcDisplay} USDC`;
|
||||
}
|
||||
} else if (usdcToken) {
|
||||
state.walletBalanceUSDC = "invalid token";
|
||||
}
|
||||
|
||||
refreshOverview();
|
||||
logLine("wallet balances refreshed", {
|
||||
wallet: address,
|
||||
native: state.walletBalanceNative || "",
|
||||
usdc: state.walletBalanceUSDC || "",
|
||||
});
|
||||
}
|
||||
|
||||
async function onCopyWallet() {
|
||||
const address = requireWallet();
|
||||
if (!navigator?.clipboard?.writeText) {
|
||||
throw new Error("clipboard API unavailable");
|
||||
}
|
||||
await navigator.clipboard.writeText(address);
|
||||
setFlowStatus("wallet address copied");
|
||||
logLine("wallet copied", { wallet: address });
|
||||
}
|
||||
|
||||
async function onSignIntent() {
|
||||
if (!state.lastIntent) {
|
||||
throw new Error("create intent before signing");
|
||||
@ -649,7 +348,7 @@ async function onSendMembershipTx() {
|
||||
params: [txRequest],
|
||||
});
|
||||
$("confirmTxHash").value = txHash;
|
||||
logLine("EDUT ID tx sent", {
|
||||
logLine("membership tx sent", {
|
||||
quote_id: state.lastQuote.quote_id,
|
||||
tx_hash: txHash,
|
||||
payer_wallet: from,
|
||||
@ -682,48 +381,10 @@ async function onVerify() {
|
||||
if (out.designation_code) {
|
||||
$("designationCode").value = out.designation_code;
|
||||
}
|
||||
state.walletSessionToken = String(out.session_token || "").trim();
|
||||
state.walletSessionExpiresAt = String(out.session_expires_at || "").trim();
|
||||
logLine("wallet verify", out);
|
||||
if (state.walletSessionToken) {
|
||||
logLine("wallet session active", {
|
||||
wallet: requireWallet(),
|
||||
session_expires_at: state.walletSessionExpiresAt || "unknown",
|
||||
});
|
||||
}
|
||||
refreshOverview();
|
||||
}
|
||||
|
||||
async function onRefreshSession() {
|
||||
const out = await request(
|
||||
"POST",
|
||||
"/secret/wallet/session/refresh",
|
||||
{ wallet: requireWallet() },
|
||||
{ skipSessionPreflight: true },
|
||||
);
|
||||
if (out.session_token) {
|
||||
state.walletSessionToken = String(out.session_token).trim();
|
||||
}
|
||||
if (out.session_expires_at) {
|
||||
state.walletSessionExpiresAt = String(out.session_expires_at).trim();
|
||||
}
|
||||
logLine("wallet session refresh", out);
|
||||
refreshOverview();
|
||||
return out;
|
||||
}
|
||||
|
||||
async function onRevokeSession() {
|
||||
const out = await request(
|
||||
"POST",
|
||||
"/secret/wallet/session/revoke",
|
||||
{ wallet: requireWallet() },
|
||||
{ skipSessionPreflight: true },
|
||||
);
|
||||
logLine("wallet session revoke", out);
|
||||
clearWalletSession("manual_revoke", { wallet: requireWallet() });
|
||||
return out;
|
||||
}
|
||||
|
||||
async function onStatus() {
|
||||
const out = await request(
|
||||
"GET",
|
||||
@ -733,7 +394,7 @@ async function onStatus() {
|
||||
if (out.designation_code) {
|
||||
$("designationCode").value = out.designation_code;
|
||||
}
|
||||
logLine("EDUT ID status", out);
|
||||
logLine("membership status", out);
|
||||
refreshOverview(out);
|
||||
return out;
|
||||
}
|
||||
@ -761,7 +422,7 @@ async function onQuote() {
|
||||
$("quoteId").value = out.quote_id || "";
|
||||
$("quoteValue").value = out.value || "";
|
||||
$("quotePayer").value = out.payer_wallet || "";
|
||||
logLine("EDUT ID quote", out);
|
||||
logLine("membership quote", out);
|
||||
return out;
|
||||
}
|
||||
|
||||
@ -795,14 +456,14 @@ async function onConfirmMembership() {
|
||||
identity_attested_by: out.identity_attested_by || "",
|
||||
identity_attestation_id: out.identity_attestation_id || "",
|
||||
};
|
||||
logLine("EDUT ID confirm", out);
|
||||
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 EDUT ID`);
|
||||
throw new Error(`${actionLabel} requires active membership`);
|
||||
}
|
||||
if (opts.requireOnramp && !isOnrampAttested(status.identity_assurance_level)) {
|
||||
throw new Error(`${actionLabel} requires onramp_attested identity assurance`);
|
||||
@ -846,17 +507,17 @@ async function confirmMembershipWithRetry(maxAttempts = 8, intervalMs = 2500) {
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
throw lastErr || new Error("EDUT ID confirmation failed");
|
||||
throw lastErr || new Error("membership confirm failed");
|
||||
}
|
||||
|
||||
async function onRunMembershipFlow() {
|
||||
setFlowStatus("connecting wallet");
|
||||
await onConnectWallet();
|
||||
|
||||
setFlowStatus("checking EDUT ID");
|
||||
setFlowStatus("checking membership");
|
||||
const status = await onStatus();
|
||||
if (String(status.status || "").toLowerCase() === "active") {
|
||||
setFlowStatus("EDUT ID already active");
|
||||
setFlowStatus("membership already active");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -866,9 +527,9 @@ async function onRunMembershipFlow() {
|
||||
await onSignIntent();
|
||||
setFlowStatus("verifying intent");
|
||||
await onVerify();
|
||||
setFlowStatus("quoting EDUT ID");
|
||||
setFlowStatus("quoting membership");
|
||||
await onQuote();
|
||||
setFlowStatus("sending EDUT ID transaction");
|
||||
setFlowStatus("sending membership transaction");
|
||||
await onSendMembershipTx();
|
||||
|
||||
const txHash = $("confirmTxHash").value.trim();
|
||||
@ -877,14 +538,14 @@ async function onRunMembershipFlow() {
|
||||
}
|
||||
setFlowStatus("waiting for chain confirmation");
|
||||
await waitForTxMined(txHash);
|
||||
setFlowStatus("confirming EDUT ID with API");
|
||||
setFlowStatus("confirming membership with API");
|
||||
await confirmMembershipWithRetry();
|
||||
setFlowStatus("refreshing status");
|
||||
const refreshed = await onStatus();
|
||||
if (isOnrampAttested(refreshed.identity_assurance_level)) {
|
||||
setFlowStatus("EDUT ID flow complete (attested)");
|
||||
setFlowStatus("membership flow complete (attested)");
|
||||
} else {
|
||||
setFlowStatus("EDUT ID active (unattested)");
|
||||
setFlowStatus("membership active (unattested)");
|
||||
}
|
||||
}
|
||||
|
||||
@ -977,7 +638,6 @@ async function onInstallConfirm() {
|
||||
entitlement_id: $("entitlementId").value.trim(),
|
||||
package_hash: $("packageHash").value.trim(),
|
||||
runtime_version: $("runtimeVersion").value.trim(),
|
||||
operation_mode: operationMode(),
|
||||
installed_at: nowISO(),
|
||||
launcher_receipt_hash: `receipt-${Date.now()}`,
|
||||
});
|
||||
@ -1150,7 +810,7 @@ async function onQuickActivate() {
|
||||
async function onQuickRefresh() {
|
||||
const status = await onStatus();
|
||||
if (String(status.status || "").toLowerCase() !== "active") {
|
||||
setFlowStatus("EDUT ID inactive");
|
||||
setFlowStatus("membership inactive");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@ -1200,16 +860,6 @@ async function onOfflineRenew() {
|
||||
logLine("offline renew", out);
|
||||
}
|
||||
|
||||
async function onModeHuman() {
|
||||
setOperationMode("human_manual", "quick_toggle");
|
||||
setFlowStatus("mode set: human_manual");
|
||||
}
|
||||
|
||||
async function onModeAuto() {
|
||||
setOperationMode("worker_auto", "quick_toggle");
|
||||
setFlowStatus("mode set: worker_auto");
|
||||
}
|
||||
|
||||
function bind(id, handler) {
|
||||
const el = $(id);
|
||||
if (!el) {
|
||||
@ -1228,17 +878,11 @@ bind("btnQuickConnect", onQuickConnect);
|
||||
bind("btnQuickActivate", onQuickActivate);
|
||||
bind("btnQuickRefresh", onQuickRefresh);
|
||||
bind("btnQuickInstallStatus", onQuickInstallStatus);
|
||||
bind("btnRefreshBalances", onRefreshBalances);
|
||||
bind("btnCopyWallet", onCopyWallet);
|
||||
bind("btnModeHuman", onModeHuman);
|
||||
bind("btnModeAuto", onModeAuto);
|
||||
bind("btnConnectWallet", onConnectWallet);
|
||||
bind("btnRunMembershipFlow", onRunMembershipFlow);
|
||||
bind("btnIntent", onIntent);
|
||||
bind("btnSignIntent", onSignIntent);
|
||||
bind("btnVerify", onVerify);
|
||||
bind("btnRefreshSession", onRefreshSession);
|
||||
bind("btnRevokeSession", onRevokeSession);
|
||||
bind("btnStatus", onStatus);
|
||||
bind("btnQuote", onQuote);
|
||||
bind("btnSignPayerProof", onSignPayerProof);
|
||||
@ -1260,14 +904,6 @@ bind("btnCheckoutConfirm", onCheckoutConfirm);
|
||||
bind("btnRunCheckoutFlow", onRunCheckoutFlow);
|
||||
bind("btnListEntitlements", onListEntitlements);
|
||||
|
||||
const operationModeSelect = $("operationMode");
|
||||
if (operationModeSelect) {
|
||||
operationModeSelect.addEventListener("change", () => {
|
||||
setOperationMode(operationModeSelect.value, "advanced_select");
|
||||
});
|
||||
}
|
||||
|
||||
refreshModeUI();
|
||||
logLine("launcher shell ready", {
|
||||
api_base: baseURL(),
|
||||
chain_id: chainID(),
|
||||
|
||||
@ -3,27 +3,23 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>EDUT ID Manager</title>
|
||||
<title>EDUT Launcher</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<header class="hero">
|
||||
<h1>EDUT ID Manager</h1>
|
||||
<p>Deterministic identity, access, and control surface.</p>
|
||||
<h1>EDUT Launcher</h1>
|
||||
<p>Deterministic infrastructure control surface.</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Control Surface</h2>
|
||||
<div class="actions">
|
||||
<button id="btnQuickConnect">Connect wallet</button>
|
||||
<button id="btnQuickActivate">Activate EDUT ID</button>
|
||||
<button id="btnQuickActivate">Activate membership</button>
|
||||
<button id="btnQuickRefresh">Refresh status + feed</button>
|
||||
<button id="btnQuickInstallStatus">Governance status</button>
|
||||
<button id="btnRefreshBalances">Refresh balances</button>
|
||||
<button id="btnCopyWallet">Copy address</button>
|
||||
<button id="btnModeHuman" class="mode-active">Human mode</button>
|
||||
<button id="btnModeAuto">Auto mode</button>
|
||||
</div>
|
||||
<p id="flowStatus" class="flow-status">ready</p>
|
||||
<div class="grid two">
|
||||
@ -32,27 +28,15 @@
|
||||
<p id="summaryWallet">not connected</p>
|
||||
</article>
|
||||
<article class="stat">
|
||||
<h3>Session</h3>
|
||||
<p id="summarySession">none</p>
|
||||
</article>
|
||||
<article class="stat">
|
||||
<h3>Wallet Funds</h3>
|
||||
<p id="summaryFunds">-- ETH | -- USDC</p>
|
||||
</article>
|
||||
<article class="stat">
|
||||
<h3><span class="term-inline">EDUT ID<details class="inline-help"><summary aria-label="What is EDUT ID?">?</summary><p>EDUT ID is your one-time identity credential used for checkout, activation, and ownership proof.</p></details></span></h3>
|
||||
<h3>Membership</h3>
|
||||
<p id="summaryMembership">unknown</p>
|
||||
</article>
|
||||
<article class="stat">
|
||||
<h3>Mode</h3>
|
||||
<p id="summaryMode">human_manual</p>
|
||||
</article>
|
||||
<article class="stat">
|
||||
<h3><span class="term-inline">Designation<details class="inline-help"><summary aria-label="What is designation?">?</summary><p>Designation is the protocol reference created during wallet intent and carried through activation evidence.</p></details></span></h3>
|
||||
<h3>Designation</h3>
|
||||
<p id="summaryDesignation">-</p>
|
||||
</article>
|
||||
<article class="stat">
|
||||
<h3><span class="term-inline">Identity Assurance<details class="inline-help"><summary aria-label="What is identity assurance?">?</summary><p>Assurance level records how identity was attested, such as direct crypto flow or on-ramp attestation.</p></details></span></h3>
|
||||
<h3>Identity Assurance</h3>
|
||||
<p id="summaryAssurance">unknown</p>
|
||||
</article>
|
||||
<article class="stat">
|
||||
@ -102,24 +86,18 @@
|
||||
Chain ID
|
||||
<input id="chainId" type="number" value="84532" />
|
||||
</label>
|
||||
<label>
|
||||
USDC token address
|
||||
<input id="usdcTokenAddress" value="0x036cbd53842c5426634e7929541ec2318f3dcf7e" />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="subpanel">
|
||||
<h2>EDUT ID Flow Controls</h2>
|
||||
<h2>Membership Flow Controls</h2>
|
||||
<div class="actions">
|
||||
<button id="btnRunMembershipFlow">Run EDUT ID flow</button>
|
||||
<button id="btnRunMembershipFlow">Run membership flow</button>
|
||||
<button id="btnConnectWallet">Connect wallet</button>
|
||||
<button id="btnIntent">Create intent</button>
|
||||
<button id="btnSignIntent">Sign intent (EIP-712)</button>
|
||||
<button id="btnVerify">Verify signature</button>
|
||||
<button id="btnRefreshSession">Refresh session</button>
|
||||
<button id="btnRevokeSession">Revoke session</button>
|
||||
<button id="btnStatus">EDUT ID status</button>
|
||||
<button id="btnStatus">Membership status</button>
|
||||
</div>
|
||||
<div class="grid three">
|
||||
<label>
|
||||
@ -160,8 +138,8 @@
|
||||
<div class="actions">
|
||||
<button id="btnQuote">Get quote</button>
|
||||
<button id="btnSignPayerProof">Sign payer proof</button>
|
||||
<button id="btnSendMembershipTx">Send EDUT ID tx</button>
|
||||
<button id="btnConfirmMembership">Confirm EDUT ID tx</button>
|
||||
<button id="btnSendMembershipTx">Send membership tx</button>
|
||||
<button id="btnConfirmMembership">Confirm membership tx</button>
|
||||
</div>
|
||||
<div class="grid three">
|
||||
<label>
|
||||
@ -332,13 +310,6 @@
|
||||
<input id="runtimeVersion" />
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
Operation mode
|
||||
<select id="operationMode">
|
||||
<option value="human_manual">human_manual</option>
|
||||
<option value="worker_auto">worker_auto</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Package hash
|
||||
<input id="packageHash" />
|
||||
|
||||
@ -53,51 +53,6 @@ body {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.term-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.inline-help {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.inline-help > summary {
|
||||
list-style: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 14px;
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: #0f1828;
|
||||
}
|
||||
|
||||
.inline-help > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.inline-help > p {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 0;
|
||||
width: min(300px, 80vw);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #0f1828;
|
||||
color: var(--text);
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0 0 10px;
|
||||
color: var(--muted);
|
||||
@ -194,22 +149,10 @@ button:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
button.mode-active {
|
||||
border-color: #4ed380;
|
||||
background: #1f4a33;
|
||||
color: #e0ffe9;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(0.5);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.event-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
|
||||
@ -12,4 +12,3 @@
|
||||
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`.
|
||||
13. `L-013` Launcher emits signing authority class in governance activation evidence and defaults owner-driven activation to `identity_human`.
|
||||
|
||||
@ -18,24 +18,6 @@ Launcher integrates with EDUT web/backend contracts as follows:
|
||||
12. `GET /governance/install/status`
|
||||
13. `GET /member/channel/events`
|
||||
|
||||
## Wallet Session Contract
|
||||
|
||||
1. `POST /secret/wallet/verify` returns `session_token` and `session_expires_at`.
|
||||
2. Launcher must attach session token on wallet-scoped calls using:
|
||||
- `Authorization: Bearer <session_token>` (preferred)
|
||||
- `X-Edut-Session: <session_token>` (compatibility)
|
||||
3. Wallet change must clear cached session token before further calls.
|
||||
4. Endpoints that require membership/admin authority can fail with:
|
||||
- `wallet_session_required`
|
||||
- `wallet_session_invalid`
|
||||
- `wallet_session_expired`
|
||||
- `wallet_session_mismatch`
|
||||
|
||||
## Runtime Mode Signal
|
||||
|
||||
1. Launcher install-confirm payload carries `operation_mode` (`human_manual` or `worker_auto`).
|
||||
2. Mode signal is deterministic evidence input for governance activation policy and receipt hashing.
|
||||
|
||||
## Deterministic Requirements
|
||||
|
||||
1. No runtime activation without entitlement proof.
|
||||
@ -44,4 +26,3 @@ Launcher integrates with EDUT web/backend contracts as follows:
|
||||
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`.
|
||||
7. Governance activation evidence must include signing authority class (`identity_human` vs delegated).
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
3. Governance install path fails closed on invalid evidence.
|
||||
4. Marketplace and status APIs are called with app-session auth.
|
||||
5. Wallet v1 acceptance criteria pass (`docs/wallet-v1-product-spec.md`).
|
||||
6. Owner/admin actions block unless `identity_assurance_level=onramp_attested`.
|
||||
|
||||
## Blockers
|
||||
|
||||
@ -15,4 +14,3 @@
|
||||
2. Any path that leaks private key material.
|
||||
3. Any path that bypasses entitlement checks for governance activation.
|
||||
4. Any launch flow that exposes seed phrase by default.
|
||||
5. Any owner support/install action that proceeds without required identity assurance.
|
||||
|
||||
@ -127,8 +127,6 @@ Technical details are available only in expanded view:
|
||||
3. Recovery path must exist but remain opt-in in onboarding.
|
||||
4. Sensitive operations fail closed on secure storage errors.
|
||||
5. Wallet export (seed/private key) requires explicit authenticated flow.
|
||||
6. AI/delegated automation must never use the human identity signer key directly.
|
||||
7. Any delegated signing authority must be explicit, scoped, and revocable.
|
||||
|
||||
## Asset/Display Model
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user