Every open Vest tab renews the login on its own, and each renewal moves the session on: a token from before still reads, but Vest refuses to mint an account's token with it (401, code 1002). The extension relayed the first tab's token, so mints failed most of the time with two tabs open, and each refusal dropped the whole session — every later account in a history pass failed "no Vest session", and new accounts waited out 5-minute retries for up to an hour (2026-10-28/29). The client now fails only that account's call on a refused mint, keeps the session and the cached account tokens, and logs the refusal now and then with the login token's life left. An active account with no history is read again every minute even while a whole pass's retry waits; a pass reads those first and inactive accounts last, whose failures no longer set the retry. Exits, lone take-profits and flattens wait up to 90s for the session and the account's token instead of failing at once; entries still go now or not at all. Closed accounts' history is read through the login first. Extension 0.18.0: the hook records each token's session id and takes sign-in tokens too; the relay reads every Vest tab and Vest's own current-session record, and sends the token that session holds, else the newest, saying in the popup when none is current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
167 lines
9.6 KiB
JavaScript
167 lines
9.6 KiB
JavaScript
// Checks for the extension's Vest relay: the hook in the Vest tab
|
|
// (extension/vest-relay.js) and the choice of which tab's token the background
|
|
// relays (extension/background.js pageVestSession / pickVestSession).
|
|
//
|
|
// npm run check
|
|
//
|
|
// The hook runs in a sandbox standing in for the Vest page — its own XHR and
|
|
// fetch, answered here; the background's functions are lifted out of its
|
|
// source, since it is a service worker and not a module. Nothing reaches Vest
|
|
// or Chrome. The answers are shaped like the Vest app's own (index bundle,
|
|
// 2026-10-29): a renewal's carries refreshTokenId, a sign-in's the login's
|
|
// refreshToken too, an account's renewal a refreshToken in its request.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import vm from 'node:vm';
|
|
|
|
const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8');
|
|
const HOOK = read('extension/vest-relay.js');
|
|
const BACKGROUND = read('extension/background.js');
|
|
|
|
const GATEWAY = 'https://api-gateway.hz.vestmarkets.com';
|
|
const WALLET = '0x7ce10771819f80130e204ddb5ca1522e48a9b451';
|
|
const answer = (extra) => JSON.stringify({ accessToken: 'eyJ.login.sig', accessExpiresAtMs: 1_800_000_000_000, primaryAddr: WALLET, userId: 'u', ...extra });
|
|
|
|
/** A Vest page with the hook run in it (after `before`, a hook already
|
|
* there): `xhr(url, body, status, text)` and `fetch(...)` play one of the
|
|
* app's requests through it. */
|
|
function page(before = {}) {
|
|
class FakeXHR {
|
|
constructor() { this.listeners = []; this.responseType = ''; }
|
|
open(method, url) { this.url = url; }
|
|
send() {}
|
|
addEventListener(type, fn) { if (type === 'loadend') this.listeners.push(fn); }
|
|
}
|
|
const answers = [];
|
|
const sandbox = {
|
|
XMLHttpRequest: FakeXHR, URL,
|
|
fetch: async () => answers.shift(),
|
|
...before,
|
|
};
|
|
sandbox.window = sandbox;
|
|
vm.createContext(sandbox);
|
|
vm.runInContext(HOOK, sandbox);
|
|
return {
|
|
sandbox,
|
|
run: () => vm.runInContext(HOOK, sandbox),
|
|
xhr(url, body, status, text) {
|
|
const x = new sandbox.XMLHttpRequest();
|
|
x.open('POST', url);
|
|
x.send(body);
|
|
x.status = status;
|
|
x.responseText = text;
|
|
for (const fn of x.listeners) fn();
|
|
},
|
|
async fetch(url, body, status, text) {
|
|
answers.push({ status, clone: () => ({ text: async () => text }) });
|
|
await sandbox.window.fetch(url, { method: 'POST', body });
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
},
|
|
};
|
|
}
|
|
|
|
test("the hook keeps the login's renewed token with its session id, and an account's never", async () => {
|
|
const p = page();
|
|
assert.equal(p.sandbox.__afVestHook, 2);
|
|
p.xhr(`${GATEWAY}/v3/auth/refresh`, JSON.stringify({ requestId: 'r1', rememberMe: true }), 200, answer({ refreshTokenId: 'rt-1' }));
|
|
assert.equal(p.sandbox.__afVestLogin.accessToken, 'eyJ.login.sig');
|
|
assert.equal(p.sandbox.__afVestLogin.refreshTokenId, 'rt-1');
|
|
assert.equal(p.sandbox.__afVestLogin.primaryAddr, WALLET);
|
|
|
|
// An account's renewal, by its request, then by its answer alone.
|
|
p.xhr(`${GATEWAY}/v3/auth/refresh`, JSON.stringify({ refreshToken: 'acct-rt', requestId: 'r2' }), 200, answer({ accessToken: 'eyJ.acct.sig', refreshTokenId: 'rt-a' }));
|
|
await p.fetch(`${GATEWAY}/v3/auth/refresh`, undefined, 200, answer({ accessToken: 'eyJ.acct2.sig', refreshToken: 'acct-rt', refreshTokenId: 'rt-b' }));
|
|
assert.equal(p.sandbox.__afVestLogin.accessToken, 'eyJ.login.sig');
|
|
|
|
// The login's next renewal, through fetch; a refused one leaves it be.
|
|
await p.fetch(`${GATEWAY}/v3/auth/refresh`, JSON.stringify({ requestId: 'r3' }), 200, answer({ accessToken: 'eyJ.login2.sig', refreshTokenId: 'rt-2' }));
|
|
p.xhr(`${GATEWAY}/v3/auth/refresh`, JSON.stringify({ requestId: 'r4' }), 401, answer({ accessToken: 'eyJ.no.sig', refreshTokenId: 'rt-x' }));
|
|
assert.equal(p.sandbox.__afVestLogin.accessToken, 'eyJ.login2.sig');
|
|
assert.equal(p.sandbox.__afVestLogin.refreshTokenId, 'rt-2');
|
|
});
|
|
|
|
test("a sign-in's token is taken, though its answer carries the login's refresh token", () => {
|
|
const p = page();
|
|
p.xhr(`${GATEWAY}/v3/auth/register`, JSON.stringify({ primaryAddr: WALLET }), 200, answer({ accessToken: 'eyJ.signin.sig', refreshToken: 'login-rt', refreshTokenId: 'rt-s' }));
|
|
assert.equal(p.sandbox.__afVestLogin.accessToken, 'eyJ.signin.sig');
|
|
assert.equal(p.sandbox.__afVestLogin.refreshTokenId, 'rt-s');
|
|
});
|
|
|
|
test('the hook installs once, and beside a version-1 hook', () => {
|
|
const p = page({ __afVestHook: true, __afVestSession: { accessToken: 'eyJ.old.sig', expiresAt: 1 } });
|
|
assert.equal(p.sandbox.__afVestHook, 2);
|
|
const send = p.sandbox.XMLHttpRequest.prototype.send;
|
|
p.run();
|
|
assert.equal(p.sandbox.XMLHttpRequest.prototype.send, send);
|
|
});
|
|
|
|
// The background's own functions, out of its source.
|
|
function lift(name) {
|
|
const m = BACKGROUND.match(new RegExp(`^function ${name}\\([\\s\\S]*?^}`, 'm'));
|
|
assert.ok(m, `${name} not found in background.js`);
|
|
return m[0];
|
|
}
|
|
const { isCurrentVestSession, pickVestSession } = new Function(`${lift('isCurrentVestSession')}\n${lift('pickVestSession')}\nreturn { isCurrentVestSession, pickVestSession };`)();
|
|
const pageVestSession = (window, stored) => new Function('window', 'localStorage', `${lift('pageVestSession')}\nreturn pageVestSession();`)(
|
|
window, { getItem: (key) => (key === 'vest-auth-session' ? stored : null) },
|
|
);
|
|
|
|
const later = (min) => Date.now() + min * 60_000;
|
|
const session = (token, expiresAt, refreshTokenId) => ({ accessToken: token, expiresAt, primaryAddr: WALLET, userId: 'u', ...(refreshTokenId !== undefined ? { refreshTokenId } : {}) });
|
|
const record = (refreshTokenId, accessTokenExpiresAt) => JSON.stringify({ refreshTokenId, accessTokenExpiresAt, refreshTokenExpiresAt: later(43200), rememberMe: true });
|
|
|
|
test("a tab reports its live token (version 2's, else version 1's) and the app's current session", () => {
|
|
const expiresAt = later(12);
|
|
let out = pageVestSession({ __afVestHook: 2, __afVestLogin: session('eyJ.v2.sig', expiresAt, 'rt-2'), __afVestSession: session('eyJ.v1.sig', expiresAt) }, record('rt-2', expiresAt));
|
|
assert.equal(out.hooked, true);
|
|
assert.equal(out.session.accessToken, 'eyJ.v2.sig');
|
|
assert.deepEqual(out.current, { refreshTokenId: 'rt-2', accessTokenExpiresAt: expiresAt });
|
|
|
|
// Hooked by version 1 only: its token, and the tab asks for version 2.
|
|
out = pageVestSession({ __afVestHook: true, __afVestSession: session('eyJ.v1.sig', later(9)) }, record('rt-3', later(14)));
|
|
assert.equal(out.hooked, false);
|
|
assert.equal(out.session.accessToken, 'eyJ.v1.sig');
|
|
|
|
// A 0.17.0 hook kept an account's renewal too: version 2's login token
|
|
// wins over it, though it expires sooner.
|
|
out = pageVestSession({ __afVestHook: 2, __afVestLogin: session('eyJ.v2.sig', later(5), 'rt-1'), __afVestSession: session('eyJ.acct.sig', later(13)) }, null);
|
|
assert.equal(out.session.accessToken, 'eyJ.v2.sig');
|
|
assert.equal(out.current, null);
|
|
|
|
// Nearly expired, or a record that doesn't read.
|
|
out = pageVestSession({ __afVestHook: 2, __afVestLogin: session('eyJ.v2.sig', Date.now() + 20_000, 'rt-1') }, '{nope');
|
|
assert.equal(out.session, null);
|
|
assert.equal(out.current, null);
|
|
});
|
|
|
|
test("the token relayed is the one the current session holds, whichever tab has it — not the first tab's", () => {
|
|
// Tab 1 renewed first; tab 2 renewed after it, so the session is tab 2's.
|
|
const tab1 = { hooked: true, session: session('eyJ.tab1.sig', later(6), 'rt-1'), current: { refreshTokenId: 'rt-2', accessTokenExpiresAt: later(13) } };
|
|
const tab2 = { hooked: true, session: session('eyJ.tab2.sig', later(13), 'rt-2'), current: { refreshTokenId: 'rt-2', accessTokenExpiresAt: later(13) } };
|
|
assert.deepEqual(pickVestSession([tab1, tab2]), { session: tab2.session, current: true });
|
|
assert.equal(isCurrentVestSession(tab1.session, tab1.current), false);
|
|
|
|
// Tab 1 renews again: now its token is the current one.
|
|
const rt3 = { refreshTokenId: 'rt-3', accessTokenExpiresAt: later(15) };
|
|
const tab1b = { ...tab1, session: session('eyJ.tab1b.sig', later(15), 'rt-3'), current: rt3 };
|
|
assert.equal(pickVestSession([{ ...tab2, current: rt3 }, tab1b]).session.accessToken, 'eyJ.tab1b.sig');
|
|
|
|
// Version-1 hooks keep no id: matched by the expiry the app keeps with it.
|
|
const v1a = { hooked: false, session: session('eyJ.v1a.sig', later(4)), current: { refreshTokenId: 'rt-9', accessTokenExpiresAt: later(11) } };
|
|
const v1b = { hooked: false, session: session('eyJ.v1b.sig', v1a.current.accessTokenExpiresAt), current: v1a.current };
|
|
assert.deepEqual(pickVestSession([v1a, v1b]), { session: v1b.session, current: true });
|
|
});
|
|
|
|
test('with no tab holding the current session the newest is relayed, and said; with no record, unjudged', () => {
|
|
const current = { refreshTokenId: 'rt-5', accessTokenExpiresAt: later(14) };
|
|
const a = { hooked: true, session: session('eyJ.a.sig', later(3), 'rt-3'), current };
|
|
const b = { hooked: true, session: session('eyJ.b.sig', later(8), 'rt-4'), current };
|
|
assert.deepEqual(pickVestSession([a, b]), { session: b.session, current: false });
|
|
assert.deepEqual(pickVestSession([{ ...a, current: null }, { ...b, current: null }]), { session: b.session, current: null });
|
|
// A tab with no token yet still shows the record.
|
|
assert.equal(pickVestSession([{ hooked: true, session: null, current }, { ...b, current: null }]).current, false);
|
|
assert.equal(pickVestSession([{ hooked: true, session: null, current }]), null);
|
|
assert.equal(pickVestSession([]), null);
|
|
});
|