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
+29 -4
View File
@@ -49,11 +49,11 @@ import { firmInstruments } from './instruments';
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext } from './platform-client';
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage } from './platform-client';
import { encodeContractName, decodeContractName, roundToStep, scoreCandidate } from './dxtrade-helpers';
import { vestSessionFor, dropVestSession, listVestSessions } from './vest-session';
import { vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from './vest-session';
import {
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, nextVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
parseTxn, bookTxns, awaitingLegs, unrealizedPnl, hourlyVolatility, marketOpen, num, toMs, formatDecimal, resetSafeStop,
parseHistoryPosition, VEST_STATUS_ACTIVE, VEST_TXN,
parseHistoryPosition, jwtClaimStrings, accountScopedToken, VEST_STATUS_ACTIVE, VEST_TXN,
} from './vest-helpers';
import type { VestCapitalAccount, VestPosition, VestOpenOrder, VestTxn, VestTrade } from './vest-helpers';
@@ -218,6 +218,8 @@ export class VestClient implements PlatformClient {
/** Open positions already in the trades table, by id → size stored. */
private recordedOpen = new Map<string, number>();
private flatAmounts = new Map<number, number>();
/** When a relayed token last turned out to be one account's own. */
private scopedRefusedAt = 0;
private gateNext: Record<Bucket, number> = { read: 0, trade: 0, history: 0 };
private suspectAccounts = new Map<number, number>();
@@ -261,7 +263,9 @@ export class VestClient implements PlatformClient {
const other = listVestSessions().find((s) => s.primaryAddr !== this.wallet && s.expiresAt > Date.now());
const msg = other
? `no Vest session for ${this.wallet} — the Vest tab here is signed in as ${other.primaryAddr}; sign in as ${this.wallet} there, or remove this firm's credential on the dashboard to switch to the tab's login`
: `no Vest session for ${this.wallet} — keep a Vest tab signed in to that wallet open in Chrome with the AutoFirmer extension`;
: Date.now() - this.scopedRefusedAt < 30 * 60_000
? `no Vest session for ${this.wallet} — the extension relayed one account's own token, not the login's; reload the Vest tab (or update the extension) to relay the login's`
: `no Vest session for ${this.wallet} — keep a Vest tab signed in to that wallet open in Chrome with the AutoFirmer extension`;
if (this.lastFetchErrors['login'] !== msg) console.warn(`${this.tag} ${msg}`);
this.lastAuthError = this.lastFetchErrors['login'] = msg;
return null;
@@ -472,7 +476,8 @@ export class VestClient implements PlatformClient {
*/
private async requestSync(): Promise<void> {
if (this.intentionalDisconnect) return;
if (!this.userToken()) return;
const token = this.userToken();
if (!token) return;
const startedAt = Date.now();
if (this.syncInFlight) {
const runningMs = startedAt - this.syncInFlightSince;
@@ -499,6 +504,16 @@ export class VestClient implements PlatformClient {
delete this.lastFetchErrors['login'];
const accounts = (cap.data?.accounts ?? []).map(parseCapitalAccount).filter((a): a is VestCapitalAccount => a !== null);
// One account's own token (the Vest app renews those through the
// same endpoint the extension watches) sees that account alone:
// syncing on it would drop every other account from the list.
const hasPrimary = (bal.data?.accounts ?? []).some((b) => num(b.account_type) === 1);
if (accountScopedToken({ capitalIds: accounts.map((a) => a.id), hasPrimary, claims: jwtClaimStrings(token) })) {
refuseVestSession(this.wallet, token);
this.scopedRefusedAt = Date.now();
console.warn(`${this.tag} the relayed token is one Vest account's own (it sees ${accounts.length} account, no primary), not the login's — refused; keeping the last account list until the login's token comes`);
return;
}
const free = new Map<string, number>();
for (const b of bal.data?.accounts ?? []) {
const amount = num(b.amount);
@@ -565,6 +580,16 @@ export class VestClient implements PlatformClient {
+ `${a.targetEquity ? `, target ${a.targetEquity}` : ''}`;
}
this.accountList = list;
// An account gone from Vest's list (breached, closed) takes its
// position with it — a stale one here would hold the lane.
for (const key of Object.keys(this.positions)) {
const id = Number(key);
if (!list.some((a) => a.id === id)) {
delete this.positions[id];
this.positionsDetail.delete(id);
this.seenOpenAt.delete(id);
}
}
this.settleSuspects();
this.checkNakedPositions();