/** * Vest sessions relayed by the extension. * * Vest logs a person in through Google/Privy in the browser, and refreshes * the session with a device key the browser will not export, so this * machine cannot log in by itself. Instead the AutoFirmer extension watches * the signed-in Vest tab: whenever the Vest app renews its own ~15-minute * access token, the extension posts it here (POST /api/vest/session). The * Vest client (lib/vest-class.ts) trades with the latest one for its wallet * and mints per-account tokens from it. * * Kept in memory only — never in the DB, the logs, a report or a backup. A * restart forgets them; the Vest tab's next renewal (or the extension's * next tick) relays a fresh one within minutes. */ export interface VestSession { accessToken: string; /** Epoch ms the access token stops working. */ expiresAt: number; /** The wallet the Vest login belongs to, lower-case. */ primaryAddr: string; userId: string; receivedAt: number; } /** What the dashboard and the extension may see of a session: never the token. */ export interface VestSessionInfo { primaryAddr: string; userId: string; expiresAt: number; receivedAt: number; } const g = globalThis as typeof globalThis & { __vestSessions?: Map; __vestPrevious?: Map; __vestRefused?: Map; }; function store(): Map { return (g.__vestSessions ??= new Map()); } /** The session each wallet's current one replaced — what a refused token * falls back to. */ function previous(): Map { return (g.__vestPrevious ??= new Map()); } /** Tokens the client found to be one account's, not the login's (by their * tail — enough to tell them apart, never the whole token), with when. */ function refused(): Map { return (g.__vestRefused ??= new Map()); } const REFUSED_KEEP = 50; /** Shortest life a relayed token must have left to be taken. */ const MIN_REMAINING_MS = 30_000; /** Longest a relayed token may claim to live — a Vest access token is * ~15 minutes; anything far beyond that is not one. */ const MAX_REMAINING_MS = 6 * 60 * 60_000; /** Take a relayed session. Returns why it was refused, or null. A token for * a wallet only ever replaces one that expires sooner. */ export function putVestSession(input: { accessToken: unknown; expiresAt: unknown; primaryAddr: unknown; userId: unknown }, now = Date.now()): string | null { const token = typeof input.accessToken === 'string' ? input.accessToken.trim() : ''; const expiresAt = Number(input.expiresAt); const addr = typeof input.primaryAddr === 'string' ? input.primaryAddr.trim().toLowerCase() : ''; if (!/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token) || token.length > 8192) return 'accessToken is not a JWT'; if (!/^0x[0-9a-f]{40}$/.test(addr)) return 'primaryAddr is not a wallet address'; if (!Number.isFinite(expiresAt) || expiresAt - now < MIN_REMAINING_MS) return 'token already expired'; if (expiresAt - now > MAX_REMAINING_MS) return 'expiresAt is implausibly far away'; if (refused().has(token.slice(-32))) return "that is one Vest account's token, not the login's — reload the Vest tab, or update the extension"; const prev = store().get(addr); if (prev && prev.expiresAt > expiresAt && prev.accessToken !== token) return null; // keep the fresher one if (prev && prev.accessToken !== token) previous().set(addr, prev); store().set(addr, { accessToken: token, expiresAt, primaryAddr: addr, userId: typeof input.userId === 'string' ? input.userId : '', receivedAt: now, }); return null; } /** The live session for a wallet, or null when none has been relayed or it * is about to expire. */ export function vestSessionFor(primaryAddr: string, now = Date.now(), minRemainingMs = MIN_REMAINING_MS): VestSession | null { const s = store().get(primaryAddr.trim().toLowerCase()); return s && s.expiresAt - now >= minRemainingMs ? s : null; } /** The client found the wallet's token to be one account's own (see * accountScopedToken): drop it, and take it no more if it's relayed again — * the login's own token, when the Vest app next renews it, takes its place. */ export function refuseVestSession(primaryAddr: string, token: string, now = Date.now()): void { const key = primaryAddr.trim().toLowerCase(); refused().set(token.slice(-32), now); while (refused().size > REFUSED_KEEP) refused().delete(refused().keys().next().value as string); if (store().get(key)?.accessToken !== token) return; store().delete(key); // Back to the token it replaced, if that one is still good. const before = previous().get(key); previous().delete(key); if (before && !refused().has(before.accessToken.slice(-32)) && before.expiresAt - now >= MIN_REMAINING_MS) store().set(key, before); } /** Drop a wallet's session — the gateway refused it. */ export function dropVestSession(primaryAddr: string, token: string): void { const key = primaryAddr.trim().toLowerCase(); if (store().get(key)?.accessToken === token) store().delete(key); } /** Every relayed session, newest first, without tokens. */ export function listVestSessions(): VestSessionInfo[] { return [...store().values()] .sort((a, b) => b.receivedAt - a.receivedAt) .map(({ primaryAddr, userId, expiresAt, receivedAt }) => ({ primaryAddr, userId, expiresAt, receivedAt })); }