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:
co-authored by
Claude Opus 5.5
parent
95cc3bd23f
commit
a0e7c5b7c2
+50
-2
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user