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
+50 -2
View File
@@ -11,9 +11,10 @@ import assert from 'node:assert/strict';
import {
isVestWalletAddress, vestAccountId, num, toMs, formatDecimal, vestDayOf, lastVestResetMs, parseCapitalAccount,
vestAccountName, parsePosition, awaitingLegs, parseOpenOrder, unrealizedPnl, parseTxn, bookTxns, txnKind, VEST_TXN,
hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS, nextVestResetMs, resetSafeStop, parseHistoryPosition,
hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS, nextVestResetMs, resetSafeStop, parseHistoryPosition, jwtClaimStrings,
accountScopedToken,
} from '../lib/vest-helpers.ts';
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions } from '../lib/vest-session.ts';
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from '../lib/vest-session.ts';
const iso = (s) => Date.parse(s);
@@ -296,3 +297,50 @@ test('parseHistoryPosition: open, snake_case, seconds, and junk', () => {
assert.equal(parseHistoryPosition({ positionId: 'p2', symbol: 'GC-PERP' }), null, 'no open time');
assert.equal(parseHistoryPosition(null), null);
});
test('an account\'s own token is told from the login\'s', () => {
const b64 = (o) => Buffer.from(JSON.stringify(o)).toString('base64url');
const acct = '275e765a-2d3d-5de3-8386-71aaa847c0e8';
const accountToken = `${b64({ alg: 'ES256' })}.${b64({ sub: '3cab1794-47bc-523b-91e6-64a9ad26dcbf', acc: { id: acct.toUpperCase() } })}.sig`;
assert.ok(jwtClaimStrings(accountToken).includes(acct));
assert.deepEqual(jwtClaimStrings('not.a-jwt.at-all'), []);
assert.deepEqual(jwtClaimStrings('nodots'), []);
// The login sees every account and its primary one.
assert.equal(accountScopedToken({ capitalIds: [acct, 'b', 'c'], hasPrimary: true, claims: [] }), false);
// One account and no primary: an account's token, whatever its claims.
assert.equal(accountScopedToken({ capitalIds: [acct], hasPrimary: false, claims: [] }), true);
// One account with a primary: only when the token names that account.
assert.equal(accountScopedToken({ capitalIds: [acct], hasPrimary: true, claims: jwtClaimStrings(accountToken) }), true);
assert.equal(accountScopedToken({ capitalIds: [acct], hasPrimary: true, claims: ['3cab1794-47bc-523b-91e6-64a9ad26dcbf'] }), false);
// A login with no capital accounts yet.
assert.equal(accountScopedToken({ capitalIds: [], hasPrimary: true, claims: [] }), false);
});
test('a refused account token falls back to the login\'s and stays refused', () => {
const now = iso('2026-10-27T01:00:00Z');
const wallet = '0x00000000000000000000000000000000000000aa';
const login = 'eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJsb2dpbiJ9.loginsignature-loginsignature-loginsig';
const account = 'eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJhY2N0In0.accountsignature-accountsignature-acct';
assert.equal(putVestSession({ accessToken: login, expiresAt: now + 600_000, primaryAddr: wallet, userId: 'u' }, now), null);
// The account's token lives longer, so it replaces the login's...
assert.equal(putVestSession({ accessToken: account, expiresAt: now + 900_000, primaryAddr: wallet, userId: 'u' }, now), null);
assert.equal(vestSessionFor(wallet, now).accessToken, account);
// ...until the client finds it sees one account: back to the login's.
refuseVestSession(wallet, account, now);
assert.equal(vestSessionFor(wallet, now).accessToken, login);
// Relayed again (the old extension posts it every minute): refused.
assert.match(putVestSession({ accessToken: account, expiresAt: now + 900_000, primaryAddr: wallet, userId: 'u' }, now), /one Vest account's token/);
assert.equal(vestSessionFor(wallet, now).accessToken, login);
// The login's next renewal is taken as usual.
const renewed = 'eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJsb2dpbiJ9.renewedsignature-renewedsignature-rene';
assert.equal(putVestSession({ accessToken: renewed, expiresAt: now + 1_200_000, primaryAddr: wallet, userId: 'u' }, now), null);
assert.equal(vestSessionFor(wallet, now).accessToken, renewed);
// Refusing a token that isn't held changes nothing held.
refuseVestSession(wallet, 'x.y.zzz', now);
assert.equal(vestSessionFor(wallet, now).accessToken, renewed);
// No good token to fall back to: none at all.
const wallet2 = '0x00000000000000000000000000000000000000bb';
assert.equal(putVestSession({ accessToken: account.replace('acct', 'accu'), expiresAt: now + 900_000, primaryAddr: wallet2, userId: 'u' }, now), null);
refuseVestSession(wallet2, account.replace('acct', 'accu'), now);
assert.equal(vestSessionFor(wallet2, now), null);
});