- 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).
78 lines
2.6 KiB
JavaScript
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);
|
|
});
|
|
}
|