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>
84 lines
3.9 KiB
JavaScript
84 lines
3.9 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 and the login's wallet.
|
|
* This hook reads them off the app's own XHRs and keeps the latest on
|
|
* `window.__afVestSession`, where background.js collects it and relays it to
|
|
* the local dashboard, whose Vest client trades with it.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
(() => {
|
|
if (window.__afVestHook) return;
|
|
window.__afVestHook = true;
|
|
|
|
const AUTH = /^https:\/\/api-gateway\.hz\.vestmarkets\.com\/v3\/auth\/(refresh|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;
|
|
if (typeof body.refreshToken === 'string' && body.refreshToken) return; // an account's token, renewed
|
|
const expiresAt = Number(body.accessExpiresAtMs);
|
|
if (!Number.isFinite(expiresAt)) return;
|
|
window.__afVestSession = {
|
|
accessToken: body.accessToken,
|
|
expiresAt,
|
|
primaryAddr: typeof body.primaryAddr === 'string' ? body.primaryAddr : '',
|
|
userId: typeof body.userId === 'string' ? body.userId : '',
|
|
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;
|
|
};
|
|
})();
|