import { describe, it, expect, beforeAll } from 'vitest'; import bcrypt from 'bcrypt'; import { pool } from '../../lib/db/pool.js'; import { resetDb } from '../helpers/db.js'; import { migrateUp } from '../../lib/db/migrate.js'; import * as agents from '../../lib/db/repos/agents.js'; // verifyToken must be O(1): a public "selector" indexes the one candidate row, // then bcrypt verifies only that row's "verifier". (Previously it bcrypt-scanned // every token — code-review/security-sweep HIGH finding.) const owner = { kind: 'user', id: null }; let agent; beforeAll(async () => { await resetDb(); await migrateUp(); agent = await agents.create({ slug: 'tok', name: 'Tok', kind: 'claude', model: 'sonnet', capabilities: { read: true }, scopes: {} }, owner); }); describe('selector+verifier tokens', () => { it('new tokens are vk_. and verify O(1) by selector', async () => { const { token } = await agents.createToken(agent.id, 'k1'); expect(token).toMatch(/^vk_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/); // the selector is stored in plaintext and indexed const selector = token.slice(3, token.indexOf('.')); const { rows } = await pool.query('SELECT selector FROM agent_tokens WHERE selector=$1', [selector]); expect(rows).toHaveLength(1); const found = await agents.verifyToken(token); expect(found.id).toBe(agent.id); }); it('a tampered verifier (right selector, wrong secret) does not verify', async () => { const { token } = await agents.createToken(agent.id, 'k2'); const tampered = token.slice(0, -3) + 'AAA'; expect(await agents.verifyToken(tampered)).toBeNull(); }); it('legacy tokens (selector NULL, full-plaintext hash) still verify', async () => { const legacyPlain = 'vk_legacyflatToken1234567890'; const hash = await bcrypt.hash(legacyPlain, 12); await pool.query( `INSERT INTO agent_tokens(agent_id, label, token_hash, selector) VALUES($1,'legacy',$2,NULL)`, [agent.id, hash] ); const found = await agents.verifyToken(legacyPlain); expect(found.id).toBe(agent.id); }); });