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`
|
2. `Activate membership`
|
||||||
3. `Refresh status + feed`
|
3. `Refresh status + feed`
|
||||||
4. `Governance status`
|
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
|
6. Pull-first updates feed + support ticket action
|
||||||
7. Identity assurance visibility (`none` / `crypto_direct_unattested` / `sponsored_unattested` / `onramp_attested`)
|
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):
|
Advanced integration controls (collapsible):
|
||||||
|
|
||||||
@ -46,9 +44,8 @@ Advanced integration controls (collapsible):
|
|||||||
3. Membership quote + confirm primitives
|
3. Membership quote + confirm primitives
|
||||||
4. Marketplace offer list + checkout quote/send/confirm primitives
|
4. Marketplace offer list + checkout quote/send/confirm primitives
|
||||||
5. Member channel register/poll 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
|
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:
|
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`.
|
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.
|
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:
|
Policy behavior in launcher shell:
|
||||||
|
|
||||||
1. Membership is required for all member-channel polling flows.
|
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.
|
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.
|
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:
|
Run locally:
|
||||||
|
|
||||||
|
|||||||
400
app/app.js
400
app/app.js
@ -9,11 +9,6 @@ const state = {
|
|||||||
lastCheckoutQuote: null,
|
lastCheckoutQuote: null,
|
||||||
lastStatus: null,
|
lastStatus: null,
|
||||||
channelReady: false,
|
channelReady: false,
|
||||||
walletSessionToken: "",
|
|
||||||
walletSessionExpiresAt: "",
|
|
||||||
walletSessionRefreshInFlight: null,
|
|
||||||
walletBalanceNative: "",
|
|
||||||
walletBalanceUSDC: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function nowISO() {
|
function nowISO() {
|
||||||
@ -58,122 +53,6 @@ function isOnrampAttested(value) {
|
|||||||
return normalizeAssurance(value) === "onramp_attested";
|
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() {
|
function injectedProvider() {
|
||||||
return globalThis.ethereum || null;
|
return globalThis.ethereum || null;
|
||||||
}
|
}
|
||||||
@ -192,41 +71,6 @@ function utf8ToHex(value) {
|
|||||||
.join("")}`;
|
.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) {
|
function logLine(label, payload) {
|
||||||
const log = $("log");
|
const log = $("log");
|
||||||
const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`;
|
const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`;
|
||||||
@ -239,29 +83,9 @@ function setSummary(id, value) {
|
|||||||
el.textContent = 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) {
|
function refreshOverview(statusPayload) {
|
||||||
const currentWallet = wallet();
|
const currentWallet = wallet();
|
||||||
setSummary("summaryWallet", currentWallet || "not connected");
|
setSummary("summaryWallet", currentWallet || "not connected");
|
||||||
setSummary("summaryFunds", walletBalanceSummary());
|
|
||||||
setSummary("summarySession", sessionSummary());
|
|
||||||
refreshModeUI();
|
|
||||||
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 || "-");
|
||||||
@ -269,7 +93,6 @@ function refreshOverview(statusPayload) {
|
|||||||
setSummary("summaryAssurance", assurance);
|
setSummary("summaryAssurance", assurance);
|
||||||
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
||||||
setSummary("summaryLastSync", nowISO());
|
setSummary("summaryLastSync", nowISO());
|
||||||
refreshActionLocks(statusPayload);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const designation = $("designationCode")?.value?.trim() || "-";
|
const designation = $("designationCode")?.value?.trim() || "-";
|
||||||
@ -277,7 +100,6 @@ function refreshOverview(statusPayload) {
|
|||||||
const assurance = assuranceDisplay(state.lastStatus?.identity_assurance_level);
|
const assurance = assuranceDisplay(state.lastStatus?.identity_assurance_level);
|
||||||
setSummary("summaryAssurance", assurance);
|
setSummary("summaryAssurance", assurance);
|
||||||
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked");
|
||||||
refreshActionLocks();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFlowStatus(message) {
|
function setFlowStatus(message) {
|
||||||
@ -291,24 +113,15 @@ function sleep(ms) {
|
|||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function request(method, path, body, options = {}) {
|
async function request(method, path, body) {
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
const opts = {
|
const opts = {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers: { "Content-Type": "application/json" },
|
||||||
};
|
};
|
||||||
if (body !== undefined) {
|
if (body !== undefined) {
|
||||||
opts.body = JSON.stringify(body);
|
opts.body = JSON.stringify(body);
|
||||||
}
|
}
|
||||||
const res = await fetch(`${baseURL()}${path}`, opts);
|
const res = await fetch(`${baseURL()}${path}`, opts);
|
||||||
captureSessionHeaders(res);
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
let json = {};
|
let json = {};
|
||||||
if (text.trim() !== "") {
|
if (text.trim() !== "") {
|
||||||
@ -319,16 +132,7 @@ async function request(method, path, body, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorMessage = String(json?.error || res.statusText || "request failed");
|
throw new Error(`${res.status} ${res.statusText}: ${JSON.stringify(json)}`);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@ -357,38 +161,6 @@ function principalRole() {
|
|||||||
return $("principalRole").value.trim();
|
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) {
|
function renderEvents(events) {
|
||||||
const list = $("eventList");
|
const list = $("eventList");
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
@ -469,20 +241,12 @@ function buildIntentTypedData(intent, origin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onConnectWallet() {
|
async function onConnectWallet() {
|
||||||
const previousWallet = normalizedAddress($("walletAddress").value);
|
|
||||||
const provider = await requireProvider();
|
const provider = await requireProvider();
|
||||||
const accounts = await provider.request({ method: "eth_requestAccounts" });
|
const accounts = await provider.request({ method: "eth_requestAccounts" });
|
||||||
if (!accounts || accounts.length === 0) {
|
if (!accounts || accounts.length === 0) {
|
||||||
throw new Error("wallet provider returned no accounts");
|
throw new Error("wallet provider returned no accounts");
|
||||||
}
|
}
|
||||||
const nextWallet = normalizedAddress(accounts[0]);
|
$("walletAddress").value = normalizedAddress(accounts[0]);
|
||||||
$("walletAddress").value = nextWallet;
|
|
||||||
if (previousWallet && previousWallet !== nextWallet) {
|
|
||||||
clearWalletSession("wallet_changed", {
|
|
||||||
previous_wallet: previousWallet,
|
|
||||||
next_wallet: nextWallet,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const chainHex = await provider.request({ method: "eth_chainId" });
|
const chainHex = await provider.request({ method: "eth_chainId" });
|
||||||
const providerChainID = Number.parseInt(chainHex, 16);
|
const providerChainID = Number.parseInt(chainHex, 16);
|
||||||
if (Number.isFinite(providerChainID) && providerChainID !== chainID()) {
|
if (Number.isFinite(providerChainID) && providerChainID !== chainID()) {
|
||||||
@ -497,74 +261,9 @@ async function onConnectWallet() {
|
|||||||
provider_chain_id: providerChainID,
|
provider_chain_id: providerChainID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await onRefreshBalances();
|
|
||||||
} catch (err) {
|
|
||||||
logLine("wallet balance refresh warning", { error: String(err) });
|
|
||||||
}
|
|
||||||
refreshOverview();
|
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() {
|
async function onSignIntent() {
|
||||||
if (!state.lastIntent) {
|
if (!state.lastIntent) {
|
||||||
throw new Error("create intent before signing");
|
throw new Error("create intent before signing");
|
||||||
@ -649,7 +348,7 @@ async function onSendMembershipTx() {
|
|||||||
params: [txRequest],
|
params: [txRequest],
|
||||||
});
|
});
|
||||||
$("confirmTxHash").value = txHash;
|
$("confirmTxHash").value = txHash;
|
||||||
logLine("EDUT ID tx sent", {
|
logLine("membership tx sent", {
|
||||||
quote_id: state.lastQuote.quote_id,
|
quote_id: state.lastQuote.quote_id,
|
||||||
tx_hash: txHash,
|
tx_hash: txHash,
|
||||||
payer_wallet: from,
|
payer_wallet: from,
|
||||||
@ -682,48 +381,10 @@ async function onVerify() {
|
|||||||
if (out.designation_code) {
|
if (out.designation_code) {
|
||||||
$("designationCode").value = 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);
|
logLine("wallet verify", out);
|
||||||
if (state.walletSessionToken) {
|
|
||||||
logLine("wallet session active", {
|
|
||||||
wallet: requireWallet(),
|
|
||||||
session_expires_at: state.walletSessionExpiresAt || "unknown",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
refreshOverview();
|
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() {
|
async function onStatus() {
|
||||||
const out = await request(
|
const out = await request(
|
||||||
"GET",
|
"GET",
|
||||||
@ -733,7 +394,7 @@ async function onStatus() {
|
|||||||
if (out.designation_code) {
|
if (out.designation_code) {
|
||||||
$("designationCode").value = out.designation_code;
|
$("designationCode").value = out.designation_code;
|
||||||
}
|
}
|
||||||
logLine("EDUT ID status", out);
|
logLine("membership status", out);
|
||||||
refreshOverview(out);
|
refreshOverview(out);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@ -761,7 +422,7 @@ async function onQuote() {
|
|||||||
$("quoteId").value = out.quote_id || "";
|
$("quoteId").value = out.quote_id || "";
|
||||||
$("quoteValue").value = out.value || "";
|
$("quoteValue").value = out.value || "";
|
||||||
$("quotePayer").value = out.payer_wallet || "";
|
$("quotePayer").value = out.payer_wallet || "";
|
||||||
logLine("EDUT ID quote", out);
|
logLine("membership quote", out);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -795,14 +456,14 @@ async function onConfirmMembership() {
|
|||||||
identity_attested_by: out.identity_attested_by || "",
|
identity_attested_by: out.identity_attested_by || "",
|
||||||
identity_attestation_id: out.identity_attestation_id || "",
|
identity_attestation_id: out.identity_attestation_id || "",
|
||||||
};
|
};
|
||||||
logLine("EDUT ID confirm", out);
|
logLine("membership confirm", out);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requireMembershipState(actionLabel, opts = {}) {
|
async function requireMembershipState(actionLabel, opts = {}) {
|
||||||
const status = await onStatus();
|
const status = await onStatus();
|
||||||
if (String(status.status || "").toLowerCase() !== "active") {
|
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)) {
|
if (opts.requireOnramp && !isOnrampAttested(status.identity_assurance_level)) {
|
||||||
throw new Error(`${actionLabel} requires onramp_attested identity assurance`);
|
throw new Error(`${actionLabel} requires onramp_attested identity assurance`);
|
||||||
@ -846,17 +507,17 @@ async function confirmMembershipWithRetry(maxAttempts = 8, intervalMs = 2500) {
|
|||||||
await sleep(intervalMs);
|
await sleep(intervalMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw lastErr || new Error("EDUT ID confirmation failed");
|
throw lastErr || new Error("membership confirm failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRunMembershipFlow() {
|
async function onRunMembershipFlow() {
|
||||||
setFlowStatus("connecting wallet");
|
setFlowStatus("connecting wallet");
|
||||||
await onConnectWallet();
|
await onConnectWallet();
|
||||||
|
|
||||||
setFlowStatus("checking EDUT ID");
|
setFlowStatus("checking membership");
|
||||||
const status = await onStatus();
|
const status = await onStatus();
|
||||||
if (String(status.status || "").toLowerCase() === "active") {
|
if (String(status.status || "").toLowerCase() === "active") {
|
||||||
setFlowStatus("EDUT ID already active");
|
setFlowStatus("membership already active");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -866,9 +527,9 @@ async function onRunMembershipFlow() {
|
|||||||
await onSignIntent();
|
await onSignIntent();
|
||||||
setFlowStatus("verifying intent");
|
setFlowStatus("verifying intent");
|
||||||
await onVerify();
|
await onVerify();
|
||||||
setFlowStatus("quoting EDUT ID");
|
setFlowStatus("quoting membership");
|
||||||
await onQuote();
|
await onQuote();
|
||||||
setFlowStatus("sending EDUT ID transaction");
|
setFlowStatus("sending membership transaction");
|
||||||
await onSendMembershipTx();
|
await onSendMembershipTx();
|
||||||
|
|
||||||
const txHash = $("confirmTxHash").value.trim();
|
const txHash = $("confirmTxHash").value.trim();
|
||||||
@ -877,14 +538,14 @@ async function onRunMembershipFlow() {
|
|||||||
}
|
}
|
||||||
setFlowStatus("waiting for chain confirmation");
|
setFlowStatus("waiting for chain confirmation");
|
||||||
await waitForTxMined(txHash);
|
await waitForTxMined(txHash);
|
||||||
setFlowStatus("confirming EDUT ID with API");
|
setFlowStatus("confirming membership with API");
|
||||||
await confirmMembershipWithRetry();
|
await confirmMembershipWithRetry();
|
||||||
setFlowStatus("refreshing status");
|
setFlowStatus("refreshing status");
|
||||||
const refreshed = await onStatus();
|
const refreshed = await onStatus();
|
||||||
if (isOnrampAttested(refreshed.identity_assurance_level)) {
|
if (isOnrampAttested(refreshed.identity_assurance_level)) {
|
||||||
setFlowStatus("EDUT ID flow complete (attested)");
|
setFlowStatus("membership flow complete (attested)");
|
||||||
} else {
|
} else {
|
||||||
setFlowStatus("EDUT ID active (unattested)");
|
setFlowStatus("membership active (unattested)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -977,7 +638,6 @@ async function onInstallConfirm() {
|
|||||||
entitlement_id: $("entitlementId").value.trim(),
|
entitlement_id: $("entitlementId").value.trim(),
|
||||||
package_hash: $("packageHash").value.trim(),
|
package_hash: $("packageHash").value.trim(),
|
||||||
runtime_version: $("runtimeVersion").value.trim(),
|
runtime_version: $("runtimeVersion").value.trim(),
|
||||||
operation_mode: operationMode(),
|
|
||||||
installed_at: nowISO(),
|
installed_at: nowISO(),
|
||||||
launcher_receipt_hash: `receipt-${Date.now()}`,
|
launcher_receipt_hash: `receipt-${Date.now()}`,
|
||||||
});
|
});
|
||||||
@ -1150,7 +810,7 @@ async function onQuickActivate() {
|
|||||||
async function onQuickRefresh() {
|
async function onQuickRefresh() {
|
||||||
const status = await onStatus();
|
const status = await onStatus();
|
||||||
if (String(status.status || "").toLowerCase() !== "active") {
|
if (String(status.status || "").toLowerCase() !== "active") {
|
||||||
setFlowStatus("EDUT ID inactive");
|
setFlowStatus("membership inactive");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -1200,16 +860,6 @@ async function onOfflineRenew() {
|
|||||||
logLine("offline renew", out);
|
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) {
|
function bind(id, handler) {
|
||||||
const el = $(id);
|
const el = $(id);
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@ -1228,17 +878,11 @@ bind("btnQuickConnect", onQuickConnect);
|
|||||||
bind("btnQuickActivate", onQuickActivate);
|
bind("btnQuickActivate", onQuickActivate);
|
||||||
bind("btnQuickRefresh", onQuickRefresh);
|
bind("btnQuickRefresh", onQuickRefresh);
|
||||||
bind("btnQuickInstallStatus", onQuickInstallStatus);
|
bind("btnQuickInstallStatus", onQuickInstallStatus);
|
||||||
bind("btnRefreshBalances", onRefreshBalances);
|
|
||||||
bind("btnCopyWallet", onCopyWallet);
|
|
||||||
bind("btnModeHuman", onModeHuman);
|
|
||||||
bind("btnModeAuto", onModeAuto);
|
|
||||||
bind("btnConnectWallet", onConnectWallet);
|
bind("btnConnectWallet", onConnectWallet);
|
||||||
bind("btnRunMembershipFlow", onRunMembershipFlow);
|
bind("btnRunMembershipFlow", onRunMembershipFlow);
|
||||||
bind("btnIntent", onIntent);
|
bind("btnIntent", onIntent);
|
||||||
bind("btnSignIntent", onSignIntent);
|
bind("btnSignIntent", onSignIntent);
|
||||||
bind("btnVerify", onVerify);
|
bind("btnVerify", onVerify);
|
||||||
bind("btnRefreshSession", onRefreshSession);
|
|
||||||
bind("btnRevokeSession", onRevokeSession);
|
|
||||||
bind("btnStatus", onStatus);
|
bind("btnStatus", onStatus);
|
||||||
bind("btnQuote", onQuote);
|
bind("btnQuote", onQuote);
|
||||||
bind("btnSignPayerProof", onSignPayerProof);
|
bind("btnSignPayerProof", onSignPayerProof);
|
||||||
@ -1260,14 +904,6 @@ bind("btnCheckoutConfirm", onCheckoutConfirm);
|
|||||||
bind("btnRunCheckoutFlow", onRunCheckoutFlow);
|
bind("btnRunCheckoutFlow", onRunCheckoutFlow);
|
||||||
bind("btnListEntitlements", onListEntitlements);
|
bind("btnListEntitlements", onListEntitlements);
|
||||||
|
|
||||||
const operationModeSelect = $("operationMode");
|
|
||||||
if (operationModeSelect) {
|
|
||||||
operationModeSelect.addEventListener("change", () => {
|
|
||||||
setOperationMode(operationModeSelect.value, "advanced_select");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshModeUI();
|
|
||||||
logLine("launcher shell ready", {
|
logLine("launcher shell ready", {
|
||||||
api_base: baseURL(),
|
api_base: baseURL(),
|
||||||
chain_id: chainID(),
|
chain_id: chainID(),
|
||||||
|
|||||||
@ -3,27 +3,23 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<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" />
|
<link rel="stylesheet" href="./style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="shell">
|
<main class="shell">
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<h1>EDUT ID Manager</h1>
|
<h1>EDUT Launcher</h1>
|
||||||
<p>Deterministic identity, access, and control surface.</p>
|
<p>Deterministic infrastructure control surface.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Control Surface</h2>
|
<h2>Control Surface</h2>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="btnQuickConnect">Connect wallet</button>
|
<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="btnQuickRefresh">Refresh status + feed</button>
|
||||||
<button id="btnQuickInstallStatus">Governance status</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>
|
</div>
|
||||||
<p id="flowStatus" class="flow-status">ready</p>
|
<p id="flowStatus" class="flow-status">ready</p>
|
||||||
<div class="grid two">
|
<div class="grid two">
|
||||||
@ -32,27 +28,15 @@
|
|||||||
<p id="summaryWallet">not connected</p>
|
<p id="summaryWallet">not connected</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat">
|
<article class="stat">
|
||||||
<h3>Session</h3>
|
<h3>Membership</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>
|
|
||||||
<p id="summaryMembership">unknown</p>
|
<p id="summaryMembership">unknown</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat">
|
<article class="stat">
|
||||||
<h3>Mode</h3>
|
<h3>Designation</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>
|
|
||||||
<p id="summaryDesignation">-</p>
|
<p id="summaryDesignation">-</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat">
|
<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>
|
<p id="summaryAssurance">unknown</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat">
|
<article class="stat">
|
||||||
@ -102,24 +86,18 @@
|
|||||||
Chain ID
|
Chain ID
|
||||||
<input id="chainId" type="number" value="84532" />
|
<input id="chainId" type="number" value="84532" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
|
||||||
USDC token address
|
|
||||||
<input id="usdcTokenAddress" value="0x036cbd53842c5426634e7929541ec2318f3dcf7e" />
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="subpanel">
|
<section class="subpanel">
|
||||||
<h2>EDUT ID Flow Controls</h2>
|
<h2>Membership Flow Controls</h2>
|
||||||
<div class="actions">
|
<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="btnConnectWallet">Connect wallet</button>
|
||||||
<button id="btnIntent">Create intent</button>
|
<button id="btnIntent">Create intent</button>
|
||||||
<button id="btnSignIntent">Sign intent (EIP-712)</button>
|
<button id="btnSignIntent">Sign intent (EIP-712)</button>
|
||||||
<button id="btnVerify">Verify signature</button>
|
<button id="btnVerify">Verify signature</button>
|
||||||
<button id="btnRefreshSession">Refresh session</button>
|
<button id="btnStatus">Membership status</button>
|
||||||
<button id="btnRevokeSession">Revoke session</button>
|
|
||||||
<button id="btnStatus">EDUT ID status</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid three">
|
<div class="grid three">
|
||||||
<label>
|
<label>
|
||||||
@ -160,8 +138,8 @@
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="btnQuote">Get quote</button>
|
<button id="btnQuote">Get quote</button>
|
||||||
<button id="btnSignPayerProof">Sign payer proof</button>
|
<button id="btnSignPayerProof">Sign payer proof</button>
|
||||||
<button id="btnSendMembershipTx">Send EDUT ID tx</button>
|
<button id="btnSendMembershipTx">Send membership tx</button>
|
||||||
<button id="btnConfirmMembership">Confirm EDUT ID tx</button>
|
<button id="btnConfirmMembership">Confirm membership tx</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid three">
|
<div class="grid three">
|
||||||
<label>
|
<label>
|
||||||
@ -332,13 +310,6 @@
|
|||||||
<input id="runtimeVersion" />
|
<input id="runtimeVersion" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
|
||||||
Operation mode
|
|
||||||
<select id="operationMode">
|
|
||||||
<option value="human_manual">human_manual</option>
|
|
||||||
<option value="worker_auto">worker_auto</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
<label>
|
||||||
Package hash
|
Package hash
|
||||||
<input id="packageHash" />
|
<input id="packageHash" />
|
||||||
|
|||||||
@ -53,51 +53,6 @@ body {
|
|||||||
color: var(--accent);
|
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 {
|
.note {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@ -194,22 +149,10 @@ button:hover {
|
|||||||
filter: brightness(1.08);
|
filter: brightness(1.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.mode-active {
|
|
||||||
border-color: #4ed380;
|
|
||||||
background: #1f4a33;
|
|
||||||
color: #e0ffe9;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:active {
|
button:active {
|
||||||
transform: translateY(1px);
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
filter: grayscale(0.5);
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-list {
|
.event-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@ -12,4 +12,3 @@
|
|||||||
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.
|
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`.
|
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`
|
12. `GET /governance/install/status`
|
||||||
13. `GET /member/channel/events`
|
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
|
## Deterministic Requirements
|
||||||
|
|
||||||
1. No runtime activation without entitlement proof.
|
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.
|
4. Event inbox polling remains canonical even if push unavailable.
|
||||||
5. Identity assurance is evaluated independently from membership state.
|
5. Identity assurance is evaluated independently from membership state.
|
||||||
6. Owner/admin launcher actions must require `identity_assurance_level=onramp_attested`.
|
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.
|
3. Governance install path fails closed on invalid evidence.
|
||||||
4. Marketplace and status APIs are called with app-session auth.
|
4. Marketplace and status APIs are called with app-session auth.
|
||||||
5. Wallet v1 acceptance criteria pass (`docs/wallet-v1-product-spec.md`).
|
5. Wallet v1 acceptance criteria pass (`docs/wallet-v1-product-spec.md`).
|
||||||
6. Owner/admin actions block unless `identity_assurance_level=onramp_attested`.
|
|
||||||
|
|
||||||
## Blockers
|
## Blockers
|
||||||
|
|
||||||
@ -15,4 +14,3 @@
|
|||||||
2. Any path that leaks private key material.
|
2. Any path that leaks private key material.
|
||||||
3. Any path that bypasses entitlement checks for governance activation.
|
3. Any path that bypasses entitlement checks for governance activation.
|
||||||
4. Any launch flow that exposes seed phrase by default.
|
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.
|
3. Recovery path must exist but remain opt-in in onboarding.
|
||||||
4. Sensitive operations fail closed on secure storage errors.
|
4. Sensitive operations fail closed on secure storage errors.
|
||||||
5. Wallet export (seed/private key) requires explicit authenticated flow.
|
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
|
## Asset/Display Model
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user