Files
cayce-ops/runtime/relay-watcher.mjs
T
cayce 33ae345fbd
ci / lint-test-leaks (pull_request) Failing after 3s
runtime: outbox UNICO (imports absolutos) + relay-watcher con kill switch + guard R1-R8
- outbox.mjs solo en /srv/cayce-core; daemon/social/mail/ops lo importan por ruta absoluta. deploy.sh ya no instala copias.
- relay-watcher.mjs: cada 5 min cruza eventos de la pubkey en 5 relays con outbound_drafts; sin aprobacion => PUBLISH_OFF + DM + audit. Probado: detecto 2 kind0 bypass reales (post-hoc registrados).
- guard R1-R8 (18 tests) + blocklist + check-single-outbox al check diario.
- stale duplicados archivados en forensics/scripts-archive-20261102 (nostr-daemon.mjs, dm-debug, publish-10050, dm-test2, find-followers).
2026-11-02 00:41:57 +00:00

61 lines
3.5 KiB
JavaScript

// relay-watcher — cualquier evento de Cayce en relays sin aprobacion registrada
// => kill switch + DM al supervisor + fila de auditoria. Cada 5 min (timer).
import { SimplePool } from 'nostr-tools'
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
import { createRequire } from 'node:module'
const req = createRequire(import.meta.url)
const pg = req('/srv/cayce-core/node_modules/pg')
const env = Object.fromEntries(readFileSync('/srv/cayce-core/.env','utf8').split('\n').filter(l=>l.includes('=')).map(l=>[l.slice(0,l.indexOf('=')),l.slice(l.indexOf('=')+1)]))
const pool = new pg.Pool({ connectionString: env.PGURL })
const PK = '59ef3b5df26b92cb73986dc86af6294ca59a0a70f648ed0a92ccdb059fd42855'
const RELAYS = ['wss://relay.damus.io','wss://nos.lol','wss://relay.primal.net','wss://relay.snort.social','wss://nostr-pub.wellorder.net']
const CURSOR = '/srv/cayce-core/.watcher-cursor'
const KILL = '/srv/cayce-core/PUBLISH_OFF'
const now = Math.floor(Date.now()/1000)
const cursor = existsSync(CURSOR) ? Number(readFileSync(CURSOR,'utf8').trim()) : now - 3600
const since = Math.max(cursor - 60, now - 3600)
const p = new SimplePool({ eoseSubTimeout: 12000 })
const evs = await Promise.race([
p.querySync(RELAYS, { authors: [PK], since }, 15000).catch(() => []),
new Promise(r => setTimeout(() => r([]), 20000)),
])
p.close(RELAYS)
const uniq = new Map(); for (const e of evs) uniq.set(e.id, e)
let violations = []
for (const e of uniq.values()) {
let ok = false
if ([0, 3, 10050].includes(e.kind)) {
const { rows } = await pool.query(
"SELECT 1 FROM outbound_drafts WHERE channel='nostr' AND type IN ('profile','contact_list','inbox_relays') AND ts BETWEEN to_timestamp($1) - interval '15 minutes' AND to_timestamp($1) + interval '15 minutes' LIMIT 1",
[e.created_at])
ok = rows.length > 0
} else if (e.kind === 5) {
const { rows } = await pool.query("SELECT 1 FROM outbound_drafts WHERE type='deletion' AND meta->>'kind5_id' = $1 LIMIT 1", [e.id])
ok = rows.length > 0
} else {
const { rows } = await pool.query(
"SELECT 1 FROM outbound_drafts WHERE meta->>'event_id' = $1 OR (channel='nostr' AND content = left($2, 7900) AND ts BETWEEN to_timestamp($3) - interval '25 minutes' AND to_timestamp($3) + interval '25 minutes') LIMIT 1",
[e.id, String(e.content||''), e.created_at])
ok = rows.length > 0
}
if (!ok) violations.push(e)
}
let maxTs = since
for (const e of uniq.values()) maxTs = Math.max(maxTs, e.created_at)
writeFileSync(CURSOR, String(maxTs))
if (violations.length) {
writeFileSync(KILL, 'watcher ' + new Date().toISOString() + ' — ' + violations.map(v => v.id).join(','))
for (const v of violations) {
await pool.query("INSERT INTO guard_rejections (channel, type, reasons, content) VALUES ('nostr','watcher_violation',$1,$2)",
['evento sin aprobacion: kind ' + v.kind + ' id ' + v.id, 'KILL SWITCH ACTIVADO. ' + String(v.content||'').slice(0, 300)])
}
const SUP = (readFileSync('/srv/cayce-nostr/.env','utf8').match(/^SUPERVISOR_NPUB_HEX=(.+)$/m)?.[1] || '').trim()
const msg = 'WATCHER: ' + violations.length + ' evento(s) SIN aprobacion del guard. Kill switch ACTIVADO (PUBLISH_OFF). ' + violations.map(v => v.id.slice(0,16) + ' kind' + v.kind).join(' · ')
try { await fetch('http://127.0.0.1:3011/dm', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ to_hex: SUP, content: msg }) }) } catch {}
console.log('[watcher] VIOLACION:', msg)
} else {
console.log('[watcher] ok:', uniq.size, 'eventos revisados, 0 sin aprobacion')
}
await pool.end()
process.exit(0)