Vest: the relayed login binds itself to an unset Vest firm
A Vest firm's credential is the wallet of the login it trades. It no longer has to be typed or clicked in: when the extension relays a Vest tab's session and a Vest firm here has no credential, that wallet is saved as the credential (lib/vest-wallet.ts), the client starts, and the backup is pushed. - A saved wallet is never replaced. When the tab is signed in to another login, the firm now says which one and how to switch: sign in as the saved wallet, or remove the credential. - With two logins relayed at once nothing binds itself; the one-click form decides. - Extension 0.17.1 re-posts the session every minute even when it hasn't changed. The dashboard holds it in memory only, so a restart there was unauthenticated until the tab's next renewal (up to ~15 min); now it is back within a minute, and a firm created later binds within a minute too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
96fdd9ca23
commit
769d96883f
@@ -223,8 +223,12 @@ instance cannot log in by itself. Instead:
|
|||||||
2. The extension relays the tab's short-lived access token to this dashboard
|
2. The extension relays the tab's short-lived access token to this dashboard
|
||||||
(`POST /api/vest/session`, held in memory only) every time the Vest app
|
(`POST /api/vest/session`, held in memory only) every time the Vest app
|
||||||
renews it. If no Vest tab is open, the extension opens one in the background.
|
renews it. If no Vest tab is open, the extension opens one in the background.
|
||||||
3. On the dashboard, the Vest firm's credential is the **wallet address** of
|
3. The Vest firm's credential on this dashboard is the **wallet address** of
|
||||||
that login. The credential form offers the relayed wallet with one click.
|
that login, and it binds itself: the first relayed session fills it in when
|
||||||
|
the firm has none. A saved wallet is never replaced. If the tab is signed in
|
||||||
|
to a different login, the firm says so; remove its credential to switch.
|
||||||
|
With two logins relayed at once, nothing binds by itself, and the credential
|
||||||
|
form offers each relayed wallet with one click.
|
||||||
|
|
||||||
With no live session for the wallet, the client sits unauthenticated and says
|
With no live session for the wallet, the client sits unauthenticated and says
|
||||||
so; it never tries to log in.
|
so; it never tries to log in.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { putVestSession, listVestSessions } from '@/lib/vest-session';
|
import { putVestSession, listVestSessions } from '@/lib/vest-session';
|
||||||
|
import { autoBindVestWallet } from '@/lib/vest-wallet';
|
||||||
import { getTradableFirms } from '@/lib/firm-catalog';
|
import { getTradableFirms } from '@/lib/firm-catalog';
|
||||||
import { getClients } from '@/lib/clients';
|
import { getClients } from '@/lib/clients';
|
||||||
import { corsJson, corsPreflight } from '../../autobuyer/cors';
|
import { corsJson, corsPreflight } from '../../autobuyer/cors';
|
||||||
@@ -15,7 +16,10 @@ function wantedWallets(): string[] {
|
|||||||
/**
|
/**
|
||||||
* The extension's Vest relay (lib/vest-session.ts): POST the access token the
|
* The extension's Vest relay (lib/vest-session.ts): POST the access token the
|
||||||
* signed-in Vest tab just renewed — { accessToken, expiresAt, primaryAddr,
|
* signed-in Vest tab just renewed — { accessToken, expiresAt, primaryAddr,
|
||||||
* userId } — and it is held in memory for this machine's Vest client. GET
|
* userId } — and it is held in memory for this machine's Vest client. A Vest
|
||||||
|
* firm here with no credential yet is bound to that login's wallet
|
||||||
|
* (lib/vest-wallet.ts). The extension posts again every minute, so a restart
|
||||||
|
* here is back on the session within a minute. GET
|
||||||
* lists what has been relayed (never the tokens) and which wallets this
|
* lists what has been relayed (never the tokens) and which wallets this
|
||||||
* machine wants, so the extension knows whether to keep a Vest tab open and
|
* machine wants, so the extension knows whether to keep a Vest tab open and
|
||||||
* the dashboard can offer the address as the firm's credential.
|
* the dashboard can offer the address as the firm's credential.
|
||||||
@@ -38,6 +42,7 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (refused) return corsJson({ ok: false, reason: refused }, { status: 400 });
|
if (refused) return corsJson({ ok: false, reason: refused }, { status: 400 });
|
||||||
|
|
||||||
|
const bound = autoBindVestWallet();
|
||||||
const wanted = wantedWallets();
|
const wanted = wantedWallets();
|
||||||
const addr = String(body.primaryAddr).toLowerCase();
|
const addr = String(body.primaryAddr).toLowerCase();
|
||||||
// A client waiting on this wallet's session syncs now, not at its next poll.
|
// A client waiting on this wallet's session syncs now, not at its next poll.
|
||||||
@@ -45,7 +50,7 @@ export async function POST(req: NextRequest) {
|
|||||||
const vest = client as { onVestSession?: (wallet: string) => void };
|
const vest = client as { onVestSession?: (wallet: string) => void };
|
||||||
vest.onVestSession?.(addr);
|
vest.onVestSession?.(addr);
|
||||||
}
|
}
|
||||||
return corsJson({ ok: true, used: wanted.includes(addr), wanted });
|
return corsJson({ ok: true, used: wanted.includes(addr), wanted, bound });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
|
|||||||
+6
-4
@@ -209,9 +209,11 @@ function CredentialForm({ firm, onSaved }: { firm: FirmConfig; onSaved: () => vo
|
|||||||
return <LoginForm firm={firm} onSaved={onSaved} />;
|
return <LoginForm firm={firm} onSaved={onSaved} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A Vest firm's credential: the wallet its Vest login belongs to. The
|
/** A Vest firm's credential: the wallet its Vest login belongs to. It binds
|
||||||
* extension has relayed the session of any Vest tab signed in on this
|
* itself as soon as the extension relays a Vest tab signed in on this
|
||||||
* machine, so those wallets are offered with one click. */
|
* machine (lib/vest-wallet.ts); this form is for when it can't — two logins
|
||||||
|
* relayed at once, or none yet — and offers the relayed wallets with one
|
||||||
|
* click. */
|
||||||
function VestWalletForm({ firm, onSaved }: { firm: FirmConfig; onSaved: () => void }) {
|
function VestWalletForm({ firm, onSaved }: { firm: FirmConfig; onSaved: () => void }) {
|
||||||
const [wallet, setWallet] = useState('');
|
const [wallet, setWallet] = useState('');
|
||||||
const [seen, setSeen] = useState<string[]>([]);
|
const [seen, setSeen] = useState<string[]>([]);
|
||||||
@@ -260,7 +262,7 @@ function VestWalletForm({ firm, onSaved }: { firm: FirmConfig; onSaved: () => vo
|
|||||||
))}
|
))}
|
||||||
<input
|
<input
|
||||||
className="border border-slate-200 rounded-md px-2 py-1 text-xs bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 w-72 font-mono"
|
className="border border-slate-200 rounded-md px-2 py-1 text-xs bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 w-72 font-mono"
|
||||||
placeholder={seen.length > 0 ? 'or another wallet 0x…' : 'Vest wallet 0x… (sign in to Vest in Chrome first)'}
|
placeholder={seen.length > 0 ? 'or another wallet 0x…' : 'Sign in to Vest in Chrome: it binds itself (or paste 0x…)'}
|
||||||
value={wallet}
|
value={wallet}
|
||||||
onChange={(e) => setWallet(e.target.value)}
|
onChange={(e) => setWallet(e.target.value)}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter' && valid) void save(wallet); }}
|
onKeyDown={(e) => { if (e.key === 'Enter' && valid) void save(wallet); }}
|
||||||
|
|||||||
+3
-2
@@ -87,8 +87,9 @@ log in to Vest by itself. The extension relays the browser's session instead:
|
|||||||
|
|
||||||
```
|
```
|
||||||
vest-relay.js (Vest tab, MAIN world) keeps the access token the Vest app last renewed
|
vest-relay.js (Vest tab, MAIN world) keeps the access token the Vest app last renewed
|
||||||
background.js relayVestSession lifts it every 10s, posts it when it changes
|
background.js relayVestSession lifts it every 10s, posts it when it changes (and every minute)
|
||||||
dashboard POST /api/vest/session holds it in memory for the firm's Vest client
|
dashboard POST /api/vest/session holds it in memory for the firm's Vest client,
|
||||||
|
and binds an unset Vest firm to the login's wallet
|
||||||
```
|
```
|
||||||
|
|
||||||
- The hook only reads the Vest app's own `/v3/auth/refresh` and `/register`
|
- The hook only reads the Vest app's own `/v3/auth/refresh` and `/register`
|
||||||
|
|||||||
@@ -1208,11 +1208,15 @@ async function tick() {
|
|||||||
const VEST_PATTERN = 'https://next.vestmarkets.com/*';
|
const VEST_PATTERN = 'https://next.vestmarkets.com/*';
|
||||||
const VEST_URL = 'https://next.vestmarkets.com/portfolio';
|
const VEST_URL = 'https://next.vestmarkets.com/portfolio';
|
||||||
const VEST_EVERY_MS = 10_000;
|
const VEST_EVERY_MS = 10_000;
|
||||||
|
/** The session is posted again this often even unchanged: the dashboard
|
||||||
|
* keeps it in memory only, so a restart there needs it back. */
|
||||||
|
const VEST_REPOST_MS = 60_000;
|
||||||
/** A Vest tab opened for the relay isn't opened again for this long. */
|
/** A Vest tab opened for the relay isn't opened again for this long. */
|
||||||
const VEST_REOPEN_MS = 10 * 60_000;
|
const VEST_REOPEN_MS = 10 * 60_000;
|
||||||
|
|
||||||
let vestLastCheck = 0;
|
let vestLastCheck = 0;
|
||||||
let vestLastRelayed = '';
|
let vestLastRelayed = '';
|
||||||
|
let vestLastPostAt = 0;
|
||||||
let vestOpenedAt = 0;
|
let vestOpenedAt = 0;
|
||||||
|
|
||||||
/** Runs in the Vest tab's MAIN world: the session vest-relay.js holds, while
|
/** Runs in the Vest tab's MAIN world: the session vest-relay.js holds, while
|
||||||
@@ -1276,7 +1280,7 @@ async function relayVestSession(cfg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fingerprint = `${found.expiresAt}:${found.accessToken.length}:${found.accessToken.slice(-16)}`;
|
const fingerprint = `${found.expiresAt}:${found.accessToken.length}:${found.accessToken.slice(-16)}`;
|
||||||
if (fingerprint === vestLastRelayed) return;
|
if (fingerprint === vestLastRelayed && now - vestLastPostAt < VEST_REPOST_MS) return;
|
||||||
const res = await fetch(`${cfg.apiBase}/api/vest/session`, {
|
const res = await fetch(`${cfg.apiBase}/api/vest/session`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -1288,9 +1292,12 @@ async function relayVestSession(cfg) {
|
|||||||
if (res.status === 404) throw new Error('this dashboard is too old for the Vest relay');
|
if (res.status === 404) throw new Error('this dashboard is too old for the Vest relay');
|
||||||
if (!res.ok) throw new Error(`dashboard refused the session: ${body.reason || body.error || `HTTP ${res.status}`}`);
|
if (!res.ok) throw new Error(`dashboard refused the session: ${body.reason || body.error || `HTTP ${res.status}`}`);
|
||||||
vestLastRelayed = fingerprint;
|
vestLastRelayed = fingerprint;
|
||||||
|
vestLastPostAt = now;
|
||||||
const short = `${found.primaryAddr.slice(0, 6)}…${found.primaryAddr.slice(-4)}`;
|
const short = `${found.primaryAddr.slice(0, 6)}…${found.primaryAddr.slice(-4)}`;
|
||||||
const mins = Math.max(0, Math.round((found.expiresAt - Date.now()) / 60000));
|
const mins = Math.max(0, Math.round((found.expiresAt - Date.now()) / 60000));
|
||||||
await setState({ vest: `Relayed ${short}${body.used ? '' : ' (no Vest firm here uses this wallet)'} · good ${mins}m` });
|
const note = Array.isArray(body.bound) && body.bound.length > 0 ? ` · bound to ${body.bound.join(', ')}`
|
||||||
|
: body.used ? '' : ' (no Vest firm here uses this wallet)';
|
||||||
|
await setState({ vest: `Relayed ${short}${note} · good ${mins}m` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 2FA: the person's authenticator code for the firm site you are on ───────
|
// ── 2FA: the person's authenticator code for the firm site you are on ───────
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "AutoFirmer Capture",
|
"name": "AutoFirmer Capture",
|
||||||
"version": "0.17.0",
|
"version": "0.17.1",
|
||||||
|
|
||||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxdbOMRR+58a9dHlbIKtlALtS3E4eltBGqfTnUc0lzJuWj5ZA4+5YFSdUnxv5N1ogxwpJUGkUV3MnKJuZnTekCmsNwjCy41a20Xtc7jQLHrIBZkesB6cB+d0l9Vo8M6EC3ZMD8Iv6MdAalU3GZh1Hn9vm24NjJ+xUT1qNIVHEcNUIb9evYobR/XHv+JAp6bOFnaLM+3wpyYKe4efMhsz0F86u9thRNhQnraTqUWfnKNpsHPK2zivaYHrziEEWX7Y3EsMHkf2FRhkUe7IDoxeJM49DBToTo9sAJUkcgLeGCGZLo7H3mcxyn/zERYR+rD3k5xdeBtjoZHXFx2+FanLb9wIDAQAB",
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxdbOMRR+58a9dHlbIKtlALtS3E4eltBGqfTnUc0lzJuWj5ZA4+5YFSdUnxv5N1ogxwpJUGkUV3MnKJuZnTekCmsNwjCy41a20Xtc7jQLHrIBZkesB6cB+d0l9Vo8M6EC3ZMD8Iv6MdAalU3GZh1Hn9vm24NjJ+xUT1qNIVHEcNUIb9evYobR/XHv+JAp6bOFnaLM+3wpyYKe4efMhsz0F86u9thRNhQnraTqUWfnKNpsHPK2zivaYHrziEEWX7Y3EsMHkf2FRhkUe7IDoxeJM49DBToTo9sAJUkcgLeGCGZLo7H3mcxyn/zERYR+rD3k5xdeBtjoZHXFx2+FanLb9wIDAQAB",
|
||||||
"update_url": "https://ext.juicerroom.com/updates.xml",
|
"update_url": "https://ext.juicerroom.com/updates.xml",
|
||||||
|
|||||||
+5
-2
@@ -49,7 +49,7 @@ import { firmInstruments } from './instruments';
|
|||||||
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext } from './platform-client';
|
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext } from './platform-client';
|
||||||
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage } from './platform-client';
|
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage } from './platform-client';
|
||||||
import { encodeContractName, decodeContractName, roundToStep, scoreCandidate } from './dxtrade-helpers';
|
import { encodeContractName, decodeContractName, roundToStep, scoreCandidate } from './dxtrade-helpers';
|
||||||
import { vestSessionFor, dropVestSession } from './vest-session';
|
import { vestSessionFor, dropVestSession, listVestSessions } from './vest-session';
|
||||||
import {
|
import {
|
||||||
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
|
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
|
||||||
parseTxn, bookTxns, awaitingLegs, unrealizedPnl, hourlyVolatility, marketOpen, num, toMs, formatDecimal,
|
parseTxn, bookTxns, awaitingLegs, unrealizedPnl, hourlyVolatility, marketOpen, num, toMs, formatDecimal,
|
||||||
@@ -231,7 +231,10 @@ export class VestClient implements PlatformClient {
|
|||||||
private userToken(): string | null {
|
private userToken(): string | null {
|
||||||
const session = vestSessionFor(this.wallet, Date.now(), USER_TOKEN_MIN_MS);
|
const session = vestSessionFor(this.wallet, Date.now(), USER_TOKEN_MIN_MS);
|
||||||
if (session) return session.accessToken;
|
if (session) return session.accessToken;
|
||||||
const msg = `no Vest session for ${this.wallet} — keep a Vest tab signed in to that wallet open in Chrome with the AutoFirmer extension`;
|
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`;
|
||||||
if (this.lastFetchErrors['login'] !== msg) console.warn(`${this.tag} ${msg}`);
|
if (this.lastFetchErrors['login'] !== msg) console.warn(`${this.tag} ${msg}`);
|
||||||
this.lastAuthError = this.lastFetchErrors['login'] = msg;
|
this.lastAuthError = this.lastFetchErrors['login'] = msg;
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Binds the Vest login the extension relays to this machine's Vest firm, so
|
||||||
|
* nobody has to copy a wallet address into the dashboard.
|
||||||
|
*
|
||||||
|
* A Vest firm's only credential here is the wallet of the login whose session
|
||||||
|
* it trades (lib/vest-session.ts). One person per machine, and Vest allows one
|
||||||
|
* profile per person, so the Vest tab signed in on this machine is the one to
|
||||||
|
* trade. It is bound automatically when:
|
||||||
|
* - the firm has no credential yet. A saved one is never replaced: someone
|
||||||
|
* chose it, and a different login in the tab is a mistake to flag (the
|
||||||
|
* client says so), not one to follow;
|
||||||
|
* - exactly one wallet has a live relayed session. Two logins in two tabs
|
||||||
|
* can't be told apart, so the dashboard's one-click form decides.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getCachedCatalog } from './firm-catalog';
|
||||||
|
import { getCredentials, setCredentials } from './db';
|
||||||
|
import { listVestSessions } from './vest-session';
|
||||||
|
import { reconcileClients } from './clients';
|
||||||
|
import { scheduleBackupPush } from './backup-sync';
|
||||||
|
|
||||||
|
/** Bind the relayed wallet to every Vest firm here that has no credential.
|
||||||
|
* Returns the names of the firms it bound. */
|
||||||
|
export function autoBindVestWallet(now = Date.now()): string[] {
|
||||||
|
const live = [...new Set(listVestSessions().filter((s) => s.expiresAt > now).map((s) => s.primaryAddr))];
|
||||||
|
if (live.length !== 1) return [];
|
||||||
|
const wallet = live[0];
|
||||||
|
const bound: string[] = [];
|
||||||
|
for (const firm of getCachedCatalog()) {
|
||||||
|
if (firm.platform !== 'vest') continue;
|
||||||
|
const creds = getCredentials(firm.id);
|
||||||
|
if (creds && (creds.apiKey || creds.username || creds.password)) continue;
|
||||||
|
setCredentials(firm.id, firm.name, '', '', wallet);
|
||||||
|
bound.push(firm.name);
|
||||||
|
console.log(`[vest] ${firm.name}: bound to wallet ${wallet}, the Vest login signed in on this machine`);
|
||||||
|
}
|
||||||
|
if (bound.length > 0) {
|
||||||
|
reconcileClients();
|
||||||
|
scheduleBackupPush();
|
||||||
|
}
|
||||||
|
return bound;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user