SkateSpots/scripts/drill-relay.mjs
SkateSpots c89d32d92e chore(backup): offsite on the CVM host (SFTP) + proven restore drill
- Offsite backup is now live: restic over SFTP to the CVM host via a scoped
  ~/.ssh/config alias (no host/key in tracked files); restic.env (600, outside
  the repo) holds the repo URL + generated password. Verified: first push
  dedupes to ~32 MiB and 'restic check --read-data-subset=10%' is clean.
- scripts/drill-relay.mjs: throwaway in-memory NIP-01 relay (ws) for restore
  drills. Ran the loop end-to-end against the real archive: 114/114 events
  restored, 0 missing.

Scripts/docs only — the deploy timer skips this (no Netlify credits).
2026-09-28 13:16:53 +07:00

78 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
// Throwaway in-memory NIP-01 relay for RESTORE DRILLS.
//
// node scripts/drill-relay.mjs [port] # default 7777
//
// Proves the backup loop end-to-end without touching production or any third
// party: start this, `node scripts/restore-relay.mjs <archive> --to
// ws://localhost:7777`, then query it back and assert every archived event
// arrived. Deliberately minimal — no persistence, no auth, no relay policy.
import { WebSocketServer } from 'ws';
const PORT = Number(process.argv[2] || 7777);
const events = new Map();
function matches(event, filter) {
if (filter.ids && !filter.ids.includes(event.id)) return false;
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
if (filter.authors && !filter.authors.includes(event.pubkey)) return false;
if (filter.since && event.created_at < filter.since) return false;
if (filter.until && event.created_at > filter.until) return false;
for (const key of Object.keys(filter)) {
if (!key.startsWith('#')) continue;
const tag = key.slice(1);
const wanted = filter[key];
if (!Array.isArray(wanted)) continue;
if (!wanted.some((v) => event.tags.some(([n, x]) => n === tag && x === v))) return false;
}
return true;
}
const wss = new WebSocketServer({ port: PORT });
wss.on('connection', (ws) => {
ws.on('message', (raw) => {
let msg;
try {
msg = JSON.parse(raw.toString());
} catch {
return;
}
const [type] = msg;
if (type === 'EVENT') {
const event = msg[1];
if (event?.id) {
events.set(event.id, event);
ws.send(JSON.stringify(['OK', event.id, true, '']));
} else {
ws.send(JSON.stringify(['OK', '', false, 'invalid']));
}
return;
}
if (type === 'REQ') {
const sub = msg[1];
const filters = msg.slice(2);
const out = new Map();
for (const filter of filters) {
const matched = [...events.values()]
.filter((e) => matches(e, filter))
.sort((a, b) => b.created_at - a.created_at);
const limited = typeof filter.limit === 'number' ? matched.slice(0, filter.limit) : matched;
for (const e of limited) out.set(e.id, e);
}
for (const e of out.values()) ws.send(JSON.stringify(['EVENT', sub, e]));
ws.send(JSON.stringify(['EOSE', sub]));
}
});
});
console.log(`[drill-relay] ws://localhost:${PORT} — in-memory, throwaway`);
for (const sig of ['SIGINT', 'SIGTERM']) {
process.on(sig, () => {
console.log(`[drill-relay] stopping (${events.size} events held)`);
wss.close();
process.exit(0);
});
}