Add launcher wallet session refresh and revoke flows
This commit is contained in:
parent
c92676194f
commit
87b6a321c4
@ -46,6 +46,7 @@ Advanced integration controls (collapsible):
|
||||
5. Member channel register/poll primitives
|
||||
6. Governance install + lease primitives (with explicit `operation_mode`)
|
||||
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:
|
||||
|
||||
@ -55,6 +56,7 @@ Wallet automation helpers remain available in advanced controls:
|
||||
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:
|
||||
|
||||
|
||||
154
app/app.js
154
app/app.js
@ -11,6 +11,7 @@ const state = {
|
||||
channelReady: false,
|
||||
walletSessionToken: "",
|
||||
walletSessionExpiresAt: "",
|
||||
walletSessionRefreshInFlight: null,
|
||||
};
|
||||
|
||||
function nowISO() {
|
||||
@ -62,9 +63,109 @@ function sessionSummary() {
|
||||
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 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;
|
||||
}
|
||||
@ -145,7 +246,10 @@ function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function request(method, path, body) {
|
||||
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;
|
||||
@ -159,6 +263,7 @@ async function request(method, path, body) {
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
const res = await fetch(`${baseURL()}${path}`, opts);
|
||||
captureSessionHeaders(res);
|
||||
const text = await res.text();
|
||||
let json = {};
|
||||
if (text.trim() !== "") {
|
||||
@ -169,7 +274,16 @@ async function request(method, path, body) {
|
||||
}
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(`${res.status} ${res.statusText}: ${JSON.stringify(json)}`);
|
||||
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;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
@ -291,9 +405,7 @@ async function onConnectWallet() {
|
||||
const nextWallet = normalizedAddress(accounts[0]);
|
||||
$("walletAddress").value = nextWallet;
|
||||
if (previousWallet && previousWallet !== nextWallet) {
|
||||
state.walletSessionToken = "";
|
||||
state.walletSessionExpiresAt = "";
|
||||
logLine("wallet session reset", {
|
||||
clearWalletSession("wallet_changed", {
|
||||
previous_wallet: previousWallet,
|
||||
next_wallet: nextWallet,
|
||||
});
|
||||
@ -444,6 +556,36 @@ async function onVerify() {
|
||||
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",
|
||||
@ -943,6 +1085,8 @@ 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);
|
||||
|
||||
@ -101,6 +101,8 @@
|
||||
<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">Membership status</button>
|
||||
</div>
|
||||
<div class="grid three">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user