chore: remove 3 redundant AP patches, trim 2 to site-specific only
Deploy Indiekit Server / deploy (push) Successful in 1m21s

Patches integrated into @rmdes/indiekit-endpoint-activitypub source:
- patch-ap-mastodon-accounts.mjs (deleted)
- patch-ap-mastodon-notifications.mjs (deleted)
- patch-ap-oauth-token-expiry-fix.mjs (deleted)
- patch-ap-syndication.mjs: dedup block removed (now in source)
- patch-ap-mastodon-misc.mjs: rewritten to og-image only (~70 lines)

Remaining AP patches are site-specific and stay.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sven
2026-04-15 09:47:16 +02:00
parent fd174f2738
commit da74eb8625
6 changed files with 41 additions and 723 deletions
+2 -2
View File
@@ -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-oauth-token-expiry-fix.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-accounts.mjs && node scripts/patch-ap-mastodon-notifications.mjs && node scripts/patch-ap-mastodon-misc.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-oauth-token-expiry-fix.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-accounts.mjs && node scripts/patch-ap-mastodon-notifications.mjs && node scripts/patch-ap-mastodon-misc.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-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-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-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",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
-127
View File
@@ -1,127 +0,0 @@
/**
* Consolidated patch: Mastodon account resolution fixes.
*
* Absorbs:
* - patch-ap-actor-cache-await
* Await the ap_actor_cache MongoDB write so the entry exists before the
* search response is sent, making follow reliable after server restarts.
*
* - patch-ap-resolve-account-timeout-safe
* Fix unhandled rejection crash when withTimeout() timer fires before the
* original promise settles, and parallelise collection count fetches with
* Promise.allSettled (max 5 s instead of worst-case 15 s).
*
* Both patches target:
* lib/mastodon/helpers/resolve-account.js
*/
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-mastodon-accounts";
const PATCHES = [
{
name: "ap-actor-cache-await",
files: apPath("lib/mastodon/helpers/resolve-account.js"),
marker: "// [patch] ap-actor-cache-await",
oldSnippet: ` // Persist actor URL mapping to MongoDB so follow/unfollow survives server restarts
if (collections?.ap_actor_cache && actorUrl) {
const hashId = remoteActorId(actorUrl);
collections.ap_actor_cache.updateOne(
{ _id: hashId },
{ $set: { actorUrl, updatedAt: new Date() } },
{ upsert: true },
).catch(() => {}); // fire-and-forget, non-fatal
}`,
newSnippet: ` // Persist actor URL mapping to MongoDB so follow/unfollow survives server restarts
// [patch] ap-actor-cache-await
if (collections?.ap_actor_cache && actorUrl) {
const hashId = remoteActorId(actorUrl);
await collections.ap_actor_cache.updateOne(
{ _id: hashId },
{ $set: { actorUrl, updatedAt: new Date() } },
{ upsert: true },
).catch(() => {}); // non-fatal, but now awaited so entry exists before response
}`,
},
{
name: "ap-resolve-account-timeout-safe",
files: apPath("lib/mastodon/helpers/resolve-account.js"),
marker: "// [patch] ap-resolve-account-timeout-safe",
oldSnippet: ` const withTimeout = (promise, ms = 5000) =>
Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms))]);
let followersCount = 0;
let followingCount = 0;
let statusesCount = 0;
try {
const followers = await withTimeout(actor.getFollowers());
if (followers?.totalItems != null) followersCount = followers.totalItems;
} catch { /* ignore */ }
try {
const following = await withTimeout(actor.getFollowing());
if (following?.totalItems != null) followingCount = following.totalItems;
} catch { /* ignore */ }
try {
const outbox = await withTimeout(actor.getOutbox());
if (outbox?.totalItems != null) statusesCount = outbox.totalItems;
} catch { /* ignore */ }`,
newSnippet: ` const withTimeout = (promise, ms = 5000) => { // [patch] ap-resolve-account-timeout-safe
const abort = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms));
promise.catch(() => {}); // suppress unhandled rejection if timeout settles first
return Promise.race([promise, abort]);
};
// Fetch collection counts in parallel (max 5 s each) [patch] ap-resolve-account-timeout-safe
const [followersResult, followingResult, outboxResult] = await Promise.allSettled([
withTimeout(actor.getFollowers()),
withTimeout(actor.getFollowing()),
withTimeout(actor.getOutbox()),
]);
const followersCount = followersResult.status === "fulfilled" && followersResult.value?.totalItems != null ? followersResult.value.totalItems : 0;
const followingCount = followingResult.status === "fulfilled" && followingResult.value?.totalItems != null ? followingResult.value.totalItems : 0;
const statusesCount = outboxResult.status === "fulfilled" && outboxResult.value?.totalItems != null ? outboxResult.value.totalItems : 0;`,
},
];
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)`);
+34 -398
View File
@@ -1,17 +1,19 @@
/**
* Consolidated patch: AP compose, OG image, repost announce, interactions
* send guard, interactions cleanup preserve, interactions accounts uid,
* inbox ignore View activity, inbox skip View activity parse.
* Patch: AP OG image generation in jf2-to-as2.js.
*
* Absorbs:
* Adds `image` property to ActivityStreams objects using:
* - post photo attachment (if present), or
* - generated OG image at /og/{slug}.png
*
* Note: All other patches previously in this file have been integrated
* into the @rmdes/indiekit-endpoint-activitypub source directly:
* - patch-ap-compose-default-checked
* - patch-ap-og-image
* - patch-ap-repost-announce-fix
* - patch-ap-interactions-send-guard
* - patch-ap-interactions-cleanup-preserve
* - patch-ap-interactions-accounts-uid
* - patch-inbox-ignore-view-activity
* - patch-inbox-skip-view-activity-parse
* - patch-inbox-skip-view-activity-parse (raw body fixes)
*/
import { access, readFile, writeFile } from "node:fs/promises";
@@ -30,31 +32,9 @@ 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";
}
// applyRegexPatch: like applyPatch but uses a regex instead of a literal string match.
// oldRegex must be a RegExp (tested + replaced against src).
async function applyRegexPatch(filePath, marker, oldRegex, newSnippet) {
if (!(await fileExists(filePath))) return "file_not_found";
const src = await readFile(filePath, "utf8");
if (src.includes(marker)) return "already_applied";
if (!oldRegex.test(src)) return "snippet_not_found";
await writeFile(filePath, src.replace(oldRegex, newSnippet), "utf8");
return "applied";
}
const SCRIPT = "patch-ap-mastodon-misc";
// ── patch-ap-og-image: regex-based, two replacements per file ─────────────────
// This patch is handled separately below the main loop because it uses regexes
// and applies two independent replacements to the same file.
const OG_MARKER = "// og-image-v2";
const OG_CANDIDATES = apPath("lib/jf2-to-as2.js");
@@ -92,379 +72,35 @@ const NEW_AS2 = ` const _ogPhotoF = properties.photo && asArray(properties.phot
});
}`;
// ── Standard patches (string-replace) ─────────────────────────────────────────
const PATCHES = [
// ── patch-ap-compose-default-checked ──────────────────────────────────────
{
name: "ap-compose-default-checked",
files: [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/controllers/compose.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/controllers/compose.js",
],
marker: "// [patch] ap-compose-default-checked",
oldSnippet: ` // Default-check only AP (Fedify) and Bluesky targets
// "@rick@rmendes.net" = AP Fedify, "@rmendes.net" = Bluesky
for (const target of syndicationTargets) {
const name = target.name || "";
target.defaultChecked = name === "@rick@rmendes.net" || name === "@rmendes.net";
}`,
newSnippet: ` // Pre-check syndication targets based on their configured checked state // [patch] ap-compose-default-checked
for (const target of syndicationTargets) { // [patch] ap-compose-default-checked
target.defaultChecked = target.checked === true; // [patch] ap-compose-default-checked
} // [patch] ap-compose-default-checked`,
},
// ── patch-ap-repost-announce-fix ──────────────────────────────────────────
{
name: "ap-repost-announce-fix",
files: apPath("lib/jf2-to-as2.js"),
marker: "// [patch] ap-repost-announce-fix",
oldSnippet: ` // Reposts are always public — upstream @rmdes addressing
if (postType === "repost") {
const repostOf = properties["repost-of"];
if (!repostOf) return null;
const repostContent = properties.content?.html || properties.content || "";
if (!repostContent) {
// Pure repost — send as a native Announce (boost) so remote servers
// can display it as a boost of the original post.
return new Announce({
actor: actorUri,
object: new URL(repostOf),
to: new URL("https://www.w3.org/ns/activitystreams#Public"),
});
}
// Has commentary — fall through to Create(Note) so the text is federated.
// The note content block below handles the "repost" post-type.
}`,
newSnippet: ` // Reposts are always public — upstream @rmdes addressing
if (postType === "repost") {
const repostOf = Array.isArray(properties["repost-of"])
? properties["repost-of"][0]
: properties["repost-of"];
if (!repostOf) return null;
const repostContent = properties.content?.html || properties.content || "";
if (!repostContent) {
// Only send Announce if repost-of is an ActivityPub URL.
// Non-AP URLs (web articles) cannot be federated as a boost — fall
// through to Create(Note) which renders as "🔁 <link>" on the fediverse.
if (await isApUrl(repostOf)) { // [patch] ap-repost-announce-fix
const actorPath = new URL(actorUrl).pathname;
const mp = actorPath.replace(/\\/users\\/[^/]+$/, "");
const postRelPath = (properties.url || "")
.replace(publicationUrl.replace(/\\/$/, ""), "")
.replace(/^\\//, "")
.replace(/\\/$/, "");
const announceId = \`\${publicationUrl.replace(/\\/$/, "")}\${mp}/activities/boost/\${postRelPath}\`;
return new Announce({
id: new URL(announceId),
actor: actorUri,
object: new URL(repostOf),
to: new URL("https://www.w3.org/ns/activitystreams#Public"),
cc: new URL(\`\${actorUrl.replace(/\\/$/, "")}/followers\`),
});
}
}
// Has commentary or non-AP repost-of URL — fall through to Create(Note) so the text is federated.
// The note content block below handles the "repost" post-type.
}`,
},
// ── patch-ap-interactions-send-guard: likePost ────────────────────────────
{
name: "ap-interactions-send-guard-like",
files: [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/helpers/interactions.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/helpers/interactions.js",
],
marker: "// [patch] ap-interactions-send-guard",
oldSnippet: ` if (recipient) {
await ctx.sendActivity({ identifier: handle }, recipient, like, {
orderingKey: targetUrl,
});
}`,
newSnippet: ` if (recipient) {
try { // [patch] ap-interactions-send-guard
await ctx.sendActivity({ identifier: handle }, recipient, like, {
orderingKey: targetUrl,
});
} catch { /* delivery failed — interaction still recorded locally */ }
}`,
},
// ── patch-ap-interactions-send-guard: boostPost ───────────────────────────
{
name: "ap-interactions-send-guard-boost",
files: [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/helpers/interactions.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/helpers/interactions.js",
],
marker: "// [patch] ap-interactions-send-guard",
oldSnippet: ` // Send to followers
await ctx.sendActivity({ identifier: handle }, "followers", announce, {
preferSharedInbox: true,
syncCollection: true,
orderingKey: targetUrl,
});`,
newSnippet: ` // Send to followers
try { // [patch] ap-interactions-send-guard
await ctx.sendActivity({ identifier: handle }, "followers", announce, {
preferSharedInbox: true,
syncCollection: true,
orderingKey: targetUrl,
});
} catch { /* delivery failed — interaction still recorded locally */ }`,
},
// ── patch-ap-interactions-cleanup-preserve ────────────────────────────────
{
name: "ap-interactions-cleanup-preserve",
files: apPath("lib/timeline-cleanup.js"),
marker: "// [patch] ap-interactions-cleanup-preserve",
oldSnippet: ` const removedUids = toDelete.map((item) => item.uid).filter(Boolean);
// Delete old timeline items by UID
const deleteResult = await collections.ap_timeline.deleteMany({
_id: { $in: toDelete.map((item) => item._id) },
});
// Clean up stale interactions for removed items
let interactionsRemoved = 0;
if (removedUids.length > 0 && collections.ap_interactions) {
const interactionResult = await collections.ap_interactions.deleteMany({
objectUrl: { $in: removedUids },
});
interactionsRemoved = interactionResult.deletedCount || 0;
}`,
newSnippet: ` const removedUids = toDelete.map((item) => item.uid).filter(Boolean);
// Preserve items the user has interacted with (liked, bookmarked, boosted). // [patch] ap-interactions-cleanup-preserve
// Deleting them would silently remove entries from the Favourites/Bookmarks pages.
let interactedUids = new Set();
if (removedUids.length > 0 && collections.ap_interactions) {
const interacted = await collections.ap_interactions.distinct("objectUrl");
interactedUids = new Set(interacted);
}
const itemsToDelete = toDelete.filter((item) => !interactedUids.has(item.uid));
const uidsToDelete = itemsToDelete.map((item) => item.uid).filter(Boolean);
if (!itemsToDelete.length) {
return { removed: 0, interactionsRemoved: 0 };
}
// Delete old timeline items by UID
const deleteResult = await collections.ap_timeline.deleteMany({
_id: { $in: itemsToDelete.map((item) => item._id) },
});
// Clean up stale interactions for removed items
let interactionsRemoved = 0;
if (uidsToDelete.length > 0 && collections.ap_interactions) {
const interactionResult = await collections.ap_interactions.deleteMany({
objectUrl: { $in: uidsToDelete },
});
interactionsRemoved = interactionResult.deletedCount || 0;
}`,
},
// ── patch-ap-interactions-accounts-uid ───────────────────────────────────
{
name: "ap-interactions-accounts-uid",
files: [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/accounts.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/accounts.js",
],
marker: "// [patch] ap-interactions-accounts-uid",
oldSnippet: ` const lookupUrls = items.flatMap((i) => [i.uid, i.url].filter(Boolean));
if (lookupUrls.length > 0) {
const interactions = await collections.ap_interactions
.find({ objectUrl: { $in: lookupUrls } })
.toArray();
for (const ix of interactions) {
if (ix.type === "like") favouritedIds.add(ix.objectUrl);
else if (ix.type === "boost") rebloggedIds.add(ix.objectUrl);
else if (ix.type === "bookmark") bookmarkedIds.add(ix.objectUrl);
}
}`,
newSnippet: ` const urlToUid = new Map(); // [patch] ap-interactions-accounts-uid
for (const i of items) {
if (i.uid) {
urlToUid.set(i.uid, i.uid);
if (i.url && i.url !== i.uid) urlToUid.set(i.url, i.uid);
}
}
const lookupUrls = [...urlToUid.keys()];
if (lookupUrls.length > 0) {
const interactions = await collections.ap_interactions
.find({ objectUrl: { $in: lookupUrls } })
.toArray();
for (const ix of interactions) {
const uid = urlToUid.get(ix.objectUrl) || ix.objectUrl;
if (ix.type === "like") favouritedIds.add(uid);
else if (ix.type === "boost") rebloggedIds.add(uid);
else if (ix.type === "bookmark") bookmarkedIds.add(uid);
}
}`,
},
// ── patch-inbox-ignore-view-activity: import ──────────────────────────────
{
name: "inbox-ignore-view-activity-import",
files: apPath("lib/inbox-listeners.js"),
marker: "// View imported",
oldSnippet: ` Undo,
Update,
} from "@fedify/fedify/vocab";`,
newSnippet: ` Undo,
Update,
View, // View imported
} from "@fedify/fedify/vocab";`,
},
// ── patch-inbox-ignore-view-activity: handler ────────────────────────────
// Two variants: one for files with the console.warn block (original upstream),
// one for files with the simpler enqueueActivity-only structure (evolved upstream).
{
name: "inbox-ignore-view-activity-handler",
files: apPath("lib/inbox-listeners.js"),
marker: "// PeerTube View handler",
oldSnippet: ` console.info(\`[ActivityPub] Flag received from \${reporterName} — \${reportedIds.length} objects reported\`);
} catch (error) {
console.warn("[ActivityPub] Flag handler error:", error.message);
}
});
}`,
newSnippet: ` console.info(\`[ActivityPub] Flag received from \${reporterName} — \${reportedIds.length} objects reported\`);
} catch (error) {
console.warn("[ActivityPub] Flag handler error:", error.message);
}
})
// ── View (PeerTube watch) ─────────────────────────────────────────────
// PeerTube broadcasts View (WatchAction) activities to all followers
// whenever someone watches a video. Fedify has no built-in handler for
// this type, producing noisy "Unsupported activity type" log errors.
// Silently accept and discard. // PeerTube View handler
.on(View, async () => {});
}`,
},
{
name: "inbox-ignore-view-activity-handler-v2",
files: apPath("lib/inbox-listeners.js"),
marker: "// PeerTube View handler",
oldSnippet: ` await enqueueActivity(collections, {
activityType: "Flag",
actorUrl,
rawJson: await flag.toJsonLd(),
});
});
}`,
newSnippet: ` await enqueueActivity(collections, {
activityType: "Flag",
actorUrl,
rawJson: await flag.toJsonLd(),
});
})
// ── View (PeerTube watch) ─────────────────────────────────────────────
// PeerTube broadcasts View (WatchAction) activities to all followers
// whenever someone watches a video. Fedify has no built-in handler for
// this type, producing noisy "Unsupported activity type" log errors.
// Silently accept and discard. // PeerTube View handler
.on(View, async () => {});
}`,
},
// ── patch-inbox-skip-view-activity-parse: upgrade v1 raw-body fix ─────────
{
name: "from-express-request-raw-body-fix",
files: apPath("lib/federation-bridge.js"),
marker: "req._rawBody || JSON.stringify",
oldSnippet: ` // PeerTube activity+json body fix
if (ct.includes("application/json") || ct.includes("activity+json") || ct.includes("ld+json")) {
body = JSON.stringify(req.body);
} else if (ct.includes("application/x-www-form-urlencoded")) {`,
newSnippet: ` // PeerTube activity+json body fix
if (ct.includes("application/json") || ct.includes("activity+json") || ct.includes("ld+json")) {
// Use original raw bytes when available (set by createFedifyMiddleware buffer guard).
// JSON.stringify() changes byte layout, breaking Fedify's HTTP Signature Digest check.
body = req._rawBody || JSON.stringify(req.body); // raw body digest fix
} else if (ct.includes("application/x-www-form-urlencoded")) {`,
},
// ── patch-inbox-skip-view-activity-parse: middleware buffer _rawBody fix ──
{
name: "inbox-buffer-raw-body-fix",
files: apPath("lib/federation-bridge.js"),
marker: "req._rawBody = _raw",
oldSnippet: ` const _chunks = [];
for await (const _chunk of req) {
_chunks.push(Buffer.isBuffer(_chunk) ? _chunk : Buffer.from(_chunk));
}
try {
req.body = JSON.parse(Buffer.concat(_chunks).toString("utf8"));`,
newSnippet: ` const _chunks = [];
for await (const _chunk of req) {
_chunks.push(Buffer.isBuffer(_chunk) ? _chunk : Buffer.from(_chunk));
}
const _raw = Buffer.concat(_chunks); // raw body digest fix
req._rawBody = _raw; // Preserve original bytes for Fedify HTTP Signature Digest verification
try {
req.body = JSON.parse(_raw.toString("utf8"));`,
},
];
// ── Run standard patches ───────────────────────────────────────────────────────
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`);
}
let ogDone = false;
for (const f of OG_CANDIDATES) {
if (!(await fileExists(f))) continue;
const src = await readFile(f, "utf8");
if (src.includes(OG_MARKER)) {
console.log(`[postinstall] ${SCRIPT}: ap-og-image already applied in ${f}`);
ogDone = true; break;
}
if (!done) console.log(`[postinstall] ${SCRIPT}: ${p.name} — no target file found`);
}
// ── Run og-image patch (regex-based, two replacements per file) ───────────────
{
let ogDone = false;
for (const f of OG_CANDIDATES) {
if (!(await fileExists(f))) continue;
const src = await readFile(f, "utf8");
if (src.includes(OG_MARKER)) {
console.log(`[postinstall] ${SCRIPT}: ap-og-image already applied in ${f}`);
ogDone = true; break;
}
let updated = src;
let changed = false;
if (CN_BLOCK_RE.test(updated)) {
updated = updated.replace(CN_BLOCK_RE, NEW_CN);
changed = true;
} else {
console.warn(`[postinstall] ${SCRIPT}: ap-og-image — jf2ToActivityStreams OG block not found in ${f}`);
}
if (AS2_BLOCK_RE.test(updated)) {
updated = updated.replace(AS2_BLOCK_RE, NEW_AS2);
changed = true;
} else {
console.warn(`[postinstall] ${SCRIPT}: ap-og-image — jf2ToAS2Activity OG block not found in ${f}`);
}
if (changed && updated !== src) {
await writeFile(f, updated, "utf8");
console.log(`[postinstall] ${SCRIPT}: applied ap-og-image to ${f}`);
total++; ogDone = true; break;
}
let updated = src;
let changed = false;
if (CN_BLOCK_RE.test(updated)) {
updated = updated.replace(CN_BLOCK_RE, NEW_CN);
changed = true;
} else {
console.warn(`[postinstall] ${SCRIPT}: ap-og-image — jf2ToActivityStreams OG block not found in ${f}`);
}
if (AS2_BLOCK_RE.test(updated)) {
updated = updated.replace(AS2_BLOCK_RE, NEW_AS2);
changed = true;
} else {
console.warn(`[postinstall] ${SCRIPT}: ap-og-image — jf2ToAS2Activity OG block not found in ${f}`);
}
if (changed && updated !== src) {
await writeFile(f, updated, "utf8");
console.log(`[postinstall] ${SCRIPT}: applied ap-og-image to ${f}`);
total++; ogDone = true; break;
}
if (!ogDone) console.log(`[postinstall] ${SCRIPT}: ap-og-image — no target file found or no changes`);
}
if (!ogDone) console.log(`[postinstall] ${SCRIPT}: ap-og-image — no target file found or no changes`);
console.log(`[postinstall] ${SCRIPT}: done (${total} patch(es) applied)`);
@@ -1,86 +0,0 @@
/**
* Patch: include notif.url in batchFetchStatuses so mention notifications
* resolve their associated status correctly.
*
* Root cause:
* batchFetchStatuses() only collected notif.targetUrl for the batch lookup.
* serializeNotification() looks up mentions by notif.url (the incoming
* reply URL), not notif.targetUrl (the own post being replied to). These
* are different URLs, so the statusMap never has an entry for the mention →
* fallback fires → status.id = notif._id.toString() (a notification ObjectId,
* not a timeline ObjectId) → Phanpy uses that ID for subsequent requests →
* GET /api/v1/statuses/:id/context returns 404 because ap_timeline has no
* document with that _id.
*
* Fix:
* Collect both notif.url and notif.targetUrl so the batch covers all URL
* shapes used by any notification type.
*/
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-mastodon-notifications";
const PATCHES = [
{
name: "ap-notifications-status-lookup",
files: apPath("lib/mastodon/routes/notifications.js"),
marker: "// [patch] ap-notifications-status-lookup",
oldSnippet: ` const targetUrls = [
...new Set(
notifications
.map((n) => n.targetUrl)
.filter(Boolean),
),
];`,
newSnippet: ` const targetUrls = [ // [patch] ap-notifications-status-lookup
...new Set(
notifications
.flatMap((n) => [n.targetUrl, n.url])
.filter(Boolean),
),
];`,
},
];
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)`);
@@ -1,80 +0,0 @@
/**
* Patch: fix OAuth access tokens expiring 10 minutes after login.
*
* Root cause:
* The authorization code document is created with expiresAt = now + 10 min
* (the authorization code TTL). When the access token is issued via the
* authorization_code grant, the existing document is updated in-place with
* $set: { accessToken, refreshToken, ... } but expiresAt is never cleared.
*
* The tokenRequired middleware then rejects the token once 10 minutes pass:
* if (token.expiresAt && token.expiresAt < new Date()) return null;
*
* The refresh_token grant already has $unset: { expiresAt: "" } — this patch
* applies the same fix to the authorization_code exchange path.
*
* Symptom: Mastodon clients (Phanpy, etc.) receive 200 on /api/v2/instance but
* 401 on all authenticated endpoints ~10 minutes after logging in.
*/
import { access, readFile, writeFile } from "node:fs/promises";
const MARKER = "// [patch] ap-oauth-token-expiry-fix";
const candidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/oauth.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/oauth.js",
];
const OLD_SNIPPET = ` {
$set: {
accessToken,
refreshToken,
refreshExpiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL),
},
},`;
const NEW_SNIPPET = ` {
$set: {
accessToken,
refreshToken,
refreshExpiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL),
},
$unset: { expiresAt: "" }, ${MARKER}
},`;
async function exists(p) {
try { await access(p); return true; } catch { return false; }
}
let totalPatched = 0;
let totalChecked = 0;
for (const filePath of candidates) {
if (!(await exists(filePath))) continue;
totalChecked++;
const source = await readFile(filePath, "utf8");
if (source.includes(MARKER)) {
console.log(`[postinstall] patch-ap-oauth-token-expiry-fix: already applied to ${filePath}`);
continue;
}
if (!source.includes(OLD_SNIPPET)) {
console.warn(`[postinstall] patch-ap-oauth-token-expiry-fix: snippet not found in ${filePath} (upstream changed?)`);
continue;
}
const updated = source.replace(OLD_SNIPPET, NEW_SNIPPET);
await writeFile(filePath, updated, "utf8");
console.log(`[postinstall] Applied patch-ap-oauth-token-expiry-fix to ${filePath}`);
totalPatched++;
}
if (totalChecked === 0) {
console.log("[postinstall] patch-ap-oauth-token-expiry-fix: no target files found");
} else if (totalPatched === 0) {
console.log("[postinstall] patch-ap-oauth-token-expiry-fix: already up to date");
} else {
console.log(`[postinstall] patch-ap-oauth-token-expiry-fix: patched ${totalPatched} file(s)`);
}
+5 -30
View File
@@ -2,15 +2,14 @@
* Consolidated patch: AP syndication guards in syndicator.js.
*
* Absorbs:
* - patch-ap-syndicate-dedup (prevent double-posting)
* - patch-ap-syndicate-skip-checkin (skip location checkins)
* - patch-ap-syndicate-skip-draft (skip draft posts)
* - patch-ap-syndicate-skip-unlisted (skip unlisted posts)
*
* ORDER MATTERS: dedup → checkin → draft → unlisted
* checkin/draft/unlisted chain (each newSnippet is the next oldSnippet).
* dedup patches a separate anchor and must run first (before checkin
* rewrites the surrounding context).
* Note: patch-ap-syndicate-dedup integrated into source directly.
*
* ORDER MATTERS: checkin → draft → unlisted
* Each newSnippet is the next oldSnippet.
*/
import { access, readFile, writeFile } from "node:fs/promises";
@@ -45,31 +44,7 @@ async function applyPatch(filePath, marker, oldSnippet, newSnippet) {
const SCRIPT = "patch-ap-syndication";
const PATCHES = [
// ORDER: dedup → checkin → draft → unlisted (each builds on prior output)
{
name: "dedup",
files: apPath("lib/syndicator.js"),
marker: "// [patch] ap-syndicate-dedup",
oldSnippet: ` try {
const actorUrl = plugin._getActorUrl();`,
newSnippet: ` // Dedup: skip re-federation if we've already sent an activity for this URL. // [patch] ap-syndicate-dedup
// ap_activities is the authoritative record of "already federated".
try {
const existingActivity = await plugin._collections.ap_activities?.findOne({
direction: "outbound",
type: { $in: ["Create", "Announce", "Update"] },
objectUrl: properties.url,
});
if (existingActivity) {
console.info(\`[ActivityPub] Skipping duplicate syndication for \${properties.url} — already sent (\${existingActivity.type})\`);
return properties.url || undefined;
}
} catch { /* DB unavailable — proceed */ }
try {
const actorUrl = plugin._getActorUrl();`,
},
// ORDER: checkin → draft → unlisted (each builds on prior output)
{
name: "skip-checkin",