runtime: outbox UNICO (imports absolutos) + relay-watcher con kill switch + guard R1-R8
ci / lint-test-leaks (pull_request) Failing after 3s
ci / lint-test-leaks (pull_request) Failing after 3s
- 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).
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
// 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)
|
||||
Reference in New Issue
Block a user