Point SKATE_RELAY, the default relay pools, the DM/CVM/zap relay arrays, the cvm-server + push-service defaults, and the archive default at wss://relay.skate-spots.org, keeping the legacy hosted relay as a fallback during the client-update window. Restored 115/115 archived events; the relay runs behind Caddy with TLS. Docs + AGENTS.md updated.
159 lines
5.8 KiB
JavaScript
159 lines
5.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* transfer-content.mjs — re-publish one key's addressable spots/shops under
|
|
* another key ("ownership transfer").
|
|
*
|
|
* Why a script and not a relay move: addressable events (kind 30818 spots,
|
|
* 30017 shops) are SIGNED BY THEIR AUTHOR. To move them you must re-sign each
|
|
* event with the destination key and publish it, and (optionally) publish a
|
|
* NIP-09 delete for the original signed by the source key. That needs BOTH
|
|
* private keys.
|
|
*
|
|
* Usage:
|
|
* # 1. Preview what would move (read-only, no keys needed):
|
|
* node scripts/transfer-content.mjs \
|
|
* --from npub1... --to npub1... --dry-run
|
|
*
|
|
* # 2. Do it (keys via env — NEVER commit or paste them in chat):
|
|
* TO_NSEC=nsec1... node scripts/transfer-content.mjs \
|
|
* --from npub1... --to npub1... --yes
|
|
*
|
|
* # 3. Optionally delete the originals too (needs the SOURCE key):
|
|
* TO_NSEC=nsec1... FROM_NSEC=nsec1... node scripts/transfer-content.mjs \
|
|
* --from npub1... --to npub1... --delete-originals --yes
|
|
*
|
|
* Safety:
|
|
* - `--yes` is required to publish anything; without it this is a dry run.
|
|
* - Re-signed events keep the same kind/content/tags/d, with a fresh
|
|
* created_at. A `client` tag on the original is preserved.
|
|
* - The source events are left untouched unless --delete-originals.
|
|
*/
|
|
import { SimplePool, nip19, finalizeEvent } from 'nostr-tools';
|
|
|
|
const SPOT = 30818;
|
|
const SHOP = 30017;
|
|
|
|
const DEFAULT_RELAYS = [
|
|
'wss://relay.skate-spots.org',
|
|
'wss://relay.damus.io',
|
|
'wss://nos.lol',
|
|
];
|
|
|
|
function parseArgs(argv) {
|
|
const out = { relays: DEFAULT_RELAYS };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === '--from') out.from = argv[++i];
|
|
else if (a === '--to') out.to = argv[++i];
|
|
else if (a === '--relays') out.relays = argv[++i].split(',').map((s) => s.trim()).filter(Boolean);
|
|
else if (a === '--delete-originals') out.deleteOriginals = true;
|
|
else if (a === '--yes') out.yes = true;
|
|
else if (a === '--dry-run') out.dryRun = true;
|
|
}
|
|
out.dryRun = out.dryRun || !out.yes; // publish requires an explicit --yes
|
|
return out;
|
|
}
|
|
|
|
function toHex(id) {
|
|
if (/^[0-9a-f]{64}$/i.test(id)) return id.toLowerCase();
|
|
const d = nip19.decode(id);
|
|
if (d.type !== 'npub' && d.type !== 'nprofile') throw new Error(`Not an npub: ${id}`);
|
|
return d.data.pubkey ?? d.data;
|
|
}
|
|
|
|
function skFromEnv(name) {
|
|
const v = process.env[name];
|
|
if (!v) return null;
|
|
const d = nip19.decode(v.trim());
|
|
if (d.type !== 'nsec') throw new Error(`${name} is not an nsec`);
|
|
return d.data;
|
|
}
|
|
|
|
const tag = (ev, name) => ev.tags.find((t) => t[0] === name)?.[1];
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (!args.from || !args.to) {
|
|
console.error('Usage: node scripts/transfer-content.mjs --from <npub> --to <npub> [--delete-originals] [--yes]');
|
|
process.exit(1);
|
|
}
|
|
const fromHex = toHex(args.from);
|
|
const toHexId = toHex(args.to);
|
|
if (fromHex === toHexId) throw new Error('--from and --to are the same key.');
|
|
|
|
const pool = new SimplePool();
|
|
let events = [];
|
|
try {
|
|
events = await pool.querySync(args.relays, { authors: [fromHex], kinds: [SPOT, SHOP] });
|
|
} finally {
|
|
// keep the pool open for publishing below
|
|
}
|
|
|
|
const spots = events.filter((e) => e.kind === SPOT);
|
|
const shops = events.filter((e) => e.kind === SHOP);
|
|
console.log(`Source ${args.from.slice(0, 16)}… → destination ${args.to.slice(0, 16)}…`);
|
|
console.log(`Found ${spots.length} spot(s) + ${shops.length} shop(s) on ${args.relays.length} relays.`);
|
|
for (const e of [...spots, ...shops]) {
|
|
const d = tag(e, 'd') ?? '?';
|
|
const label = tag(e, 'title') ?? tag(e, 'name') ?? '';
|
|
console.log(` • kind ${e.kind} d=${d} ${label}`);
|
|
}
|
|
|
|
const toSk = skFromEnv('TO_NSEC');
|
|
const fromSk = skFromEnv('FROM_NSEC');
|
|
|
|
if (args.dryRun) {
|
|
console.log('\nDRY RUN — nothing published. Re-run with --yes (and TO_NSEC / FROM_NSEC) to apply.');
|
|
pool.close(args.relays);
|
|
return;
|
|
}
|
|
|
|
if (!toSk) throw new Error('Set TO_NSEC (the destination key) to publish, or use --dry-run.');
|
|
|
|
// 1) Re-sign each event with the destination key.
|
|
let ok = 0;
|
|
for (const e of [...spots, ...shops]) {
|
|
const unsigned = {
|
|
kind: e.kind,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: e.tags,
|
|
content: e.content,
|
|
};
|
|
const signed = finalizeEvent(unsigned, toSk);
|
|
const results = await Promise.allSettled(pool.publish(args.relays, signed));
|
|
const accepted = results.filter((r) => r.status === 'fulfilled').length;
|
|
if (accepted > 0) ok++;
|
|
console.log(` republished kind ${e.kind} d=${tag(e, 'd')} → ${accepted}/${args.relays.length} relays (${signed.id.slice(0, 8)}…)`);
|
|
}
|
|
console.log(`\nRe-published ${ok}/${spots.length + shops.length} events as the destination key.`);
|
|
|
|
// 2) Optionally delete the originals (NIP-09) with the source key.
|
|
if (args.deleteOriginals) {
|
|
if (!fromSk) throw new Error('--delete-originals needs FROM_NSEC (the source key).');
|
|
let deleted = 0;
|
|
for (const e of [...spots, ...shops]) {
|
|
const unsigned = {
|
|
kind: 5,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
['e', e.id],
|
|
['a', `${e.kind}:${fromHex}:${tag(e, 'd') ?? ''}`],
|
|
],
|
|
content: 'Transferred to another key',
|
|
};
|
|
const signed = finalizeEvent(unsigned, fromSk);
|
|
const results = await Promise.allSettled(pool.publish(args.relays, signed));
|
|
if (results.some((r) => r.status === 'fulfilled')) deleted++;
|
|
}
|
|
console.log(`Published ${deleted} NIP-09 delete(s) for the originals.`);
|
|
} else {
|
|
console.log('Originals left in place. Pass --delete-originals (with FROM_NSEC) to remove them.');
|
|
}
|
|
|
|
pool.close(args.relays);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('\n✖', err instanceof Error ? err.message : err);
|
|
process.exit(1);
|
|
});
|