From a14a68ca8f855598be1c5b2d9cbea5a8197edd41 Mon Sep 17 00:00:00 2001 From: Sven Date: Sat, 25 Apr 2026 17:13:42 +0200 Subject: [PATCH] fix: sync postinstall/serve scripts with actual scripts directory Commit 774261f removed 6 AP patch scripts (integrated into fork) but left their references in package.json, causing npm ci to fail with exit 1. Also commits 3 server-side-only scripts that were missing from the repo. - Remove: patch-ap-startup-gate-bypass, patch-ap-federation-infra, patch-ap-syndication, patch-ap-mastodon-statuses, patch-ap-mastodon-misc, patch-ap-oauth-token-expiry (from both postinstall and serve) - Add to postinstall: patch-microsub-compose-draft-guard, patch-microsub-no-bookmark-autofollow, patch-microsub-reader-ap-dispatch, patch-ap-enrich-actor-data, patch-ap-stubs-remove-duplicate-routes, patch-session-maxage - Commit 3 previously server-only scripts to repo Co-Authored-By: Claude Sonnet 4.6 --- package.json | 4 +- scripts/patch-ap-enrich-actor-data.mjs | 237 ++++++++++++++++++ ...patch-ap-stubs-remove-duplicate-routes.mjs | 108 ++++++++ scripts/patch-session-maxage.mjs | 41 +++ 4 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 scripts/patch-ap-enrich-actor-data.mjs create mode 100644 scripts/patch-ap-stubs-remove-duplicate-routes.mjs create mode 100644 scripts/patch-session-maxage.mjs diff --git a/package.json b/package.json index 957f16d6..0eb2f220 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "index.js", "scripts": { "preinstall": "node scripts/setup-gitea-url-rewrite.mjs", - "postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && 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-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.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 scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-put-fallback.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-startup-gate-bypass.mjs && node scripts/patch-ap-federation-infra.mjs && node scripts/patch-ap-syndication.mjs && node scripts/patch-ap-mastodon-statuses.mjs && node scripts/patch-ap-mastodon-misc.mjs && node scripts/patch-ap-oauth-token-expiry.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node scripts/patch-microsub-batch-concurrency.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/preflight-startup-gate.mjs && node scripts/patch-ap-startup-gate-bypass.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-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.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 scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-put-fallback.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-microsub-no-bookmark-autofollow.mjs && node scripts/patch-microsub-batch-concurrency.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-federation-infra.mjs && node scripts/patch-ap-syndication.mjs && node scripts/patch-ap-mastodon-statuses.mjs && node scripts/patch-ap-mastodon-misc.mjs && node scripts/patch-ap-oauth-token-expiry.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node --max-old-space-size=1024 --max-semi-space-size=32 --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", + "postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && 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-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.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 scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-put-fallback.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-microsub-no-bookmark-autofollow.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-batch-concurrency.mjs && node scripts/patch-ap-enrich-actor-data.mjs && node scripts/patch-ap-stubs-remove-duplicate-routes.mjs && node scripts/patch-session-maxage.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/preflight-startup-gate.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-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.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 scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-put-fallback.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-microsub-no-bookmark-autofollow.mjs && node scripts/patch-microsub-batch-concurrency.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node scripts/patch-ap-enrich-actor-data.mjs && node scripts/patch-ap-stubs-remove-duplicate-routes.mjs && node scripts/patch-session-maxage.mjs && node --max-old-space-size=1024 --max-semi-space-size=32 --require ./metrics-shim.cjs 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-ap-enrich-actor-data.mjs b/scripts/patch-ap-enrich-actor-data.mjs new file mode 100644 index 00000000..9a47acd5 --- /dev/null +++ b/scripts/patch-ap-enrich-actor-data.mjs @@ -0,0 +1,237 @@ +/** + * patch-ap-enrich-actor-data.mjs + * + * Fixes avatar and handle extraction in three code paths: + * 1. followActor() in index.js — outbound follow stores wrong handle + empty avatar + * 2. inbox-listeners.js — incoming Follow stores wrong handle + empty avatar + * 3. inbox-handlers.js — Update handler refreshes with wrong handle + empty avatar + * + * Root cause: All three use `remoteActor.preferredUsername` (no domain) for handle + * and `remoteActor.icon` (sync property, often falsy) for avatar. + * Fix: Use `getIcon()` async getter and build `@user@domain` from URL. + */ +import { readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const BASE = resolve("/usr/local/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub"); + +const patchSpecs = [ + // ─── Patch 1: followActor() in index.js ───────────────────────────────── + { + name: "followActor-avatar-handle", + marker: "// [patch] ap-enrich-actor-follow", + candidates: [resolve(BASE, "index.js")], + oldSnippet: ` const actorHandle = + actorInfo.handle || + remoteActor.preferredUsername?.toString() || + ""; + const avatar = + actorInfo.photo || + (remoteActor.icon + ? (await remoteActor.icon)?.url?.href || "" + : "");`, + newSnippet: ` // [patch] ap-enrich-actor-follow + let _enrichedAvatar = ""; // [patch] ap-enrich-actor-follow + try { // [patch] ap-enrich-actor-follow + if (typeof remoteActor.getIcon === "function") { // [patch] ap-enrich-actor-follow + const _iconObj = await remoteActor.getIcon(); // [patch] ap-enrich-actor-follow + _enrichedAvatar = _iconObj?.url?.href || ""; // [patch] ap-enrich-actor-follow + } // [patch] ap-enrich-actor-follow + } catch { /* icon fetch failed */ } // [patch] ap-enrich-actor-follow + let _enrichedHandle = ""; // [patch] ap-enrich-actor-follow + try { // [patch] ap-enrich-actor-follow + const _username = remoteActor.preferredUsername?.toString() || ""; // [patch] ap-enrich-actor-follow + if (_username && actorUrl) { // [patch] ap-enrich-actor-follow + const _domain = new URL(actorUrl).hostname; // [patch] ap-enrich-actor-follow + _enrichedHandle = \`@\${_username}@\${_domain}\`; // [patch] ap-enrich-actor-follow + } // [patch] ap-enrich-actor-follow + } catch { /* URL parse failed */ } // [patch] ap-enrich-actor-follow + let _enrichedBanner = ""; // [patch] ap-enrich-actor-follow + try { // [patch] ap-enrich-actor-follow + if (typeof remoteActor.getImage === "function") { // [patch] ap-enrich-actor-follow + const _imgObj = await remoteActor.getImage(); // [patch] ap-enrich-actor-follow + _enrichedBanner = _imgObj?.url?.href || ""; // [patch] ap-enrich-actor-follow + } // [patch] ap-enrich-actor-follow + } catch { /* banner fetch failed */ } // [patch] ap-enrich-actor-follow + const actorHandle = actorInfo.handle || _enrichedHandle || remoteActor.preferredUsername?.toString() || ""; // [patch] ap-enrich-actor-follow + const avatar = actorInfo.photo || _enrichedAvatar || ""; // [patch] ap-enrich-actor-follow`, + }, + + // ─── Patch 2: Also store banner in followActor updateOne ──────────────── + { + name: "followActor-store-banner", + marker: "// [patch] ap-enrich-actor-follow-banner", + candidates: [resolve(BASE, "index.js")], + oldSnippet: ` sharedInbox, + followedAt: new Date().toISOString(), + source: "reader",`, + newSnippet: ` sharedInbox, + banner: _enrichedBanner || "", // [patch] ap-enrich-actor-follow-banner + followedAt: new Date().toISOString(), + source: "reader",`, + }, + + // ─── Patch 3: inbox-listeners.js follower creation ────────────────────── + { + name: "inbox-listeners-follower-avatar-handle", + marker: "// [patch] ap-enrich-follower-data", + candidates: [resolve(BASE, "lib/inbox-listeners.js")], + oldSnippet: ` const followerData = { + actorUrl: followerUrl, + handle: followerActor.preferredUsername?.toString() || "", + name: followerName, + avatar: followerActor.icon + ? (await followerActor.icon)?.url?.href || "" + : "", + inbox: followerActor.inbox?.id?.href || "", + sharedInbox: followerActor.endpoints?.sharedInbox?.href || "", + };`, + newSnippet: ` // Enrich avatar and handle using proper Fedify async getters // [patch] ap-enrich-follower-data + let _fAvatar = ""; // [patch] ap-enrich-follower-data + try { // [patch] ap-enrich-follower-data + if (typeof followerActor.getIcon === "function") { // [patch] ap-enrich-follower-data + const _fIcon = await followerActor.getIcon(); // [patch] ap-enrich-follower-data + _fAvatar = _fIcon?.url?.href || ""; // [patch] ap-enrich-follower-data + } // [patch] ap-enrich-follower-data + } catch { /* icon fetch failed */ } // [patch] ap-enrich-follower-data + let _fHandle = ""; // [patch] ap-enrich-follower-data + try { // [patch] ap-enrich-follower-data + const _fUsername = followerActor.preferredUsername?.toString() || ""; // [patch] ap-enrich-follower-data + if (_fUsername && followerUrl) { // [patch] ap-enrich-follower-data + const _fDomain = new URL(followerUrl).hostname; // [patch] ap-enrich-follower-data + _fHandle = \`@\${_fUsername}@\${_fDomain}\`; // [patch] ap-enrich-follower-data + } // [patch] ap-enrich-follower-data + } catch { /* URL parse failed */ } // [patch] ap-enrich-follower-data + let _fBanner = ""; // [patch] ap-enrich-follower-data + try { // [patch] ap-enrich-follower-data + if (typeof followerActor.getImage === "function") { // [patch] ap-enrich-follower-data + const _fImg = await followerActor.getImage(); // [patch] ap-enrich-follower-data + _fBanner = _fImg?.url?.href || ""; // [patch] ap-enrich-follower-data + } // [patch] ap-enrich-follower-data + } catch { /* banner fetch failed */ } // [patch] ap-enrich-follower-data + const followerData = { // [patch] ap-enrich-follower-data + actorUrl: followerUrl, + handle: _fHandle || followerActor.preferredUsername?.toString() || "", + name: followerName, + avatar: _fAvatar, + banner: _fBanner, + inbox: followerActor.inbox?.id?.href || "", + sharedInbox: followerActor.endpoints?.sharedInbox?.href || "", + };`, + }, + + // ─── Patch 4: inbox-handlers.js Update handler ───────────────────────── + { + name: "inbox-handlers-update-avatar-handle", + marker: "// [patch] ap-enrich-update-handler", + candidates: [resolve(BASE, "lib/inbox-handlers.js")], + oldSnippet: ` const existing = await collections.ap_followers.findOne({ actorUrl }); + if (existing) { + await collections.ap_followers.updateOne( + { actorUrl }, + { + $set: { + name: + actorObj.name?.toString() || + actorObj.preferredUsername?.toString() || + actorUrl, + handle: actorObj.preferredUsername?.toString() || "", + avatar: actorObj.icon + ? (await actorObj.icon)?.url?.href || "" + : "", + updatedAt: new Date().toISOString(), + }, + }, + ); + } +}`, + newSnippet: ` const existing = await collections.ap_followers.findOne({ actorUrl }); + if (existing) { // [patch] ap-enrich-update-handler + let _uAvatar = ""; // [patch] ap-enrich-update-handler + try { // [patch] ap-enrich-update-handler + if (typeof actorObj.getIcon === "function") { // [patch] ap-enrich-update-handler + const _uIcon = await actorObj.getIcon(); // [patch] ap-enrich-update-handler + _uAvatar = _uIcon?.url?.href || ""; // [patch] ap-enrich-update-handler + } // [patch] ap-enrich-update-handler + } catch { /* icon fetch failed */ } // [patch] ap-enrich-update-handler + let _uHandle = ""; // [patch] ap-enrich-update-handler + try { // [patch] ap-enrich-update-handler + const _uUsername = actorObj.preferredUsername?.toString() || ""; // [patch] ap-enrich-update-handler + if (_uUsername && actorUrl) { // [patch] ap-enrich-update-handler + const _uDomain = new URL(actorUrl).hostname; // [patch] ap-enrich-update-handler + _uHandle = \`@\${_uUsername}@\${_uDomain}\`; // [patch] ap-enrich-update-handler + } // [patch] ap-enrich-update-handler + } catch { /* URL parse failed */ } // [patch] ap-enrich-update-handler + let _uBanner = ""; // [patch] ap-enrich-update-handler + try { // [patch] ap-enrich-update-handler + if (typeof actorObj.getImage === "function") { // [patch] ap-enrich-update-handler + const _uImg = await actorObj.getImage(); // [patch] ap-enrich-update-handler + _uBanner = _uImg?.url?.href || ""; // [patch] ap-enrich-update-handler + } // [patch] ap-enrich-update-handler + } catch { /* banner fetch failed */ } // [patch] ap-enrich-update-handler + const _updateFields = { // [patch] ap-enrich-update-handler + name: actorObj.name?.toString() || actorObj.preferredUsername?.toString() || actorUrl, + handle: _uHandle || actorObj.preferredUsername?.toString() || "", + avatar: _uAvatar, + updatedAt: new Date().toISOString(), + }; // [patch] ap-enrich-update-handler + if (_uBanner) _updateFields.banner = _uBanner; // [patch] ap-enrich-update-handler + await collections.ap_followers.updateOne( // [patch] ap-enrich-update-handler + { actorUrl }, + { $set: _updateFields }, + ); + // Also update ap_following if we follow this actor // [patch] ap-enrich-update-handler + const existingFollowing = await collections.ap_following.findOne({ actorUrl }); // [patch] ap-enrich-update-handler + if (existingFollowing) { // [patch] ap-enrich-update-handler + await collections.ap_following.updateOne( // [patch] ap-enrich-update-handler + { actorUrl }, // [patch] ap-enrich-update-handler + { $set: _updateFields }, // [patch] ap-enrich-update-handler + ); // [patch] ap-enrich-update-handler + } // [patch] ap-enrich-update-handler + } +}`, + }, +]; + +let applied = 0; +let skipped = 0; + +for (const spec of patchSpecs) { + const { name, marker, candidates, oldSnippet, newSnippet } = spec; + + let targetFile = null; + for (const candidate of candidates) { + try { + readFileSync(candidate, "utf8"); + targetFile = candidate; + break; + } catch { /* try next */ } + } + + if (!targetFile) { + console.warn(`[${name}] No target file found among candidates`); + skipped++; + continue; + } + + const content = readFileSync(targetFile, "utf8"); + + if (content.includes(marker)) { + console.log(`[${name}] Already applied (marker found), skipping`); + skipped++; + continue; + } + + if (!content.includes(oldSnippet)) { + console.warn(`[${name}] Old snippet not found in ${targetFile}`); + skipped++; + continue; + } + + const patched = content.replace(oldSnippet, newSnippet); + writeFileSync(targetFile, patched, "utf8"); + console.log(`[${name}] ✅ Patched ${targetFile}`); + applied++; +} + +console.log(`\nDone: ${applied} applied, ${skipped} skipped`); diff --git a/scripts/patch-ap-stubs-remove-duplicate-routes.mjs b/scripts/patch-ap-stubs-remove-duplicate-routes.mjs new file mode 100644 index 00000000..5e0168f8 --- /dev/null +++ b/scripts/patch-ap-stubs-remove-duplicate-routes.mjs @@ -0,0 +1,108 @@ +/** + * patch-ap-stubs-remove-duplicate-routes.mjs + * + * Removes duplicate account sub-routes from stubs.js that are already + * properly implemented in accounts.js. Since accountsRouter is registered + * before stubsRouter in router.js, these are dead code that could confuse + * maintenance. + * + * Removes: + * - GET /api/v1/accounts/:id/statuses (duplicate of accounts.js) + * - GET /api/v1/accounts/:id/followers (duplicate of accounts.js) + * - GET /api/v1/accounts/:id/following (duplicate of accounts.js) + */ +import { readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const MARKER = "// [patch] ap-stubs-dedup-account-routes"; +const TARGET = resolve( + "/usr/local/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/stubs.js", +); + +let content; +try { + content = readFileSync(TARGET, "utf8"); +} catch { + console.warn("Target file not found:", TARGET); + process.exit(0); +} + +if (content.includes(MARKER)) { + console.log("Already applied (marker found), skipping"); + process.exit(0); +} + +// Remove the duplicate /api/v1/accounts/:id/statuses handler +const statusesBlock = `// ─── Account statuses ─────────────────────────────────────────────────────── + +router.get("/api/v1/accounts/:id/statuses", async (req, res, next) => { + try { + const collections = req.app.locals.mastodonCollections; + const baseUrl = \`\${req.protocol}://\${req.get("host")}\`; + + // Try to find the profile to see if this is the local user + const profile = await collections.ap_profile.findOne({}); + const isLocal = profile && profile._id.toString() === req.params.id; + + if (isLocal && profile?.url) { + // Return statuses authored by local user + const { serializeStatus } = await import("../entities/status.js"); + const { parseLimit } = await import("../helpers/pagination.js"); + + const limit = parseLimit(req.query.limit); + const items = await collections.ap_timeline + .find({ "author.url": profile.url, isContext: { $ne: true } }) + .sort({ _id: -1 }) + .limit(limit) + .toArray(); + + const statuses = items.map((item) => + serializeStatus(item, { + baseUrl, + favouritedIds: new Set(), + rebloggedIds: new Set(), + bookmarkedIds: new Set(), + pinnedIds: new Set(), + }), + ); + + return res.json(statuses); + } + + // Remote account or unknown — return empty + res.json([]); + } catch (error) { + next(error); + } +});`; + +const followersBlock = `// ─── Account followers/following ──────────────────────────────────────────── + +router.get("/api/v1/accounts/:id/followers", (req, res) => { + res.json([]); +}); + +router.get("/api/v1/accounts/:id/following", (req, res) => { + res.json([]); +});`; + +let patched = content; + +if (patched.includes(statusesBlock)) { + patched = patched.replace(statusesBlock, `${MARKER} — removed duplicate accounts/:id/statuses, /followers, /following (implemented in accounts.js)`); +} else { + console.warn("Could not find statuses block to remove"); +} + +if (patched.includes(followersBlock)) { + patched = patched.replace(followersBlock, ""); +} else { + console.warn("Could not find followers/following block to remove"); +} + +if (patched !== content) { + writeFileSync(TARGET, patched, "utf8"); + console.log("✅ Removed duplicate account routes from stubs.js"); +} else { + console.warn("No changes made"); +} diff --git a/scripts/patch-session-maxage.mjs b/scripts/patch-session-maxage.mjs new file mode 100644 index 00000000..0c9499d0 --- /dev/null +++ b/scripts/patch-session-maxage.mjs @@ -0,0 +1,41 @@ +import { access, readFile, writeFile } from "node:fs/promises"; + +const candidates = [ + "node_modules/@indiekit/indiekit/config/express.js", +]; + +const marker = "SESSION_MAX_AGE_DAYS"; +const from = "maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days,"; +const to = "maxAge: Number.parseInt(process.env.SESSION_MAX_AGE_DAYS || '30', 10) * 24 * 60 * 60 * 1000, // SESSION_MAX_AGE_DAYS days"; + +async function exists(filePath) { + try { await access(filePath); return true; } catch { return false; } +} + +let checked = 0; +let patched = 0; + +for (const filePath of candidates) { + if (!(await exists(filePath))) continue; + checked += 1; + + const source = await readFile(filePath, "utf8"); + if (source.includes(marker)) { + console.log("[postinstall] Session maxAge patch already applied"); + continue; + } + + if (!source.includes(from)) { + console.warn("[postinstall] Skipping session maxAge patch: upstream format changed"); + continue; + } + + await writeFile(filePath, source.replace(from, to), "utf8"); + patched += 1; +} + +if (checked === 0) { + console.log("[postinstall] No indiekit express config found"); +} else if (patched > 0) { + console.log("[postinstall] Patched session maxAge to use SESSION_MAX_AGE_DAYS env var (default 30)"); +}