// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini /** * PLAN §12 core items 4, 5, 11 (CLI side): token creation with every description family (no-shell * argv and through a real shell), verbatim storage, deterministic `token list` (active tokens) and * `token list all` (every token) as padded tables, their usage errors, deletion. */ import fs from 'node:fs'; import { test, expect, assertDbInvariants, countRows, expectCliFailure, expectCliSuccess, goQuote, msToNanos, parseTokenList, readOperations, readToken, readTokens, rfc3339ToNanos, RFC3339NANO_UTC_RE, shellQuote, EMPTY_TOKEN_LIST, USAGE_TEXT, fileDigest, formatTokenList, padTable, runeLength, type CliRunner, type TokenListRow, } from '../../harness'; import { expectedTokenListStdout, stateDigest } from '../../harness/personas'; const hex = (s: string | Buffer) => Buffer.from(s as string).toString('hex').toUpperCase(); /** Creates a token, asserting created_at lies within the invocation window. */ async function createChecked(cli: CliRunner, dbPath: string, description: string | undefined): Promise { const before = msToNanos(Date.now() - 1); const token = await cli.tokenNew(description); const after = msToNanos(Date.now() + 1); const row = readToken(dbPath, token)!; expect(row, 'row exists').toBeTruthy(); expect(row.descriptionHex, 'description stored byte-for-byte').toBe(hex(description ?? '')); expect(row.active).toBe(1); expect(row.deleted_at).toBeNull(); expect(row.created_at).toMatch(RFC3339NANO_UTC_RE); const created = rfc3339ToNanos(row.created_at); expect(created >= before && created <= after, `created_at ${row.created_at} within the invocation window`).toBe(true); return token; } const DESCRIPTIONS: Array<[string, string | undefined]> = [ ['omitted (no argument)', undefined], ['explicit empty argument', ''], ['normal words', 'ci pipeline'], ['whitespace only', ' \t '], ['leading/trailing spaces', ' padded value '], ['newlines, tabs, CR', 'line1\nline2\tcol\r\nend'], ['ASCII punctuation', `!"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~`], ['Unicode letters, emoji, RTL, combining', 'café ☕ 日本語 עברית 🚀 é Ωmega'], ['non-printable Unicode (NBSP, ZWSP, LS, NEL, tag)', ' nb​z
x\u0085y\u{E0001}'], ['ASCII control characters and ESC sequences', '\x01\x07\x08\x1b[31mred\x1b[0m\x7f'], ['SQL injection payload', `'); DROP TABLE tokens; --`], ['SQL quote games', `x' OR '1'='1`], ['shell metacharacters', '$(touch /tmp/pwned) `id` ; rm -rf / && echo $HOME | cat > /dev/null &'], ['looks like an option', '--help'], ['looks like a subcommand', 'delete'], ['Go format verbs', '%s %d %v %!x(MISSING) %%'], ['backslash escapes kept literal', '\\n \\t \\x00 \\u0041'], ['very long ASCII (100 000 bytes)', 'x'.repeat(100_000)], ['long multi-byte (30 000 × 日 = 90 000 bytes)', '日'.repeat(30_000)], ['maximum single argv string (131 071 bytes)', 'a'.repeat(131_071)], ]; test.describe('token new: description stored verbatim (argv, no shell)', () => { for (const [name, description] of DESCRIPTIONS) { test(name, async ({ cli, ws }) => { const token = await createChecked(cli, ws.dbPath, description); const rows = await cli.tokenList(); expect(rows).toHaveLength(1); expect(rows[0].token).toBe(token); expect(rows[0].descriptionQuoted, 'DESCRIPTION column is Go-quoted').toBe(goQuote(description ?? '')); expect(rows[0].descriptionBytes.equals(Buffer.from(description ?? '')), 'list round-trips the bytes').toBe(true); expect(rows[0].active).toBe(true); expect(rows[0].deletedAt).toBeNull(); assertDbInvariants(ws.dbPath, { tokenCount: 1, operationCount: 0 }); }); } test('one argument larger than the kernel limit is refused by the OS, not the app', async ({ cli }) => { // MAX_ARG_STRLEN is 131072 bytes including the terminating NUL: exec fails before dpsapi runs. await expect(cli.run(['token', 'new', 'a'.repeat(131_072)])).rejects.toThrow(/harness:spawn|E2BIG/); }); }); test.describe('token new through a real shell (quoting)', () => { const tricky = [ 'two words', `it's "quoted"`, '$HOME and $(id) and `id`', 'semi;colon|pipe&>redirect { for (const d of tricky) { await test.step(JSON.stringify(d), async () => { const r = await cli.shell(`"$DPSAPI_BIN" token new ${shellQuote(d)}`); expectCliSuccess(r, { stderr: '' }); expect(r.stdout).toMatch(/^[A-Za-z0-9_-]{43}\n$/); expect(readToken(ws.dbPath, r.stdout.trim())!.descriptionHex).toBe(hex(d)); }); } const rows = await cli.tokenList(); expect(rows.map((r) => r.description).sort()).toEqual([...tricky].sort()); assertDbInvariants(ws.dbPath, { tokenCount: tricky.length }); }); test('unquoted multi-word description is a usage error and creates nothing', async ({ cli, ws }) => { const r = await cli.shell('"$DPSAPI_BIN" token new ci pipeline'); expectCliFailure(r, { exitCode: 2, message: 'usage error: token new takes at most one argument' }); expect(fs.existsSync(ws.dbPath), 'usage errors never open/create the database').toBe(false); }); test('double quotes expand variables in the shell (the app stores what it receives)', async ({ cli, ws }) => { const r = await cli.shell('"$DPSAPI_BIN" token new "home=$HOME_FOR_TEST"', { env: { HOME_FOR_TEST: '/home/someone' } }); expectCliSuccess(r); expect(readToken(ws.dbPath, r.stdout.trim())!.description).toBe('home=/home/someone'); }); test('invalid UTF-8 bytes from the shell are stored exactly and listed with \\x escapes', async ({ cli, ws }) => { const r = await cli.shell(`"$DPSAPI_BIN" token new "$(printf 'bad\\377byte\\300\\200')"`); expectCliSuccess(r); const token = r.stdout.trim(); expect(readToken(ws.dbPath, token)!.descriptionHex).toBe('626164FF62797465C080'); const rows = await cli.tokenList(); expect(rows[0].descriptionQuoted).toBe('"bad\\xffbyte\\xc0\\x80"'); expect(rows[0].descriptionBytes.toString('hex').toUpperCase()).toBe('626164FF62797465C080'); assertDbInvariants(ws.dbPath); }); }); test.describe('token list', () => { test('fresh database: padded header only (list and list all), database and schema created', async ({ cli, ws }) => { expect(EMPTY_TOKEN_LIST).toBe('TOKEN DESCRIPTION ACTIVE CREATED_AT DELETED_AT\n'); for (const args of [['token', 'list'], ['token', 'list', 'all']]) { const db = ws.path(`fresh-${args.length}.db`); expect(fs.existsSync(db)).toBe(false); const r = await cli.run(args, { env: { DB_PATH: db } }); expectCliSuccess(r, { stdout: EMPTY_TOKEN_LIST, stderr: '' }); expect(fs.existsSync(db), `${args.join(' ')}: database created`).toBe(true); assertDbInvariants(db, { tokenCount: 0, operationCount: 0 }); } }); test('only inactive tokens: token list prints the header only, token list all prints them', async ({ cli, ws }) => { const a = await cli.tokenNew('gone a'); const b = await cli.tokenNew('gone b'); await cli.tokenDelete(a); await cli.tokenDelete(b); expectCliSuccess(await cli.run(['token', 'list']), { stdout: EMPTY_TOKEN_LIST, stderr: '' }); const all = await cli.tokenListAll(); expect(all.map((r) => [r.token, r.active, r.deletedAt !== null])).toEqual([ [a, false, true], [b, false, true], ]); expect((await cli.run(['token', 'list', 'all'])).stdout).toBe(expectedTokenListStdout(ws.dbPath, 'all')); }); test('deterministic, ordered by created_at then token, exact values for active and inactive rows', async ({ cli, ws }) => { test.slow(); const tokens: string[] = []; // sequential and concurrent creations (concurrent ones may share timestamps closely) for (let i = 0; i < 6; i++) tokens.push(await cli.tokenNew(`seq ${i}`)); tokens.push(...(await Promise.all(Array.from({ length: 12 }, (_, i) => cli.tokenNew(`par ${i}`))))); const deleted = [tokens[1], tokens[7], tokens[12]]; for (const t of deleted) await cli.tokenDelete(t); const a = await cli.run(['token', 'list', 'all']); const b = await cli.run(['token', 'list', 'all']); expectCliSuccess(a, { stderr: '' }); expect(b.stdout, 'two consecutive listings are byte-identical').toBe(a.stdout); expect(a.stdout, 'token list all equals the stored rows (padded table)').toBe(expectedTokenListStdout(ws.dbPath, 'all')); const rows = parseTokenList(a.stdout); expect(rows).toHaveLength(tokens.length); expect(new Set(rows.map((r) => r.token))).toEqual(new Set(tokens)); // plain `token list`: exactly the active rows, same order, its own (narrower or equal) widths const active = await cli.run(['token', 'list']); expectCliSuccess(active, { stderr: '' }); expect(active.stdout, 'token list equals the stored active rows').toBe(expectedTokenListStdout(ws.dbPath, 'active')); expect(parseTokenList(active.stdout).map((r) => r.token)).toEqual(rows.filter((r) => r.active).map((r) => r.token)); // ordering: created_at (as time, not string), then token (bytewise) const key = (r: TokenListRow) => [rfc3339ToNanos(r.createdAt), r.token] as const; for (let i = 1; i < rows.length; i++) { const [ta, ka] = key(rows[i - 1]); const [tb, kb] = key(rows[i]); expect(ta < tb || (ta === tb && ka < kb), `row ${i} ordered after row ${i - 1}`).toBe(true); } // every row equals the database const db = new Map(readTokens(ws.dbPath).map((t) => [t.token, t])); for (const r of rows) { const d = db.get(r.token)!; expect(r.createdAt).toBe(d.created_at); expect(r.deletedAt).toBe(d.deleted_at); expect(r.active).toBe(d.active === 1); expect(r.descriptionQuoted).toBe(goQuote(d.description)); expect(r.active).toBe(!deleted.includes(r.token)); if (!r.active) { expect(r.deletedAt).toMatch(RFC3339NANO_UTC_RE); expect(rfc3339ToNanos(r.deletedAt!) >= rfc3339ToNanos(r.createdAt)).toBe(true); expect(r.line).toMatch(new RegExp(` false ${r.createdAt.replace(/\./g, '\\.')} {2,}${r.deletedAt!.replace(/\./g, '\\.')}$`)); } else { expect(r.line).toMatch(new RegExp(` true ${r.createdAt.replace(/\./g, '\\.')} {2,}-$`)); } } assertDbInvariants(ws.dbPath, { tokenCount: 18, operationCount: 0 }); }); }); test.describe('token list [all]: active by default, every token with "all", padded table', () => { test('default hides inactive tokens; all shows them with ACTIVE false and DELETED_AT set; both byte-exact padded tables', async ({ cli, ws }) => { const keep = await cli.tokenNew('keep'); // the longest description belongs to a token that gets deleted: widths differ between the two listings const long = await cli.tokenNew('a much longer description that only token list all prints'); const other = await cli.tokenNew('other'); await cli.tokenDelete(long); const active = await cli.run(['token', 'list']); const all = await cli.run(['token', 'list', 'all']); expectCliSuccess(active, { stderr: '' }); expectCliSuccess(all, { stderr: '' }); const db = new Map(readTokens(ws.dbPath).map((t) => [t.token, t])); const entry = (t: string) => { const d = db.get(t)!; return { token: t, description: d.description, active: d.active === 1, createdAt: d.created_at, deletedAt: d.deleted_at }; }; expect(active.stdout, 'token list: active rows only').toBe(formatTokenList([keep, other].map(entry))); expect(all.stdout, 'token list all: every row').toBe(formatTokenList([keep, long, other].map(entry))); // explicit layout of the default listing (widths computed over the printed lines only) const k = entry(keep); const o = entry(other); const cw = 2 + Math.max(10, k.createdAt.length, o.createdAt.length); expect(active.stdout).toBe( 'TOKEN' + ' '.repeat(40) + 'DESCRIPTION ACTIVE ' + 'CREATED_AT'.padEnd(cw) + 'DELETED_AT\n' + `${keep} "keep" true ${k.createdAt.padEnd(cw)}-\n` + `${other} "other" true ${o.createdAt.padEnd(cw)}-\n`, ); const deleted = parseTokenList(all.stdout).find((r) => r.token === long)!; expect(deleted.active).toBe(false); expect(deleted.deletedAt).toBe(db.get(long)!.deleted_at); expect(deleted.deletedAt).toMatch(RFC3339NANO_UTC_RE); expect(active.stdout.includes(long), 'the inactive token is not printed by token list').toBe(false); expect(active.stdout.split('\n')[0].length, 'narrower DESCRIPTION column without the long inactive row').toBeLessThan(all.stdout.split('\n')[0].length); for (const out of [active.stdout, all.stdout]) expect(out.split('\n').filter((l) => / $/.test(l)), 'no trailing whitespace').toEqual([]); expect(readOperations(ws.dbPath)).toEqual([]); assertDbInvariants(ws.dbPath, { tokenCount: 3, operationCount: 0 }); }); test('descriptions with runs of spaces, escaped quotes, multi-byte runes and escapes: columns aligned in runes, parser round-trips', async ({ cli, ws }) => { const descriptions = ['', 'two spaces three', 'say "hi" \\ back', '日本語', '🚀🚀', 'e\u0301 combining', 'tab\there', '\x01\x7f\xff', '"', ' ', 'x'.repeat(70)]; const tokens: string[] = []; for (const d of descriptions) tokens.push(await cli.tokenNew(d)); await cli.tokenDelete(tokens[3]); for (const scope of ['active', 'all'] as const) { const r = await cli.run(scope === 'all' ? ['token', 'list', 'all'] : ['token', 'list']); expectCliSuccess(r, { stderr: '' }); expect(r.stdout, `${scope}: exact padded table`).toBe(expectedTokenListStdout(ws.dbPath, scope)); const rows = parseTokenList(r.stdout); const want = scope === 'all' ? descriptions : descriptions.filter((_, i) => i !== 3); expect(rows.map((x) => x.description).sort()).toEqual([...want].sort()); expect(rows.every((x) => x.descriptionQuoted === goQuote(x.descriptionBytes))).toBe(true); // every cell of a column starts at the same rune offset (display width is irrelevant to the app) const lines = r.stdout.slice(0, -1).split('\n'); const offset = (l: string, cell: string) => runeLength(l.slice(0, l.indexOf(cell))); const hdr = lines[0]; for (const x of rows) { expect(offset(x.line, x.descriptionQuoted), 'DESCRIPTION column').toBe(offset(hdr, 'DESCRIPTION')); expect(runeLength(x.line) - runeLength(x.line.slice(x.line.lastIndexOf(x.deletedAt ?? '-'))), 'DELETED_AT column').toBe(offset(hdr, 'DELETED_AT')); } expect(padTable([lines[0].split(/ +/), ...rows.map((x) => [x.token, x.descriptionQuoted, String(x.active), x.createdAt, x.deletedAt ?? '-'])])).toBe(r.stdout); } }); const UNKNOWN_ARGUMENT = 'unknown token list argument'; const TOO_MANY = 'token list takes at most one argument'; const LIST_USAGE: Array<[string[], string]> = [ [['ALL'], UNKNOWN_ARGUMENT], [['All'], UNKNOWN_ARGUMENT], [[''], UNKNOWN_ARGUMENT], [['--all'], UNKNOWN_ARGUMENT], [['-a'], UNKNOWN_ARGUMENT], [[' all'], UNKNOWN_ARGUMENT], [['all '], UNKNOWN_ARGUMENT], [['all\n'], UNKNOWN_ARGUMENT], [['\u0430ll'], UNKNOWN_ARGUMENT], [['active'], UNKNOWN_ARGUMENT], [['inactive'], UNKNOWN_ARGUMENT], [['extra'], UNKNOWN_ARGUMENT], [['all', 'all'], TOO_MANY], [['all', ''], TOO_MANY], [['', 'all'], TOO_MANY], [['ALL', 'x'], TOO_MANY], [['a', 'b', 'c'], TOO_MANY], ]; test(`${LIST_USAGE.length} invalid token list arguments → exit 2, exact stderr (nothing echoed), no database created, existing one byte-identical`, async ({ cli, ws }) => { const run = async (dbPath: string) => { for (const [args, msg] of LIST_USAGE) { const r = await cli.run(['token', 'list', ...args], { env: { DB_PATH: dbPath } }); expectCliFailure(r, { exitCode: 2, message: `usage error: ${msg}`, usage: true }); expect(r.stderr, JSON.stringify(args)).toBe(`dpsapi: usage error: ${msg}\n${USAGE_TEXT}`); } }; await run(ws.dbPath); expect(fs.existsSync(ws.dbPath), 'usage errors never open/create the database').toBe(false); expect(fs.readdirSync(ws.dir)).toEqual([]); const t = await cli.tokenNew('existing'); await cli.tokenDelete(t); await cli.tokenNew('still active'); const file = fileDigest(ws.dbPath); const state = stateDigest(ws.dbPath); await run(ws.dbPath); expect(fileDigest(ws.dbPath)).toBe(file); expect(stateDigest(ws.dbPath)).toBe(state); }); }); test.describe('token delete', () => { test('deactivates silently; repeated/unknown/near-match deletions fail without changing state', async ({ cli, ws }) => { const keep = await cli.tokenNew('keep'); const victim = await cli.tokenNew('victim'); const before = msToNanos(Date.now() - 1); const r = await cli.run(['token', 'delete', victim]); const after = msToNanos(Date.now() + 1); expectCliSuccess(r, { stdout: '', stderr: '' }); const row = readToken(ws.dbPath, victim)!; expect(row.active).toBe(0); expect(row.deleted_at).toMatch(RFC3339NANO_UTC_RE); const del = rfc3339ToNanos(row.deleted_at!); expect(del >= before && del <= after, 'deleted_at within the delete invocation').toBe(true); expect(readToken(ws.dbPath, keep)!.active).toBe(1); const snapshot = JSON.stringify(readTokens(ws.dbPath)); const flip = (s: string) => s.replace(/[a-zA-Z]/, (c) => (c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase())); const failures: Array<[string, string, string]> = [ ['already inactive', victim, 'token already inactive'], ['unknown', 'A'.repeat(43), 'token not found'], ['case changed', flip(keep), 'token not found'], ['leading space', ` ${keep}`, 'token not found'], ['trailing newline', `${keep}\n`, 'token not found'], ['prefix', keep.slice(0, 42), 'token not found'], ['extended', keep + 'x', 'token not found'], ['empty string', '', 'token not found'], ['SQL wildcard', '%', 'token not found'], ['SQL injection', `' OR '1'='1`, 'token not found'], ]; for (const [name, arg, message] of failures) { await test.step(name, async () => { const f = await cli.run(['token', 'delete', arg]); expectCliFailure(f, { exitCode: 1, message }); if (arg.length > 0) expect(f.stderr.includes(arg), 'supplied token never echoed').toBe(false); expect(JSON.stringify(readTokens(ws.dbPath)), 'no token row changed').toBe(snapshot); }); } const rows = await cli.tokenListAll(); expect(rows.map((x) => [x.token, x.active])).toEqual([ [keep, true], [victim, false], ]); expect((await cli.tokenList()).map((x) => x.token), 'token list hides the deleted token').toEqual([keep]); expect(readOperations(ws.dbPath), 'CLI commands never write audit rows').toEqual([]); assertDbInvariants(ws.dbPath, { tokenCount: 2, operationCount: 0 }); }); test('a deleted token stays deleted and keeps its original created_at and description', async ({ cli, ws }) => { const t = await cli.tokenNew('history'); const created = readToken(ws.dbPath, t)!.created_at; await cli.tokenDelete(t); const first = readToken(ws.dbPath, t)!; expectCliFailure(await cli.run(['token', 'delete', t]), { exitCode: 1, message: 'token already inactive' }); const second = readToken(ws.dbPath, t)!; expect(second).toEqual(first); expect(second.created_at).toBe(created); expect(second.description).toBe('history'); expect(countRows(ws.dbPath, 'tokens')).toBe(1); }); });