This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { describe, it } from 'node:test';
|
||||
import { schnorr } from '@noble/curves/secp256k1.js';
|
||||
import { getPublicKey, nip19, verifyEvent, type Event } from 'nostr-tools';
|
||||
import { buzzWorkspaceId } from '../src/routes/buzz';
|
||||
import {
|
||||
BuzzNotifier,
|
||||
decodeBuzzAuthorization,
|
||||
normaliseBuzzRelayUrl,
|
||||
parseAndVerifyBuzzAuthTag,
|
||||
} from '../src/services/buzz';
|
||||
import { NotificationDeliveryError } from '../src/services/notifier';
|
||||
import { loadConfig } from '../src/lib/config';
|
||||
|
||||
const AGENT_KEY = `${'0'.repeat(63)}1`;
|
||||
const CHANNEL = '10000000-0000-4000-8000-000000000001';
|
||||
const envelope = {
|
||||
idempotencyKey: 'buzz:link:stage-event',
|
||||
destination: CHANNEL,
|
||||
workspaceId: 'buzz.example.com',
|
||||
notification: {
|
||||
kind: 'stage_change' as const,
|
||||
accountId: '20000000-0000-4000-8000-000000000002',
|
||||
dealId: '30000000-0000-4000-8000-000000000003',
|
||||
dealSide: 'demand' as const,
|
||||
dealName: 'Reserved H100 cluster',
|
||||
fromStage: 'proposal',
|
||||
toStage: 'procurement',
|
||||
changedAt: '2026-08-13T12:00:00.000Z',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Buzz relay identity', () => {
|
||||
it('normalises the WebSocket URL used by Buzz clients into its HTTP bridge community', () => {
|
||||
assert.equal(normaliseBuzzRelayUrl('wss://buzz.example.com/'), 'https://buzz.example.com');
|
||||
assert.equal(buzzWorkspaceId('wss://buzz.example.com/'), 'buzz.example.com');
|
||||
});
|
||||
|
||||
it('accepts official hex and nsec representations as the same identity', () => {
|
||||
const nsec = nip19.nsecEncode(Uint8Array.from(Buffer.from(AGENT_KEY, 'hex')));
|
||||
const fromHex = new BuzzNotifier({ relayUrl: 'https://buzz.example.com', privateKey: AGENT_KEY });
|
||||
const fromNsec = new BuzzNotifier({ relayUrl: 'https://buzz.example.com', privateKey: nsec });
|
||||
assert.equal(fromHex.workspaceId, fromNsec.workspaceId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Buzz configuration', () => {
|
||||
const base = { DATABASE_URL: 'postgres://pig:pig@localhost:5432/pig', NODE_ENV: 'test' };
|
||||
|
||||
it('requires relay and private identity together', () => {
|
||||
assert.throws(
|
||||
() => loadConfig({ ...base, BUZZ_RELAY_URL: 'https://buzz.example.com' }),
|
||||
/BUZZ_PRIVATE_KEY/,
|
||||
);
|
||||
assert.throws(
|
||||
() => loadConfig({ ...base, BUZZ_PRIVATE_KEY: AGENT_KEY }),
|
||||
/BUZZ_RELAY_URL/,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the Buzz private identity in server configuration only', () => {
|
||||
const config = loadConfig({
|
||||
...base,
|
||||
BUZZ_RELAY_URL: 'https://buzz.example.com',
|
||||
BUZZ_PRIVATE_KEY: AGENT_KEY,
|
||||
});
|
||||
assert.equal(config.BUZZ_RELAY_URL, 'https://buzz.example.com');
|
||||
assert.equal(config.BUZZ_PRIVATE_KEY, AGENT_KEY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Buzz signed delivery', () => {
|
||||
it('reuses the signed message event id while refreshing NIP-98 replay nonces', async () => {
|
||||
const requests: { event: Event; auth: Event }[] = [];
|
||||
let nonce = 0;
|
||||
const notifier = new BuzzNotifier({
|
||||
relayUrl: 'https://buzz.example.com',
|
||||
privateKey: AGENT_KEY,
|
||||
now: () => new Date('2026-08-13T12:00:01.000Z'),
|
||||
nonce: () => `nonce-${++nonce}`,
|
||||
fetchImpl: async (_input, init) => {
|
||||
const event = JSON.parse(String(init?.body)) as Event;
|
||||
const headers = new Headers(init?.headers);
|
||||
const auth = decodeBuzzAuthorization(headers.get('authorization') ?? '');
|
||||
assert.ok(auth);
|
||||
requests.push({ event, auth });
|
||||
return Response.json({ event_id: event.id, accepted: true, message: '' });
|
||||
},
|
||||
});
|
||||
|
||||
await notifier.send(envelope);
|
||||
await notifier.send(envelope);
|
||||
await notifier.send({ ...envelope, idempotencyKey: 'buzz:link:different-event' });
|
||||
|
||||
assert.equal(requests[0]?.event.id, requests[1]?.event.id);
|
||||
assert.notEqual(requests[0]?.event.id, requests[2]?.event.id);
|
||||
assert.notEqual(requests[0]?.auth.id, requests[1]?.auth.id);
|
||||
assert.equal(requests[0]?.event.kind, 9);
|
||||
assert.ok(verifyEvent(requests[0]!.event));
|
||||
assert.deepEqual(requests[0]?.event.tags[0], ['h', CHANNEL]);
|
||||
const payloadTag = requests[0]?.auth.tags.find((tag) => tag[0] === 'payload');
|
||||
assert.equal(
|
||||
payloadTag?.[1],
|
||||
createHash('sha256').update(JSON.stringify(requests[0]!.event)).digest('hex'),
|
||||
);
|
||||
});
|
||||
|
||||
it('refuses to route a link from another relay community', async () => {
|
||||
let called = false;
|
||||
const notifier = new BuzzNotifier({
|
||||
relayUrl: 'https://buzz.example.com',
|
||||
privateKey: AGENT_KEY,
|
||||
fetchImpl: async () => {
|
||||
called = true;
|
||||
return Response.json({});
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
notifier.send({ ...envelope, workspaceId: 'other.example.com' }),
|
||||
(error: unknown) =>
|
||||
error instanceof NotificationDeliveryError &&
|
||||
error.code === 'buzz_workspace_mismatch' &&
|
||||
!error.retryable,
|
||||
);
|
||||
assert.equal(called, false);
|
||||
});
|
||||
|
||||
it('marks relay throttling retryable without exposing response content', async () => {
|
||||
const notifier = new BuzzNotifier({
|
||||
relayUrl: 'https://buzz.example.com',
|
||||
privateKey: AGENT_KEY,
|
||||
fetchImpl: async () =>
|
||||
new Response('{"error":"rate-limited: private detail"}', {
|
||||
status: 429,
|
||||
headers: { 'retry-after': '9' },
|
||||
}),
|
||||
});
|
||||
await assert.rejects(
|
||||
notifier.send(envelope),
|
||||
(error: unknown) =>
|
||||
error instanceof NotificationDeliveryError &&
|
||||
error.code === 'buzz_rate_limited' &&
|
||||
error.retryable &&
|
||||
error.retryAfterMs === 9_000 &&
|
||||
!error.message.includes('private detail'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Buzz owner attestation', () => {
|
||||
it('verifies NIP-OA against the configured agent before sending it', () => {
|
||||
const ownerKey = Uint8Array.from(Buffer.from(`${'0'.repeat(63)}2`, 'hex'));
|
||||
const agentPublicKey = getPublicKey(Uint8Array.from(Buffer.from(AGENT_KEY, 'hex')));
|
||||
const conditions = 'kind=9';
|
||||
const digest = createHash('sha256')
|
||||
.update(`nostr:agent-auth:${agentPublicKey}:${conditions}`)
|
||||
.digest();
|
||||
const tag = JSON.stringify([
|
||||
'auth',
|
||||
getPublicKey(ownerKey),
|
||||
conditions,
|
||||
Buffer.from(schnorr.sign(digest, ownerKey)).toString('hex'),
|
||||
]);
|
||||
assert.deepEqual(parseAndVerifyBuzzAuthTag(tag, agentPublicKey), JSON.parse(tag));
|
||||
const tampered = JSON.parse(tag) as string[];
|
||||
tampered[3] = `${tampered[3]![0] === '0' ? '1' : '0'}${tampered[3]!.slice(1)}`;
|
||||
assert.throws(
|
||||
() => parseAndVerifyBuzzAuthTag(JSON.stringify(tampered), agentPublicKey),
|
||||
/BUZZ_AUTH_TAG/,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user