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>
97 lines
4.7 KiB
JavaScript
97 lines
4.7 KiB
JavaScript
/* AutoFirmer — Vest session hook.
|
|
*
|
|
* Runs in the page's MAIN world on next.vestmarkets.com, at document_start
|
|
* (manifest content script) or injected later into a tab that was already open
|
|
* (background.js relayVestSession). Installs once per page.
|
|
*
|
|
* Vest's app renews its own ~15-minute access token by itself: at load, then
|
|
* whenever one nears expiry (POST /v3/auth/refresh, or /v3/auth/register at
|
|
* sign-in). Those answers carry the token, its expiry, the login's wallet and
|
|
* the session it belongs to (refreshTokenId). This hook reads them off the
|
|
* app's own XHRs and keeps the latest on `window.__afVestLogin`, where
|
|
* background.js collects it and relays it to the local dashboard, whose Vest
|
|
* client trades with it.
|
|
*
|
|
* Every tab renews on its own, and each renewal moves the login on to a new
|
|
* session (localStorage `vest-auth-session`): a token from before still reads,
|
|
* but Vest refuses to mint an account's token with it (401, code 1002). The
|
|
* session id lets background.js relay the tab whose token is the current one.
|
|
*
|
|
* It sends nothing anywhere, changes no request and never renews anything
|
|
* itself: the session stays the app's, refreshed by the app with its own
|
|
* device key, so the tab is never logged out by the bot.
|
|
*
|
|
* The app also renews each trading account's own token through the same
|
|
* /v3/auth/refresh, sending that account's refresh token in the body (the
|
|
* login's rides in a cookie) and getting it back renewed. Those tokens see
|
|
* one account only, so they are skipped: relaying one would leave the
|
|
* dashboard with a single account. A sign-in's answer carries the login's
|
|
* own refresh token, and is taken.
|
|
*
|
|
* Version 2. A page hooked by version 1 (`__afVestHook === true`, keeping
|
|
* `__afVestSession`, no session id, no sign-ins) takes this one beside it.
|
|
*/
|
|
|
|
(() => {
|
|
if (window.__afVestHook === 2) return;
|
|
window.__afVestHook = 2;
|
|
|
|
const AUTH = /^https:\/\/api-gateway\.hz\.vestmarkets\.com\/v3\/auth\/(refresh|register)(\?|$)/;
|
|
const SIGN_IN = /\/v3\/auth\/register(\?|$)/;
|
|
|
|
/** A request body that carries a refresh token: an account's renewal. */
|
|
function accountRenewal(reqBody) {
|
|
return typeof reqBody === 'string' && /"refreshToken"\s*:\s*"[^"]/.test(reqBody);
|
|
}
|
|
|
|
function take(url, status, text, accountRequest) {
|
|
if (accountRequest || status < 200 || status >= 300 || !AUTH.test(url)) return;
|
|
let body;
|
|
try { body = JSON.parse(text); } catch { return; }
|
|
if (!body || typeof body.accessToken !== 'string' || !body.accessToken) return;
|
|
// A renewal answering with a refresh token is an account's.
|
|
if (!SIGN_IN.test(url) && typeof body.refreshToken === 'string' && body.refreshToken) return;
|
|
const expiresAt = Number(body.accessExpiresAtMs);
|
|
if (!Number.isFinite(expiresAt)) return;
|
|
window.__afVestLogin = {
|
|
accessToken: body.accessToken,
|
|
expiresAt,
|
|
primaryAddr: typeof body.primaryAddr === 'string' ? body.primaryAddr : '',
|
|
userId: typeof body.userId === 'string' ? body.userId : '',
|
|
refreshTokenId: typeof body.refreshTokenId === 'string' && body.refreshTokenId ? body.refreshTokenId : null,
|
|
at: Date.now(),
|
|
};
|
|
}
|
|
|
|
const open = XMLHttpRequest.prototype.open;
|
|
const send = XMLHttpRequest.prototype.send;
|
|
XMLHttpRequest.prototype.open = function (method, url) {
|
|
try { this.__afVestUrl = String(url); } catch { /* ignore */ }
|
|
return open.apply(this, arguments);
|
|
};
|
|
XMLHttpRequest.prototype.send = function (reqBody) {
|
|
if (this.__afVestUrl && AUTH.test(this.__afVestUrl)) {
|
|
let acct = false;
|
|
try { acct = accountRenewal(reqBody); } catch { /* ignore */ }
|
|
this.addEventListener('loadend', () => {
|
|
try {
|
|
if (this.responseType === '' || this.responseType === 'text') take(this.__afVestUrl, this.status, this.responseText, acct);
|
|
else if (this.responseType === 'json') take(this.__afVestUrl, this.status, JSON.stringify(this.response), acct);
|
|
} catch { /* never break the app */ }
|
|
});
|
|
}
|
|
return send.apply(this, arguments);
|
|
};
|
|
|
|
const fetch0 = window.fetch;
|
|
window.fetch = async function (input, init) {
|
|
const res = await fetch0.apply(this, arguments);
|
|
try {
|
|
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input && input.url;
|
|
const acct = accountRenewal(init && init.body);
|
|
if (url && AUTH.test(url)) res.clone().text().then((t) => take(url, res.status, t, acct)).catch(() => {});
|
|
} catch { /* never break the app */ }
|
|
return res;
|
|
};
|
|
})();
|