mirror of
https://github.com/block/buzz.git
synced 2026-10-29 21:41:33 +00:00
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>
115 lines
7.3 KiB
TypeScript
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}); }
|
|
});
|