diff --git a/package.json b/package.json index 3d0924da..c69ace13 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "postinstall": "node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs", - "serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", + "postinstall": "node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs", + "serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/scripts/patch-federation-unlisted-guards.mjs b/scripts/patch-federation-unlisted-guards.mjs new file mode 100644 index 00000000..62c215ef --- /dev/null +++ b/scripts/patch-federation-unlisted-guards.mjs @@ -0,0 +1,207 @@ +import { access, readFile, writeFile } from "node:fs/promises"; + +const endpointSyndicateCandidates = [ + "node_modules/@indiekit/endpoint-syndicate/lib/utils.js", + "node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/utils.js", +]; + +const activityPubIndexCandidates = [ + "node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", +]; + +const activityPubFederationSetupCandidates = [ + "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js", +]; + +const patchSpecs = [ + { + name: "endpoint-syndicate-source-url-unlisted-guard", + candidates: endpointSyndicateCandidates, + oldSnippet: ` postData = await postsCollection.findOne({ + "properties.url": url, + });`, + newSnippet: ` postData = await postsCollection.findOne({ + "properties.url": url, + "properties.post-status": { + $ne: "draft", + }, + // Exclude unlisted posts from automatic syndication/federation. + "properties.visibility": { + $ne: "unlisted", + }, + });`, + }, + { + name: "endpoint-syndicate-get-post-data-pending-unlisted-guard", + candidates: endpointSyndicateCandidates, + oldSnippet: ` "properties.post-status": { + $ne: "draft", + }, + })`, + newSnippet: ` "properties.post-status": { + $ne: "draft", + }, + // Exclude unlisted posts from automatic syndication/federation. + "properties.visibility": { + $ne: "unlisted", + }, + })`, + }, + { + name: "endpoint-syndicate-get-all-post-data-unlisted-guard", + candidates: endpointSyndicateCandidates, + oldSnippet: ` "properties.post-status": { + $ne: "draft", + }, + })`, + newSnippet: ` "properties.post-status": { + $ne: "draft", + }, + // Exclude unlisted posts from automatic syndication/federation. + "properties.visibility": { + $ne: "unlisted", + }, + })`, + }, + { + name: "activitypub-syndicator-unlisted-guard", + candidates: activityPubIndexCandidates, + oldSnippet: ` async syndicate(properties) { + if (!self._federation) { + return undefined; + } + + try {`, + newSnippet: ` async syndicate(properties) { + if (!self._federation) { + return undefined; + } + + const visibility = String(properties?.visibility || "").toLowerCase(); + if (visibility === "unlisted") { + console.info( + "[ActivityPub] Skipping federation for unlisted post: " + + (properties?.url || "unknown"), + ); + await logActivity(self._collections.ap_activities, { + direction: "outbound", + type: "Syndicate", + actorUrl: self._publicationUrl, + objectUrl: properties?.url, + summary: "Syndication skipped: post visibility is unlisted", + }).catch(() => {}); + return undefined; + } + + try {`, + }, + { + name: "activitypub-outbox-unlisted-guard", + candidates: activityPubFederationSetupCandidates, + oldSnippet: ` const pageSize = 20; + const skip = cursor ? Number.parseInt(cursor, 10) : 0; + const total = await postsCollection.countDocuments(); + + const posts = await postsCollection + .find()`, + newSnippet: ` const pageSize = 20; + const skip = cursor ? Number.parseInt(cursor, 10) : 0; + const federationVisibilityQuery = { + "properties.post-status": { $ne: "draft" }, + "properties.visibility": { $ne: "unlisted" }, + }; + const total = await postsCollection.countDocuments( + federationVisibilityQuery, + ); + + const posts = await postsCollection + .find(federationVisibilityQuery)`, + }, + { + name: "activitypub-outbox-counter-unlisted-guard", + candidates: activityPubFederationSetupCandidates, + oldSnippet: ` .setCounter(async (ctx, identifier) => { + if (identifier !== handle) return 0; + const postsCollection = collections.posts; + if (!postsCollection) return 0; + return await postsCollection.countDocuments(); + })`, + newSnippet: ` .setCounter(async (ctx, identifier) => { + if (identifier !== handle) return 0; + const postsCollection = collections.posts; + if (!postsCollection) return 0; + return await postsCollection.countDocuments({ + "properties.post-status": { $ne: "draft" }, + "properties.visibility": { $ne: "unlisted" }, + }); + })`, + }, + { + name: "activitypub-object-dispatch-unlisted-guard", + candidates: activityPubFederationSetupCandidates, + oldSnippet: ` const post = await collections.posts.findOne({ "properties.url": postUrl }); + if (!post) return null;`, + newSnippet: ` const post = await collections.posts.findOne({ "properties.url": postUrl }); + if (!post) return null; + if (post?.properties?.["post-status"] === "draft") return null; + if (post?.properties?.visibility === "unlisted") return null;`, + }, +]; + +async function exists(filePath) { + try { + await access(filePath); + return true; + } catch { + return false; + } +} + +const checkedFiles = new Set(); +const patchedFiles = new Set(); + +for (const spec of patchSpecs) { + let foundAnyTarget = false; + + for (const filePath of spec.candidates) { + if (!(await exists(filePath))) { + continue; + } + + foundAnyTarget = true; + checkedFiles.add(filePath); + + const source = await readFile(filePath, "utf8"); + + let updated = source; + let replacements = 0; + + if (source.includes(spec.oldSnippet)) { + updated = source.replace(spec.oldSnippet, spec.newSnippet); + replacements = 1; + } + + if (replacements === 0 || updated === source) { + continue; + } + + await writeFile(filePath, updated, "utf8"); + patchedFiles.add(filePath); + } + + if (!foundAnyTarget) { + console.log(`[postinstall] ${spec.name}: no target files found`); + } +} + +if (checkedFiles.size === 0) { + console.log("[postinstall] No federation patch targets found"); +} else if (patchedFiles.size === 0) { + console.log("[postinstall] federation unlisted guards already patched"); +} else { + console.log( + `[postinstall] Patched federation unlisted guards in ${patchedFiles.size}/${checkedFiles.size} file(s)`, + ); +}