Show the person's 2FA code on the firm site the extension is on

On a firm site, the extension draws a small corner chip with this
machine's person's authenticator code for that site, a countdown, and a
Copy button. It can be minimised to a "2FA" pill for the tab's session.

- /api/autobuyer/otp?host= relays to the master's
  /api/otp?person=<instance_id>&host=, which matches the host to a firm's
  Site link and the person's assigned codes. No master link, a master
  that can't be reached, or no match all come back as no codes. An answer
  without scope: 'person' is refused: an older master ignores ?person=
  and would return every stored code.
- The chip lives in a shadow root, counts down on its own, refetches
  when a code expires, and stays hidden for a minute after the runner
  last located something, so automated clicks never land on it. Copy
  uses execCommand inside the click, since the async Clipboard API can
  stall on a permission prompt, and leaves focus in the site's code field.
- Apex (*.apextraderfunding.com) joins host_permissions, which also
  brings it into capture like the other firm sites. Version 0.16.0.
- CatalogFirm mirrors the master's new optional siteUrl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-29 17:19:32 -05:00
co-authored by Claude Opus 5.5
parent 58e7414849
commit d6bb5a7221
5 changed files with 330 additions and 2 deletions
+35
View File
@@ -0,0 +1,35 @@
import { getSetting } from '@/lib/db';
import { corsJson, corsPreflight } from '../cors';
/** The extension's 2FA lookup. It knows the tab's host; this dashboard knows
* which person it is (the master-issued `instance_id`) and where the master
* is, so it asks the master for this person's codes on that site and passes
* them through.
*
* Never an error status: no link to the master, or a master that can't be
* reached, is just "no codes" with a reason — the extension shows nothing. */
export async function GET(request: Request) {
const host = new URL(request.url).searchParams.get('host')?.trim().toLowerCase();
if (!host) return corsJson({ codes: [], reason: 'host is required' });
const master = getSetting('master_dashboard_url')?.trim().replace(/\/+$/, '');
const person = getSetting('instance_id')?.trim();
if (!master || !person) return corsJson({ codes: [], reason: 'not linked to master' });
const url = `${master}/api/otp?person=${encodeURIComponent(person)}&host=${encodeURIComponent(host)}`;
try {
const res = await fetch(url, { cache: 'no-store', signal: AbortSignal.timeout(5000) });
if (!res.ok) return corsJson({ codes: [], reason: `master answered ${res.status}` });
const data = (await res.json()) as { codes?: unknown[]; serverTime?: number; scope?: string };
// A master from before per-person lookups ignores ?person= and answers
// with every stored code — never show those.
if (data.scope !== 'person') return corsJson({ codes: [], reason: 'master is too old for 2FA lookups' });
return corsJson({ codes: data.codes ?? [], serverTime: data.serverTime ?? Date.now() });
} catch (err) {
return corsJson({ codes: [], reason: `master unreachable: ${(err as Error).message}` });
}
}
export async function OPTIONS() {
return corsPreflight();
}