Vest: relay the login's token, never one account's

The Vest app renews each trading account's own token through the same
POST /v3/auth/refresh as the login's (with that account's refresh token
in the body). The extension relayed those too, and the dashboard kept
whichever token lived longest — so while someone traded in an account on
Vest, Me synced with a token that sees that account alone and dropped
the other 24 from the list.

Extension 0.17.2 skips account renewals. The dashboard also refuses a
token that sees one account and no primary (or names that account in
its claims), keeps the last account list, and falls back to the login's
token it replaced. An account gone from Vest's list no longer leaves a
stale position holding the lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-26 19:32:40 -05:00
co-authored by Claude Opus 5.5
parent 95cc3bd23f
commit a0e7c5b7c2
7 changed files with 177 additions and 15 deletions
+36 -1
View File
@@ -32,12 +32,30 @@ export interface VestSessionInfo {
receivedAt: number;
}
const g = globalThis as typeof globalThis & { __vestSessions?: Map<string, VestSession> };
const g = globalThis as typeof globalThis & {
__vestSessions?: Map<string, VestSession>;
__vestPrevious?: Map<string, VestSession>;
__vestRefused?: Map<string, number>;
};
function store(): Map<string, VestSession> {
return (g.__vestSessions ??= new Map());
}
/** The session each wallet's current one replaced — what a refused token
* falls back to. */
function previous(): Map<string, VestSession> {
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<string, number> {
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
@@ -54,8 +72,10 @@ export function putVestSession(input: { accessToken: unknown; expiresAt: unknown
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,
@@ -73,6 +93,21 @@ export function vestSessionFor(primaryAddr: string, now = Date.now(), minRemaini
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();