diff --git a/scripts/patch-ap-self-follow-guard.mjs b/scripts/patch-ap-self-follow-guard.mjs new file mode 100644 index 00000000..b3d93474 --- /dev/null +++ b/scripts/patch-ap-self-follow-guard.mjs @@ -0,0 +1,84 @@ +/** + * Patch: Prevent self-follows in the inbox Follow handler. + * + * When the server follows itself (e.g. via Mona), the self-entry in + * ap_followers causes Fedify to attempt delivery to the local shared + * inbox on every sendActivity("followers") call. Since the node jail + * has no outbound internet, this produces infinite ECONNRESET retries. + * + * This patch adds an early return in the Follow listener when the + * follower's actor URL matches the server's own publicationUrl. + */ + +import { access, readFile, writeFile } from "node:fs/promises"; + +const AP_BASE = "@rmdes/indiekit-endpoint-activitypub"; +const AP_ROOTS = [ + `node_modules/${AP_BASE}`, + `node_modules/@indiekit/indiekit/node_modules/${AP_BASE}`, +]; + +function apPath(rel) { + return AP_ROOTS.map(r => `${r}/${rel}`); +} + +async function fileExists(p) { + try { await access(p); return true; } catch { return false; } +} + +async function applyPatch(filePath, marker, oldSnippet, newSnippet) { + if (!(await fileExists(filePath))) return "file_not_found"; + const src = await readFile(filePath, "utf8"); + if (src.includes(marker)) return "already_applied"; + if (!src.includes(oldSnippet)) return "snippet_not_found"; + await writeFile(filePath, src.replace(oldSnippet, newSnippet), "utf8"); + return "applied"; +} + +const SCRIPT = "patch-ap-self-follow-guard"; +const MARKER = "// [patch] ap-self-follow-guard"; + +const PATCHES = [ + { + name: "self-follow-guard", + files: apPath("lib/inbox-listeners.js"), + marker: MARKER, + oldSnippet: ` const actorUrl = follow.actorId?.href || ""; + if (await isServerBlocked(actorUrl, collections)) return; + await touchKeyFreshness(collections, actorUrl); + await resetDeliveryStrikes(collections, actorUrl);`, + newSnippet: ` const actorUrl = follow.actorId?.href || ""; + if (await isServerBlocked(actorUrl, collections)) return; + + // Reject self-follows: if the follower is our own actor, skip. // [patch] ap-self-follow-guard + // Self-follows cause infinite delivery retries because Fedify + // tries to POST to our own shared inbox, which is unreachable + // from within the jail (no outbound internet). + if (collections._publicationUrl && actorUrl.startsWith(collections._publicationUrl)) { + console.info(\`[ActivityPub] Ignoring self-follow from \${actorUrl}\`); + return; + } + + await touchKeyFreshness(collections, actorUrl); + await resetDeliveryStrikes(collections, actorUrl);`, + }, +]; + +let total = 0; +for (const p of PATCHES) { + let done = false; + for (const f of p.files) { + const r = await applyPatch(f, p.marker, p.oldSnippet, p.newSnippet); + if (r === "applied") { + console.log(`[postinstall] ${SCRIPT}: applied ${p.name} to ${f}`); + total++; done = true; break; + } else if (r === "already_applied") { + console.log(`[postinstall] ${SCRIPT}: ${p.name} already applied in ${f}`); + done = true; break; + } else if (r === "snippet_not_found") { + console.warn(`[postinstall] ${SCRIPT}: ${p.name} — snippet not found in ${f}, skipping`); + } + } + if (!done) console.log(`[postinstall] ${SCRIPT}: ${p.name} — no target file found`); +} +console.log(`[postinstall] ${SCRIPT}: done (${total} patch(es) applied)`);