#!/usr/bin/env node // Restore a SkateSpots archive (from scripts/archive-relay.mjs) to a relay. // // Publishes the RAW archived events verbatim — signatures are re-verified first, // so anything tampered with is dropped. Addressable events are safe to republish: // the newest `created_at` wins, so replaying a snapshot converges to the same // state. This is the "prove you can get it back" half of the backup. // // node scripts/restore-relay.mjs --dry-run // node scripts/restore-relay.mjs --to wss://your.relay // // There is deliberately NO default target: restoring is a deliberate act, and // the common mistake is publishing a snapshot back to the live relay. Use // --dry-run to preview, then point --to at the relay you actually want. import { readFileSync } from 'node:fs'; import { gunzipSync } from 'node:zlib'; import { SimplePool, verifyEvent } from 'nostr-tools'; const args = process.argv.slice(2); const file = args.find((a) => !a.startsWith('--')); const toIndex = args.indexOf('--to'); const TO = toIndex >= 0 ? args[toIndex + 1] : null; const DRY = args.includes('--dry-run'); if (!file) { console.error('usage: node scripts/restore-relay.mjs [--dry-run | --to wss://relay]'); process.exit(2); } if (!TO && !DRY) { console.error('[restore] refusing to publish without an explicit --to (use --dry-run to preview)'); process.exit(2); } const lines = gunzipSync(readFileSync(file)).toString('utf8').split('\n').filter(Boolean); const events = []; let invalid = 0; for (const line of lines) { try { const ev = JSON.parse(line); if (verifyEvent(ev)) events.push(ev); else invalid += 1; } catch { invalid += 1; } } const byKind = {}; for (const e of events) byKind[e.kind] = (byKind[e.kind] || 0) + 1; console.log(`[restore] ${events.length} valid events (${invalid} invalid/ignored) from ${file}`); console.log( '[restore] kinds:', Object.entries(byKind) .sort((a, b) => b[1] - a[1]) .map(([k, n]) => `${k}:${n}`) .join(' '), ); if (DRY) { console.log('[restore] dry run — nothing published'); process.exit(0); } const pool = new SimplePool(); let ok = 0; let fail = 0; let index = 0; const CONCURRENCY = 10; async function worker() { while (index < events.length) { const ev = events[index++]; try { const results = await Promise.allSettled(pool.publish([TO], ev)); if (results.some((r) => r.status === 'fulfilled')) ok += 1; else fail += 1; } catch { fail += 1; } if ((ok + fail) % 100 === 0) process.stdout.write(`\r[restore] ${ok + fail}/${events.length}…`); } } await Promise.all(Array.from({ length: Math.min(CONCURRENCY, events.length) }, worker)); pool.close([TO]); process.stdout.write(`\r[restore] done — ${ok} accepted, ${fail} failed \n`); if (fail) process.exit(1);