/** * Send AP Move activity: blog.giersig.eu/activitypub/users/svemagie * → svemagie.net/activitypub/users/svemagie * * Run AFTER: * 1. ap_profile updated with movedTo + alsoKnownAs * 2. indiekit restarted so new actor doc is live at svemagie.net * * Usage: node send-move-activity.mjs [--dry-run] */ import { createSign, createHash } from "node:crypto"; import { MongoClient } from "mongodb"; import { readFileSync } from "node:fs"; const DRY_RUN = process.argv.includes("--dry-run"); const OLD_ACTOR = "https://blog.giersig.eu/activitypub/users/svemagie"; const NEW_ACTOR = "https://svemagie.net/activitypub/users/svemagie"; const KEY_ID = `${OLD_ACTOR}#main-key`; // MongoDB connection (same as indiekit .env) const MONGO_URL = "mongodb://indiekit:hasag@10.100.0.20:27017/indiekit?authSource=admin"; async function signRequest(method, url, body, privateKeyPem) { const parsed = new URL(url); const date = new Date().toUTCString(); const bodyBytes = Buffer.from(body, "utf-8"); const digest = "SHA-256=" + createHash("sha256").update(bodyBytes).digest("base64"); const signingString = [ `(request-target): ${method.toLowerCase()} ${parsed.pathname}`, `host: ${parsed.host}`, `date: ${date}`, `digest: ${digest}`, ].join("\n"); const signer = createSign("RSA-SHA256"); signer.update(signingString); const signature = signer.sign(privateKeyPem, "base64"); const sigHeader = [ `keyId="${KEY_ID}"`, `algorithm="rsa-sha256"`, `headers="(request-target) host date digest"`, `signature="${signature}"`, ].join(","); return { date, digest, signature: sigHeader }; } async function deliver(inbox, body, privateKeyPem) { const { date, digest, signature } = await signRequest("POST", inbox, body, privateKeyPem); const parsed = new URL(inbox); if (DRY_RUN) { console.log(`[dry-run] POST ${inbox}`); return { ok: true, status: 0 }; } const res = await fetch(inbox, { method: "POST", headers: { "Content-Type": "application/activity+json", "Accept": "application/activity+json", "Host": parsed.host, "Date": date, "Digest": digest, "Signature": signature, }, body, signal: AbortSignal.timeout(15000), }); return { ok: res.ok, status: res.status }; } async function main() { const client = new MongoClient(MONGO_URL); await client.connect(); const db = client.db("indiekit"); // Get RSA private key const keyDoc = await db.collection("ap_keys").findOne({ type: "rsa" }); if (!keyDoc?.privateKeyPem) throw new Error("RSA private key not found in ap_keys"); const privateKeyPem = keyDoc.privateKeyPem; // Get unique inboxes const followers = await db.collection("ap_followers").find({}).toArray(); const inboxMap = new Map(); for (const f of followers) { const target = f.sharedInbox || f.inbox; if (target && !inboxMap.has(target)) inboxMap.set(target, f.handle); } console.log(`Delivering Move to ${inboxMap.size} unique inboxes${DRY_RUN ? " [DRY RUN]" : ""}...`); const activity = JSON.stringify({ "@context": "https://www.w3.org/ns/activitystreams", id: `${NEW_ACTOR}#move-${Date.now()}`, type: "Move", actor: OLD_ACTOR, object: OLD_ACTOR, target: NEW_ACTOR, to: ["https://www.w3.org/ns/activitystreams#Public"], }); let ok = 0, fail = 0; for (const [inbox, handle] of inboxMap) { try { const result = await deliver(inbox, activity, privateKeyPem); if (result.ok || result.status === 202) { console.log(` ✓ ${inbox} (via ${handle})`); ok++; } else { console.log(` ✗ ${inbox} — HTTP ${result.status}`); fail++; } } catch (e) { console.log(` ✗ ${inbox} — ${e.message}`); fail++; } // Small delay between deliveries await new Promise(r => setTimeout(r, 500)); } await client.close(); console.log(`\nDone: ${ok} ok, ${fail} failed of ${inboxMap.size} targets`); } main().catch(e => { console.error(e); process.exit(1); });