From 0eebaceee14735712846afac649ae54840b945dd Mon Sep 17 00:00:00 2001 From: zai-agent Date: Wed, 16 Sep 2026 20:24:06 +0000 Subject: [PATCH] buzz: realtime WS listener (NIP-42 auth, instant push) replaces 45s polling --- buzz/ws-listener/.gitignore | 1 + buzz/ws-listener/listener-raw.mjs | 73 +++++++++++++++++++++++++++++++ buzz/ws-listener/package.json | 16 +++++++ buzz/ws-listener/run.sh | 5 +++ 4 files changed, 95 insertions(+) create mode 100644 buzz/ws-listener/.gitignore create mode 100644 buzz/ws-listener/listener-raw.mjs create mode 100644 buzz/ws-listener/package.json create mode 100755 buzz/ws-listener/run.sh diff --git a/buzz/ws-listener/.gitignore b/buzz/ws-listener/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/buzz/ws-listener/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/buzz/ws-listener/listener-raw.mjs b/buzz/ws-listener/listener-raw.mjs new file mode 100644 index 0000000..531d46a --- /dev/null +++ b/buzz/ws-listener/listener-raw.mjs @@ -0,0 +1,73 @@ +// Raw WebSocket listener for Buzz #general — no relay abstraction. +// Handles NIP-42 auth explicitly: on ["AUTH", challenge] we sign kind 22242. +// On a matching kind-9 event from another pubkey: spool to inbox, exit 0. +import { finalizeEvent, getPublicKey } from 'nostr-tools/pure' +import fs from 'node:fs' + +const RELAY = 'wss://ch4t.buzz' +const CH = 'cc2aac17-af28-47ba-8563-c2cbb8633084' +const ME = '4abe5fcdf9695be34bdfc8fe82297aaaf0cf65b5ca92b6c789f6f8783b5ee197' +const INBOX = '/home/opc/.buzz-inbox' +const MAX_LIFETIME = 4 * 3600 * 1000 + +const keyText = fs.readFileSync('/home/opc/buzz-agent-key.txt', 'utf8') +const SK = Uint8Array.from(keyText.match(/SECRET:\s*(\S+)/)[1].match(/.{2}/g).map((h) => parseInt(h, 16))) +const MY_PUBKEY = getPublicKey(SK) + +const ws = new WebSocket(RELAY) +const seen = new Set() +let authEventId = null +let subscribed = false + +function subscribe() { + if (subscribed) return + subscribed = true + ws.send(JSON.stringify(['REQ', 'general', { kinds: [9], '#h': [CH], since: Math.floor(Date.now() / 1000) }])) + console.error('subscribed') +} + +function sendAuth(challenge) { + const ev = finalizeEvent( + { kind: 22242, created_at: Math.floor(Date.now() / 1000), tags: [['relay', RELAY], ['challenge', challenge]], content: '' }, + SK + ) + authEventId = ev.id + ws.send(JSON.stringify(['AUTH', ev])) + console.error('sent NIP-42 auth response') +} + +ws.onopen = () => { + console.error('ws open — waiting for auth challenge') +} +ws.onmessage = (msg) => { + let data + try { data = JSON.parse(msg.data) } catch { return } + const [type, ...rest] = data + if (type === 'AUTH') { + console.error('auth challenge received') + sendAuth(rest[0]) + return + } + if (type === 'OK') { + if (rest[0] === authEventId && rest[1] === true) { + console.error('auth OK — subscribing') + subscribe() + } + return + } + if (type === 'NOTICE') { console.error('NOTICE:', rest[0]); return } + if (type === 'EVENT') { + const [, subid, evt] = data + if (subid !== 'general') return + if (evt.pubkey === ME || seen.has(evt.id)) return + seen.add(evt.id) + fs.appendFileSync(INBOX, JSON.stringify(evt) + '\n') + console.error('message received from', evt.pubkey.slice(0, 10)) + process.exit(0) + } + if (type === 'EOSE') console.error('EOSE — waiting for live events') +} +ws.onclose = () => { console.error('ws closed'); process.exit(2) } +ws.onerror = () => {} + +setTimeout(() => { console.error('lifetime elapsed'); process.exit(3) }, MAX_LIFETIME) diff --git a/buzz/ws-listener/package.json b/buzz/ws-listener/package.json new file mode 100644 index 0000000..52eb444 --- /dev/null +++ b/buzz/ws-listener/package.json @@ -0,0 +1,16 @@ +{ + "name": "ws-listener", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "nostr-tools": "^2.25.2" + } +} diff --git a/buzz/ws-listener/run.sh b/buzz/ws-listener/run.sh new file mode 100755 index 0000000..c54fd60 --- /dev/null +++ b/buzz/ws-listener/run.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# ZCode presence: WS listener exits when a message arrives -> agent wakes. +cd "$(dirname "$0")" +[ -d node_modules ] || npm install nostr-tools >/dev/null 2>&1 +exec node listener-raw.mjs