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
|
5. Member channel register/poll primitives
|
||||||
6. Governance install + lease primitives (with explicit `operation_mode`)
|
6. Governance install + lease primitives (with explicit `operation_mode`)
|
||||||
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:
|
||||||
|
|
||||||
@ -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`.
|
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`.
|
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:
|
||||||
|
|
||||||
|
|||||||
154
app/app.js
154
app/app.js
@ -11,6 +11,7 @@ const state = {
|
|||||||
channelReady: false,
|
channelReady: false,
|
||||||
walletSessionToken: "",
|
walletSessionToken: "",
|
||||||
walletSessionExpiresAt: "",
|
walletSessionExpiresAt: "",
|
||||||
|
walletSessionRefreshInFlight: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function nowISO() {
|
function nowISO() {
|
||||||
@ -62,9 +63,109 @@ function sessionSummary() {
|
|||||||
if (!state.walletSessionExpiresAt) {
|
if (!state.walletSessionExpiresAt) {
|
||||||
return "active";
|
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})`;
|
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() {
|
function injectedProvider() {
|
||||||
return globalThis.ethereum || null;
|
return globalThis.ethereum || null;
|
||||||
}
|
}
|
||||||
@ -145,7 +246,10 @@ function sleep(ms) {
|
|||||||
return new Promise((resolve) => setTimeout(resolve, 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" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
if (state.walletSessionToken) {
|
if (state.walletSessionToken) {
|
||||||
headers["X-Edut-Session"] = state.walletSessionToken;
|
headers["X-Edut-Session"] = state.walletSessionToken;
|
||||||
@ -159,6 +263,7 @@ async function request(method, path, body) {
|
|||||||
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() !== "") {
|
||||||
@ -169,7 +274,16 @@ async function request(method, path, body) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
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;
|
return json;
|
||||||
}
|
}
|
||||||
@ -291,9 +405,7 @@ async function onConnectWallet() {
|
|||||||
const nextWallet = normalizedAddress(accounts[0]);
|
const nextWallet = normalizedAddress(accounts[0]);
|
||||||
$("walletAddress").value = nextWallet;
|
$("walletAddress").value = nextWallet;
|
||||||
if (previousWallet && previousWallet !== nextWallet) {
|
if (previousWallet && previousWallet !== nextWallet) {
|
||||||
state.walletSessionToken = "";
|
clearWalletSession("wallet_changed", {
|
||||||
state.walletSessionExpiresAt = "";
|
|
||||||
logLine("wallet session reset", {
|
|
||||||
previous_wallet: previousWallet,
|
previous_wallet: previousWallet,
|
||||||
next_wallet: nextWallet,
|
next_wallet: nextWallet,
|
||||||
});
|
});
|
||||||
@ -444,6 +556,36 @@ async function onVerify() {
|
|||||||
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",
|
||||||
@ -943,6 +1085,8 @@ 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);
|
||||||
|
|||||||
@ -101,6 +101,8 @@
|
|||||||
<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="btnRevokeSession">Revoke session</button>
|
||||||
<button id="btnStatus">Membership status</button>
|
<button id="btnStatus">Membership status</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid three">
|
<div class="grid three">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user