diff --git a/scripts/patch-actor-aliases-successor.mjs b/scripts/patch-actor-aliases-successor.mjs new file mode 100644 index 00000000..fd9b772b --- /dev/null +++ b/scripts/patch-actor-aliases-successor.mjs @@ -0,0 +1,39 @@ +/** + * Patch @rmdes/indiekit-endpoint-activitypub federation-setup.js: + * - personOptions.alsoKnownAs → personOptions.aliases (Fedify uses 'aliases') + * - Add movedTo → personOptions.successor (Fedify uses 'successor') + */ + +import { readFileSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const pkg = require.resolve("@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js"); +let src = readFileSync(pkg, "utf-8"); + +const MARKER = "// patch-actor-aliases-successor: applied"; +if (src.includes(MARKER)) { + console.log("[patch-actor-aliases-successor] Already applied"); + process.exit(0); +} + +// Fix 1: alsoKnownAs → aliases +const OLD_ALIASES = ` if (profile.alsoKnownAs?.length > 0) { + personOptions.alsoKnownAs = profile.alsoKnownAs.map((u) => new URL(u)); + }`; +const NEW_ALIASES = ` if (profile.alsoKnownAs?.length > 0) { + personOptions.aliases = profile.alsoKnownAs.map((u) => new URL(u)); + } + if (profile.movedTo) { + personOptions.successor = new URL(profile.movedTo); + } + ${MARKER}`; + +if (!src.includes(OLD_ALIASES)) { + console.error("[patch-actor-aliases-successor] Could not find target block — check federation-setup.js version"); + process.exit(1); +} + +src = src.replace(OLD_ALIASES, NEW_ALIASES); +writeFileSync(pkg, src, "utf-8"); +console.log("[patch-actor-aliases-successor] Applied: alsoKnownAs→aliases, added movedTo→successor"); diff --git a/scripts/send-move-activity.mjs b/scripts/send-move-activity.mjs new file mode 100644 index 00000000..bc179abd --- /dev/null +++ b/scripts/send-move-activity.mjs @@ -0,0 +1,130 @@ +/** + * 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); });