// Making the things a browser is asked for, not only finding them. // // A freelance brief wants a CV. A marketplace wants a portfolio PDF. A signup wants a code from an // authenticator app. A menu opens only under the pointer. Every one of those is an ordinary part of // earning money online, and every one of them stopped the work dead while the browser could find, // read and click perfectly well. These tests pin the parts that must be exactly right — the RFC // vectors, what counts as a secret, the print wrapper, the filename — and that the tools are on the // palette, in the registry, and within every role's reach. import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import craft from '../src/tools/craft.js'; import parts from '../src/tools/craft-parts.js'; import tools from '../src/tools/index.js'; import agent from '../src/agent.js'; import roles from '../src/roles.js'; const { totp, base32Decode, secretFrom, documentHtml, safeName } = parts; const craftSrc = readFileSync(fileURLToPath(new URL('../src/tools/craft.js', import.meta.url)), 'utf8') + readFileSync(fileURLToPath(new URL('../src/tools/craft-parts.js', import.meta.url)), 'utf8'); const NEW = ['make_document', 'save_totp_secret', 'totp_code', 'hover']; describe('the four are real hands, reachable by every role', () => { it('each is on the palette, in the registry, and described', () => { for (const n of NEW) { const def = agent.TOOLS.find((t) => t.function && t.function.name === n); expect(def, `${n} is on the palette`).toBeTruthy(); expect(String(def.function.description).length).toBeGreaterThan(60); expect(tools.has(n), `${n} is registered`).toBe(true); } // and nothing but tools: Object.assign would turn any other export into a callable name the // palette does not carry (the registry's own guard catches that, so keep this module honest). expect(Object.keys(craft).sort()).toEqual([...NEW].sort()); }); it('a role with its own tool list can still reach them — and attach what it made', () => { for (const role of ['general', 'research.reddit', 'qa.web']) { const got = roles.toolsFor(role, agent.TOOLS).map((t) => t.function.name); for (const n of [...NEW, 'upload_file']) expect(got, `${role} → ${n}`).toContain(n); } }); }); describe('an authenticator app, in the browser', () => { it('matches the RFC 6238 vectors (SHA-1, 30s) — the thing that must be exactly right', () => { // RFC 6238 appendix B, seed "12345678901234567890" in base32, truncated to six digits. const S = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'; expect(totp(S, { now: 59000 }).code).toBe('287082'); expect(totp(S, { now: 1111111109000 }).code).toBe('081804'); expect(totp(S, { now: 1111111111000 }).code).toBe('050471'); expect(totp(S, { now: 1234567890000 }).code).toBe('005924'); expect(totp(S, { now: 2000000000000 }).code).toBe('279037'); }); it('says how long the code has left, and rolls at the step boundary', () => { expect(totp('JBSWY3DPEHPK3PXP', { now: 30000 }).secondsLeft).toBe(30); expect(totp('JBSWY3DPEHPK3PXP', { now: 59000 }).secondsLeft).toBe(1); const a = totp('JBSWY3DPEHPK3PXP', { now: 29000 }).code; const b = totp('JBSWY3DPEHPK3PXP', { now: 31000 }).code; expect(a).not.toBe(b); }); it('decodes base32 the way a page actually prints it — spaced, padded, lower case', () => { const want = base32Decode('JBSWY3DPEHPK3PXP'); expect(base32Decode('jbsw y3dp ehpk 3pxp')).toEqual(want); expect(base32Decode('JBSWY3DPEHPK3PXP======')).toEqual(want); expect(base32Decode('')).toHaveLength(0); // 0, 1, 8 and 9 are not base32 — a string of only those decodes to nothing, and nothing is null. expect(totp('0189 !!! 0189')).toBeNull(); expect(totp('')).toBeNull(); }); it('takes the secret from an otpauth link, a labelled line, or the bare key', () => { expect(secretFrom('otpauth://totp/Upwork:carla?secret=JBSWY3DPEHPK3PXP&issuer=Upwork')).toBe('JBSWY3DPEHPK3PXP'); expect(secretFrom('Secret: JBSW Y3DP EHPK 3PXP')).toBe('JBSWY3DPEHPK3PXP'); expect(secretFrom('JBSWY3DPEHPK3PXP')).toBe('JBSWY3DPEHPK3PXP'); expect(secretFrom('scan the QR code')).toBe(''); expect(secretFrom('')).toBe(''); }); it('stores the secret with the logins, mode 600 — and never hands it back', () => { expect(craftSrc).toMatch(/TOTP_FILE = path\.join\(PROFILE_DIR, 'totp\.json'\)/); expect(craftSrc).toMatch(/\{ mode: 0o600 \}/); // The only thing an observation may carry is the six digits of the moment. The secret is read to // compute them and goes nowhere else: no observe(), step() or memo() line may interpolate it. expect(craftSrc).toMatch(/never shown again/); const said = craftSrc.split('\n').filter((l) => /ctx\.(observe|step|memo)\(/.test(l)); expect(said.length).toBeGreaterThan(6); for (const line of said) expect(line, line.trim().slice(0, 70)).not.toMatch(/\bsecret\b\s*[}`]|row\.secret|a\.secret/); }); }); describe('a document, as a document', () => { it('wraps a fragment with a print stylesheet, and leaves a whole document alone', () => { const out = documentHtml('