Files
buzz/beehive/test/buzz-owner-key.test.ts
Logan Johnson 9aef40a3f6 fix(beehive): derive first-use owner directly from Buzz key
Bypass manual routing for Buzz sign-in, derive and validate identity in the controller, and initialize only public owner routing using the current Host relay after the stale/cancel fence. Retained owners still must match; custody remains session-only.

Regression drives the real Node/Bun UI once with no controller owner configured and an explicit synthetic credential fixture. Focused 12-test checks, typecheck, and first-use PTY walkthrough pass. Earlier broad-suite 400-second failure remains unresolved; no production Keychain access or native rebuild.

Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-09-16 13:22:09 -04:00

115 lines
7.3 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { nip19 } from 'nostr-tools';
import { readBuzzOwnerKey, buzzOwnerKeyErrors } from '../src/buzz-owner-key.ts';
import { createControllerConfig } from '../src/controller-config.ts';
import { managerCredential } from '../src/manager-credential.ts';
import { ManagerController } from '../src/manager-controller.ts';
import { bootstrapHostIdentity } from '../src/host-identity.ts';
import { isolatedFileCredentials } from './isolated-file-credentials.ts';
import { publicKey } from '../src/protocol.ts';
const secret = '1'.repeat(64), owner = publicKey(secret);
const nsec = nip19.nsecEncode(Buffer.from(secret, 'hex'));
const blob = JSON.stringify({ identity: nsec, 'agent:fixture': 'unrelated-secret-not-returned' });
test('explicit Buzz signin validates actual blob identity and excludes all other bytes', async () => {
for (const [label, read, reason] of [
['success', () => blob, undefined],
['missing item', () => null, 'missing'],
['missing identity', () => '{}', 'missing'],
['denied', () => { throw Error(secret); }, 'access'],
['cancelled OS prompt', () => { throw Error(nsec); }, 'access'],
['malformed JSON', () => '{'+secret, 'malformed'],
['malformed nsec', () => JSON.stringify({identity:secret}), 'malformed'],
['invalid scalar', () => JSON.stringify({identity:nip19.nsecEncode(new Uint8Array(32))}), 'malformed'],
['wrong owner', () => JSON.stringify({identity:nip19.nsecEncode(Buffer.from('2'.repeat(64),'hex'))}), 'mismatch'],
] as const) {
const home = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-buzz-owner-'));
createControllerConfig(join(home,'.beehive','owner'),owner,'wss://fixture.invalid');
let reads = 0, connections = 0, directoryReads = 0;
const outputs: unknown[] = [];
const c = new ManagerController(home, s => outputs.push(s), async (input) => {
assert.equal((input as any).action, 'signin-buzz');
const result = readBuzzOwnerKey((input as any).owner, () => { reads++; return read(); });
if (!result.ok) throw Error(buzzOwnerKeyErrors[result.reason]);
assert.deepEqual(Object.keys(result).sort(), ['ok','secret']);
return result;
}, ((_root: string, relay: string, key: string) => {
connections++; assert.equal(key, secret); assert.equal(relay, 'wss://fixture.invalid');
return { ready: Promise.resolve(), connected:false, status:()=>[], close() {} };
}) as any, async () => undefined, async (_relay,key) => { assert.equal(key,secret); directoryReads++; return []; });
try {
c.snapshot(); assert.equal(reads, 0, label);
const result = await c.request({id:1,action:'signin-buzz',values:{owner,relay:'wss://fixture.invalid'}});
assert.equal(reads,1,label);
assert.equal(result.state, reason ? 'failed' : 'completed',label);
assert.equal(connections,reason ? 0 : 1,label);
const config = join(home,'.beehive','owner','controller.json');
if (reason) { assert.equal(c.snapshot().owner,undefined); assert.deepEqual(JSON.parse(readFileSync(config,'utf8')), {version:1,owner,relay:'wss://fixture.invalid'}); }
else {
assert.equal(c.snapshot().owner,owner);
assert.deepEqual(JSON.parse(readFileSync(config,'utf8')), {version:1,owner,relay:'wss://fixture.invalid'});
await c.request({id:2,action:'directory-refresh'});
assert.equal(reads,1,'refresh reuses explicit session, never rereads Buzz');
assert.equal(directoryReads,2);
await c.request({id:3,action:'signout'}); assert.equal(c.snapshot().owner,undefined);
}
const output = JSON.stringify(outputs);
for (const privateValue of [secret,nsec,'unrelated-secret-not-returned']) assert.ok(!output.includes(privateValue),label);
} finally { c.close(); rmSync(home,{recursive:true,force:true}); }
}
});
test('Buzz late completion and untrusted helper mismatch cannot configure or connect', async () => {
for (const mode of ['cancel','close','mismatch','malformed','first-use-cancel','first-use-close','first-use-malformed'] as const) {
const home = mkdtempSync(join(tmpdir(),'beehive-pairing-cli-buzz-stale-'));
bootstrapHostIdentity(join(home,'.beehive','host'),'Fixture',owner,'wss://fixture.invalid',isolatedFileCredentials(join(home,'credentials.json')));
if (!mode.startsWith('first-use')) createControllerConfig(join(home,'.beehive','owner'),owner,'wss://fixture.invalid');
let finish!: (value: unknown) => void;
const c = new ManagerController(home,()=>{},()=>new Promise(resolve=>{finish=resolve;}), (()=>{throw Error('must never connect');}) as any, async()=>undefined);
try {
const pending = c.request({id:1,action:'signin-buzz',values:{owner,relay:'wss://fixture.invalid'}});
if (mode.endsWith('cancel')) c.cancel();
if (mode.endsWith('close')) c.close();
finish({ok:true,secret:mode === 'mismatch' ? '2'.repeat(64) : mode.endsWith('malformed') ? nsec : secret});
assert.notEqual((await pending).state,'completed');
assert.equal(c.snapshot().owner,undefined);
assert.equal(existsSync(join(home,'.beehive','owner','controller.json')),!mode.startsWith('first-use'));
assert.ok(!JSON.stringify(c.snapshot()).includes(nsec));
} finally { c.close(); rmSync(home,{recursive:true,force:true}); }
}
});
test('actual signin child reads only the canonical item; safe missing/access errors and no Beehive copy', async () => {
const helper = new URL('./buzz-owner-child-fixture.ts', import.meta.url);
assert.deepEqual(await managerCredential({action:'signin-buzz',owner:null,fixture:{blob}},new AbortController().signal,helper),{ok:true,secret});
assert.deepEqual(await managerCredential({action:'signin-buzz',owner,fixture:{blob}},new AbortController().signal,helper),{ok:true,secret});
for (const [fixture, pattern] of [[{blob:null},/No Buzz owner key/],[{denied:nsec},/locked, denied, cancelled/],[{blob:'invalid'},/malformed/],[{blob},/does not match/]] as const) {
await assert.rejects(managerCredential({action:'signin-buzz',owner:pattern.source.includes('does not match') ? publicKey('2'.repeat(64)) : owner,fixture},new AbortController().signal,helper), error => {
assert.match(String(error),pattern); assert.ok(!String(error).includes(nsec)); return true;
});
}
});
test('retained owner and relay win over supplied Buzz routing, including mismatch', async () => {
const home = mkdtempSync(join(tmpdir(),'beehive-pairing-cli-buzz-retained-'));
const directory = join(home,'.beehive','owner');
createControllerConfig(directory,owner,'wss://fixture.invalid');
const before = readFileSync(join(directory,'controller.json'));
const c = new ManagerController(home,()=>{},async input => {
assert.equal((input as any).owner,owner);
return {ok:true,secret:'2'.repeat(64)};
}, (()=>{throw Error('must never connect');}) as any, async()=>undefined);
try {
assert.equal((await c.request({id:1,action:'signin-buzz',values:{owner:publicKey('2'.repeat(64)),relay:'wss://other.invalid'}})).state,'failed');
assert.deepEqual(readFileSync(join(directory,'controller.json')),before);
assert.equal(c.snapshot().owner,undefined);
} finally { c.close(); rmSync(home,{recursive:true,force:true}); }
});