From 1cf5b01788e23d5f059717149c1fed14fcf12c89 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 14:39:21 +0000 Subject: [PATCH 01/59] fix(ap): fix OG image not included in ActivityPub activities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: jf2-to-as2.js (both 842fc5af and 45f8ba9) uses a regex expecting date-based URLs like /articles/YYYY/MM/DD/slug/ to extract the post date for the OG image filename. This blog uses flat URLs like /articles/slug/ so the regex never matches — no `image` property is set on Note/Article ActivityPub objects and fediverse clients show no preview card. Fix: - Add scripts/patch-ap-og-image.mjs: replaces the URL-pattern regex with slug-from-last-path-segment + date-from-properties.published, producing /og/{year}-{month}-{day}-{slug}.png matching the Eleventy OG filenames. Handles both 842fc5af (slug-only URL) and 45f8ba9 (date+slug) variants. Applied to both jf2ToActivityStreams() and jf2ToAS2Activity(). - Register patch in package.json postinstall and serve after patch-ap-repost-commentary. - Bump package-lock.json to 45f8ba9 fork commit (correct date+slug filename format, likes-as-bookmarks, announce cc reverted). https://claude.ai/code/session_0124D41vdLYE3DkJxhPqYthX --- README.md | 17 +++- package-lock.json | 2 +- package.json | 4 +- scripts/patch-ap-og-image.mjs | 141 ++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 scripts/patch-ap-og-image.mjs diff --git a/README.md b/README.md index ef7135a7..5a34da7c 100644 --- a/README.md +++ b/README.md @@ -160,15 +160,25 @@ Posts are converted from Indiekit's JF2 format to ActivityStreams 2.0 in two mod ### AP-specific patches -These patches are applied to `node_modules` via postinstall and at serve startup. They're needed because the lockfile pins the fork to v2.10.1 which predates some fixes, and because some fixes cannot be upstreamed. +These patches are applied to `node_modules` via postinstall and at serve startup. They're needed because some fixes cannot be upstreamed or because they adapt upstream behaviour to this blog's specific URL structure. | Patch | Target | What it does | |---|---|---| | `patch-ap-allow-private-address` | federation-setup.js | Adds `signatureTimeWindow` and `allowPrivateAddress` to `createFederation()` | | `patch-ap-url-lookup-api` | Adds new route | Public `GET /activitypub/api/ap-url` resolves blog URL → AP object URL | +| `patch-ap-og-image` | jf2-to-as2.js | Fixes OG image URL generation — see below | | `patch-federation-unlisted-guards` | endpoint-syndicate | Prevents unlisted posts from being re-syndicated (AP fork has this natively) | | `patch-endpoint-activitypub-locales` | locales | Injects German (`de`) translations for the AP endpoint UI | +**`patch-ap-og-image.mjs`** +The fork (both 842fc5af and 45f8ba9) attempts to derive the OG image path by matching a date-based URL pattern like `/articles/2024/01/15/slug/`. This blog uses flat URLs (`/articles/slug/`) with no date component, so the regex never matches and no `image` property is set on ActivityPub objects — Mastodon and other clients never show a preview card. + +The patch replaces the URL-pattern extraction with: +1. Slug from the last URL path segment. +2. Date from `properties.published` (ISO-8601 string). + +This produces the correct `/og/{year}-{month}-{day}-{slug}.png` filename that the Eleventy build generates for per-post OG images. Applied to both `jf2ToActivityStreams()` (plain JSON-LD) and `jf2ToAS2Activity()` (Fedify vocab objects). + ### AP environment variables | Variable | Default | Purpose | @@ -648,6 +658,11 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ## Changelog +### 2026-03-20 + +**fix(ap): fix OG image not included in ActivityPub activities** +The fork's OG image code expected date-based URLs (`/articles/YYYY/MM/DD/slug/`) but this blog uses flat URLs (`/articles/slug/`). The regex never matched so no `image` property was set and Mastodon/fediverse clients showed no preview card. Added `patch-ap-og-image.mjs` which extracts the slug from the URL's last path segment and the date from `properties.published`, producing the correct `/og/{year}-{month}-{day}-{slug}.png` filename. Also updated `package-lock.json` to pull the `45f8ba9` fork commit (likes-as-bookmarks + correct date+slug OG filename format). + ### 2026-03-19 **feat: deliver likes as bookmarks, revert announce cc, add OG images** (`45f8ba9` in svemagie/indiekit-endpoint-activitypub) diff --git a/package-lock.json b/package-lock.json index 013fafd7..b5ed206a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2357,7 +2357,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "2.15.4", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#842fc5af2ab946d6cc2f2abfbe4728da3edaf3da", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#45f8ba93c0202fabf460bdd03e9ee758faa0a457", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", diff --git a/package.json b/package.json index 9c4c09e8..d9a08a7d 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "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-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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.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-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-microsub-reader-ap-dispatch.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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node 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-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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.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-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-microsub-reader-ap-dispatch.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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.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-ap-og-image.mjs b/scripts/patch-ap-og-image.mjs new file mode 100644 index 00000000..89b2675a --- /dev/null +++ b/scripts/patch-ap-og-image.mjs @@ -0,0 +1,141 @@ +/** + * Patch: fix OG image URL generation in ActivityPub jf2-to-as2.js. + * + * Root cause: + * Both 842fc5af and 45f8ba9 versions of jf2-to-as2.js try to extract the + * post date from the URL using a regex that expects date-based URLs like + * /articles/2024/01/15/slug/ but this blog uses flat URLs like /articles/slug/. + * The regex never matches so the `image` property is never set — no OG image + * preview card reaches Mastodon or other fediverse servers. + * + * Fix: + * Replace the date-from-URL regex with an approach that: + * 1. Extracts the slug from the last path segment of the post URL. + * 2. Reads the date from properties.published (ISO-8601 string). + * Constructs /og/{year}-{month}-{day}-{slug}.png — the filename pattern that + * the Eleventy build generates for static OG preview images. + * + * Both jf2ToActivityStreams() (plain JSON-LD) and jf2ToAS2Activity() (Fedify + * vocab objects) are patched. Both 842fc5af and 45f8ba9 variants are handled + * so the patch works regardless of which commit npm install resolved. + */ + +import { access, readFile, writeFile } from "node:fs/promises"; + +const candidates = [ + "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", +]; + +const MARKER = "// og-image fix"; + +// --------------------------------------------------------------------------- +// Use JS regex patterns to locate the OG image blocks. +// Both 842fc5af and 45f8ba9 share the same variable names (ogMatch / ogMatchF) +// and the same if-block structure, differing only in the URL construction. +// +// Pattern: matches from "const ogMatch[F] = postUrl && postUrl.match(" to the +// closing "}" (2-space indent) of the if block. +// --------------------------------------------------------------------------- +const CN_BLOCK_RE = + / const ogMatch = postUrl && postUrl\.match\([^\n]+\n if \(ogMatch\) \{[\s\S]*?\n \}/; + +const AS2_BLOCK_RE = + / const ogMatchF = postUrl && postUrl\.match\([^\n]+\n if \(ogMatchF\) \{[\s\S]*?\n \}/; + +// --------------------------------------------------------------------------- +// Replacement: extract slug from URL last segment, date from published ISO string. +// Build /og/{year}-{month}-{day}-{slug}.png to match the Eleventy OG filenames. +// +// Template literal note: backslashes in regex literals inside the injected code +// are doubled here so they survive the template literal → string conversion: +// \\\/ → \/ (escaped slash in regex) +// \\d → \d (digit class) +// [\\\w-] → [\w-] (word char class) +// --------------------------------------------------------------------------- +const NEW_CN = ` const ogSlug = postUrl && postUrl.match(/\\/([\\\w-]+)\\/?$/)?.[1]; // og-image fix + const ogPub = properties.published && properties.published.match(/^(\\d{4})-(\\d{2})-(\\d{2})/); // og-image fix + if (ogSlug && ogPub) { // og-image fix + object.image = { + type: "Image", + url: \`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogPub[1]}-\${ogPub[2]}-\${ogPub[3]}-\${ogSlug}.png\`, // og-image fix + mediaType: "image/png", + }; + }`; + +const NEW_AS2 = ` const ogSlugF = postUrl && postUrl.match(/\\/([\\\w-]+)\\/?$/)?.[1]; // og-image fix + const ogPubF = properties.published && properties.published.match(/^(\\d{4})-(\\d{2})-(\\d{2})/); // og-image fix + if (ogSlugF && ogPubF) { // og-image fix + noteOptions.image = new Image({ + url: new URL(\`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogPubF[1]}-\${ogPubF[2]}-\${ogPubF[3]}-\${ogSlugF}.png\`), // og-image fix + mediaType: "image/png", + }); + }`; + +// --------------------------------------------------------------------------- + +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] patch-ap-og-image: already applied to ${filePath}`); + continue; + } + + let updated = source; + let changed = false; + + // Fix the jf2ToActivityStreams OG block + if (CN_BLOCK_RE.test(updated)) { + updated = updated.replace(CN_BLOCK_RE, NEW_CN); + changed = true; + } else { + console.warn( + `[postinstall] patch-ap-og-image: jf2ToActivityStreams OG block not found in ${filePath} — skipping`, + ); + } + + // Fix the jf2ToAS2Activity OG block + if (AS2_BLOCK_RE.test(updated)) { + updated = updated.replace(AS2_BLOCK_RE, NEW_AS2); + changed = true; + } else { + console.warn( + `[postinstall] patch-ap-og-image: jf2ToAS2Activity OG block not found in ${filePath} — skipping`, + ); + } + + if (!changed || updated === source) { + console.log(`[postinstall] patch-ap-og-image: no changes applied to ${filePath}`); + continue; + } + + await writeFile(filePath, updated, "utf8"); + patched += 1; + console.log(`[postinstall] Applied patch-ap-og-image to ${filePath}`); +} + +if (checked === 0) { + console.log("[postinstall] patch-ap-og-image: no target files found"); +} else if (patched === 0) { + console.log("[postinstall] patch-ap-og-image: already up to date"); +} else { + console.log(`[postinstall] patch-ap-og-image: patched ${patched}/${checked} file(s)`); +} From 769720b33f9e371a04c9664212832c93707bc3c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 20:04:19 +0000 Subject: [PATCH 02/59] fix(ap): use slug-only OG image path /og/{slug}.png The previous fix incorrectly generated /og/{year}-{month}-{day}-{slug}.png but the Eleventy blog generates OG images at /og/{slug}.png (e.g. /og/2615b.png). Remove the unnecessary date extraction and simplify to slug-only. https://claude.ai/code/session_0124D41vdLYE3DkJxhPqYthX --- README.md | 10 +++------- scripts/patch-ap-og-image.mjs | 29 ++++++++++++----------------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 5a34da7c..7a14dfad 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ Posts are converted from Indiekit's JF2 format to ActivityStreams 2.0 in two mod - Permalink appended to content body - Nested hashtags normalized: `on/art/music` → `#music` (Mastodon doesn't support path-style tags) - Sensitive posts flagged with `sensitive: true`; summary doubles as CW text for notes -- Per-post OG image added to Note/Article objects (`/og/{year}-{month}-{day}-{slug}.png`) for fediverse preview cards +- Per-post OG image added to Note/Article objects (`/og/{slug}.png`) for fediverse preview cards ### Express ↔ Fedify bridge @@ -173,11 +173,7 @@ These patches are applied to `node_modules` via postinstall and at serve startup **`patch-ap-og-image.mjs`** The fork (both 842fc5af and 45f8ba9) attempts to derive the OG image path by matching a date-based URL pattern like `/articles/2024/01/15/slug/`. This blog uses flat URLs (`/articles/slug/`) with no date component, so the regex never matches and no `image` property is set on ActivityPub objects — Mastodon and other clients never show a preview card. -The patch replaces the URL-pattern extraction with: -1. Slug from the last URL path segment. -2. Date from `properties.published` (ISO-8601 string). - -This produces the correct `/og/{year}-{month}-{day}-{slug}.png` filename that the Eleventy build generates for per-post OG images. Applied to both `jf2ToActivityStreams()` (plain JSON-LD) and `jf2ToAS2Activity()` (Fedify vocab objects). +The patch replaces the broken date-from-URL regex with a simple last-path-segment extraction, producing `/og/{slug}.png` — the actual filename the Eleventy build generates (e.g. `/og/2615b.png`). Applied to both `jf2ToActivityStreams()` (plain JSON-LD) and `jf2ToAS2Activity()` (Fedify vocab objects). ### AP environment variables @@ -661,7 +657,7 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-20 **fix(ap): fix OG image not included in ActivityPub activities** -The fork's OG image code expected date-based URLs (`/articles/YYYY/MM/DD/slug/`) but this blog uses flat URLs (`/articles/slug/`). The regex never matched so no `image` property was set and Mastodon/fediverse clients showed no preview card. Added `patch-ap-og-image.mjs` which extracts the slug from the URL's last path segment and the date from `properties.published`, producing the correct `/og/{year}-{month}-{day}-{slug}.png` filename. Also updated `package-lock.json` to pull the `45f8ba9` fork commit (likes-as-bookmarks + correct date+slug OG filename format). +The fork's OG image code expected date-based URLs (`/articles/YYYY/MM/DD/slug/`) but this blog uses flat URLs (`/articles/slug/`). The regex never matched so no `image` property was set and Mastodon/fediverse clients showed no preview card. Added `patch-ap-og-image.mjs` which extracts the slug from the URL's last path segment and constructs `/og/{slug}.png` — the actual Eleventy OG filename format (e.g. `/og/2615b.png`). Also updated `package-lock.json` to pull the `45f8ba9` fork commit (likes-as-bookmarks, announce cc reverted). ### 2026-03-19 diff --git a/scripts/patch-ap-og-image.mjs b/scripts/patch-ap-og-image.mjs index 89b2675a..8874707e 100644 --- a/scripts/patch-ap-og-image.mjs +++ b/scripts/patch-ap-og-image.mjs @@ -3,17 +3,15 @@ * * Root cause: * Both 842fc5af and 45f8ba9 versions of jf2-to-as2.js try to extract the - * post date from the URL using a regex that expects date-based URLs like + * post slug from the URL using a regex that expects date-based URLs like * /articles/2024/01/15/slug/ but this blog uses flat URLs like /articles/slug/. * The regex never matches so the `image` property is never set — no OG image * preview card reaches Mastodon or other fediverse servers. * * Fix: - * Replace the date-from-URL regex with an approach that: - * 1. Extracts the slug from the last path segment of the post URL. - * 2. Reads the date from properties.published (ISO-8601 string). - * Constructs /og/{year}-{month}-{day}-{slug}.png — the filename pattern that - * the Eleventy build generates for static OG preview images. + * Replace the date-from-URL regex with a simple last-path-segment extraction. + * Constructs /og/{slug}.png — the actual filename pattern the Eleventy build + * generates for static OG preview images (e.g. /og/2615b.png). * * Both jf2ToActivityStreams() (plain JSON-LD) and jf2ToAS2Activity() (Fedify * vocab objects) are patched. Both 842fc5af and 45f8ba9 variants are handled @@ -44,30 +42,27 @@ const AS2_BLOCK_RE = / const ogMatchF = postUrl && postUrl\.match\([^\n]+\n if \(ogMatchF\) \{[\s\S]*?\n \}/; // --------------------------------------------------------------------------- -// Replacement: extract slug from URL last segment, date from published ISO string. -// Build /og/{year}-{month}-{day}-{slug}.png to match the Eleventy OG filenames. +// Replacement: extract slug from last URL path segment. +// Build /og/{slug}.png to match the Eleventy OG filenames (e.g. /og/2615b.png). // -// Template literal note: backslashes in regex literals inside the injected code -// are doubled here so they survive the template literal → string conversion: +// Template literal note: backslashes inside the injected regex are doubled so +// they survive the template literal → string conversion: // \\\/ → \/ (escaped slash in regex) -// \\d → \d (digit class) // [\\\w-] → [\w-] (word char class) // --------------------------------------------------------------------------- const NEW_CN = ` const ogSlug = postUrl && postUrl.match(/\\/([\\\w-]+)\\/?$/)?.[1]; // og-image fix - const ogPub = properties.published && properties.published.match(/^(\\d{4})-(\\d{2})-(\\d{2})/); // og-image fix - if (ogSlug && ogPub) { // og-image fix + if (ogSlug) { // og-image fix object.image = { type: "Image", - url: \`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogPub[1]}-\${ogPub[2]}-\${ogPub[3]}-\${ogSlug}.png\`, // og-image fix + url: \`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogSlug}.png\`, // og-image fix mediaType: "image/png", }; }`; const NEW_AS2 = ` const ogSlugF = postUrl && postUrl.match(/\\/([\\\w-]+)\\/?$/)?.[1]; // og-image fix - const ogPubF = properties.published && properties.published.match(/^(\\d{4})-(\\d{2})-(\\d{2})/); // og-image fix - if (ogSlugF && ogPubF) { // og-image fix + if (ogSlugF) { // og-image fix noteOptions.image = new Image({ - url: new URL(\`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogPubF[1]}-\${ogPubF[2]}-\${ogPubF[3]}-\${ogSlugF}.png\`), // og-image fix + url: new URL(\`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogSlugF}.png\`), // og-image fix mediaType: "image/png", }); }`; From 072b803308e59e680ba8d5e324f78e645ecd0f13 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 10:00:43 +0100 Subject: [PATCH 03/59] chore: update youtube endpoint fork (fix double video embed) Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 808123c3..5d851fbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2668,7 +2668,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-youtube": { "version": "1.3.0", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-youtube.git#ae0b87940177ab93b1b222049c6f830a3935143c", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-youtube.git#b8e2b6472f9dda9371ab0ed07b9d09d7c1783d6a", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", From 2a674c8eea1ebde30b904bba39870ea50cbbad73 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 11:10:23 +0100 Subject: [PATCH 04/59] feat(draft): prevent draft posts from being syndicated or federated Add two new patches: - patch-ap-skip-draft-syndication: guards the AP syndicator's syndicate() method against draft posts (mirrors existing unlisted visibility check) - patch-microsub-compose-draft-guard: forwards post-status from microsub compose to Micropub and suppresses mp-syndicate-to targets for drafts The syndicate endpoint DB queries already filter post-status != draft (patch-federation-unlisted-guards). These patches add defence in depth at the AP syndicator and at the microsub compose submission layer. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 4 +- scripts/patch-ap-skip-draft-syndication.mjs | 109 ++++++++++++++ .../patch-microsub-compose-draft-guard.mjs | 139 ++++++++++++++++++ 3 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 scripts/patch-ap-skip-draft-syndication.mjs create mode 100644 scripts/patch-microsub-compose-draft-guard.mjs diff --git a/package.json b/package.json index 67fc3d18..44c1c6d4 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "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-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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-like-note-dispatcher.mjs && node scripts/patch-ap-like-activity-id.mjs && node scripts/patch-ap-like-activity-dispatcher.mjs && node scripts/patch-ap-url-lookup-api-like.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-remove-federation-diag.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-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-microsub-reader-ap-dispatch.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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-like-note-dispatcher.mjs && node scripts/patch-ap-like-activity-id.mjs && node scripts/patch-ap-like-activity-dispatcher.mjs && node scripts/patch-ap-url-lookup-api-like.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-remove-federation-diag.mjs && node 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-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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-like-note-dispatcher.mjs && node scripts/patch-ap-like-activity-id.mjs && node scripts/patch-ap-like-activity-dispatcher.mjs && node scripts/patch-ap-url-lookup-api-like.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-remove-federation-diag.mjs && node scripts/patch-ap-skip-draft-syndication.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-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-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-like-note-dispatcher.mjs && node scripts/patch-ap-like-activity-id.mjs && node scripts/patch-ap-like-activity-dispatcher.mjs && node scripts/patch-ap-url-lookup-api-like.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-remove-federation-diag.mjs && node scripts/patch-ap-skip-draft-syndication.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-ap-skip-draft-syndication.mjs b/scripts/patch-ap-skip-draft-syndication.mjs new file mode 100644 index 00000000..51869106 --- /dev/null +++ b/scripts/patch-ap-skip-draft-syndication.mjs @@ -0,0 +1,109 @@ +/** + * Patch: add a post-status === "draft" guard to the ActivityPub syndicator's + * syndicate() method, mirroring the existing visibility === "unlisted" guard. + * + * Without this patch, a draft post that somehow reaches the AP syndicator + * directly (bypassing the syndicate-endpoint DB-level filter) would be + * federated to followers. + */ +import { access, readFile, writeFile } from "node:fs/promises"; + +const candidates = [ + "node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", +]; + +const oldSnippet = ` 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; + }`; + +const newSnippet = ` const postStatus = String(properties?.["post-status"] || "").toLowerCase(); + if (postStatus === "draft") { + console.info( + "[ActivityPub] Skipping federation for draft post: " + + (properties?.url || "unknown"), + ); + await logActivity(self._collections.ap_activities, { + direction: "outbound", + type: "Syndicate", + actorUrl: self._publicationUrl, + objectUrl: properties?.url, + summary: "Syndication skipped: post is a draft", + }).catch(() => {}); + 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; + }`; + +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(newSnippet)) { + continue; + } + + if (!source.includes(oldSnippet)) { + console.warn( + `[postinstall] Skipping ap-skip-draft-syndication patch for ${filePath}: upstream format changed`, + ); + continue; + } + + const updated = source.replace(oldSnippet, newSnippet); + await writeFile(filePath, updated, "utf8"); + patched += 1; +} + +if (checked === 0) { + console.log("[postinstall] No AP endpoint files found for draft guard patch"); +} else if (patched === 0) { + console.log("[postinstall] ap-skip-draft-syndication patch already applied"); +} else { + console.log( + `[postinstall] Patched AP draft syndication guard in ${patched} file(s)`, + ); +} diff --git a/scripts/patch-microsub-compose-draft-guard.mjs b/scripts/patch-microsub-compose-draft-guard.mjs new file mode 100644 index 00000000..6c3a00ef --- /dev/null +++ b/scripts/patch-microsub-compose-draft-guard.mjs @@ -0,0 +1,139 @@ +/** + * Patch: honour post-status in the microsub compose submitCompose handler. + * + * When a post is submitted via the microsub compose form with + * post-status: draft: + * 1. Forward the post-status to Micropub so the post is saved as a draft. + * 2. Suppress all mp-syndicate-to targets — draft posts must never be + * syndicated (not to Mastodon, Bluesky, or ActivityPub). + * + * The syndicate endpoint already filters out drafts at the DB-query level + * (patch-federation-unlisted-guards), and the AP syndicator has its own + * guard (patch-ap-skip-draft-syndication), but preventing syndication + * targets from being stored in the first place is the cleanest approach. + */ +import { access, readFile, writeFile } from "node:fs/promises"; + +const candidates = [ + "node_modules/@rmdes/indiekit-endpoint-microsub/lib/controllers/reader.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-microsub/lib/controllers/reader.js", +]; + +const patchSpecs = [ + { + name: "microsub-compose-extract-post-status", + oldSnippet: [ + ` const syndicateTo = request.body["mp-syndicate-to"];`, + ``, + ` // Debug logging`, + ` console.info(`, + ` "[Microsub] submitCompose request.body:",`, + ` JSON.stringify(request.body),`, + ` );`, + ` console.info("[Microsub] Extracted values:", {`, + ` content,`, + ` inReplyTo,`, + ` likeOf,`, + ` repostOf,`, + ` bookmarkOf,`, + ` syndicateTo,`, + ` });`, + ].join("\n"), + newSnippet: [ + ` const syndicateTo = request.body["mp-syndicate-to"];`, + ` const postStatus = request.body["post-status"];`, + ` const isDraft = postStatus === "draft";`, + ``, + ` // Debug logging`, + ` console.info(`, + ` "[Microsub] submitCompose request.body:",`, + ` JSON.stringify(request.body),`, + ` );`, + ` console.info("[Microsub] Extracted values:", {`, + ` content,`, + ` inReplyTo,`, + ` likeOf,`, + ` repostOf,`, + ` bookmarkOf,`, + ` syndicateTo,`, + ` postStatus,`, + ` });`, + ].join("\n"), + }, + { + name: "microsub-compose-draft-suppresses-syndication", + oldSnippet: [ + ` // Add syndication targets`, + ` if (syndicateTo) {`, + ` const targets = Array.isArray(syndicateTo) ? syndicateTo : [syndicateTo];`, + ` for (const target of targets) {`, + ` micropubData.append("mp-syndicate-to", target);`, + ` }`, + ` }`, + ].join("\n"), + newSnippet: [ + ` // Set post status (e.g. draft) — must be appended before syndication logic`, + ` if (postStatus) {`, + ` micropubData.append("post-status", postStatus);`, + ` }`, + ``, + ` // Add syndication targets — suppressed entirely for draft posts`, + ` if (syndicateTo && !isDraft) {`, + ` const targets = Array.isArray(syndicateTo) ? syndicateTo : [syndicateTo];`, + ` for (const target of targets) {`, + ` micropubData.append("mp-syndicate-to", target);`, + ` }`, + ` }`, + ].join("\n"), + }, +]; + +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) { + for (const filePath of candidates) { + if (!(await exists(filePath))) { + continue; + } + + checkedFiles.add(filePath); + + const source = await readFile(filePath, "utf8"); + + if (source.includes(spec.newSnippet)) { + // Already patched + continue; + } + + if (!source.includes(spec.oldSnippet)) { + console.warn( + `[postinstall] Skipping ${spec.name} patch for ${filePath}: upstream format changed`, + ); + continue; + } + + const updated = source.replace(spec.oldSnippet, spec.newSnippet); + await writeFile(filePath, updated, "utf8"); + patchedFiles.add(filePath); + } +} + +if (checkedFiles.size === 0) { + console.log("[postinstall] No microsub reader files found for draft guard patch"); +} else if (patchedFiles.size === 0) { + console.log("[postinstall] microsub compose draft guard already applied"); +} else { + console.log( + `[postinstall] Patched microsub compose draft guard in ${patchedFiles.size} file(s)`, + ); +} From 3d361c85aa2afc1f24d9dc29da1d6433e8ca3b26 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 11:27:03 +0100 Subject: [PATCH 05/59] chore(deps): update activitypub fork to v3.8.1 (tags.pub hashtag discovery) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5d851fbb..aa6571a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2356,8 +2356,8 @@ } }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { - "version": "3.6.8", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#f029c3128e4f47a4213c01264b816d76c170095e", + "version": "3.8.1", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#790c9a037540248b2d95a0d425fa5093899d15ed", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 67eacf6b999a2d9f145f82da1a834117718c7a05 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 11:40:57 +0100 Subject: [PATCH 06/59] fix(webmention): livefetch v6 with diagnostic log + reset-stale v11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit livefetch v6: - Adds console.log showing which property links were built per post (e.g. "in-reply-to" for replies) — makes it debuggable without server access - Fixes retryPatchedBlock to include the two comment lines the retry patch actually inserts (was missing them, causing "Target block not found" on fresh upstream → retry → livefetch path) - Adds v5 to priorMarkersNoContinue with contentToProcess-line end detection so v5 → v6 in-place upgrade works correctly reset-stale: bump to v11 to retry ca3d8 and any other posts stuck before v5/v6 deployment. Co-Authored-By: Claude Opus 4.6 --- scripts/patch-webmention-sender-livefetch.mjs | 41 +++++++++++++++---- .../patch-webmention-sender-reset-stale.mjs | 2 +- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/scripts/patch-webmention-sender-livefetch.mjs b/scripts/patch-webmention-sender-livefetch.mjs index 0bb9b099..529cf1a9 100644 --- a/scripts/patch-webmention-sender-livefetch.mjs +++ b/scripts/patch-webmention-sender-livefetch.mjs @@ -22,7 +22,7 @@ import { access, readFile, writeFile } from "node:fs/promises"; const filePath = "node_modules/@rmdes/indiekit-endpoint-webmention-sender/lib/controllers/webmention-sender.js"; -const patchMarker = "// [patched:livefetch:v5]"; +const patchMarker = "// [patched:livefetch:v6]"; // Original upstream code const originalBlock = ` // If no content, try fetching the published page @@ -64,6 +64,8 @@ const retryPatchedBlock = ` // If no content, try fetching the published if (!contentToProcess) { if (fetchFailed) { + // Page not yet available — skip and retry on next poll rather than + // permanently marking this post as sent with zero webmentions. console.log(\`[webmention] Page not yet available for \${postUrl}, will retry next poll\`); continue; } @@ -72,7 +74,7 @@ const retryPatchedBlock = ` // If no content, try fetching the published continue; }`; -const newBlock = ` // [patched:livefetch:v5] Build synthetic h-entry HTML from stored post properties. +const newBlock = ` // [patched:livefetch:v6] Build synthetic h-entry HTML from stored post properties. // The stored properties already contain all microformat target URLs // (in-reply-to, like-of, bookmark-of, repost-of) and content.html has inline // links — no live page fetch needed, and no exposure to internal DNS issues. @@ -95,7 +97,8 @@ const newBlock = ` // [patched:livefetch:v5] Build synthetic h-entry HTML } } const _bodyHtml = post.properties.content?.html || post.properties.content?.value || ""; - const contentToProcess = \`
\${_anchors.join("")}\${_bodyHtml ? \`
\${_bodyHtml}
\` : ""}
\`;`; + const contentToProcess = \`
\${_anchors.join("")}\${_bodyHtml ? \`
\${_bodyHtml}
\` : ""}
\`; + console.log(\`[webmention] Built synthetic h-entry for \${postUrl}: \${_anchors.length} prop link(s) [\${Object.entries(_propLinks).filter(([p]) => post.properties[p]).map(([p]) => p).join(", ") || "none"}]\`);`; async function exists(p) { try { @@ -114,21 +117,26 @@ if (!(await exists(filePath))) { const source = await readFile(filePath, "utf8"); if (source.includes(patchMarker)) { - console.log("[patch-webmention-sender-livefetch] Already patched (v5)"); + console.log("[patch-webmention-sender-livefetch] Already patched (v6)"); process.exit(0); } -// For v1–v4: extract the old patched block by finding the marker and the -// closing "continue;\n }" that ends the if (!contentToProcess) block. -const priorMarkers = [ +// Extract the old patched block by finding the marker and the end of the block. +// v1–v4 end with "continue;\n }" (the if (!contentToProcess) block). +// v5+ end with the contentToProcess assignment line (no continue block). +const priorMarkersWithContinue = [ "// [patched:livefetch:v4]", "// [patched:livefetch:v3]", "// [patched:livefetch:v2]", "// [patched:livefetch]", ]; +const priorMarkersNoContinue = [ + "// [patched:livefetch:v5]", +]; let oldPatchBlock = null; -for (const marker of priorMarkers) { + +for (const marker of priorMarkersWithContinue) { if (!source.includes(marker)) continue; const startIdx = source.lastIndexOf(` ${marker}`); const endMarker = " continue;\n }"; @@ -139,6 +147,21 @@ for (const marker of priorMarkers) { } } +if (!oldPatchBlock) { + for (const marker of priorMarkersNoContinue) { + if (!source.includes(marker)) continue; + const startIdx = source.lastIndexOf(` ${marker}`); + // v5 block ends with the contentToProcess = `...`; line + // Find the semicolon that closes the last template literal on that line + const endMarker = '""}`;\n'; + const endSearch = source.indexOf(endMarker, startIdx); + if (startIdx !== -1 && endSearch !== -1) { + oldPatchBlock = source.slice(startIdx, endSearch + endMarker.length); + break; + } + } +} + const targetBlock = oldPatchBlock ? oldPatchBlock : source.includes(originalBlock) @@ -162,4 +185,4 @@ if (!patched.includes(patchMarker)) { } await writeFile(filePath, patched, "utf8"); -console.log("[patch-webmention-sender-livefetch] Patched successfully (v5)"); +console.log("[patch-webmention-sender-livefetch] Patched successfully (v6)"); diff --git a/scripts/patch-webmention-sender-reset-stale.mjs b/scripts/patch-webmention-sender-reset-stale.mjs index 6ca0e507..5a471162 100644 --- a/scripts/patch-webmention-sender-reset-stale.mjs +++ b/scripts/patch-webmention-sender-reset-stale.mjs @@ -9,7 +9,7 @@ import { MongoClient } from "mongodb"; import config from "../indiekit.config.mjs"; -const MIGRATION_ID = "webmention-sender-reset-stale-v10"; +const MIGRATION_ID = "webmention-sender-reset-stale-v11"; const mongodbUrl = config.application?.mongodbUrl; if (!mongodbUrl) { From 33c10eafaf82dd10b8867b2b114a9f5f5a90351a Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 15:31:48 +0100 Subject: [PATCH 07/59] fix(deps): update activitypub fork (tags.pub AP JSON + signature fixes) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa6571a3..82400c8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2356,8 +2356,8 @@ } }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { - "version": "3.8.1", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#790c9a037540248b2d95a0d425fa5093899d15ed", + "version": "3.8.3", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#50c89f7c4ee16297bab7c03bf0a870b14b058679", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 64b489d2bcab8702efa08ac75a8edb7db992c305 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 15:52:10 +0100 Subject: [PATCH 08/59] chore(deps): sync activitypub fork with upstream/main (merge 14 behind commits) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 82400c8d..b36ce52b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2357,7 +2357,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.3", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#50c89f7c4ee16297bab7c03bf0a870b14b058679", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#6089df0c277635752c5f0d9fa72ed0f9ea9c7b94", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 786cf5b7e95750845b49476f4fadb4920ab80169 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 15:56:24 +0100 Subject: [PATCH 09/59] fix(deps): update activitypub fork (duplicate import fix) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index b36ce52b..234343b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2357,7 +2357,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.3", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#6089df0c277635752c5f0d9fa72ed0f9ea9c7b94", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#b3eb579696f8b2350cadecef3c4193473b2029b2", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 599312ba2f0b9c3e7f15340d6fb6fede2d746079 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 16:27:44 +0100 Subject: [PATCH 10/59] fix: bookmark main version --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 234343b8..b9d90187 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@rmdes/indiekit-endpoint-github": "^1.2.3", "@rmdes/indiekit-endpoint-homepage": "^1.0.22", "@rmdes/indiekit-endpoint-lastfm": "^1.0.12", - "@rmdes/indiekit-endpoint-microsub": "github:svemagie/indiekit-endpoint-microsub#bookmarks-import", + "@rmdes/indiekit-endpoint-microsub": "github:svemagie/indiekit-endpoint-microsub", "@rmdes/indiekit-endpoint-podroll": "^1.0.11", "@rmdes/indiekit-endpoint-posts": "^1.0.0-beta.44", "@rmdes/indiekit-endpoint-readlater": "github:rmdes/indiekit-endpoint-readlater", diff --git a/package.json b/package.json index 44c1c6d4..61f62837 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@rmdes/indiekit-endpoint-github": "^1.2.3", "@rmdes/indiekit-endpoint-homepage": "^1.0.22", "@rmdes/indiekit-endpoint-lastfm": "^1.0.12", - "@rmdes/indiekit-endpoint-microsub": "github:svemagie/indiekit-endpoint-microsub#bookmarks-import", + "@rmdes/indiekit-endpoint-microsub": "github:svemagie/indiekit-endpoint-microsub", "@rmdes/indiekit-endpoint-podroll": "^1.0.11", "@rmdes/indiekit-endpoint-posts": "^1.0.0-beta.44", "@rmdes/indiekit-endpoint-readlater": "github:rmdes/indiekit-endpoint-readlater", From c5a905b9be3d083343739a184bdb0f7f5d03fc92 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 16:30:14 +0100 Subject: [PATCH 11/59] fix(deps): update activitypub fork (remove duplicate import merge artifact) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index b9d90187..2414bbf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2357,7 +2357,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.3", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#b3eb579696f8b2350cadecef3c4193473b2029b2", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#95564a3e73c6f87b99ee0b61dc2ceff2e03ee68c", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 49e8a109c813f5deac93907851301cb894279d43 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 16:33:24 +0100 Subject: [PATCH 12/59] fix(deps): update activitypub fork (remove duplicate cachedUrl merge artifact) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 2414bbf7..9fa30ec2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2357,7 +2357,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.3", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#95564a3e73c6f87b99ee0b61dc2ceff2e03ee68c", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#0c077c2588c2cd28291022bd408f7e028e438bcf", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 4b1dfa20be3831b11d47af0c4c05d83a47b85716 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 17:26:12 +0100 Subject: [PATCH 13/59] fix(patches): update federation-diag silence patch for new fork version The fork added an Accept-header upgrade block between the diagnostic log and the return statement, breaking the OLD_SNIPPET match. Patch now handles both the original form and the updated form. Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 616 +++++++++++--------- scripts/patch-ap-remove-federation-diag.mjs | 32 +- 2 files changed, 363 insertions(+), 285 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9fa30ec2..60469e1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,15 +62,15 @@ } }, "node_modules/@atproto/api": { - "version": "0.14.22", - "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.14.22.tgz", - "integrity": "sha512-ziXPau+sUdFovObSnsoN7JbOmUw1C5e5L28/yXf3P8vbEnSS3HVVGD1jYcscBYY34xQqi4bVDpwMYx/4yRsTuQ==", + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.19.4.tgz", + "integrity": "sha512-fYNM62vdXxer0h8a9Jzl4/ag9uFIe0nTO+LkC6KTlx1yUDigrAoQMMbllIiCWj62GhUMxAkHabk/BZjjVAfKng==", "license": "MIT", "dependencies": { - "@atproto/common-web": "^0.4.1", - "@atproto/lexicon": "^0.4.10", - "@atproto/syntax": "^0.4.0", - "@atproto/xrpc": "^0.6.12", + "@atproto/common-web": "^0.4.18", + "@atproto/lexicon": "^0.6.2", + "@atproto/syntax": "^0.5.1", + "@atproto/xrpc": "^0.7.7", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", @@ -78,30 +78,21 @@ } }, "node_modules/@atproto/common-web": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.18.tgz", - "integrity": "sha512-ilImzP+9N/mtse440kN60pGrEzG7wi4xsV13nGeLrS+Zocybc/ISOpKlbZM13o+twPJ+Q7veGLw9CtGg0GAFoQ==", + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.19.tgz", + "integrity": "sha512-3BTi58p5WpT+9/zb6UZrdsXcfPo5P45UJm0E4iwHLILr+jc37CuBj9JReDSZ4U0i9RTrI3ZkfySyZ9bd+LnMsw==", "license": "MIT", "dependencies": { - "@atproto/lex-data": "^0.0.13", - "@atproto/lex-json": "^0.0.13", - "@atproto/syntax": "^0.5.0", + "@atproto/lex-data": "^0.0.14", + "@atproto/lex-json": "^0.0.14", + "@atproto/syntax": "^0.5.1", "zod": "^3.23.8" } }, - "node_modules/@atproto/common-web/node_modules/@atproto/syntax": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.5.0.tgz", - "integrity": "sha512-UA2DSpGdOQzUQ4gi5SH+NEJz/YR3a3Fg3y2oh+xETDSiTRmA4VhHRCojhXAVsBxUT6EnItw190C/KN+DWW90kw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, "node_modules/@atproto/lex-data": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.13.tgz", - "integrity": "sha512-7Z7RwZ1Y/JzBF/Tcn/I4UJ/vIGfh5zn1zjv0KX+flke2JtgFkSE8uh2hOtqgBQMNqE3zdJFM+dcSWln86hR3MQ==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.14.tgz", + "integrity": "sha512-53DUa9664SS76nGAMYopWsO10OH0AAdf7P/HSKB6Wzx3iqe6lk/K61QZnKxOG1LreYl5CfvIJU6eNf4txI6GlQ==", "license": "MIT", "dependencies": { "multiformats": "^9.9.0", @@ -111,44 +102,44 @@ } }, "node_modules/@atproto/lex-json": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.13.tgz", - "integrity": "sha512-hwLhkKaIHulGJpt0EfXAEWdrxqM2L1tV/tvilzhMp3QxPqYgXchFnrfVmLsyFDx6P6qkH1GsX/XC2V36U0UlPQ==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.14.tgz", + "integrity": "sha512-6lPkDKqe7teEu4WrN5q7400cvZKgYS3uwUMvzG3F9XkgVYhOwSDCtouV/nSLBbpvo3l9OP0kiigtclcNcyekww==", "license": "MIT", "dependencies": { - "@atproto/lex-data": "^0.0.13", + "@atproto/lex-data": "^0.0.14", "tslib": "^2.8.1" } }, "node_modules/@atproto/lexicon": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.14.tgz", - "integrity": "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.2.tgz", + "integrity": "sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==", "license": "MIT", "dependencies": { - "@atproto/common-web": "^0.4.2", - "@atproto/syntax": "^0.4.0", + "@atproto/common-web": "^0.4.18", + "@atproto/syntax": "^0.5.0", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "node_modules/@atproto/syntax": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", - "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.5.1.tgz", + "integrity": "sha512-J8DJjgKgACIyCTbpfvoTnf7+ofTx1kxTGO7KAftkC+jczaMdQhKdgIBAg2DaYy+80cvYGTHy5q/HI9qMAwGbWw==", "license": "MIT", "dependencies": { "tslib": "^2.8.1" } }, "node_modules/@atproto/xrpc": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.12.tgz", - "integrity": "sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w==", + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.7.tgz", + "integrity": "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==", "license": "MIT", "dependencies": { - "@atproto/lexicon": "^0.4.10", + "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, @@ -176,9 +167,9 @@ } }, "node_modules/@borewit/text-codec": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", - "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", "license": "MIT", "funding": { "type": "github", @@ -330,9 +321,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "license": "MIT", "optional": true, "dependencies": { @@ -772,9 +763,9 @@ "license": "MIT" }, "node_modules/@fedify/debugger": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@fedify/debugger/-/debugger-2.0.3.tgz", - "integrity": "sha512-Crs9ibtJ1kWUDAetZ9GtoATSj3Hu+KRhgwQuTW3sb+kqgXJhhKNoXFteC3T713A+f7ZFaWfy1rMSrp5HSlrIHA==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@fedify/debugger/-/debugger-2.0.7.tgz", + "integrity": "sha512-439rX7f6zxXuBfLCQefP5JSv1osdjG4IimuRrIQgb5xaG1VlSaixHn4eN94ii64TGD+6PUGbTFYZhRmnnYlwAA==", "dependencies": { "@js-temporal/polyfill": "^0.5.1", "@logtape/logtape": "^2.0.0", @@ -785,22 +776,22 @@ "hono": "^4.0.0" }, "peerDependencies": { - "@fedify/fedify": "^2.0.3" + "@fedify/fedify": "^2.0.7" } }, "node_modules/@fedify/fedify": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@fedify/fedify/-/fedify-2.0.3.tgz", - "integrity": "sha512-XQ9NevJz/Ch7vKVPU+sEn5ybRllQULcX8gkNUS+F2LoPyq4b1qqRwvcLXcWA+RNXH551jyWJFGo36ufEo4Xuyw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@fedify/fedify/-/fedify-2.0.7.tgz", + "integrity": "sha512-2/GYm/ukjg4t3+HXBgfxkoq1KUCGPJTxxmanmd+B4aGOmHX5PNfteSxpQxrHYzUGQNk46KFKnDF3+t6v2JKCfA==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" ], "license": "MIT", "dependencies": { - "@fedify/vocab": "2.0.3", - "@fedify/vocab-runtime": "2.0.3", - "@fedify/webfinger": "2.0.3", + "@fedify/vocab": "2.0.7", + "@fedify/vocab-runtime": "2.0.7", + "@fedify/webfinger": "2.0.7", "@js-temporal/polyfill": "^0.5.1", "@logtape/logtape": "^2.0.0", "@opentelemetry/api": "^1.9.0", @@ -811,7 +802,6 @@ "es-toolkit": "1.43.0", "json-canon": "^1.0.1", "jsonld": "^9.0.0", - "multicodec": "^3.2.1", "structured-field-values": "^2.0.4", "uri-template-router": "^1.0.0", "url-template": "^3.1.1", @@ -824,9 +814,9 @@ } }, "node_modules/@fedify/redis": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@fedify/redis/-/redis-2.0.3.tgz", - "integrity": "sha512-viTE9tDPZl0+3tHOG0eNKxXVDCoJDvK1u1WfcTDC9bVtNe9NsbVRshCc/r01/YfTxuoS9aHSNWUzgpm1nV21Fg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@fedify/redis/-/redis-2.0.7.tgz", + "integrity": "sha512-e9YfkDJxItaWibslo+D0+FAq+eEFG3t7GxjE51FRjQwKZj7+FK2BhGD5lOtlTwq00StRqmf2WYcjMC03lcE3cg==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" @@ -837,23 +827,23 @@ "@logtape/logtape": "^2.0.0" }, "peerDependencies": { - "@fedify/fedify": "^2.0.3", + "@fedify/fedify": "^2.0.7", "ioredis": "^5.8.2" } }, "node_modules/@fedify/vocab": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@fedify/vocab/-/vocab-2.0.3.tgz", - "integrity": "sha512-dfIsU9+2YHYLk9jBGspFyWBGzkwfxXf/d2da3yc/oT4ZaZiPt7KZjKeTuws0xsiBCAewwz/4v5ASoTa/aVHAeA==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@fedify/vocab/-/vocab-2.0.7.tgz", + "integrity": "sha512-jg1KpI2Yke26NcHrK8HS/OUeTYiCgcj98IvynBxgfzZY2MeZ1Bc7wg4VDRdHfz+TEMe9GwkF47Flj6DVcar0Zw==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" ], "license": "MIT", "dependencies": { - "@fedify/vocab-runtime": "2.0.3", - "@fedify/vocab-tools": "2.0.3", - "@fedify/webfinger": "2.0.3", + "@fedify/vocab-runtime": "2.0.7", + "@fedify/vocab-tools": "2.0.7", + "@fedify/webfinger": "2.0.7", "@js-temporal/polyfill": "^0.5.1", "@logtape/logtape": "^2.0.0", "@multiformats/base-x": "^4.0.1", @@ -861,7 +851,6 @@ "asn1js": "^3.0.6", "es-toolkit": "1.43.0", "jsonld": "^9.0.0", - "multicodec": "^3.2.1", "pkijs": "^3.3.3" }, "engines": { @@ -871,9 +860,9 @@ } }, "node_modules/@fedify/vocab-runtime": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@fedify/vocab-runtime/-/vocab-runtime-2.0.3.tgz", - "integrity": "sha512-1JHyWPVo0PF1XzZfr5bumw9OjR88ElYrM5/9/m2vpgSsJB6lSPXpbc53s79UVsAkavi1yNNgpnPTgSvhW3DFcA==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@fedify/vocab-runtime/-/vocab-runtime-2.0.7.tgz", + "integrity": "sha512-6YKAC1/Xk5GYUuWwX6BwhmMjLmSEiyJWZuu+u25aJ+0i9b93Cw2aGCuorTndvFZcpZ+TDGiEQfA0qVe6knJWcg==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" @@ -885,7 +874,7 @@ "@opentelemetry/api": "^1.9.0", "asn1js": "^3.0.6", "byte-encodings": "^1.0.11", - "multicodec": "^3.2.1", + "jsonld": "^9.0.0", "pkijs": "^3.3.3" }, "engines": { @@ -895,9 +884,9 @@ } }, "node_modules/@fedify/vocab-tools": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@fedify/vocab-tools/-/vocab-tools-2.0.3.tgz", - "integrity": "sha512-H3a0j+1STsfn7Ccgjd4CcKl5mwgVevYFZaXUcbBhXTRv72MrrzWPi9K33kuZSpr1ULh4pXapoeXOidE2hxz33A==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@fedify/vocab-tools/-/vocab-tools-2.0.7.tgz", + "integrity": "sha512-4jz6b0keXab6kjRCnr8oSLnHgdir30xPWV4HkQhVMIQROW6SVyUPMjRxr1+iuzUx70DqHuNdkDJPXX+1gPezNg==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" @@ -916,16 +905,16 @@ } }, "node_modules/@fedify/webfinger": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@fedify/webfinger/-/webfinger-2.0.3.tgz", - "integrity": "sha512-KjCDtGXFBY+iAgrPpKhBHV7/gfJ9MoIu4x95Ukd0pCwxzXAfFqC4nTRINivJ2OuN2QKdqDgtATG2VrCRM+teVg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@fedify/webfinger/-/webfinger-2.0.7.tgz", + "integrity": "sha512-JNSBGQHekvvGNWbIRaI05WDNh4xO13SWa/WG6JMxaabQKnnerXVyy1M36zKbEEaXASvANk3qGkXLPZCae3dJmg==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" ], "license": "MIT", "dependencies": { - "@fedify/vocab-runtime": "2.0.3", + "@fedify/vocab-runtime": "2.0.7", "@logtape/logtape": "^2.0.0", "@opentelemetry/api": "^1.9.0", "es-toolkit": "1.43.0" @@ -937,13 +926,10 @@ } }, "node_modules/@gar/promise-retry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz", - "integrity": "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", "license": "MIT", - "dependencies": { - "retry": "^0.13.1" - }, "engines": { "node": "^20.17.0 || >=22.9.0" } @@ -1040,6 +1026,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1056,6 +1045,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1072,6 +1064,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1088,6 +1083,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1104,6 +1102,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1120,6 +1121,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1136,6 +1140,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1152,6 +1159,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1168,6 +1178,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1190,6 +1203,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1212,6 +1228,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1234,6 +1253,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1256,6 +1278,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1278,6 +1303,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1300,6 +1328,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1322,6 +1353,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1415,12 +1449,12 @@ }, "node_modules/@indiekit/endpoint-auth": { "name": "@rmdes/indiekit-endpoint-auth", - "version": "1.0.0-beta.29", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-auth/-/indiekit-endpoint-auth-1.0.0-beta.29.tgz", - "integrity": "sha512-NeFqwuYWhkKxvStaDnQQNTAoTl4c4aS7uzrIuos/b2lSA7rJLmGanhzVWzZ2Pr9jH6L4CSI3rPhY5BOhEwbSOQ==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-auth/-/indiekit-endpoint-auth-1.0.0-beta.30.tgz", + "integrity": "sha512-uP5LdoDxU6qwoPuuPAqfO0S3M6JJDFC0V+OYJOkuMNp7Xo8GeITfXnHRjNOGAzgaWk/WAXDzK2uOEcwxis/vgw==", "license": "MIT", "dependencies": { - "@indiekit/error": "^1.0.0-beta.25", + "@indiekit/error": "^1.0.0-beta.27", "@indiekit/util": "^1.0.0-beta.25", "bcrypt": "^6.0.0", "express": "^5.0.0", @@ -1434,12 +1468,12 @@ }, "node_modules/@indiekit/endpoint-files": { "name": "@rmdes/indiekit-endpoint-files", - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-files/-/indiekit-endpoint-files-1.0.0.tgz", - "integrity": "sha512-RAKg+ZSxEHEwCgtyRlLelsNe6TqHtpDUi/wx7qEpiFQ2OeHyJvQ2uQcVRLra9hiD8OsFFF6syUs42dX0C8Slkg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-files/-/indiekit-endpoint-files-1.0.2.tgz", + "integrity": "sha512-m6l7HDyM9WPBDG1OsP8aGNShjyOv3ipaXPMJZjM2XZbLz91UHwV9NCwlzjaxJhMnDC+9ZLfkA0b9kagdm8uhZQ==", "license": "MIT", "dependencies": { - "@indiekit/error": "^1.0.0-beta.25", + "@indiekit/error": "^1.0.0-beta.27", "express": "^5.0.0", "express-validator": "^7.0.0" }, @@ -1515,9 +1549,9 @@ }, "node_modules/@indiekit/endpoint-posts": { "name": "@rmdes/indiekit-endpoint-posts", - "version": "1.0.0-beta.44", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-posts/-/indiekit-endpoint-posts-1.0.0-beta.44.tgz", - "integrity": "sha512-xGvsmqIOiQU0tpocDmNJw0CRvdj98zOVzpX+qzpPHBcYXSUZmbhRHd9/OaOD4dOWsmNT4dHYhmJ0OjTdUMTtUw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-posts/-/indiekit-endpoint-posts-1.0.0.tgz", + "integrity": "sha512-AHJgiG3pRpMZNZD+hF3QuwBbVoJecHL+9bWl6o8zpdVw0WtW0U+pnP0ArgcuGqiuJIYlI9z8oFeOp5Csy0WOfw==", "license": "MIT", "dependencies": { "@indiekit/endpoint-micropub": "^1.0.0-beta.27", @@ -1535,12 +1569,12 @@ }, "node_modules/@indiekit/endpoint-share": { "name": "@rmdes/indiekit-endpoint-share", - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-share/-/indiekit-endpoint-share-1.0.2.tgz", - "integrity": "sha512-rIjdvto0k97zEwDxExsXBCkusbSP/Dbn9CEHYh2675XWFZuzCdZtq+oO9SrMui5dLVl4MuGIxfGnhinCgE+n0A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-share/-/indiekit-endpoint-share-1.0.3.tgz", + "integrity": "sha512-xAB8U2rLAK8RHhp8rgmqeJGrUKjLBdtcvvhV4QbF7H6zfZIvy9zA7k+sU0oGRqkCGAvhqIMkD2ZaUpepGLMm/A==", "license": "MIT", "dependencies": { - "@indiekit/error": "^1.0.0-beta.25", + "@indiekit/error": "^1.0.0-beta.27", "express": "^5.0.0", "express-validator": "^7.0.0" }, @@ -1577,13 +1611,13 @@ }, "node_modules/@indiekit/frontend": { "name": "@rmdes/indiekit-frontend", - "version": "1.0.0-beta.38", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-frontend/-/indiekit-frontend-1.0.0-beta.38.tgz", - "integrity": "sha512-rgk9412YbomBYsp2a1l3vs9Mvbn2ZPMSlG+mpEQJJixYeq+Zeec9eftsYqiGfF4KiMoJWQu9QFg2rqj3uv0V+A==", + "version": "1.0.0-beta.39", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-frontend/-/indiekit-frontend-1.0.0-beta.39.tgz", + "integrity": "sha512-ObqgNIsVzNn1Hg2TzSwe5Amt4cHbMpmDyhfyUmGPYf6w2vhMQ/MnDQa+oLZXozIenpOKHXO0XyUFzlyxq4kS7w==", "license": "MIT", "dependencies": { "@accessible-components/tag-input": "^0.2.0", - "@indiekit/error": "^1.0.0-beta.25", + "@indiekit/error": "^1.0.0-beta.27", "@indiekit/util": "^1.0.0-beta.25", "color": "^5.0.0", "easymde": "^2.18.0", @@ -1918,9 +1952,9 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -1938,6 +1972,15 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -2138,6 +2181,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2158,6 +2204,9 @@ "cpu": [ "arm" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2178,6 +2227,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2198,6 +2250,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2218,6 +2273,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2238,6 +2296,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2398,7 +2459,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-blogroll": { "version": "1.0.23", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-blogroll.git#381b0397a55f75747af28c7221af31af3cc32c3f", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-blogroll.git#f5a62b966d1881df58014505b410547a38b3a5fb", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -2416,9 +2477,9 @@ } }, "node_modules/@rmdes/indiekit-endpoint-comments": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-comments/-/indiekit-endpoint-comments-1.0.11.tgz", - "integrity": "sha512-qUy0wr4Vc6pSnQmSLsBacm/MO3qZL02Ezg1uQiz9uM2fIUEx1dxv/G3i864ZyaCf7oW2pSOoUNtpyI3YybG+PQ==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-comments/-/indiekit-endpoint-comments-1.0.13.tgz", + "integrity": "sha512-TvWKSMsx4oRLnboQpHFzrwIif+mjKxr/2xEnQwFdnpMKiq8ED/8Hwu0UJiBu7GjSqL2EHS3J5LDbEu+5FT0twA==", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -2430,9 +2491,9 @@ } }, "node_modules/@rmdes/indiekit-endpoint-conversations": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-conversations/-/indiekit-endpoint-conversations-2.2.0.tgz", - "integrity": "sha512-o5ZiKMjcqAB1TDIvdzxPBH8Pai0vTEefyKJWhNEGt0da3ooZ9fkAuBg10vFyhMYFIVRLjRxkzKZKvrhb2e6MhQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-conversations/-/indiekit-endpoint-conversations-2.3.1.tgz", + "integrity": "sha512-/9wpbL/WcSNtJ6YVIk+2nzcgY2XVMdUA+BqYLEQIG13V4W1d80ujqEOXbZhV+bVqENGYwZtfHLx8EA9lXTNhxA==", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -2443,9 +2504,9 @@ } }, "node_modules/@rmdes/indiekit-endpoint-cv": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-cv/-/indiekit-endpoint-cv-1.0.24.tgz", - "integrity": "sha512-S6af420v6FYZspTMVr4yU6pjiyY+TszlcktMn3/Wpj4xW7vlFqGR/LC4RKhH0zRHI2A5koMVXVfl30bu1/POPw==", + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-cv/-/indiekit-endpoint-cv-1.0.25.tgz", + "integrity": "sha512-cg/JtTQRE/g/ce7svGvfvhBo3br+TvBxw4f/sUcIWAIGw1me12ldk5RBxq4OCwjN7W6LRkXYlAIphhdD8+lKvQ==", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -2460,9 +2521,9 @@ } }, "node_modules/@rmdes/indiekit-endpoint-funkwhale": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-funkwhale/-/indiekit-endpoint-funkwhale-1.0.11.tgz", - "integrity": "sha512-AbPobqh5P/2SG8l+fJ4IGJmelGr0u0lfNsNHOxkOfhEckC5iIELssj/1e/E+eW42y58StpzsdQ+xqPIT5ur9PA==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-funkwhale/-/indiekit-endpoint-funkwhale-1.0.12.tgz", + "integrity": "sha512-B/dnnIRljD54mVN8CCBpcsNC02U2i/DtM+0O6V2YizTkWVV8kFG/JzQYGedV3EDDpFckCrSq+KAi5OcQGfbdLg==", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -2476,9 +2537,9 @@ } }, "node_modules/@rmdes/indiekit-endpoint-github": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-github/-/indiekit-endpoint-github-1.2.3.tgz", - "integrity": "sha512-HTKEcwvR21u/LOJD5e3iEbeAnm3OLkxok8TG3/32DxdVdTtN4MBOgtZP58bM7IAvIBIobEVyyp0Rpyn7EG0odQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-github/-/indiekit-endpoint-github-1.2.5.tgz", + "integrity": "sha512-LxACv/uMUQHEFItW4JqlgjqEUCeW9FnVYfVTesPbPTu0d7SwM0eh0q3XzvgsGhDc6luCwbJYaxa6wbkbfI/Mbw==", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -2492,9 +2553,9 @@ } }, "node_modules/@rmdes/indiekit-endpoint-homepage": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-homepage/-/indiekit-endpoint-homepage-1.0.22.tgz", - "integrity": "sha512-lvOJOL/OK7VfibutCaHosP8Sk3kPChavHuo4ufYmigsxSI62lHsAUZbkpKrdEY6qbxJv0dr/b9Yax6Hw1jZqIA==", + "version": "1.0.23", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-homepage/-/indiekit-endpoint-homepage-1.0.23.tgz", + "integrity": "sha512-J9wpfnLIuJ74fUV212cM11RexDrQPMRPxDfbC9C6nWWy1hZTuzSqKoEOstykuALZvRlC7xHwPfyLBf842oROlg==", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -2525,8 +2586,8 @@ } }, "node_modules/@rmdes/indiekit-endpoint-microsub": { - "version": "1.0.45", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-microsub.git#1bb80588fdd154e9c74628d69aa3118ff9620422", + "version": "1.0.49", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-microsub.git#f3e8648fedfc3fa6a66a5d24c4635ab96bbc414d", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -2546,9 +2607,9 @@ } }, "node_modules/@rmdes/indiekit-endpoint-podroll": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-podroll/-/indiekit-endpoint-podroll-1.0.11.tgz", - "integrity": "sha512-ShCVRfeGntKhXUtCDOIKbAAWBHM+ssj+QVCCGNq7rFL5tSdFEKFtwVCIYS/nvrQztzN1tCxT5AjjGGrXc9Xz9g==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-podroll/-/indiekit-endpoint-podroll-1.0.13.tgz", + "integrity": "sha512-FyQyhCPRnjpCh5xu+sWocrO8NuuJ5NNmWyIhdrpQWlM49QX6mDfSXCoAFmfrETSnPZa+WQ0EsDFk9BTra4B/2A==", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -2564,9 +2625,9 @@ } }, "node_modules/@rmdes/indiekit-endpoint-posts": { - "version": "1.0.0-beta.44", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-posts/-/indiekit-endpoint-posts-1.0.0-beta.44.tgz", - "integrity": "sha512-xGvsmqIOiQU0tpocDmNJw0CRvdj98zOVzpX+qzpPHBcYXSUZmbhRHd9/OaOD4dOWsmNT4dHYhmJ0OjTdUMTtUw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-posts/-/indiekit-endpoint-posts-1.0.0.tgz", + "integrity": "sha512-AHJgiG3pRpMZNZD+hF3QuwBbVoJecHL+9bWl6o8zpdVw0WtW0U+pnP0ArgcuGqiuJIYlI9z8oFeOp5Csy0WOfw==", "license": "MIT", "dependencies": { "@indiekit/endpoint-micropub": "^1.0.0-beta.27", @@ -2583,8 +2644,8 @@ } }, "node_modules/@rmdes/indiekit-endpoint-readlater": { - "version": "1.0.5", - "resolved": "git+ssh://git@github.com/rmdes/indiekit-endpoint-readlater.git#ee09c683a3c1ff91357cfd0b17d4151299583290", + "version": "1.0.6", + "resolved": "git+ssh://git@github.com/rmdes/indiekit-endpoint-readlater.git#2d1493c14cbb0e0a437ddc2b98ab03cab57d6342", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", @@ -2705,18 +2766,18 @@ } }, "node_modules/@rmdes/indiekit-syndicator-bluesky": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/@rmdes/indiekit-syndicator-bluesky/-/indiekit-syndicator-bluesky-1.0.19.tgz", - "integrity": "sha512-+vbiA+B20TJqQumd+591Xb1lip1LbuE1jXwcjx2bQ6ZrZcgd8jeFIbeZ3IRkQ0Yg6ru7gYsYV+byTbmusu3eIQ==", + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@rmdes/indiekit-syndicator-bluesky/-/indiekit-syndicator-bluesky-1.0.20.tgz", + "integrity": "sha512-YlNDgVK1GamUAe40nUPxfdpeBibuUZrAL5fN0v2n4jtoWUrlpJnIfdRdDDG2B7O4DFPNSs4PtBJ6wY5Y+xhpjg==", "license": "MIT", "dependencies": { - "@atproto/api": "^0.14.0", + "@atproto/api": "^0.19.3", "html-to-text": "^9.0.0", "jsdom": "^24.0.0", "sharp": "^0.33.0" }, "peerDependencies": { - "@indiekit/error": "^1.0.0-beta.25", + "@indiekit/error": "^1.0.0-beta.27", "@indiekit/indiekit": "1.x", "@indiekit/util": "^1.0.0-beta.25" } @@ -2804,6 +2865,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2820,6 +2884,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2836,6 +2903,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2852,6 +2922,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2868,6 +2941,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2884,6 +2960,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2900,6 +2979,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2922,6 +3004,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2944,6 +3029,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2966,6 +3054,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2988,6 +3079,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3010,6 +3104,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3548,9 +3645,9 @@ } }, "node_modules/cacache": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", - "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", "license": "ISC", "dependencies": { "@npmcli/fs": "^5.0.0", @@ -3562,17 +3659,16 @@ "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", - "ssri": "^13.0.0", - "unique-filename": "^5.0.0" + "ssri": "^13.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -3655,6 +3751,12 @@ "canonicalize": "bin/canonicalize.js" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, "node_modules/cheerio": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", @@ -4739,9 +4841,9 @@ } }, "node_modules/feedparser": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/feedparser/-/feedparser-2.2.10.tgz", - "integrity": "sha512-WoAOooa61V8/xuKMi2pEtK86qQ3ZH/M72EEGdqlOTxxb3m6ve1NPvZcmPFs3wEDfcBbFLId2GqZ4YjsYi+h1xA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/feedparser/-/feedparser-2.3.0.tgz", + "integrity": "sha512-vLmlMV/7pLWpsPmzfvmtYxo+kmkQgjCZPDmB/H+0eMXG29NNssvlxSbpLzJ0LHRyLsGW8ewFOpfRInHjMUgX1g==", "license": "MIT", "dependencies": { "addressparser": "^1.0.1", @@ -4752,7 +4854,7 @@ "lodash.uniq": "^4.5.0", "mri": "^1.1.5", "readable-stream": "^2.3.7", - "sax": "^1.2.4" + "sax": ">=1.2.4 <1.4.4" }, "bin": { "feedparser": "bin/feedparser.js" @@ -4785,9 +4887,9 @@ } }, "node_modules/file-type": { - "version": "21.3.2", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", - "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", "license": "MIT", "dependencies": { "@tokenizer/inflate": "^0.4.1", @@ -5008,9 +5110,9 @@ "license": "ISC" }, "node_modules/h3": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.6.tgz", - "integrity": "sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.10.tgz", + "integrity": "sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg==", "license": "MIT", "dependencies": { "cookie-es": "^1.2.2", @@ -5073,9 +5175,9 @@ } }, "node_modules/hono": { - "version": "4.12.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", - "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", + "version": "4.12.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", + "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -5292,15 +5394,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -5308,9 +5401,9 @@ "license": "ISC" }, "node_modules/ioredis": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", - "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", "license": "MIT", "dependencies": { "@ioredis/commands": "1.5.1", @@ -6163,13 +6256,14 @@ } }, "node_modules/make-fetch-happen": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", - "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", + "integrity": "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==", "license": "ISC", "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", @@ -6552,9 +6646,9 @@ } }, "node_modules/mlly": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz", - "integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "license": "MIT", "dependencies": { "acorn": "^8.16.0", @@ -6643,17 +6737,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/multicodec": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/multicodec/-/multicodec-3.2.1.tgz", - "integrity": "sha512-+expTPftro8VAW8kfvcuNNNBgb9gPeNYV9dn+z1kJRWF2vih+/S79f2RVeIwmrJBUJ6NT9IUPWnZDQvegEh5pw==", - "deprecated": "This module has been superseded by the multiformats module", - "license": "MIT", - "dependencies": { - "uint8arrays": "^3.0.0", - "varint": "^6.0.0" - } - }, "node_modules/multiformats": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", @@ -7144,9 +7227,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -7207,9 +7290,9 @@ } }, "node_modules/pkijs": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz", - "integrity": "sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", "license": "BSD-3-Clause", "dependencies": { "@noble/hashes": "1.4.0", @@ -7512,15 +7595,6 @@ "node": ">=4" } }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -7608,19 +7682,31 @@ "license": "MIT" }, "node_modules/sanitize-html": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.1.tgz", - "integrity": "sha512-ehFCW+q1a4CSOWRAdX97BX/6/PDEkCqw7/0JXZAGQV57FQB3YOkTa/rrzHPeJ+Aghy4vZAFfWMYyfxIiB7F/gw==", + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.2.tgz", + "integrity": "sha512-EnffJUl46VE9uvZ0XeWzObHLurClLlT12gsOk1cHyP2Ol1P0BnBnsXmShlBmWVJM+dKieQI68R0tsPY5m/B+Jg==", "license": "MIT", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", - "htmlparser2": "^8.0.0", + "htmlparser2": "^10.1.0", "is-plain-object": "^5.0.0", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, + "node_modules/sanitize-html/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/sanitize-html/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7634,9 +7720,9 @@ } }, "node_modules/sanitize-html/node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -7648,18 +7734,15 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, "node_modules/sax": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", - "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", + "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", + "license": "BlueOak-1.0.0" }, "node_modules/saxes": { "version": "6.0.0", @@ -7942,12 +8025,6 @@ "node": ">=22" } }, - "node_modules/snakecase-keys/node_modules/change-case": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", - "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", - "license": "MIT" - }, "node_modules/snakecase-keys/node_modules/map-obj": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.2.tgz", @@ -8087,9 +8164,9 @@ } }, "node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0" @@ -8142,6 +8219,15 @@ "node": ">=16" } }, + "node_modules/svgo/node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -8173,9 +8259,9 @@ } }, "node_modules/tar": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", - "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", + "version": "7.5.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", + "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -8292,9 +8378,9 @@ } }, "node_modules/type-fest": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", - "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" @@ -8366,9 +8452,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.2.tgz", - "integrity": "sha512-P9J1HWYV/ajFr8uCqk5QixwiRKmB1wOamgS0e+o2Z4A44Ej2+thFVRLG/eA7qprx88XXhnV5Bl8LHXTURpzB3Q==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -8478,30 +8564,6 @@ "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==", "license": "MIT" }, - "node_modules/unique-filename": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", - "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", - "license": "ISC", - "dependencies": { - "unique-slug": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/unique-slug": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", - "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -8617,9 +8679,9 @@ } }, "node_modules/unstorage/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -8709,12 +8771,6 @@ "node": ">= 0.10" } }, - "node_modules/varint": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", - "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8811,9 +8867,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -8900,9 +8956,9 @@ } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/scripts/patch-ap-remove-federation-diag.mjs b/scripts/patch-ap-remove-federation-diag.mjs index 0d4b82f0..3b21fcb0 100644 --- a/scripts/patch-ap-remove-federation-diag.mjs +++ b/scripts/patch-ap-remove-federation-diag.mjs @@ -14,7 +14,8 @@ const candidates = [ const MARKER = "// ap-remove-federation-diag patch"; -const OLD_SNIPPET = ` // Diagnostic: log inbox POSTs to detect federation stalls +// Matches the original form (diag block immediately before the return) +const OLD_SNIPPET_V1 = ` // Diagnostic: log inbox POSTs to detect federation stalls if (req.method === "POST" && req.path.includes("inbox")) { const ua = req.get("user-agent") || "unknown"; const bodyParsed = req.body !== undefined && Object.keys(req.body || {}).length > 0; @@ -23,9 +24,23 @@ const OLD_SNIPPET = ` // Diagnostic: log inbox POSTs to detect federation s return self._fedifyMiddleware(req, res, next);`; -const NEW_SNIPPET = ` // ap-remove-federation-diag patch +const NEW_SNIPPET_V1 = ` // ap-remove-federation-diag patch return self._fedifyMiddleware(req, res, next);`; +// Matches the updated form (diag block followed by Accept-upgrade block before the return) +const OLD_SNIPPET_V2 = ` // Diagnostic: log inbox POSTs to detect federation stalls + if (req.method === "POST" && req.path.includes("inbox")) { + const ua = req.get("user-agent") || "unknown"; + const bodyParsed = req.body !== undefined && Object.keys(req.body || {}).length > 0; + console.info(\`[federation-diag] POST \${req.path} from=\${ua.slice(0, 60)} bodyParsed=\${bodyParsed} readable=\${req.readable}\`); + } + + // Fedify's`; + +const NEW_SNIPPET_V2 = ` // ap-remove-federation-diag patch + + // Fedify's`; + async function exists(filePath) { try { await access(filePath); @@ -50,12 +65,19 @@ for (const filePath of candidates) { continue; // already patched } - if (!source.includes(OLD_SNIPPET)) { + let matched = false; + if (source.includes(OLD_SNIPPET_V1)) { + source = source.replace(OLD_SNIPPET_V1, NEW_SNIPPET_V1); + matched = true; + } else if (source.includes(OLD_SNIPPET_V2)) { + source = source.replace(OLD_SNIPPET_V2, NEW_SNIPPET_V2); + matched = true; + } + + if (!matched) { console.log(`[postinstall] patch-ap-remove-federation-diag: snippet not found in ${filePath}`); continue; } - - source = source.replace(OLD_SNIPPET, NEW_SNIPPET); await writeFile(filePath, source, "utf8"); patched += 1; console.log(`[postinstall] Applied patch-ap-remove-federation-diag to ${filePath}`); From 93367051798617899f9c42adafd0e4bca34175dc Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:01:00 +0100 Subject: [PATCH 14/59] fix(deps): update activitypub fork (tags.pub direct follow workaround) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 60469e1a..659e096e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2417,8 +2417,8 @@ } }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { - "version": "3.8.3", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#0c077c2588c2cd28291022bd408f7e028e438bcf", + "version": "3.8.4", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#fee1706d383b4a74c57c8bd185c73371f765f928", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 7087ad53756463bedfbe72506bfc3115856bb9e1 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:02:36 +0100 Subject: [PATCH 15/59] chore(deps): sync activitypub fork with upstream/main Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 659e096e..6188fbd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.4", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#fee1706d383b4a74c57c8bd185c73371f765f928", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#a37bece8cd51f3a8992367ed778aa2d8d0ce9230", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From c230feff7f9456aee2ec708c09d6606651843fbc Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:03:37 +0100 Subject: [PATCH 16/59] docs: document 2026-03-22 activitypub upstream sync and merge artifact fixes Co-Authored-By: Claude Sonnet 4.6 --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 16766c91..ac50e83c 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,14 @@ Four packages are installed directly from GitHub forks rather than the npm regis | Dependency | Source | Reason | |---|---|---| -| `@rmdes/indiekit-endpoint-activitypub` | [svemagie/indiekit-endpoint-activitypub](https://github.com/svemagie/indiekit-endpoint-activitypub) | DM support, likes-as-bookmarks, OG images in AP objects, draft/unlisted outbox guards, merged with upstream v3.7.5 | +| `@rmdes/indiekit-endpoint-activitypub` | [svemagie/indiekit-endpoint-activitypub](https://github.com/svemagie/indiekit-endpoint-activitypub) | DM support, likes-as-bookmarks, OG images in AP objects, draft/unlisted outbox guards, merged with upstream post-3.8.1 | | `@rmdes/indiekit-endpoint-blogroll` | [svemagie/indiekit-endpoint-blogroll#bookmark-import](https://github.com/svemagie/indiekit-endpoint-blogroll/tree/bookmark-import) | Bookmark import feature | | `@rmdes/indiekit-endpoint-microsub` | [svemagie/indiekit-endpoint-microsub#bookmarks-import](https://github.com/svemagie/indiekit-endpoint-microsub/tree/bookmarks-import) | Bookmarks import feature | | `@rmdes/indiekit-endpoint-youtube` | [svemagie/indiekit-endpoint-youtube](https://github.com/svemagie/indiekit-endpoint-youtube) | OAuth 2.0 liked-videos sync as "like" posts | In `package.json` these use the `github:owner/repo[#branch]` syntax so npm fetches them directly from GitHub on install. -> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `97a902b` (merged upstream v3.7.1–v3.7.5: async signed→unsigned lookup fallback, enrichAccountStats for embedded account objects, URL/mention linkification in statuses, domain_blocking in relationships, real domain_blocks endpoint, Moderation section in federation mgmt dashboard). +> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `a37bece` (synced with upstream post-3.8.1: tags.pub hashtag discovery, AP JSON content negotiation fix, RSA Multikey removal, direct follow workaround for tags.pub identity/v1 context rejection; merge artifacts removed). --- @@ -654,6 +654,15 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ## Changelog +### 2026-03-22 + +**chore(deps): sync activitypub fork with upstream post-3.8.1** (`a37bece` in svemagie/indiekit-endpoint-activitypub) +Four upstream fixes merged since 3.8.1, plus resolution of merge artifacts introduced by the upstream sync: +- `9a0d6d20`: serve AP JSON for actor URLs received without an explicit `text/html` Accept header — fixes content negotiation for clients that omit Accept +- `4495667e`: remove RSA Multikey from `assertionMethod` in the actor document — was causing tags.pub signature verification failures +- `c71fd691`: direct follow workaround for tags.pub `identity/v1` JSON-LD context rejection — tags.pub rejects the W3C identity context on incoming follows; new `lib/direct-follow.js` sends follows without that context +- Merge artifacts removed: duplicate `import { getActorUrlFromId }` in `accounts.js`, duplicate `const cachedUrl` declaration in `resolveActorUrl`, and a stray extra `import { remoteActorId }` in `account-cache.js` — all introduced when cherry-picked commits were merged back against upstream's copy of the same changes + ### 2026-03-21 **chore(deps): merge upstream activitypub v3.7.1–v3.7.5 into fork** (`97a902b` in svemagie/indiekit-endpoint-activitypub) From dc850c350ff449dca7df79757912853b544a9de8 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:13:48 +0100 Subject: [PATCH 17/59] fix(patches): update repost-commentary patch to detect native fork implementation Fix D's old snippet (the bare `} else {` content block) still matched after the fork absorbed the repost-commentary changes natively, causing a duplicate `} else if (postType === "repost") {` block to be inserted on every deploy and preventing startup. Added NATIVE_MARKER check for the fork's own repost branch so the patch skips cleanly when the changes are already present. Co-Authored-By: Claude Sonnet 4.6 --- scripts/patch-ap-repost-commentary.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/patch-ap-repost-commentary.mjs b/scripts/patch-ap-repost-commentary.mjs index b8881c0a..bdfb779d 100644 --- a/scripts/patch-ap-repost-commentary.mjs +++ b/scripts/patch-ap-repost-commentary.mjs @@ -34,6 +34,8 @@ const candidates = [ ]; const MARKER = "// repost-commentary fix"; +// Also present when the fork has this change baked in natively (no comment marker needed) +const NATIVE_MARKER = '} else if (postType === "repost") {'; // --------------------------------------------------------------------------- // Fix A – jf2ToActivityStreams(): add commentary variable before the return @@ -123,7 +125,7 @@ for (const filePath of candidates) { checked += 1; let source = await readFile(filePath, "utf8"); - if (source.includes(MARKER)) { + if (source.includes(MARKER) || source.includes(NATIVE_MARKER)) { console.log(`[postinstall] patch-ap-repost-commentary: already applied to ${filePath}`); continue; } From 81abaee1607082c4d94c13eb6150619a8ddaaee2 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:16:22 +0100 Subject: [PATCH 18/59] chore(patches): remove patch-ap-repost-commentary (baked into fork) Repost commentary changes are now native in svemagie/indiekit-endpoint-activitypub. Patch is no longer needed and was causing a duplicate repost block on every deploy. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 4 +- scripts/patch-ap-repost-commentary.mjs | 169 ------------------------- 2 files changed, 2 insertions(+), 171 deletions(-) delete mode 100644 scripts/patch-ap-repost-commentary.mjs diff --git a/package.json b/package.json index 61f62837..cf10e9c0 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "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-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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-like-note-dispatcher.mjs && node scripts/patch-ap-like-activity-id.mjs && node scripts/patch-ap-like-activity-dispatcher.mjs && node scripts/patch-ap-url-lookup-api-like.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-remove-federation-diag.mjs && node scripts/patch-ap-skip-draft-syndication.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-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-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-like-note-dispatcher.mjs && node scripts/patch-ap-like-activity-id.mjs && node scripts/patch-ap-like-activity-dispatcher.mjs && node scripts/patch-ap-url-lookup-api-like.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-remove-federation-diag.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node 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-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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-like-note-dispatcher.mjs && node scripts/patch-ap-like-activity-id.mjs && node scripts/patch-ap-like-activity-dispatcher.mjs && node scripts/patch-ap-url-lookup-api-like.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-remove-federation-diag.mjs && node scripts/patch-ap-skip-draft-syndication.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-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-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-like-note-dispatcher.mjs && node scripts/patch-ap-like-activity-id.mjs && node scripts/patch-ap-like-activity-dispatcher.mjs && node scripts/patch-ap-url-lookup-api-like.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-remove-federation-diag.mjs && node scripts/patch-ap-skip-draft-syndication.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-ap-repost-commentary.mjs b/scripts/patch-ap-repost-commentary.mjs deleted file mode 100644 index bdfb779d..00000000 --- a/scripts/patch-ap-repost-commentary.mjs +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Patch: include commentary in ActivityPub output for reposts. - * - * Root cause (two bugs in jf2-to-as2.js): - * - * 1. jf2ToAS2Activity() (Fedify delivery) always generates a bare - * `Announce { object: }` for repost posts, even when the - * post has a body (the author's commentary). External URLs like - * fromjason.xyz don't serve ActivityPub JSON, so Mastodon receives the - * Announce but cannot fetch the object — the activity is silently dropped - * from followers' timelines. The post only appears when searched because - * Mastodon then fetches the blog's own AP Note representation directly. - * - * 2. jf2ToActivityStreams() (content negotiation / search) returns a Note - * whose `content` field is hardcoded to `🔁 `, completely ignoring - * any commentary text in properties.content. - * - * Fix: - * - jf2ToAS2Activity(): if the repost has commentary, skip the early - * Announce return and fall through to the existing Create(Note) path so - * the text is included and the activity is a proper federated Note. - * Pure reposts (no commentary) keep the Announce behaviour. - * - jf2ToAS2Activity() content block: add a `repost` branch that formats - * the note as `

🔁 ` (mirroring bookmark/like). - * - jf2ToActivityStreams(): extract commentary from properties.content and - * prepend it to the note content when present. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", -]; - -const MARKER = "// repost-commentary fix"; -// Also present when the fork has this change baked in natively (no comment marker needed) -const NATIVE_MARKER = '} else if (postType === "repost") {'; - -// --------------------------------------------------------------------------- -// Fix A – jf2ToActivityStreams(): add commentary variable before the return -// --------------------------------------------------------------------------- -const OLD_CN_VARS = ` const repostOf = properties["repost-of"]; - const postUrl = resolvePostUrl(properties.url, publicationUrl); - return { - "@context": "https://www.w3.org/ns/activitystreams",`; - -const NEW_CN_VARS = ` const repostOf = properties["repost-of"]; - const postUrl = resolvePostUrl(properties.url, publicationUrl); - const commentary = linkifyUrls(properties.content?.html || properties.content || ""); // repost-commentary fix - return { - "@context": "https://www.w3.org/ns/activitystreams",`; - -// --------------------------------------------------------------------------- -// Fix B – jf2ToActivityStreams(): use commentary in the content field -// --------------------------------------------------------------------------- -const OLD_CN_CONTENT = ` cc: [\`\${actorUrl.replace(/\\/$/, "")}/followers\`], - content: \`\\u{1F501} \${repostOf}\`,`; - -const NEW_CN_CONTENT = ` cc: [\`\${actorUrl.replace(/\\/$/, "")}/followers\`], - content: commentary // repost-commentary fix - ? \`\${commentary}

\\u{1F501} \${repostOf}\` // repost-commentary fix - : \`\\u{1F501} \${repostOf}\`, // repost-commentary fix`; - -// --------------------------------------------------------------------------- -// Fix C – jf2ToAS2Activity(): only Announce when there is no commentary; -// fall through to Create(Note) when commentary is present -// --------------------------------------------------------------------------- -const OLD_AS2_ANNOUNCE = ` if (!repostOf) return null; - return new Announce({ - actor: actorUri, - object: new URL(repostOf), - to: new URL("https://www.w3.org/ns/activitystreams#Public"), - }); - }`; - -const NEW_AS2_ANNOUNCE = ` if (!repostOf) return null; - const repostContent = properties.content?.html || properties.content || ""; // repost-commentary fix - if (!repostContent) { // repost-commentary fix - return new Announce({ - actor: actorUri, - object: new URL(repostOf), - to: new URL("https://www.w3.org/ns/activitystreams#Public"), - }); - } // repost-commentary fix - // Has commentary — fall through to Create(Note) so the text is federated // repost-commentary fix - }`; - -// --------------------------------------------------------------------------- -// Fix D – jf2ToAS2Activity() content block: add repost branch -// --------------------------------------------------------------------------- -const OLD_AS2_CONTENT = ` } else { - noteOptions.content = linkifyUrls(properties.content?.html || properties.content || ""); - }`; - -const NEW_AS2_CONTENT = ` } else if (postType === "repost") { // repost-commentary fix - const repostUrl = properties["repost-of"]; // repost-commentary fix - const commentary = linkifyUrls(properties.content?.html || properties.content || ""); // repost-commentary fix - noteOptions.content = commentary // repost-commentary fix - ? \`\${commentary}

\\u{1F501} \${repostUrl}\` // repost-commentary fix - : \`\\u{1F501} \${repostUrl}\`; // repost-commentary fix - } else { - noteOptions.content = linkifyUrls(properties.content?.html || properties.content || ""); - }`; - -// --------------------------------------------------------------------------- - -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; - let source = await readFile(filePath, "utf8"); - - if (source.includes(MARKER) || source.includes(NATIVE_MARKER)) { - console.log(`[postinstall] patch-ap-repost-commentary: already applied to ${filePath}`); - continue; - } - - let updated = source; - let changed = false; - - // Apply each replacement, warn if the old string is not found - const replacements = [ - ["Fix A (CN vars)", OLD_CN_VARS, NEW_CN_VARS], - ["Fix B (CN content)", OLD_CN_CONTENT, NEW_CN_CONTENT], - ["Fix C (AS2 announce)", OLD_AS2_ANNOUNCE, NEW_AS2_ANNOUNCE], - ["Fix D (AS2 content block)", OLD_AS2_CONTENT, NEW_AS2_CONTENT], - ]; - - for (const [label, oldStr, newStr] of replacements) { - if (updated.includes(oldStr)) { - updated = updated.replace(oldStr, newStr); - changed = true; - } else { - console.warn(`[postinstall] patch-ap-repost-commentary: ${label} snippet not found in ${filePath} — skipping`); - } - } - - if (!changed || updated === source) { - console.log(`[postinstall] patch-ap-repost-commentary: no changes applied to ${filePath}`); - continue; - } - - await writeFile(filePath, updated, "utf8"); - patched += 1; - console.log(`[postinstall] Applied patch-ap-repost-commentary to ${filePath}`); -} - -if (checked === 0) { - console.log("[postinstall] patch-ap-repost-commentary: no target files found"); -} else if (patched === 0) { - console.log("[postinstall] patch-ap-repost-commentary: already up to date"); -} else { - console.log(`[postinstall] patch-ap-repost-commentary: patched ${patched}/${checked} file(s)`); -} From 18a946c9ea69b949f3a3d17cd21952d2002f2b2e Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:21:26 +0100 Subject: [PATCH 19/59] chore(patches): remove 10 obsolete AP patches now baked into fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All of the following are now native in svemagie/indiekit-endpoint-activitypub: - patch-ap-url-lookup-api (AP URL lookup endpoint) - patch-ap-allow-private-address (allowPrivateAddress in federation-setup) - patch-ap-like-note-dispatcher (fake-Note revert) - patch-ap-like-activity-id (canonical Like activity id URI) - patch-ap-like-activity-dispatcher (Like setObjectDispatcher) - patch-ap-url-lookup-api-like (likeOf URL in /api/ap-url) - patch-ap-remove-federation-diag (inbox diagnostic log removed) - patch-ap-og-image (orphan, not in package.json) - patch-ap-normalize-nested-tags (orphan, no-op) - patch-ap-object-url-trailing-slash (orphan, no-op) patch-ap-skip-draft-syndication kept — draft guard in syndicate() not yet in fork. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 4 +- scripts/patch-ap-allow-private-address.mjs | 153 -------------- scripts/patch-ap-like-activity-dispatcher.mjs | 113 ----------- scripts/patch-ap-like-activity-id.mjs | 91 --------- scripts/patch-ap-like-note-dispatcher.mjs | 87 -------- scripts/patch-ap-normalize-nested-tags.mjs | 112 ----------- .../patch-ap-object-url-trailing-slash.mjs | 81 -------- scripts/patch-ap-og-image.mjs | 136 ------------- scripts/patch-ap-remove-federation-diag.mjs | 92 --------- scripts/patch-ap-url-lookup-api-like.mjs | 110 ---------- scripts/patch-ap-url-lookup-api.mjs | 188 ------------------ 11 files changed, 2 insertions(+), 1165 deletions(-) delete mode 100644 scripts/patch-ap-allow-private-address.mjs delete mode 100644 scripts/patch-ap-like-activity-dispatcher.mjs delete mode 100644 scripts/patch-ap-like-activity-id.mjs delete mode 100644 scripts/patch-ap-like-note-dispatcher.mjs delete mode 100644 scripts/patch-ap-normalize-nested-tags.mjs delete mode 100644 scripts/patch-ap-object-url-trailing-slash.mjs delete mode 100644 scripts/patch-ap-og-image.mjs delete mode 100644 scripts/patch-ap-remove-federation-diag.mjs delete mode 100644 scripts/patch-ap-url-lookup-api-like.mjs delete mode 100644 scripts/patch-ap-url-lookup-api.mjs diff --git a/package.json b/package.json index cf10e9c0..f4c3f82b 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "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-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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-like-note-dispatcher.mjs && node scripts/patch-ap-like-activity-id.mjs && node scripts/patch-ap-like-activity-dispatcher.mjs && node scripts/patch-ap-url-lookup-api-like.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-remove-federation-diag.mjs && node scripts/patch-ap-skip-draft-syndication.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-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-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.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-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-like-note-dispatcher.mjs && node scripts/patch-ap-like-activity-id.mjs && node scripts/patch-ap-like-activity-dispatcher.mjs && node scripts/patch-ap-url-lookup-api-like.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-remove-federation-diag.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node 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-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-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.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-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-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.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-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.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-ap-allow-private-address.mjs b/scripts/patch-ap-allow-private-address.mjs deleted file mode 100644 index 9118b8a4..00000000 --- a/scripts/patch-ap-allow-private-address.mjs +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Patch: allow Fedify to fetch URLs that resolve to private IP addresses. - * - * Root cause: - * blog.giersig.eu resolves to 10.100.0.10 (a private RFC-1918 address) - * from within the home network where the indiekit server runs. When a - * remote Fediverse server sends an activity (Like, Announce, etc.) whose - * object URL points to blog.giersig.eu, Fedify tries to dereference that - * URL to validate the object. Its built-in SSRF guard calls - * validatePublicUrl(), sees the resolved IP is private, and throws: - * - * Disallowed private URL: 'https://blog.giersig.eu/likes/ed6d1/' - * Invalid or private address: 10.100.0.10 - * - * This causes WebFinger lookups and lookupObject() calls for own-site URLs - * to fail, producing ERR-level noise in the log and breaking thread loading - * in the ActivityPub reader for local posts. - * - * Fix: - * Pass allowPrivateAddress: true to createFederation. This disables the - * SSRF IP check so Fedify can dereference own-site URLs. The network-level - * solution (split-horizon DNS returning the public IP inside the LAN) is - * cleaner but requires router/DNS changes outside the codebase. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "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 MARKER = "// allow private address fix"; - -const patchSpecs = [ - // Case 1: v2.15+ — signatureTimeWindow present, upstream comment style (no marker suffix) - { - name: "upstream-v2.15-with-signature-time-window", - oldSnippet: ` const federation = createFederation({ - kv, - queue, - // Accept signatures up to 12 h old. - // Mastodon retries failed deliveries with the original signature, which - // can be hours old by the time the delivery succeeds. - signatureTimeWindow: { hours: 12 }, - });`, - newSnippet: ` const federation = createFederation({ - kv, - queue, - // Accept signatures up to 12 h old. - // Mastodon retries failed deliveries with the original signature, which - // can be hours old by the time the delivery succeeds. - signatureTimeWindow: { hours: 12 }, - // Allow fetching own-site URLs that resolve to private IPs. // allow private address fix - // blog.giersig.eu resolves to 10.100.0.10 on the home LAN. Without this, - // Fedify's SSRF guard blocks lookupObject() / WebFinger for own posts. - allowPrivateAddress: true, - });`, - }, - // Case 2: signatureTimeWindow present with old marker comment style - { - name: "with-signature-time-window-marker", - oldSnippet: ` const federation = createFederation({ - kv, - queue, - // Accept signatures up to 12 h old. // signature time window fix - // Mastodon retries failed deliveries with the original signature, which - // can be hours old by the time the delivery succeeds. - signatureTimeWindow: { hours: 12 }, - });`, - newSnippet: ` const federation = createFederation({ - kv, - queue, - // Accept signatures up to 12 h old. // signature time window fix - // Mastodon retries failed deliveries with the original signature, which - // can be hours old by the time the delivery succeeds. - signatureTimeWindow: { hours: 12 }, - // Allow fetching own-site URLs that resolve to private IPs. // allow private address fix - // blog.giersig.eu resolves to 10.100.0.10 on the home LAN. Without this, - // Fedify's SSRF guard blocks lookupObject() / WebFinger for own posts. - allowPrivateAddress: true, - });`, - }, - // Case 3: fresh install without signatureTimeWindow — add both - { - name: "fresh-without-signature-time-window", - oldSnippet: ` const federation = createFederation({ - kv, - queue, - });`, - newSnippet: ` const federation = createFederation({ - kv, - queue, - // Accept signatures up to 12 h old. // signature time window fix - // Mastodon retries failed deliveries with the original signature, which - // can be hours old by the time the delivery succeeds. - signatureTimeWindow: { hours: 12 }, - // Allow fetching own-site URLs that resolve to private IPs. // allow private address fix - // blog.giersig.eu resolves to 10.100.0.10 on the home LAN. Without this, - // Fedify's SSRF guard blocks lookupObject() / WebFinger for own posts. - allowPrivateAddress: true, - });`, - }, -]; - -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; - let source = await readFile(filePath, "utf8"); - - if (source.includes(MARKER) || source.includes("allowPrivateAddress")) { - continue; - } - - let applied = false; - for (const spec of patchSpecs) { - if (!source.includes(spec.oldSnippet)) continue; - const updated = source.replace(spec.oldSnippet, spec.newSnippet); - if (updated === source) continue; - await writeFile(filePath, updated, "utf8"); - patched += 1; - applied = true; - console.log(`[postinstall] Applied patch-ap-allow-private-address (${spec.name}) to ${filePath}`); - break; - } - - if (!applied) { - console.log(`[postinstall] patch-ap-allow-private-address: no matching snippet in ${filePath} — skipping`); - } -} - -if (checked === 0) { - console.log("[postinstall] patch-ap-allow-private-address: no target files found"); -} else if (patched === 0) { - console.log("[postinstall] patch-ap-allow-private-address: already up to date"); -} else { - console.log(`[postinstall] patch-ap-allow-private-address: patched ${patched}/${checked} file(s)`); -} diff --git a/scripts/patch-ap-like-activity-dispatcher.mjs b/scripts/patch-ap-like-activity-dispatcher.mjs deleted file mode 100644 index 20d7409b..00000000 --- a/scripts/patch-ap-like-activity-dispatcher.mjs +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Patch: register a Fedify Like activity dispatcher in federation-setup.js. - * - * Per ActivityPub §3.1, objects with an `id` MUST be dereferenceable at that - * URI. The Like activities produced by jf2ToAS2Activity (after patch-ap-like- - * activity-id.mjs adds an id) need a corresponding Fedify object dispatcher so - * that fetching /activitypub/activities/like/{id} returns the Like activity. - * - * Fix: - * Add federation.setObjectDispatcher(Like, ...) after the Article dispatcher - * in setupObjectDispatchers(). The handler looks up the post, calls - * jf2ToAS2Activity, and returns the Like if that's what was produced. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "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 MARKER = "// ap-like-activity-dispatcher patch"; - -const OLD_SNIPPET = ` // Article dispatcher - federation.setObjectDispatcher( - Article, - \`\${mountPath}/objects/article/{+id}\`, - async (ctx, { id }) => { - const obj = await resolvePost(ctx, id); - return obj instanceof Article ? obj : null; - }, - ); -}`; - -const NEW_SNIPPET = ` // Article dispatcher - federation.setObjectDispatcher( - Article, - \`\${mountPath}/objects/article/{+id}\`, - async (ctx, { id }) => { - const obj = await resolvePost(ctx, id); - return obj instanceof Article ? obj : null; - }, - ); - - // Like activity dispatcher — makes AP-like activities dereferenceable (AP §3.1) - // ap-like-activity-dispatcher patch - federation.setObjectDispatcher( - Like, - \`\${mountPath}/activities/like/{+id}\`, - async (ctx, { id }) => { - if (!collections.posts || !publicationUrl) return null; - const postUrl = \`\${publicationUrl.replace(/\\/$/, "")}/\${id}\`; - const post = await collections.posts.findOne({ - "properties.url": { $in: [postUrl, postUrl + "/"] }, - }); - if (!post) return null; - if (post?.properties?.["post-status"] === "draft") return null; - if (post?.properties?.visibility === "unlisted") return null; - if (post.properties?.deleted) return null; - const actorUrl = ctx.getActorUri(handle).href; - const activity = await jf2ToAS2Activity(post.properties, actorUrl, publicationUrl); - return activity instanceof Like ? activity : null; - }, - ); -}`; - -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; - let source = await readFile(filePath, "utf8"); - - if (source.includes(MARKER)) { - continue; // already patched - } - - if (!source.includes(OLD_SNIPPET)) { - console.log(`[postinstall] patch-ap-like-activity-dispatcher: snippet not found in ${filePath}`); - continue; - } - - // Ensure Like is imported from @fedify/fedify/vocab (may be absent on fresh installs) - if (!source.includes(" Like,")) { - source = source.replace(" Note,", " Like,\n Note,"); - } - - source = source.replace(OLD_SNIPPET, NEW_SNIPPET); - await writeFile(filePath, source, "utf8"); - patched += 1; - console.log(`[postinstall] Applied patch-ap-like-activity-dispatcher to ${filePath}`); -} - -if (checked === 0) { - console.log("[postinstall] patch-ap-like-activity-dispatcher: no target files found"); -} else if (patched === 0) { - console.log("[postinstall] patch-ap-like-activity-dispatcher: already up to date"); -} else { - console.log(`[postinstall] patch-ap-like-activity-dispatcher: patched ${patched}/${checked} file(s)`); -} diff --git a/scripts/patch-ap-like-activity-id.mjs b/scripts/patch-ap-like-activity-id.mjs deleted file mode 100644 index 0ef1ff0a..00000000 --- a/scripts/patch-ap-like-activity-id.mjs +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Patch: add a canonical `id` to the Like activity produced by jf2ToAS2Activity. - * - * Per ActivityPub §6.2.1, activities sent from a server SHOULD have an `id` - * URI so that remote servers can dereference them. The current Like activity - * has no `id`, which means it cannot be looked up by its URL. - * - * Fix: - * In jf2-to-as2.js, derive the mount path from the actor URL and construct - * a canonical id at /activitypub/activities/like/{post-path}. - * - * This enables: - * - The Like activity dispatcher (patch-ap-like-activity-dispatcher.mjs) to - * serve the Like at its canonical URL. - * - Remote servers to dereference the Like activity by its id. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", -]; - -const MARKER = "// ap-like-activity-id patch"; - -const OLD_SNIPPET = ` return new Like({ - actor: actorUri, - object: new URL(likeOfUrl), - to: new URL("https://www.w3.org/ns/activitystreams#Public"), - });`; - -const NEW_SNIPPET = ` // ap-like-activity-id patch - // Derive mount path from actor URL (e.g. "/activitypub") so we can - // construct the canonical id without needing mountPath in options. - const actorPath = new URL(actorUrl).pathname; // e.g. "/activitypub/users/sven" - const mp = actorPath.replace(/\\/users\\/[^/]+$/, ""); // → "/activitypub" - const postRelPath = (properties.url || "") - .replace(publicationUrl.replace(/\\/$/, ""), "") - .replace(/^\\//, "") - .replace(/\\/$/, ""); // e.g. "likes/9acc3" - const likeActivityId = \`\${publicationUrl.replace(/\\/$/, "")}\${mp}/activities/like/\${postRelPath}\`; - return new Like({ - id: new URL(likeActivityId), - actor: actorUri, - object: new URL(likeOfUrl), - to: new URL("https://www.w3.org/ns/activitystreams#Public"), - });`; - -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; - let source = await readFile(filePath, "utf8"); - - if (source.includes(MARKER)) { - continue; // already patched - } - - if (!source.includes(OLD_SNIPPET)) { - console.log(`[postinstall] patch-ap-like-activity-id: snippet not found in ${filePath}`); - continue; - } - - source = source.replace(OLD_SNIPPET, NEW_SNIPPET); - await writeFile(filePath, source, "utf8"); - patched += 1; - console.log(`[postinstall] Applied patch-ap-like-activity-id to ${filePath}`); -} - -if (checked === 0) { - console.log("[postinstall] patch-ap-like-activity-id: no target files found"); -} else if (patched === 0) { - console.log("[postinstall] patch-ap-like-activity-id: already up to date"); -} else { - console.log(`[postinstall] patch-ap-like-activity-id: patched ${patched}/${checked} file(s)`); -} diff --git a/scripts/patch-ap-like-note-dispatcher.mjs b/scripts/patch-ap-like-note-dispatcher.mjs deleted file mode 100644 index ce7666ce..00000000 --- a/scripts/patch-ap-like-note-dispatcher.mjs +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Patch: REVERT the wrong ap-like-note-dispatcher change in federation-setup.js. - * - * The previous version of this script served AP-likes as fake Notes at the - * Note dispatcher URL, which violated ActivityPub semantics (Like activities - * should not be served as Notes). - * - * This rewritten version removes that fake-Note block and restores the original - * resolvePost() logic. The correct AP-compliant fixes are handled by: - * - patch-ap-like-activity-id.mjs (adds id to Like activity) - * - patch-ap-like-activity-dispatcher.mjs (registers Like object dispatcher) - * - patch-ap-url-lookup-api-like.mjs (returns likeOf URL for AP-likes in widget) - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js", -]; - -// Marker from the old wrong patch — if this is present, we need to revert -const WRONG_PATCH_MARKER = "// ap-like-note-dispatcher patch"; - -// Clean up the Like import comment added by the old patch -const OLD_IMPORT = ` Like, // Like import for ap-like-note-dispatcher patch`; -const NEW_IMPORT = ` Like,`; - -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; - let source = await readFile(filePath, "utf8"); - - if (!source.includes(WRONG_PATCH_MARKER)) { - // Already reverted (or never applied) - continue; - } - - let modified = false; - - // 1. Clean up Like import comment - if (source.includes(OLD_IMPORT)) { - source = source.replace(OLD_IMPORT, NEW_IMPORT); - modified = true; - } - - // 2. Remove fake Note block — use regex to avoid escaping issues with - // unicode escapes and template literals inside the block. - // Match from the opening comment through `return await activity.getObject();` - const fakeNoteBlock = / \/\/ Only Create activities wrap Note\/Article objects\.\n[\s\S]*? return await activity\.getObject\(\);/; - if (fakeNoteBlock.test(source)) { - source = source.replace( - fakeNoteBlock, - ` // Only Create activities wrap Note/Article objects\n if (!(activity instanceof Create)) return null;\n return await activity.getObject();`, - ); - modified = true; - } - - if (modified) { - await writeFile(filePath, source, "utf8"); - patched += 1; - console.log(`[postinstall] Reverted ap-like-note-dispatcher patch in ${filePath}`); - } -} - -if (checked === 0) { - console.log("[postinstall] patch-ap-like-note-dispatcher: no target files found"); -} else if (patched === 0) { - console.log("[postinstall] patch-ap-like-note-dispatcher: already up to date"); -} else { - console.log(`[postinstall] patch-ap-like-note-dispatcher: reverted ${patched}/${checked} file(s)`); -} diff --git a/scripts/patch-ap-normalize-nested-tags.mjs b/scripts/patch-ap-normalize-nested-tags.mjs deleted file mode 100644 index 12f29d43..00000000 --- a/scripts/patch-ap-normalize-nested-tags.mjs +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Patch: normalize nested/hierarchical tags before syndicating to the fediverse. - * - * Root cause: - * Posts use nested tag notation like `on/art/music` or `art/music`. When - * these are sent as ActivityPub Hashtag objects, the full path becomes the - * hashtag name (e.g. #on/art/music), which is invalid on Mastodon and other - * fediverse platforms. Clients display them as broken links or plain text. - * - * Fix: - * Extract only the last segment of each slash-separated tag before building - * the hashtag name. `on/art/music` → `music`, `art/music` → `music`. - * The href still links to the full category path on the publication so - * internal navigation is unaffected. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", -]; - -const MARKER = "// normalize nested tags fix"; - -const OLD_PLAIN = ` tags.push({ - type: "Hashtag", - name: \`#\${cat.replace(/\\s+/g, "")}\`, - href: \`\${publicationUrl}categories/\${encodeURIComponent(cat)}\`, - });`; - -const NEW_PLAIN = ` tags.push({ - type: "Hashtag", - name: \`#\${cat.split("/").at(-1).replace(/\\s+/g, "")}\`, // normalize nested tags fix - href: \`\${publicationUrl}categories/\${encodeURIComponent(cat)}\`, - });`; - -const OLD_FEDIFY = ` tags.push( - new Hashtag({ - name: \`#\${cat.replace(/\\s+/g, "")}\`, - href: new URL( - \`\${publicationUrl}categories/\${encodeURIComponent(cat)}\`, - ), - }), - );`; - -const NEW_FEDIFY = ` tags.push( - new Hashtag({ - name: \`#\${cat.split("/").at(-1).replace(/\\s+/g, "")}\`, // normalize nested tags fix - href: new URL( - \`\${publicationUrl}categories/\${encodeURIComponent(cat)}\`, - ), - }), - );`; - -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; - let source = await readFile(filePath, "utf8"); - - if (source.includes(MARKER)) { - continue; - } - - let updated = source; - let changed = false; - - if (source.includes(OLD_PLAIN)) { - updated = updated.replace(OLD_PLAIN, NEW_PLAIN); - changed = true; - } else { - console.log(`[postinstall] patch-ap-normalize-nested-tags: buildPlainTags snippet not found in ${filePath}`); - } - - if (source.includes(OLD_FEDIFY)) { - updated = updated.replace(OLD_FEDIFY, NEW_FEDIFY); - changed = true; - } else { - console.log(`[postinstall] patch-ap-normalize-nested-tags: buildFedifyTags snippet not found in ${filePath}`); - } - - if (!changed || updated === source) { - continue; - } - - await writeFile(filePath, updated, "utf8"); - patched += 1; - console.log(`[postinstall] Applied patch-ap-normalize-nested-tags to ${filePath}`); -} - -if (checked === 0) { - console.log("[postinstall] patch-ap-normalize-nested-tags: no target files found"); -} else if (patched === 0) { - console.log("[postinstall] patch-ap-normalize-nested-tags: already up to date"); -} else { - console.log(`[postinstall] patch-ap-normalize-nested-tags: patched ${patched}/${checked} file(s)`); -} diff --git a/scripts/patch-ap-object-url-trailing-slash.mjs b/scripts/patch-ap-object-url-trailing-slash.mjs deleted file mode 100644 index e12d094b..00000000 --- a/scripts/patch-ap-object-url-trailing-slash.mjs +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Patch: make the Fedify object dispatcher's post lookup tolerate trailing-slash - * differences between the AP object URL and the stored post URL. - * - * Root cause: - * setupObjectDispatchers resolvePost() builds postUrl from the {+id} template - * variable (e.g. "replies/bd78a") and does an exact findOne() match against - * posts.properties.url. Posts in MongoDB are stored with a trailing slash - * ("https://blog.giersig.eu/replies/bd78a/"), but the AP object URL returned - * by the /api/ap-url lookup endpoint has no trailing slash. The exact match - * fails → Fedify returns 404 → remote instance shows "Could not connect". - * - * Fix: - * Replace the single-value findOne() with a $in query that tries both the - * bare URL and the URL with a trailing slash appended. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "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 MARKER = "// trailing-slash url fix"; - -const OLD_SNIPPET = ` const postUrl = \`\${publicationUrl.replace(/\\/$/, "")}/\${id}\`; - const post = await collections.posts.findOne({ "properties.url": postUrl });`; - -const NEW_SNIPPET = ` const postUrl = \`\${publicationUrl.replace(/\\/$/, "")}/\${id}\`; // trailing-slash url fix - const post = await collections.posts.findOne({ - "properties.url": { $in: [postUrl, postUrl + "/"] }, - });`; - -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)) { - continue; - } - - if (!source.includes(OLD_SNIPPET)) { - console.log(`[postinstall] patch-ap-object-url-trailing-slash: snippet not found in ${filePath}`); - continue; - } - - const updated = source.replace(OLD_SNIPPET, NEW_SNIPPET); - - if (updated === source) { - continue; - } - - await writeFile(filePath, updated, "utf8"); - patched += 1; - console.log(`[postinstall] Applied patch-ap-object-url-trailing-slash to ${filePath}`); -} - -if (checked === 0) { - console.log("[postinstall] patch-ap-object-url-trailing-slash: no target files found"); -} else if (patched === 0) { - console.log("[postinstall] patch-ap-object-url-trailing-slash: already up to date"); -} else { - console.log(`[postinstall] patch-ap-object-url-trailing-slash: patched ${patched}/${checked} file(s)`); -} diff --git a/scripts/patch-ap-og-image.mjs b/scripts/patch-ap-og-image.mjs deleted file mode 100644 index 8874707e..00000000 --- a/scripts/patch-ap-og-image.mjs +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Patch: fix OG image URL generation in ActivityPub jf2-to-as2.js. - * - * Root cause: - * Both 842fc5af and 45f8ba9 versions of jf2-to-as2.js try to extract the - * post slug from the URL using a regex that expects date-based URLs like - * /articles/2024/01/15/slug/ but this blog uses flat URLs like /articles/slug/. - * The regex never matches so the `image` property is never set — no OG image - * preview card reaches Mastodon or other fediverse servers. - * - * Fix: - * Replace the date-from-URL regex with a simple last-path-segment extraction. - * Constructs /og/{slug}.png — the actual filename pattern the Eleventy build - * generates for static OG preview images (e.g. /og/2615b.png). - * - * Both jf2ToActivityStreams() (plain JSON-LD) and jf2ToAS2Activity() (Fedify - * vocab objects) are patched. Both 842fc5af and 45f8ba9 variants are handled - * so the patch works regardless of which commit npm install resolved. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", -]; - -const MARKER = "// og-image fix"; - -// --------------------------------------------------------------------------- -// Use JS regex patterns to locate the OG image blocks. -// Both 842fc5af and 45f8ba9 share the same variable names (ogMatch / ogMatchF) -// and the same if-block structure, differing only in the URL construction. -// -// Pattern: matches from "const ogMatch[F] = postUrl && postUrl.match(" to the -// closing "}" (2-space indent) of the if block. -// --------------------------------------------------------------------------- -const CN_BLOCK_RE = - / const ogMatch = postUrl && postUrl\.match\([^\n]+\n if \(ogMatch\) \{[\s\S]*?\n \}/; - -const AS2_BLOCK_RE = - / const ogMatchF = postUrl && postUrl\.match\([^\n]+\n if \(ogMatchF\) \{[\s\S]*?\n \}/; - -// --------------------------------------------------------------------------- -// Replacement: extract slug from last URL path segment. -// Build /og/{slug}.png to match the Eleventy OG filenames (e.g. /og/2615b.png). -// -// Template literal note: backslashes inside the injected regex are doubled so -// they survive the template literal → string conversion: -// \\\/ → \/ (escaped slash in regex) -// [\\\w-] → [\w-] (word char class) -// --------------------------------------------------------------------------- -const NEW_CN = ` const ogSlug = postUrl && postUrl.match(/\\/([\\\w-]+)\\/?$/)?.[1]; // og-image fix - if (ogSlug) { // og-image fix - object.image = { - type: "Image", - url: \`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogSlug}.png\`, // og-image fix - mediaType: "image/png", - }; - }`; - -const NEW_AS2 = ` const ogSlugF = postUrl && postUrl.match(/\\/([\\\w-]+)\\/?$/)?.[1]; // og-image fix - if (ogSlugF) { // og-image fix - noteOptions.image = new Image({ - url: new URL(\`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogSlugF}.png\`), // og-image fix - mediaType: "image/png", - }); - }`; - -// --------------------------------------------------------------------------- - -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] patch-ap-og-image: already applied to ${filePath}`); - continue; - } - - let updated = source; - let changed = false; - - // Fix the jf2ToActivityStreams OG block - if (CN_BLOCK_RE.test(updated)) { - updated = updated.replace(CN_BLOCK_RE, NEW_CN); - changed = true; - } else { - console.warn( - `[postinstall] patch-ap-og-image: jf2ToActivityStreams OG block not found in ${filePath} — skipping`, - ); - } - - // Fix the jf2ToAS2Activity OG block - if (AS2_BLOCK_RE.test(updated)) { - updated = updated.replace(AS2_BLOCK_RE, NEW_AS2); - changed = true; - } else { - console.warn( - `[postinstall] patch-ap-og-image: jf2ToAS2Activity OG block not found in ${filePath} — skipping`, - ); - } - - if (!changed || updated === source) { - console.log(`[postinstall] patch-ap-og-image: no changes applied to ${filePath}`); - continue; - } - - await writeFile(filePath, updated, "utf8"); - patched += 1; - console.log(`[postinstall] Applied patch-ap-og-image to ${filePath}`); -} - -if (checked === 0) { - console.log("[postinstall] patch-ap-og-image: no target files found"); -} else if (patched === 0) { - console.log("[postinstall] patch-ap-og-image: already up to date"); -} else { - console.log(`[postinstall] patch-ap-og-image: patched ${patched}/${checked} file(s)`); -} diff --git a/scripts/patch-ap-remove-federation-diag.mjs b/scripts/patch-ap-remove-federation-diag.mjs deleted file mode 100644 index 3b21fcb0..00000000 --- a/scripts/patch-ap-remove-federation-diag.mjs +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Patch: remove federation-diag inbox logging from the ActivityPub endpoint. - * - * The diagnostic block logs every inbox POST to detect federation stalls. - * It is no longer needed and produces noise in indiekit.log. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", -]; - -const MARKER = "// ap-remove-federation-diag patch"; - -// Matches the original form (diag block immediately before the return) -const OLD_SNIPPET_V1 = ` // Diagnostic: log inbox POSTs to detect federation stalls - if (req.method === "POST" && req.path.includes("inbox")) { - const ua = req.get("user-agent") || "unknown"; - const bodyParsed = req.body !== undefined && Object.keys(req.body || {}).length > 0; - console.info(\`[federation-diag] POST \${req.path} from=\${ua.slice(0, 60)} bodyParsed=\${bodyParsed} readable=\${req.readable}\`); - } - - return self._fedifyMiddleware(req, res, next);`; - -const NEW_SNIPPET_V1 = ` // ap-remove-federation-diag patch - return self._fedifyMiddleware(req, res, next);`; - -// Matches the updated form (diag block followed by Accept-upgrade block before the return) -const OLD_SNIPPET_V2 = ` // Diagnostic: log inbox POSTs to detect federation stalls - if (req.method === "POST" && req.path.includes("inbox")) { - const ua = req.get("user-agent") || "unknown"; - const bodyParsed = req.body !== undefined && Object.keys(req.body || {}).length > 0; - console.info(\`[federation-diag] POST \${req.path} from=\${ua.slice(0, 60)} bodyParsed=\${bodyParsed} readable=\${req.readable}\`); - } - - // Fedify's`; - -const NEW_SNIPPET_V2 = ` // ap-remove-federation-diag patch - - // Fedify's`; - -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; - let source = await readFile(filePath, "utf8"); - - if (source.includes(MARKER)) { - continue; // already patched - } - - let matched = false; - if (source.includes(OLD_SNIPPET_V1)) { - source = source.replace(OLD_SNIPPET_V1, NEW_SNIPPET_V1); - matched = true; - } else if (source.includes(OLD_SNIPPET_V2)) { - source = source.replace(OLD_SNIPPET_V2, NEW_SNIPPET_V2); - matched = true; - } - - if (!matched) { - console.log(`[postinstall] patch-ap-remove-federation-diag: snippet not found in ${filePath}`); - continue; - } - await writeFile(filePath, source, "utf8"); - patched += 1; - console.log(`[postinstall] Applied patch-ap-remove-federation-diag to ${filePath}`); -} - -if (checked === 0) { - console.log("[postinstall] patch-ap-remove-federation-diag: no target files found"); -} else if (patched === 0) { - console.log("[postinstall] patch-ap-remove-federation-diag: already up to date"); -} else { - console.log(`[postinstall] patch-ap-remove-federation-diag: patched ${patched}/${checked} file(s)`); -} diff --git a/scripts/patch-ap-url-lookup-api-like.mjs b/scripts/patch-ap-url-lookup-api-like.mjs deleted file mode 100644 index 7a5ac402..00000000 --- a/scripts/patch-ap-url-lookup-api-like.mjs +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Patch: make the /api/ap-url endpoint return the liked post URL for AP-likes. - * - * Root cause: - * For like posts where like-of is an ActivityPub URL (e.g. a Mastodon status), - * the "Also on: Fediverse" widget's authorize_interaction flow needs to send - * the user to the original AP object, not to a blog-side Note URL. - * - * The current handler always returns a /activitypub/objects/note/{id} URL, - * which 404s for AP-likes (because jf2ToAS2Activity returns a Like activity, - * not a Create(Note), so the Note dispatcher returns null). - * - * Fix: - * Before building the Note/Article URL, check whether the post is an AP-like - * (like-of is a URL that responds with application/activity+json). If it is, - * return { apUrl: likeOf } so that authorize_interaction opens the original - * AP object on the remote instance, where the user can interact with it. - * - * Non-AP likes (like-of is a plain web URL) fall through to the existing - * Note URL logic unchanged. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", -]; - -const MARKER = "// ap-url-lookup-api-like patch"; - -const OLD_SNIPPET = ` // Determine the AP object type (mirrors jf2-to-as2.js logic) - const postType = post.properties?.["post-type"]; - const isArticle = postType === "article" && !!post.properties?.name; - const objectType = isArticle ? "article" : "note";`; - -const NEW_SNIPPET = ` // Determine the AP object type (mirrors jf2-to-as2.js logic) - const postType = post.properties?.["post-type"]; - - // For AP-likes: the widget should open the liked post on the remote instance. - // We detect AP URLs the same way as jf2-to-as2.js: HEAD with activity+json Accept. - // ap-url-lookup-api-like patch - if (postType === "like") { - const likeOf = post.properties?.["like-of"] || ""; - if (likeOf) { - let isAp = false; - try { - const ctrl = new AbortController(); - const tid = setTimeout(() => ctrl.abort(), 3000); - const r = await fetch(likeOf, { - method: "HEAD", - headers: { Accept: "application/activity+json, application/ld+json" }, - signal: ctrl.signal, - }); - clearTimeout(tid); - const ct = r.headers.get("content-type") || ""; - isAp = ct.includes("activity+json") || ct.includes("ld+json"); - } catch { /* network error — treat as non-AP */ } - if (isAp) { - res.set("Cache-Control", "public, max-age=60"); - return res.json({ apUrl: likeOf }); - } - } - } - - const isArticle = postType === "article" && !!post.properties?.name; - const objectType = isArticle ? "article" : "note";`; - -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; - let source = await readFile(filePath, "utf8"); - - if (source.includes(MARKER)) { - continue; // already patched - } - - if (!source.includes(OLD_SNIPPET)) { - console.log(`[postinstall] patch-ap-url-lookup-api-like: snippet not found in ${filePath}`); - continue; - } - - source = source.replace(OLD_SNIPPET, NEW_SNIPPET); - await writeFile(filePath, source, "utf8"); - patched += 1; - console.log(`[postinstall] Applied patch-ap-url-lookup-api-like to ${filePath}`); -} - -if (checked === 0) { - console.log("[postinstall] patch-ap-url-lookup-api-like: no target files found"); -} else if (patched === 0) { - console.log("[postinstall] patch-ap-url-lookup-api-like: already up to date"); -} else { - console.log(`[postinstall] patch-ap-url-lookup-api-like: patched ${patched}/${checked} file(s)`); -} diff --git a/scripts/patch-ap-url-lookup-api.mjs b/scripts/patch-ap-url-lookup-api.mjs deleted file mode 100644 index a7d3bda0..00000000 --- a/scripts/patch-ap-url-lookup-api.mjs +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Patch: add a public GET /api/ap-url endpoint to the ActivityPub endpoint. - * - * Problem: - * The "Also on fediverse" widget on blog post pages passes the blog post URL - * (e.g. https://blog.giersig.eu/replies/bd78a/) to the Mastodon - * authorize_interaction flow: - * https://{instance}/authorize_interaction?uri={blog-post-url} - * - * When the remote instance fetches that URI with Accept: application/activity+json, - * it may hit a static file server (nginx/Caddy) that returns HTML instead of - * AP JSON, causing the interaction to fail with "Could not connect to the given - * address" or a similar error. - * - * Fix: - * Add a public API route to the AP endpoint: - * GET /activitypub/api/ap-url?post={blog-post-url} - * - * This resolves the post in MongoDB, determines its object type (Note or Article), - * and returns the canonical Fedify-served AP object URL: - * { apUrl: "https://blog.giersig.eu/activitypub/objects/note/replies/bd78a/" } - * - * The "Also on fediverse" JS widget can then call this API and use the returned - * apUrl in the authorize_interaction redirect instead of the blog post URL. - * Fedify-served URLs (/activitypub/objects/…) are always proxied to Node.js and - * will reliably return AP JSON with correct content negotiation. - * - * The patch inserts the new route in the `routesPublic` getter of index.js, - * just before the closing `return router` statement. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", -]; - -const MARKER = "// AP URL lookup endpoint"; - -const OLD_SNIPPET = ` router.all("/inbox", (req, res) => { - res - .status(405) - .set("Allow", "POST") - .type("application/activity+json") - .json({ - error: "Method Not Allowed", - message: "The shared inbox only accepts POST requests", - }); - }); - - return router; - } - - /** - * Authenticated admin routes — mounted at mountPath, behind IndieAuth. - */`; - -const NEW_SNIPPET = ` router.all("/inbox", (req, res) => { - res - .status(405) - .set("Allow", "POST") - .type("application/activity+json") - .json({ - error: "Method Not Allowed", - message: "The shared inbox only accepts POST requests", - }); - }); - - // AP URL lookup endpoint - // Public API: resolve a blog post URL → its Fedify-served AP object URL. - // GET /api/ap-url?post=https://blog.example.com/notes/foo/ - // Returns { apUrl: "https://blog.example.com/activitypub/objects/note/notes/foo/" } - // - // Use this in "Also on fediverse" widgets so that authorize_interaction - // uses a URL that is always routed to Node.js (never intercepted by a - // static file server), ensuring reliable AP content negotiation. - router.get("/api/ap-url", async (req, res) => { - try { - const postParam = req.query.post; - if (!postParam) { - return res.status(400).json({ error: "post parameter required" }); - } - - const { application } = req.app.locals; - const postsCollection = application.collections?.get("posts"); - - if (!postsCollection) { - return res.status(503).json({ error: "Database unavailable" }); - } - - const publicationUrl = (self._publicationUrl || application.url || "").replace(/\\/$/, ""); - - // Match with or without trailing slash - const postUrl = postParam.replace(/\\/$/, ""); - const post = await postsCollection.findOne({ - "properties.url": { $in: [postUrl, postUrl + "/"] }, - }); - - if (!post) { - return res.status(404).json({ error: "Post not found" }); - } - - // Draft and unlisted posts are not federated - if (post?.properties?.["post-status"] === "draft") { - return res.status(404).json({ error: "Post not found" }); - } - if (post?.properties?.visibility === "unlisted") { - return res.status(404).json({ error: "Post not found" }); - } - - // Determine the AP object type (mirrors jf2-to-as2.js logic) - const postType = post.properties?.["post-type"]; - const isArticle = postType === "article" && !!post.properties?.name; - const objectType = isArticle ? "article" : "note"; - - // Extract the path portion after the publication base URL - const resolvedUrl = (post.properties?.url || "").replace(/\\/$/, ""); - if (!resolvedUrl.startsWith(publicationUrl)) { - return res.status(500).json({ error: "Post URL does not match publication base" }); - } - const postPath = resolvedUrl.slice(publicationUrl.length).replace(/^\\//, ""); - - const mp = (self.options.mountPath || "").replace(/\\/$/, ""); - const apBase = publicationUrl; - const apUrl = \`\${apBase}\${mp}/objects/\${objectType}/\${postPath}\`; - - res.set("Cache-Control", "public, max-age=300"); - res.json({ apUrl }); - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); - - return router; - } - - /** - * Authenticated admin routes — mounted at mountPath, behind IndieAuth. - */`; - -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)) { - continue; - } - - if (!source.includes(OLD_SNIPPET)) { - console.log(`[postinstall] patch-ap-url-lookup-api: old snippet not found in ${filePath}`); - continue; - } - - const updated = source.replace(OLD_SNIPPET, NEW_SNIPPET); - - if (updated === source) { - continue; - } - - await writeFile(filePath, updated, "utf8"); - patched += 1; - console.log(`[postinstall] Applied patch-ap-url-lookup-api to ${filePath}`); -} - -if (checked === 0) { - console.log("[postinstall] patch-ap-url-lookup-api: no target files found"); -} else if (patched === 0) { - console.log("[postinstall] patch-ap-url-lookup-api: already up to date"); -} else { - console.log(`[postinstall] patch-ap-url-lookup-api: patched ${patched}/${checked} file(s)`); -} From 6f6641b34dd53f9f62f2dd4613bad90bd9c6151c Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:23:58 +0100 Subject: [PATCH 20/59] docs: document removal of 11 obsolete AP patches Co-Authored-By: Claude Sonnet 4.6 --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index ac50e83c..5256ef75 100644 --- a/README.md +++ b/README.md @@ -656,6 +656,22 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-22 +**chore(patches): remove 11 obsolete AP patch scripts** (`18a946c9e`) +All of the following features are now baked into `svemagie/indiekit-endpoint-activitypub` natively; the patch scripts were either no-ops or (in the case of `patch-ap-repost-commentary`) actively harmful (inserting a duplicate `else if` block on every deploy, preventing startup). Root cause: upstream merges absorbed our custom commits, leaving the OLD snippets absent from the source so patches silently skipped — except Fix D of repost-commentary which still matched a generic `} else {` block and corrupted `jf2-to-as2.js`. +- `patch-ap-repost-commentary` — repost commentary in AP output (Create/Note with commentary) +- `patch-ap-url-lookup-api` — `/api/ap-url` endpoint +- `patch-ap-allow-private-address` — `allowPrivateAddress: true` in `createFederation` +- `patch-ap-like-note-dispatcher` — reverted fake-Note approach for likes +- `patch-ap-like-activity-id` — canonical `id` URI on Like activities (AP §6.2.1) +- `patch-ap-like-activity-dispatcher` — `setObjectDispatcher(Like, …)` for dereferenceable like URLs (AP §3.1) +- `patch-ap-url-lookup-api-like` — `/api/ap-url` returns `likeOf` URL for AP-likes +- `patch-ap-remove-federation-diag` — removed verbose federation diagnostics inbox log +- `patch-ap-normalize-nested-tags` — `cat.split("/").at(-1)` to strip nested tag prefixes +- `patch-ap-object-url-trailing-slash` — trailing-slash normalisation on AP object URLs (3 orphan scripts not in `package.json`) +- `patch-ap-og-image` — OG image in AP objects (orphan; feature remains undeployed) + +`patch-ap-skip-draft-syndication` kept — draft guard in `syndicate()` not yet in fork. + **chore(deps): sync activitypub fork with upstream post-3.8.1** (`a37bece` in svemagie/indiekit-endpoint-activitypub) Four upstream fixes merged since 3.8.1, plus resolution of merge artifacts introduced by the upstream sync: - `9a0d6d20`: serve AP JSON for actor URLs received without an explicit `text/html` Accept header — fixes content negotiation for clients that omit Accept From ab1912b24b7a212f83b4db52954aa2f20ceccecd Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:28:24 +0100 Subject: [PATCH 21/59] fix(deps): update activitypub fork (resolveAuthor collection.get fix) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 6188fbd6..e736cbcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.4", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#a37bece8cd51f3a8992367ed778aa2d8d0ce9230", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#0a686d7ab4d0a591ae1fe0f93193b7c7a41316b9", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From df4e4061a145b3bbf06c2beaaf7d641d00882408 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:29:27 +0100 Subject: [PATCH 22/59] docs: document Mastodon client like/reblog fix and patch cleanup Co-Authored-By: Claude Sonnet 4.6 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5256ef75..544a530a 100644 --- a/README.md +++ b/README.md @@ -656,6 +656,9 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-22 +**fix(activitypub): like/reblog from Mastodon client throws "collection.get is not a function"** (`0a686d7` in svemagie/indiekit-endpoint-activitypub) +`resolveAuthor()` in `lib/resolve-author.js` called `collections.get("ap_timeline")` assuming a `Map` (correct for the native AP inbox path), but the Mastodon Client API passes `req.app.locals.mastodonCollections` as a plain object. Every favourite/reblog action from Phanpy, Elk, or any other Mastodon client hit this error. Fix: `typeof collections.get === "function"` guard selects between Map-style and object-style access so both paths work. + **chore(patches): remove 11 obsolete AP patch scripts** (`18a946c9e`) All of the following features are now baked into `svemagie/indiekit-endpoint-activitypub` natively; the patch scripts were either no-ops or (in the case of `patch-ap-repost-commentary`) actively harmful (inserting a duplicate `else if` block on every deploy, preventing startup). Root cause: upstream merges absorbed our custom commits, leaving the OLD snippets absent from the source so patches silently skipped — except Fix D of repost-commentary which still matched a generic `} else {` block and corrupted `jf2-to-as2.js`. - `patch-ap-repost-commentary` — repost commentary in AP output (Create/Note with commentary) From 6678c6797239933e42bf45148e571b1905f3bb78 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:45:28 +0100 Subject: [PATCH 23/59] fix(deps): update activitypub fork (DM visibility=direct public post leak) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index e736cbcc..0d6ee33d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.4", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#0a686d7ab4d0a591ae1fe0f93193b7c7a41316b9", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#99964e9c3ca03c7cabf0b07350d3c8c8f8f77419", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 2379a5e1d5cd9f2d5b483a517fb7c585b03ee18c Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:48:43 +0100 Subject: [PATCH 24/59] docs: document Mastodon API DM public post leak fix Co-Authored-By: Claude Sonnet 4.6 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 544a530a..331efe59 100644 --- a/README.md +++ b/README.md @@ -656,6 +656,9 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-22 +**fix(mastodon-api): DM sent from Mastodon client created a public blog post** (`99964e9` in svemagie/indiekit-endpoint-activitypub) +`POST /api/v1/statuses` with `visibility="direct"` fell through to the Micropub pipeline, which has no concept of Mastodon's `"direct"` visibility — so it created a normal public blog post. Fix: intercept `visibility === "direct"` before Micropub: resolve the `@user@domain` mention via WebFinger (Fedify lookup as fallback), build a `Create/Note` AP activity addressed only to the recipient (no public/followers `cc`), send via `ctx.sendActivity()`, store in `ap_notifications` for the DM thread view, return a minimal status JSON to the client. No blog post is created. + **fix(activitypub): like/reblog from Mastodon client throws "collection.get is not a function"** (`0a686d7` in svemagie/indiekit-endpoint-activitypub) `resolveAuthor()` in `lib/resolve-author.js` called `collections.get("ap_timeline")` assuming a `Map` (correct for the native AP inbox path), but the Mastodon Client API passes `req.app.locals.mastodonCollections` as a plain object. Every favourite/reblog action from Phanpy, Elk, or any other Mastodon client hit this error. Fix: `typeof collections.get === "function"` guard selects between Map-style and object-style access so both paths work. From 777b2cc30620facad186f62d01db04098b402ffe Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 20:57:50 +0100 Subject: [PATCH 25/59] =?UTF-8?q?fix(deps):=20update=20activitypub=20fork?= =?UTF-8?q?=20(DM=20no=20data=20=E2=80=94=20full=20status=20response)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 0d6ee33d..9983d9e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.4", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#99964e9c3ca03c7cabf0b07350d3c8c8f8f77419", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#481603391e624958aa37ab5b5582d21675cbd855", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 728fa9d6824aaef83d5aefc4dc4e3cef3c90d8e7 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 21:04:03 +0100 Subject: [PATCH 26/59] fix(deps): update activitypub fork (addTimelineItem wrong arg in DM path) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 9983d9e7..d52ad908 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.4", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#481603391e624958aa37ab5b5582d21675cbd855", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#7b838ea2958ab2b261f1fefb73e040df9435ffab", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 61fbdcd7ae0a443f0c3f3e3215cb8cf92d155c89 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 21:24:42 +0100 Subject: [PATCH 27/59] docs: document remote profile fix (lookupWithSecurity + timeouts) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 331efe59..bd525480 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Four packages are installed directly from GitHub forks rather than the npm regis In `package.json` these use the `github:owner/repo[#branch]` syntax so npm fetches them directly from GitHub on install. -> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `a37bece` (synced with upstream post-3.8.1: tags.pub hashtag discovery, AP JSON content negotiation fix, RSA Multikey removal, direct follow workaround for tags.pub identity/v1 context rejection; merge artifacts removed). +> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `ed18446` (synced with upstream post-3.8.1: tags.pub hashtag discovery, AP JSON content negotiation fix, RSA Multikey removal, direct follow workaround for tags.pub identity/v1 context rejection; merge artifacts removed; DM from Mastodon client no longer creates public blog post; remote profile resolution uses lookupWithSecurity with timeouts). --- @@ -656,6 +656,9 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-22 +**fix(mastodon): remote profile pictures and follower stats missing in Mastodon client** (`ed18446` in svemagie/indiekit-endpoint-activitypub) +`resolveRemoteAccount()` in `lib/mastodon/helpers/resolve-account.js` called `ctx.lookupObject()` directly. Servers that return 400/403 for signed GETs (e.g. some Mastodon/Pleroma instances) caused the lookup to throw, so the function returned `null` — making profile pages show no avatar and zero follower/following/statuses counts. Fix: replace with `lookupWithSecurity()` (the same signed→unsigned fallback wrapper used everywhere else in the codebase) and obtain a `documentLoader` first so the signed attempt can attach the actor's HTTP signature. Additionally wrapped `getFollowers()`, `getFollowing()`, and `getOutbox()` collection fetches in a 5-second `Promise.race` timeout so slow remote servers no longer block the profile response indefinitely. + **fix(mastodon-api): DM sent from Mastodon client created a public blog post** (`99964e9` in svemagie/indiekit-endpoint-activitypub) `POST /api/v1/statuses` with `visibility="direct"` fell through to the Micropub pipeline, which has no concept of Mastodon's `"direct"` visibility — so it created a normal public blog post. Fix: intercept `visibility === "direct"` before Micropub: resolve the `@user@domain` mention via WebFinger (Fedify lookup as fallback), build a `Create/Note` AP activity addressed only to the recipient (no public/followers `cc`), send via `ctx.sendActivity()`, store in `ap_notifications` for the DM thread view, return a minimal status JSON to the client. No blog post is created. From 615dcd849df39851f6e5e57a970fd697ef64f8de Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 22 Mar 2026 21:25:40 +0100 Subject: [PATCH 28/59] docs: document DM no-data and 404 follow-up fixes (4816033, 7b838ea) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index bd525480..eebc21af 100644 --- a/README.md +++ b/README.md @@ -662,6 +662,12 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. **fix(mastodon-api): DM sent from Mastodon client created a public blog post** (`99964e9` in svemagie/indiekit-endpoint-activitypub) `POST /api/v1/statuses` with `visibility="direct"` fell through to the Micropub pipeline, which has no concept of Mastodon's `"direct"` visibility — so it created a normal public blog post. Fix: intercept `visibility === "direct"` before Micropub: resolve the `@user@domain` mention via WebFinger (Fedify lookup as fallback), build a `Create/Note` AP activity addressed only to the recipient (no public/followers `cc`), send via `ctx.sendActivity()`, store in `ap_notifications` for the DM thread view, return a minimal status JSON to the client. No blog post is created. +**fix(mastodon-api): DM response returned "no data" in Mastodon client** (`4816033` in svemagie/indiekit-endpoint-activitypub) +After the DM was sent, the Mastodon client received a bare `{}` object instead of a proper status entity, showing "no data". Root cause: the DM path returned a hand-rolled minimal JSON object instead of calling `serializeStatus()`. Fix: build a full `timelineItem` document (matching the shape used by the home timeline) and pass it through `serializeStatus()` so all ~20 required Mastodon status fields (`id`, `account`, `media_attachments`, `tags`, `emojis`, etc.) are present. + +**fix(mastodon-api): DM 404 immediately after send, then disappeared from thread view** (`7b838ea` in svemagie/indiekit-endpoint-activitypub) +Follow-up to the "no data" fix: the DM item was never actually persisted because `addTimelineItem()` was called as `addTimelineItem(collections.ap_timeline, item)`, passing the raw MongoDB collection directly. `addTimelineItem` expects the whole `collections` object and destructures `{ ap_timeline }` from it — passing the collection itself caused `undefined.updateOne` to throw at insert time. The stored item was absent so the subsequent `GET /api/v1/statuses/:id` 404'd. Fix: pass `collections` (not `collections.ap_timeline`). + **fix(activitypub): like/reblog from Mastodon client throws "collection.get is not a function"** (`0a686d7` in svemagie/indiekit-endpoint-activitypub) `resolveAuthor()` in `lib/resolve-author.js` called `collections.get("ap_timeline")` assuming a `Map` (correct for the native AP inbox path), but the Mastodon Client API passes `req.app.locals.mastodonCollections` as a plain object. Every favourite/reblog action from Phanpy, Elk, or any other Mastodon client hit this error. Fix: `typeof collections.get === "function"` guard selects between Map-style and object-style access so both paths work. From 5f388bc9d5c18fcc51ad896e7a97cb195a84da9e Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 07:31:23 +0100 Subject: [PATCH 29/59] docs: document follower created_at fix and URL-type AP lookup (6c13eb8) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index eebc21af..f34080a3 100644 --- a/README.md +++ b/README.md @@ -656,6 +656,9 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-22 +**fix(mastodon-api): follower/following accounts show wrong created_at; URL-type AP lookup** (`6c13eb8` in svemagie/indiekit-endpoint-activitypub) +All places in `accounts.js` that build actor objects from `ap_followers`/`ap_following` documents were omitting the `createdAt` field. `serializeAccount()` fell back to `new Date().toISOString()`, so every follower and following account appeared to have joined "just now" in the Mastodon client. Fix: pass `createdAt: f.createdAt || undefined` in all five locations — the `/followers`, `/following`, `/lookup` endpoints and both branches of `resolveActorData()`. Additionally, HTTP actor URLs in `resolve-account.js` are now passed to `lookupWithSecurity()` as native `URL` objects instead of bare strings (matching Fedify's preferred type); the `acct:user@domain` WebFinger path stays as a string since WHATWG `new URL()` misparses the `@` as a user-info separator. + **fix(mastodon): remote profile pictures and follower stats missing in Mastodon client** (`ed18446` in svemagie/indiekit-endpoint-activitypub) `resolveRemoteAccount()` in `lib/mastodon/helpers/resolve-account.js` called `ctx.lookupObject()` directly. Servers that return 400/403 for signed GETs (e.g. some Mastodon/Pleroma instances) caused the lookup to throw, so the function returned `null` — making profile pages show no avatar and zero follower/following/statuses counts. Fix: replace with `lookupWithSecurity()` (the same signed→unsigned fallback wrapper used everywhere else in the codebase) and obtain a `documentLoader` first so the signed attempt can attach the actor's HTTP signature. Additionally wrapped `getFollowers()`, `getFollowing()`, and `getOutbox()` collection fetches in a 5-second `Promise.race` timeout so slow remote servers no longer block the profile response indefinitely. From 78b9f77d96b0ad64000058ce0dc85dda25ce267b Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 08:31:36 +0100 Subject: [PATCH 30/59] docs: document favourite 404 fix and profile avatar/created_at fixes (a259c79, da89554) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index f34080a3..ddbd7f30 100644 --- a/README.md +++ b/README.md @@ -654,6 +654,14 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ## Changelog +### 2026-03-23 + +**fix(mastodon-api): favourite/like returns "failed to load post" (404)** (`a259c79` in svemagie/indiekit-endpoint-activitypub) +`POST /api/v1/statuses/:id/favourite` uses `findTimelineItemById` to resolve the status by its cursor ID (ms-since-epoch). Three failure modes were found: (1) Items written through the Micropub pipeline store `published` as a JavaScript `Date` → MongoDB BSON Date; a string comparison against `decodeCursor()`'s ISO output never matches. (2) Some AP servers emit `published` with a timezone offset (`+01:00`); `String(Temporal.Instant)` preserves the offset, so the stored string and the lookup key differ. (3) Items with an invalid or missing `published` date had their cursor set to `"0"` (truthy in JS) so `serializeStatus` used `"0"` as the ID instead of falling back to `item._id.toString()`, making them permanently un-lookupable. Fixes: `encodeCursor` now returns `""` (falsy) for invalid dates; `findTimelineItemById` adds a BSON Date fallback and a ±1 s ISO range query; `extractObjectData` in `timeline-store.js` now normalises `published` to UTC ISO before storing, so future items always match the exact-string lookup. + +**fix(mastodon): profile avatars disappear after first page load; actor created_at wrong timezone** (`da89554` in svemagie/indiekit-endpoint-activitypub) +Two profile display regressions fixed: (1) `resolveRemoteAccount` fetched the correct avatar URL via `lookupWithSecurity` and applied it to the in-memory serialised status — but never stored it in the account cache. On the next request `serializeStatus` rebuilt the account from `item.author.photo` (empty for actors that were on a Secure Mode server when the timeline item was originally received), counts came from the in-memory cache so `enrichAccountStats`/`collectAccount` skipped re-fetching, and the avatar reverted to the default SVG. Fix: `cacheAccountStats` now stores `avatarUrl`; `collectAccount` always checks the cache first (before the "counts already populated" early-return) and applies `avatarUrl` + `createdAt`. (2) `actor.published` is a `Temporal.Instant`; `String()` on it preserves the original timezone offset (e.g. `+01:00`), so `created_at` in the Mastodon account entity could show a non-UTC timestamp that some clients refuse to parse. Fix: wrap in `new Date(String(...)).toISOString()` in both `resolve-account.js` and `timeline-store.js`. + ### 2026-03-22 **fix(mastodon-api): follower/following accounts show wrong created_at; URL-type AP lookup** (`6c13eb8` in svemagie/indiekit-endpoint-activitypub) From 25bcce2f1a4b014626e581f46d0764dfbc40fc96 Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 08:40:04 +0100 Subject: [PATCH 31/59] docs: document resolveAuthor timeout fix for favourite/reblog (01f6f81) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ddbd7f30..5419ba58 100644 --- a/README.md +++ b/README.md @@ -656,6 +656,9 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-23 +**fix(mastodon-api): favourite/reblog blocks on unbound resolveAuthor requests → client timeout** (`01f6f81` in svemagie/indiekit-endpoint-activitypub) +`likePost`, `unlikePost`, and `boostPost` in `lib/mastodon/helpers/interactions.js` all called `resolveAuthor()` — which makes up to 3 signed HTTP requests to the remote server (post fetch → actor fetch → `getAttributedTo()`) — with no timeout. If the remote server is slow or unreachable, the favourite/reblog HTTP response hangs until Node.js's socket default fires (~2 min). Mastodon clients (Phanpy, Elk) have their own shorter timeout and give up with "Failed to load post … Please try again later". Fix: wrap every `resolveAuthor()` call in `Promise.race()` with a 5 s cap. The interaction is still recorded in `ap_interactions` and the `Like`/`Announce` activity is still delivered when resolution succeeds within the window; on timeout, AP delivery is silently skipped but the client receives a correct 200 with the updated status (⭐ shows as toggled). + **fix(mastodon-api): favourite/like returns "failed to load post" (404)** (`a259c79` in svemagie/indiekit-endpoint-activitypub) `POST /api/v1/statuses/:id/favourite` uses `findTimelineItemById` to resolve the status by its cursor ID (ms-since-epoch). Three failure modes were found: (1) Items written through the Micropub pipeline store `published` as a JavaScript `Date` → MongoDB BSON Date; a string comparison against `decodeCursor()`'s ISO output never matches. (2) Some AP servers emit `published` with a timezone offset (`+01:00`); `String(Temporal.Instant)` preserves the offset, so the stored string and the lookup key differ. (3) Items with an invalid or missing `published` date had their cursor set to `"0"` (truthy in JS) so `serializeStatus` used `"0"` as the ID instead of falling back to `item._id.toString()`, making them permanently un-lookupable. Fixes: `encodeCursor` now returns `""` (falsy) for invalid dates; `findTimelineItemById` adds a BSON Date fallback and a ±1 s ISO range query; `extractObjectData` in `timeline-store.js` now normalises `published` to UTC ISO before storing, so future items always match the exact-string lookup. From 989ae4cb5d5d08aa164d59d578e44419a38d0b8f Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 08:52:25 +0100 Subject: [PATCH 32/59] chore: update AP fork lockfile pin to 01f6f81 (favourite + profile fixes) package-lock.json was pinned to 7b838ea, meaning all fixes since (favourite 404, resolveAuthor timeout, avatar cache, created_at normalisation) were never installed on the server. Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index d52ad908..fe5b9e1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.4", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#7b838ea2958ab2b261f1fefb73e040df9435ffab", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#01f6f81bda2f122455167c6e92df0ae5d22e93b2", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 42e06f31f8f38a0b2e69c28eb2403ddec1f20c83 Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 09:32:59 +0100 Subject: [PATCH 33/59] chore: update AP fork lockfile pin to b33932f (raw signed fetch fallback) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index fe5b9e1a..675df280 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2417,8 +2417,8 @@ } }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { - "version": "3.8.4", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#01f6f81bda2f122455167c6e92df0ae5d22e93b2", + "version": "3.8.5", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#b33932f1f6e4a04ac3cb69526c324b537d875e98", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From bc73558641eec259ea1766cc59c86d7f07233b01 Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 09:33:41 +0100 Subject: [PATCH 34/59] docs: document upstream raw signed fetch fallback merge (b33932f) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5419ba58..26808a6f 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Four packages are installed directly from GitHub forks rather than the npm regis In `package.json` these use the `github:owner/repo[#branch]` syntax so npm fetches them directly from GitHub on install. -> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `ed18446` (synced with upstream post-3.8.1: tags.pub hashtag discovery, AP JSON content negotiation fix, RSA Multikey removal, direct follow workaround for tags.pub identity/v1 context rejection; merge artifacts removed; DM from Mastodon client no longer creates public blog post; remote profile resolution uses lookupWithSecurity with timeouts). +> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `b33932f` (all upstream fixes through 2026-03-23 merged; DM support; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; remote profile resolution via lookupWithSecurity with timeouts). --- @@ -656,6 +656,9 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-23 +**merge: upstream raw signed fetch fallback for author resolution** (`c2920ca` merged into svemagie/indiekit-endpoint-activitypub as `b33932f`) +Upstream added Strategy 1b to `resolveAuthor`: a raw signed HTTP fetch for servers (e.g. wafrn) that return ActivityPub JSON without `@context`, which Fedify's JSON-LD processor rejects and which `lookupWithSecurity` therefore cannot handle. The raw fetch extracts `attributedTo`/`actor` from the plain JSON, then resolves the actor URL via `lookupWithSecurity` as normal. Resolution: combined with our existing 5-second `Promise.race` timeout — `likePost`/`unlikePost`/`boostPost` now pass `privateKey`/`keyId` to `resolveAuthor` so the signed raw fetch can attach an HTTP Signature, while the timeout still guards all three resolution strategies against slow/unreachable remotes. + **fix(mastodon-api): favourite/reblog blocks on unbound resolveAuthor requests → client timeout** (`01f6f81` in svemagie/indiekit-endpoint-activitypub) `likePost`, `unlikePost`, and `boostPost` in `lib/mastodon/helpers/interactions.js` all called `resolveAuthor()` — which makes up to 3 signed HTTP requests to the remote server (post fetch → actor fetch → `getAttributedTo()`) — with no timeout. If the remote server is slow or unreachable, the favourite/reblog HTTP response hangs until Node.js's socket default fires (~2 min). Mastodon clients (Phanpy, Elk) have their own shorter timeout and give up with "Failed to load post … Please try again later". Fix: wrap every `resolveAuthor()` call in `Promise.race()` with a 5 s cap. The interaction is still recorded in `ap_interactions` and the `Like`/`Announce` activity is still delivered when resolution succeeds within the window; on timeout, AP delivery is silently skipped but the client receives a correct 200 with the updated status (⭐ shows as toggled). From 36c7f33a3473b3ff30cb32fc715995de4bbfbf8d Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 10:05:04 +0100 Subject: [PATCH 35/59] feat(changelog): add chore and refactor commit categories The patch now recognises chore: and refactor: commit prefixes as their own categories instead of lumping them into "other". Co-Authored-By: Claude Opus 4.6 --- scripts/patch-endpoint-github-changelog-categories.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/patch-endpoint-github-changelog-categories.mjs b/scripts/patch-endpoint-github-changelog-categories.mjs index efd8e6d1..053bfb44 100644 --- a/scripts/patch-endpoint-github-changelog-categories.mjs +++ b/scripts/patch-endpoint-github-changelog-categories.mjs @@ -38,6 +38,8 @@ const newCategorize = `function categorizeCommit(title) { if (/^perf[:(]/i.test(title)) return "performance"; if (/^a11y[:(]/i.test(title)) return "accessibility"; if (/^docs[:(]/i.test(title)) return "documentation"; + if (/^chore[:(]/i.test(title)) return "chores"; + if (/^refactor[:(]/i.test(title)) return "refactor"; return "other"; } @@ -47,6 +49,8 @@ const CATEGORY_LABELS = { performance: "Performance", accessibility: "Accessibility", documentation: "Documentation", + chores: "Chores", + refactor: "Refactor", other: "Other", };`; From 7b5d1ed2c6e1b9cff9c5c3b7b17541361345f99c Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 10:53:47 +0100 Subject: [PATCH 36/59] chore: update AP fork lockfile pin to 2660a1a (timezone-aware status lookup) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 675df280..53789652 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.5", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#b33932f1f6e4a04ac3cb69526c324b537d875e98", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#2660a1a604461412683bafcf6cdfe03982796c66", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From ba1dbc6ceb3f2aeb72bef06218824478c27578c0 Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 10:54:16 +0100 Subject: [PATCH 37/59] docs: document timezone-aware status lookup fix (2660a1a) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 26808a6f..0d2d0b11 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Four packages are installed directly from GitHub forks rather than the npm regis In `package.json` these use the `github:owner/repo[#branch]` syntax so npm fetches them directly from GitHub on install. -> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `b33932f` (all upstream fixes through 2026-03-23 merged; DM support; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; remote profile resolution via lookupWithSecurity with timeouts). +> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `2660a1a` (all upstream fixes through 2026-03-23 merged; DM support; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; timezone-aware status lookup for pre-UTC-normalization timeline items; remote profile resolution via lookupWithSecurity with timeouts). --- @@ -656,6 +656,9 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-23 +**fix(mastodon-api): favourite still fails for timeline items stored with non-UTC timezone offsets** (`2660a1a` in svemagie/indiekit-endpoint-activitypub) +`findTimelineItemById` converts the cursor ID (ms-since-epoch) to a UTC ISO string via `decodeCursor`, then tries exact string match against `published` in MongoDB. The UTC normalization fix in `a259c79` / `extractObjectData` ensures NEW inbox items are stored as UTC. But items already in the database from before that deploy still carry the original server's timezone offset (e.g., `"2026-03-21T16:33:50+01:00"`). The final fallback was a `$gte`/`$lte` range query on the string representation — which fails because `"16:33:50+01:00"` is lexicographically outside the UTC range `["15:33:50Z", "15:33:51Z"]`. Fix: replace the string range query with a `$or` that covers both storage formats: (1) BSON Date direct range comparison for Micropub-generated items, and (2) MongoDB `$dateFromString` + `$toLong` numeric range for string-stored dates. `$dateFromString` parses any ISO 8601 format including timezone offsets and returns a UTC Date; `$toLong` converts to ms-since-epoch; the numeric ±1 s window always matches regardless of how the original timezone was encoded. + **merge: upstream raw signed fetch fallback for author resolution** (`c2920ca` merged into svemagie/indiekit-endpoint-activitypub as `b33932f`) Upstream added Strategy 1b to `resolveAuthor`: a raw signed HTTP fetch for servers (e.g. wafrn) that return ActivityPub JSON without `@context`, which Fedify's JSON-LD processor rejects and which `lookupWithSecurity` therefore cannot handle. The raw fetch extracts `attributedTo`/`actor` from the plain JSON, then resolves the actor URL via `lookupWithSecurity` as normal. Resolution: combined with our existing 5-second `Promise.race` timeout — `likePost`/`unlikePost`/`boostPost` now pass `privateKey`/`keyId` to `resolveAuthor` so the signed raw fetch can attach an HTTP Signature, while the timeout still guards all three resolution strategies against slow/unreachable remotes. From ec856c962da8c9b759b221a0eb03d93dfa14ff21 Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 11:16:34 +0100 Subject: [PATCH 38/59] chore: update AP fork lockfile pin to b5ebf6a (pin/unpin status) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 53789652..e3a43e55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.5", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#2660a1a604461412683bafcf6cdfe03982796c66", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#b5ebf6a1e4cabbc0ceae15950c5a49b024dc1725", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 359db328588b07402b94083271bb3017edad5f34 Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 11:17:07 +0100 Subject: [PATCH 39/59] docs: document pin/unpin status implementation (b5ebf6a) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d2d0b11..30695871 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Four packages are installed directly from GitHub forks rather than the npm regis In `package.json` these use the `github:owner/repo[#branch]` syntax so npm fetches them directly from GitHub on install. -> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `2660a1a` (all upstream fixes through 2026-03-23 merged; DM support; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; timezone-aware status lookup for pre-UTC-normalization timeline items; remote profile resolution via lookupWithSecurity with timeouts). +> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `b5ebf6a` (all upstream fixes through 2026-03-23 merged; DM support; pin/unpin status; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; timezone-aware status lookup for pre-UTC-normalization timeline items; remote profile resolution via lookupWithSecurity with timeouts). --- @@ -656,6 +656,9 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-23 +**feat(mastodon-api): implement pin/unpin status** (`b5ebf6a` in svemagie/indiekit-endpoint-activitypub) +`POST /api/v1/statuses/:id/pin` and `POST /api/v1/statuses/:id/unpin` were returning 501 "Not implemented", so "In Profil anheften" always failed in Phanpy/Elk. Fix: both routes are now implemented in `lib/mastodon/routes/statuses.js`. Pin upserts a document into `ap_featured` (the same collection the admin UI uses), enforces the existing 5-post maximum, and calls `broadcastActorUpdate()` so remote servers re-fetch the AP featured collection immediately. Unpin deletes from `ap_featured` and broadcasts the same update. `loadItemInteractions()` now also queries `ap_featured` and returns a `pinnedIds` set, so `GET /api/v1/statuses/:id` correctly reflects pin state. `broadcastActorUpdate` wired into mastodon `pluginOptions` in `index.js`. + **fix(mastodon-api): favourite still fails for timeline items stored with non-UTC timezone offsets** (`2660a1a` in svemagie/indiekit-endpoint-activitypub) `findTimelineItemById` converts the cursor ID (ms-since-epoch) to a UTC ISO string via `decodeCursor`, then tries exact string match against `published` in MongoDB. The UTC normalization fix in `a259c79` / `extractObjectData` ensures NEW inbox items are stored as UTC. But items already in the database from before that deploy still carry the original server's timezone offset (e.g., `"2026-03-21T16:33:50+01:00"`). The final fallback was a `$gte`/`$lte` range query on the string representation — which fails because `"16:33:50+01:00"` is lexicographically outside the UTC range `["15:33:50Z", "15:33:51Z"]`. Fix: replace the string range query with a `$or` that covers both storage formats: (1) BSON Date direct range comparison for Micropub-generated items, and (2) MongoDB `$dateFromString` + `$toLong` numeric range for string-stored dates. `$dateFromString` parses any ISO 8601 format including timezone offsets and returns a UTC Date; `$toLong` converts to ms-since-epoch; the numeric ±1 s window always matches regardless of how the original timezone was encoded. From 0a973266fc95010fac9c64378168bfb0f6ce4865 Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 11:33:12 +0100 Subject: [PATCH 40/59] chore: update AP fork lockfile pin to e319c34 (edit post) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index e3a43e55..9388ef2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.5", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#b5ebf6a1e4cabbc0ceae15950c5a49b024dc1725", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#e319c348d02da2064f2246334c72f59a1209bcf0", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From a7b48a2606f98365c44df3aa7521a9718dfa6da8 Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 11:33:41 +0100 Subject: [PATCH 41/59] docs: document edit post implementation (e319c34) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 30695871..6acf3034 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Four packages are installed directly from GitHub forks rather than the npm regis In `package.json` these use the `github:owner/repo[#branch]` syntax so npm fetches them directly from GitHub on install. -> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `b5ebf6a` (all upstream fixes through 2026-03-23 merged; DM support; pin/unpin status; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; timezone-aware status lookup for pre-UTC-normalization timeline items; remote profile resolution via lookupWithSecurity with timeouts). +> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `e319c34` (all upstream fixes through 2026-03-23 merged; DM support; pin/unpin status; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; timezone-aware status lookup for pre-UTC-normalization timeline items; remote profile resolution via lookupWithSecurity with timeouts). --- @@ -656,6 +656,9 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-23 +**feat(mastodon-api): implement PUT /api/v1/statuses/:id (edit post)** (`e319c34` in svemagie/indiekit-endpoint-activitypub) +`PUT /api/v1/statuses/:id` was not implemented, so "Beitrag bearbeiten" always failed. Route added to `lib/mastodon/routes/statuses.js`. Flow: (1) look up timeline item by cursor ID, 403 if not the local actor's own post; (2) build a Micropub `replace` operation for `content`, `summary`, `sensitive`, and `mp-language` and call `postData.update()` + `postContent.update()` to update the MongoDB posts collection and content file on disk; (3) patch the `ap_timeline` document in-place (`content`, `summary`, `sensitive`, `updatedAt`) — `serializeStatus` reads `updatedAt` → `edited_at`; (4) broadcast `Update(Note)` to all followers via shared inbox so remote servers display the edit pencil indicator; (5) return the serialized status. `Update` added to the top-level `@fedify/fedify/vocab` import. + **feat(mastodon-api): implement pin/unpin status** (`b5ebf6a` in svemagie/indiekit-endpoint-activitypub) `POST /api/v1/statuses/:id/pin` and `POST /api/v1/statuses/:id/unpin` were returning 501 "Not implemented", so "In Profil anheften" always failed in Phanpy/Elk. Fix: both routes are now implemented in `lib/mastodon/routes/statuses.js`. Pin upserts a document into `ap_featured` (the same collection the admin UI uses), enforces the existing 5-post maximum, and calls `broadcastActorUpdate()` so remote servers re-fetch the AP featured collection immediately. Unpin deletes from `ap_featured` and broadcasts the same update. `loadItemInteractions()` now also queries `ap_featured` and returns a `pinnedIds` set, so `GET /api/v1/statuses/:id` correctly reflects pin state. `broadcastActorUpdate` wired into mastodon `pluginOptions` in `index.js`. From e63734ee2aed5033965bac239afe059253079763 Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 23 Mar 2026 12:12:56 +0100 Subject: [PATCH 42/59] fix(start): kill node process on service stop to prevent orphaned port binding Trap previously only killed the webmention poller, leaving the node process orphaned on service stop. On restart, the new node instance would fail to bind port 3000 (EADDRINUSE) causing 502s with clean logs. Co-Authored-By: Claude Sonnet 4.6 --- start.example.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.example.sh b/start.example.sh index 537bb307..9fb3603f 100644 --- a/start.example.sh +++ b/start.example.sh @@ -104,6 +104,6 @@ WEBMENTION_ORIGIN="${PUBLICATION_URL:-${SITE_URL:-}}" ) & POLLER_PID="$!" -trap 'kill "${POLLER_PID}" 2>/dev/null || true' EXIT INT TERM +trap 'kill "${INDIEKIT_PID}" "${POLLER_PID}" 2>/dev/null || true; wait "${INDIEKIT_PID}" 2>/dev/null || true' EXIT INT TERM wait "${INDIEKIT_PID}" From b28443c844ae3f3a0a764209b1df35111b456549 Mon Sep 17 00:00:00 2001 From: Sven Date: Tue, 24 Mar 2026 19:57:15 +0100 Subject: [PATCH 43/59] chore: update AP fork lockfile pin to bd3a623 (linkify trailing punct fix) Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 9388ef2b..1a68de2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.5", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#e319c348d02da2064f2246334c72f59a1209bcf0", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#bd3a623488f7b88817bf1b14f7733a4f40c7dcca", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From 3b2925d764252f9f20f9f3ed59130df7ba8f20b1 Mon Sep 17 00:00:00 2001 From: Sven Date: Tue, 24 Mar 2026 19:57:48 +0100 Subject: [PATCH 44/59] docs: document linkify trailing punctuation fix (bd3a623) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6acf3034..6d914bde 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Four packages are installed directly from GitHub forks rather than the npm regis In `package.json` these use the `github:owner/repo[#branch]` syntax so npm fetches them directly from GitHub on install. -> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `e319c34` (all upstream fixes through 2026-03-23 merged; DM support; pin/unpin status; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; timezone-aware status lookup for pre-UTC-normalization timeline items; remote profile resolution via lookupWithSecurity with timeouts). +> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `bd3a623` (all upstream fixes through 2026-03-23 merged; DM support; pin/unpin status; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; timezone-aware status lookup for pre-UTC-normalization timeline items; remote profile resolution via lookupWithSecurity with timeouts). --- @@ -654,6 +654,11 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ## Changelog +### 2026-03-24 + +**fix(linkify): trailing punctuation included in auto-linked URLs** (`bd3a623` in svemagie/indiekit-endpoint-activitypub) +URLs at the end of a sentence (e.g. `"See https://example.com."`) had the trailing period captured as part of the URL, producing a broken link (`https://example.com.` → 404). Root cause: the regex `[^\s<"]+` in `linkifyUrls()` (`lib/jf2-to-as2.js`) and `/(https?:\/\/[^\s<>"')\]]+)/g` in `processStatusContent()` (`lib/mastodon/routes/statuses.js`) both match until whitespace or tag-open, but `.`, `,`, `;`, `:`, `!`, `?` are common sentence-ending characters that follow URLs. Fix: replace the string template in both replace calls with a callback that strips `/[.,;:!?)\]'"]+$/` from the captured URL before inserting into the `` tag. Applies to AP federation (outbox Notes) and Mastodon Client API post creation. + ### 2026-03-23 **feat(mastodon-api): implement PUT /api/v1/statuses/:id (edit post)** (`e319c34` in svemagie/indiekit-endpoint-activitypub) From 77442ec83788d2974d3c35baa5b561b8b2380d6b Mon Sep 17 00:00:00 2001 From: Sven Date: Tue, 24 Mar 2026 20:46:14 +0100 Subject: [PATCH 45/59] chore: update AP fork lockfile pin to 42f8c2d (own posts in ap_timeline) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 131 ++++++++++++++++++++++++++++++++++++++++------ package-lock.json | 2 +- 2 files changed, 117 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6d914bde..3d8bcf73 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Four packages are installed directly from GitHub forks rather than the npm regis In `package.json` these use the `github:owner/repo[#branch]` syntax so npm fetches them directly from GitHub on install. -> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm update @rmdes/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `bd3a623` (all upstream fixes through 2026-03-23 merged; DM support; pin/unpin status; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; timezone-aware status lookup for pre-UTC-normalization timeline items; remote profile resolution via lookupWithSecurity with timeouts). +> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm install github:svemagie/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `42f8c2d` (all upstream fixes through 2026-03-23 merged; DM support; pin/unpin status; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; timezone-aware status lookup for pre-UTC-normalization timeline items; own Micropub posts mirrored into ap_timeline so context/statuses endpoints work for website-authored posts). --- @@ -598,37 +598,135 @@ All of these use a shared `_toInternalUrl()` helper (injected by patch scripts) INTERNAL_FETCH_URL=http://10.100.0.10 ``` -### nginx port 80 configuration +### nginx configuration (`/usr/local/etc/nginx/sites/blog.giersig.eu.conf`) -The internal HTTP listener must: +The full vhost config lives in the web jail. Key design points: -1. **Serve content directly** (not redirect to HTTPS) -2. **Set `X-Forwarded-Proto: https`** so Indiekit's `force-https` middleware does not redirect internal requests back to HTTPS -3. Proxy dynamic routes to the node jail, serve static files from the Eleventy build output +- **ActivityPub content negotiation** — a `map` block (in `http {}`) detects AP clients by `Accept` header and routes them directly to Indiekit, bypassing `try_files`. +- **Static-first serving** — browsers hit `try_files` in `location /`; static files are served from `/usr/local/www/blog` (Eleventy `_site/` output, rsynced on deploy). Unmatched paths fall through to `@indiekit`. +- **Custom 404** — `error_page 404 /404.html` at the server level catches missing static files. `proxy_intercept_errors on` in `@indiekit` catches 404s from the Node upstream. Both serve Eleventy's generated `/404.html`. +- **Internal listener** (`10.100.0.10:80`) — used by Indiekit for self-fetches only (not internet-facing). Must not intercept errors or redirect; must set `X-Forwarded-Proto: https` so Indiekit's force-https middleware doesn't redirect. ```nginx -# Internal HTTP listener — used by Indiekit for self-fetches. -# Not exposed to the internet (firewall blocks external port 80). +# ActivityPub content negotiation — place in http {} block +map $http_accept $is_activitypub { + default 0; + "~*application/activity\+json" 1; + "~*application/ld\+json" 1; +} + +# ── 1. Internal HTTP listener (Indiekit self-fetches only) ────────────────── +# Bound to jail IP, not exposed to the internet. +# Passes responses through unmodified — no error interception. server { listen 10.100.0.10:80; server_name blog.giersig.eu; - # Tell Indiekit this is the real domain (not 10.100.0.10) and - # that TLS was terminated upstream so force-https doesn't redirect. - proxy_set_header Host blog.giersig.eu; + # Hardcode Host so Indiekit sees the real domain, not the jail IP. + # X-Forwarded-Proto https prevents force-https from redirecting. + proxy_set_header Host blog.giersig.eu; proxy_set_header X-Forwarded-Proto https; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - # Static files from Eleventy build (rsynced to /usr/local/www/blog) location /images/ { root /usr/local/www/blog; } location /og/ { root /usr/local/www/blog; } - # Everything else → Indiekit location / { proxy_pass http://10.100.0.20:3000; } } + +# ── 2. HTTP: giersig.eu + www → blog.giersig.eu ───────────────────────────── +server { + listen 80; + server_name giersig.eu www.giersig.eu; + return 301 https://blog.giersig.eu$request_uri; +} + +# ── 3. HTTP: blog.giersig.eu (ACME challenge + HTTPS redirect) ────────────── +server { + listen 80; + server_name blog.giersig.eu; + + location /.well-known/acme-challenge/ { + root /usr/local/www/letsencrypt; + } + location / { + return 301 https://blog.giersig.eu$request_uri; + } +} + +# ── 4. HTTPS: giersig.eu + www → blog.giersig.eu ──────────────────────────── +server { + listen 443 ssl; + server_name giersig.eu www.giersig.eu; + ssl_certificate /usr/local/etc/letsencrypt/live/giersig.eu/fullchain.pem; + ssl_certificate_key /usr/local/etc/letsencrypt/live/giersig.eu/privkey.pem; + include /usr/local/etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem; + return 301 https://blog.giersig.eu$request_uri; +} + +# ── 5. HTTPS: blog.giersig.eu (main) ──────────────────────────────────────── +server { + listen 443 ssl; + http2 on; + server_name blog.giersig.eu; + ssl_certificate /usr/local/etc/letsencrypt/live/blog.giersig.eu/fullchain.pem; + ssl_certificate_key /usr/local/etc/letsencrypt/live/blog.giersig.eu/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + + add_header X-Bridgy-Opt-Out "yes" always; + add_header Strict-Transport-Security "max-age=63072000" always; + + include /usr/local/etc/nginx/bots.d/ddos.conf; + include /usr/local/etc/nginx/bots.d/blockbots.conf; + + root /usr/local/www/blog; + index index.html; + + # Custom 404 — served from Eleventy build output. + # proxy_intercept_errors in @indiekit ensures upstream 404s also use this. + error_page 404 /404.html; + location = /404.html { + root /usr/local/www/blog; + internal; + } + + location = /contact { + return 301 /hello; + } + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # AP clients → proxy directly (bypasses try_files / static serving) + if ($is_activitypub) { + proxy_pass http://10.100.0.20:3000; + } + + # Browsers → static file, then directory index, then .html extension, + # then fall through to Indiekit for dynamic routes. + try_files $uri $uri/ $uri.html @indiekit; + } + + location @indiekit { + proxy_pass http://10.100.0.20:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # Intercept 404s from Node so error_page 404 above fires. + proxy_intercept_errors on; + } +} ``` ### Key environment variables (node jail `.env`) @@ -656,6 +754,9 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ### 2026-03-24 +**fix(syndicate): own Micropub posts missing from ap_timeline** (`42f8c2d` in svemagie/indiekit-endpoint-activitypub) +`GET /api/v1/statuses/:id/context` returned 404 for replies and notes authored via the website admin (Micropub pipeline). Root cause: `addTimelineItem` was only called from inbox handlers (incoming AP) and the Mastodon Client API `POST /api/v1/statuses` route (posts created through Phanpy/Elk). Posts created through Micropub (`syndicate()` in `index.js`) were sent as `Create(Note)` activities to followers but never inserted into `ap_timeline`, so the Mastodon Client API had no record to look up by ID or cursor. Fix: after `logActivity` in `syndicate()`, when the activity type is `Create`, insert the post into `ap_timeline` by mapping JF2 properties (content, summary, sensitive, visibility, inReplyTo, published, author, photo/video/audio, categories) to the timeline item shape. Uses `$setOnInsert` (atomic upsert) so re-syndication of the same URL is idempotent. + **fix(linkify): trailing punctuation included in auto-linked URLs** (`bd3a623` in svemagie/indiekit-endpoint-activitypub) URLs at the end of a sentence (e.g. `"See https://example.com."`) had the trailing period captured as part of the URL, producing a broken link (`https://example.com.` → 404). Root cause: the regex `[^\s<"]+` in `linkifyUrls()` (`lib/jf2-to-as2.js`) and `/(https?:\/\/[^\s<>"')\]]+)/g` in `processStatusContent()` (`lib/mastodon/routes/statuses.js`) both match until whitespace or tag-open, but `.`, `,`, `;`, `:`, `!`, `?` are common sentence-ending characters that follow URLs. Fix: replace the string template in both replace calls with a callback that strips `/[.,;:!?)\]'"]+$/` from the captured URL before inserting into the `` tag. Applies to AP federation (outbox Notes) and Mastodon Client API post creation. diff --git a/package-lock.json b/package-lock.json index 1a68de2b..9b39fceb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.8.5", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#bd3a623488f7b88817bf1b14f7733a4f40c7dcca", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#42f8c2d9d44f2f5db08b7518e7642ffd0cb9a3b1", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.0.0", From ad58bc45db5dbed61bae0b4bbc63f3749523a245 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 09:31:51 +0100 Subject: [PATCH 46/59] chore: update AP fork lockfile pin to 230bfd1 (upstream v3.9.x merge) Fedify 2.1.0, 5 FEPs (Tombstone, Activity Intents, indexable, NodeInfo, Collection Sync), security audit fixes, architecture refactor (syndicator.js, batch-broadcast.js, init-indexes.js, CSS split). All fork patches retained: DM support, pin/unpin, edit post, timeout guard, signed fetch, ap_timeline mirror. --- README.md | 4 +- package-lock.json | 146 +++++++++++++++++++++++++--------------------- 2 files changed, 83 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 3d8bcf73..4e57a0a4 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,13 @@ Four packages are installed directly from GitHub forks rather than the npm regis In `package.json` these use the `github:owner/repo[#branch]` syntax so npm fetches them directly from GitHub on install. -> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm install github:svemagie/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `42f8c2d` (all upstream fixes through 2026-03-23 merged; DM support; pin/unpin status; favourite/reblog timeout guard; raw signed fetch fallback for non-standard AP servers; timezone-aware status lookup for pre-UTC-normalization timeline items; own Micropub posts mirrored into ap_timeline so context/statuses endpoints work for website-authored posts). +> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm install github:svemagie/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `230bfd1` (upstream v3.9.x merged: Fedify 2.1.0, 5 FEPs — Tombstone/soft-delete, Activity Intents, indexable actor, NodeInfo enrichment, Collection Sync; security audit — XSS/CSRF/OAuth scope enforcement, rate limiting, token expiry, secret hashing; architecture refactor — syndicator.js, batch-broadcast.js, init-indexes.js, CSS split into 15 files; plus all fork patches: DM support, pin/unpin status, edit post, favourite/reblog timeout guard, raw signed fetch fallback, timezone-aware status lookup, own Micropub posts mirrored into ap_timeline). --- ## ActivityPub federation -The blog is a native ActivityPub actor (`@svemagie@blog.giersig.eu`) powered by [Fedify](https://fedify.dev/) v2.0.3 via the `@rmdes/indiekit-endpoint-activitypub` package. All federation routes are mounted at `/activitypub`. +The blog is a native ActivityPub actor (`@svemagie@blog.giersig.eu`) powered by [Fedify](https://fedify.dev/) v2.1.0 via the `@rmdes/indiekit-endpoint-activitypub` package. All federation routes are mounted at `/activitypub`. ### Actor identity diff --git a/package-lock.json b/package-lock.json index 9b39fceb..d79021bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -763,12 +763,12 @@ "license": "MIT" }, "node_modules/@fedify/debugger": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@fedify/debugger/-/debugger-2.0.7.tgz", - "integrity": "sha512-439rX7f6zxXuBfLCQefP5JSv1osdjG4IimuRrIQgb5xaG1VlSaixHn4eN94ii64TGD+6PUGbTFYZhRmnnYlwAA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fedify/debugger/-/debugger-2.1.1.tgz", + "integrity": "sha512-n3fdo3u3uZwng+4NkgkDReyKg1tJhJ4B+5qX4AA49p+NJUdOBJYihmuovoXdZ0fxO2E3UK+X0o0q+OFOdMRBKw==", "dependencies": { "@js-temporal/polyfill": "^0.5.1", - "@logtape/logtape": "^2.0.0", + "@logtape/logtape": "^2.0.5", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.5.0", "@opentelemetry/core": "^2.5.0", @@ -776,24 +776,24 @@ "hono": "^4.0.0" }, "peerDependencies": { - "@fedify/fedify": "^2.0.7" + "@fedify/fedify": "^2.1.1" } }, "node_modules/@fedify/fedify": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@fedify/fedify/-/fedify-2.0.7.tgz", - "integrity": "sha512-2/GYm/ukjg4t3+HXBgfxkoq1KUCGPJTxxmanmd+B4aGOmHX5PNfteSxpQxrHYzUGQNk46KFKnDF3+t6v2JKCfA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fedify/fedify/-/fedify-2.1.1.tgz", + "integrity": "sha512-DHhtrfBrg899Voi6W9rjDr6QDFcRQi/Ur7mmttGFnVJa5fVnXbOCZaQ9Bb9di8559Zbn+xX3sqWKJfW2v8lvAQ==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" ], "license": "MIT", "dependencies": { - "@fedify/vocab": "2.0.7", - "@fedify/vocab-runtime": "2.0.7", - "@fedify/webfinger": "2.0.7", + "@fedify/vocab": "2.1.1", + "@fedify/vocab-runtime": "2.1.1", + "@fedify/webfinger": "2.1.1", "@js-temporal/polyfill": "^0.5.1", - "@logtape/logtape": "^2.0.0", + "@logtape/logtape": "^2.0.5", "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.5.0", "@opentelemetry/sdk-trace-base": "^2.5.0", @@ -814,9 +814,9 @@ } }, "node_modules/@fedify/redis": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@fedify/redis/-/redis-2.0.7.tgz", - "integrity": "sha512-e9YfkDJxItaWibslo+D0+FAq+eEFG3t7GxjE51FRjQwKZj7+FK2BhGD5lOtlTwq00StRqmf2WYcjMC03lcE3cg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fedify/redis/-/redis-2.1.1.tgz", + "integrity": "sha512-ZJTbZ555RErVJg7RUIYIY1lzX2Ihfsy3bqnX58AUQTngYmfwgBAl9FAv275shjiSuNVNhWCr0pyjAdnQTb18YA==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" @@ -824,28 +824,28 @@ "license": "MIT", "dependencies": { "@js-temporal/polyfill": "^0.5.1", - "@logtape/logtape": "^2.0.0" + "@logtape/logtape": "^2.0.5" }, "peerDependencies": { - "@fedify/fedify": "^2.0.7", + "@fedify/fedify": "^2.1.1", "ioredis": "^5.8.2" } }, "node_modules/@fedify/vocab": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@fedify/vocab/-/vocab-2.0.7.tgz", - "integrity": "sha512-jg1KpI2Yke26NcHrK8HS/OUeTYiCgcj98IvynBxgfzZY2MeZ1Bc7wg4VDRdHfz+TEMe9GwkF47Flj6DVcar0Zw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fedify/vocab/-/vocab-2.1.1.tgz", + "integrity": "sha512-Jy5t4jAzrR0+sF0b+aQRuvZC4pvsEsutjeayJf6RVTNSs+QugvbyCz7k+GXtMvofnpP9QjuK6nUKal+c/3qfiQ==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" ], "license": "MIT", "dependencies": { - "@fedify/vocab-runtime": "2.0.7", - "@fedify/vocab-tools": "2.0.7", - "@fedify/webfinger": "2.0.7", + "@fedify/vocab-runtime": "2.1.1", + "@fedify/vocab-tools": "2.1.1", + "@fedify/webfinger": "2.1.1", "@js-temporal/polyfill": "^0.5.1", - "@logtape/logtape": "^2.0.0", + "@logtape/logtape": "^2.0.5", "@multiformats/base-x": "^4.0.1", "@opentelemetry/api": "^1.9.0", "asn1js": "^3.0.6", @@ -860,16 +860,16 @@ } }, "node_modules/@fedify/vocab-runtime": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@fedify/vocab-runtime/-/vocab-runtime-2.0.7.tgz", - "integrity": "sha512-6YKAC1/Xk5GYUuWwX6BwhmMjLmSEiyJWZuu+u25aJ+0i9b93Cw2aGCuorTndvFZcpZ+TDGiEQfA0qVe6knJWcg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fedify/vocab-runtime/-/vocab-runtime-2.1.1.tgz", + "integrity": "sha512-W/R/+AOKld4S5HuwPf8bgT8dA+apNoaF8j0wVqFcj9/nCWYrTzWX8KfiHK2kOGxK1gK1ia+AtJJ/uFUcBpbc/A==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" ], "license": "MIT", "dependencies": { - "@logtape/logtape": "^2.0.0", + "@logtape/logtape": "^2.0.5", "@multiformats/base-x": "^4.0.1", "@opentelemetry/api": "^1.9.0", "asn1js": "^3.0.6", @@ -884,9 +884,9 @@ } }, "node_modules/@fedify/vocab-tools": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@fedify/vocab-tools/-/vocab-tools-2.0.7.tgz", - "integrity": "sha512-4jz6b0keXab6kjRCnr8oSLnHgdir30xPWV4HkQhVMIQROW6SVyUPMjRxr1+iuzUx70DqHuNdkDJPXX+1gPezNg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fedify/vocab-tools/-/vocab-tools-2.1.1.tgz", + "integrity": "sha512-c64ZKjeJjqllEQ0WWPF+5s5V2PADJoL63MV5HqR/wQVvOyBpMK/oOcjLYLtwmwAgqQY2HHDOQwYvsIbTTPujTA==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" @@ -905,17 +905,17 @@ } }, "node_modules/@fedify/webfinger": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@fedify/webfinger/-/webfinger-2.0.7.tgz", - "integrity": "sha512-JNSBGQHekvvGNWbIRaI05WDNh4xO13SWa/WG6JMxaabQKnnerXVyy1M36zKbEEaXASvANk3qGkXLPZCae3dJmg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fedify/webfinger/-/webfinger-2.1.1.tgz", + "integrity": "sha512-yCtHWMA/sA1NZUvopuFQ4KEgKHHj129WyPdVSpOiWGneUw+p3sgf8ffO1oj13FYwiR3qvpIV8SMcJ9aFmOa6Ug==", "funding": [ "https://opencollective.com/fedify", "https://github.com/sponsors/dahlia" ], "license": "MIT", "dependencies": { - "@fedify/vocab-runtime": "2.0.7", - "@logtape/logtape": "^2.0.0", + "@fedify/vocab-runtime": "2.1.1", + "@logtape/logtape": "^2.0.5", "@opentelemetry/api": "^1.9.0", "es-toolkit": "1.43.0" }, @@ -1850,9 +1850,9 @@ "license": "MIT" }, "node_modules/@logtape/logtape": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@logtape/logtape/-/logtape-2.0.4.tgz", - "integrity": "sha512-Z4COeAMdedcBFuFkXaPFvDPOVuHoEom1hwNnPCIkSyojyikuNguplwPoSG+kZthWrS7GiOJo1USQyjWwIFfTKA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@logtape/logtape/-/logtape-2.0.5.tgz", + "integrity": "sha512-UizDkh20ZPJVOddRxG1F77WhHdlNl/sbQgoO8T534R7XvUBMAJ9En9f35u+meW2tRsNLvjz6R87Zanwf53tspQ==", "funding": [ "https://github.com/sponsors/dahlia" ], @@ -1982,18 +1982,18 @@ } }, "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", "engines": { "node": ">=8.0.0" } }, "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.6.0.tgz", - "integrity": "sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.6.1.tgz", + "integrity": "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ==", "license": "Apache-2.0", "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2003,9 +2003,9 @@ } }, "node_modules/@opentelemetry/core": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", - "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", + "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2018,12 +2018,12 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", - "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", + "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.0", + "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -2034,13 +2034,13 @@ } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.0.tgz", - "integrity": "sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", + "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/resources": "2.6.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -2417,15 +2417,16 @@ } }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { - "version": "3.8.5", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#42f8c2d9d44f2f5db08b7518e7642ffd0cb9a3b1", + "version": "3.10.0", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#230bfd105e51cbd27509640c845e0a51dcb6177b", "license": "MIT", "dependencies": { - "@fedify/debugger": "^2.0.0", - "@fedify/fedify": "^2.0.0", - "@fedify/redis": "^2.0.0", + "@fedify/debugger": "^2.1.0", + "@fedify/fedify": "^2.1.0", + "@fedify/redis": "^2.1.0", "@js-temporal/polyfill": "^0.5.0", "express": "^5.0.0", + "express-rate-limit": "^7.5.1", "ioredis": "^5.9.3", "sanitize-html": "^2.13.1", "unfurl.js": "^6.4.0" @@ -2439,6 +2440,21 @@ "@indiekit/frontend": "^1.0.0-beta.25" } }, + "node_modules/@rmdes/indiekit-endpoint-activitypub/node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/@rmdes/indiekit-endpoint-auth": { "version": "1.0.0-beta.25", "resolved": "https://registry.npmjs.org/@rmdes/indiekit-endpoint-auth/-/indiekit-endpoint-auth-1.0.0-beta.25.tgz", @@ -5175,9 +5191,9 @@ } }, "node_modules/hono": { - "version": "4.12.8", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", - "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", + "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", "license": "MIT", "engines": { "node": ">=16.9.0" From e4da0f99afda915b6d5dc2800bd94e48bed91882 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 09:35:54 +0100 Subject: [PATCH 47/59] chore: update AP fork lockfile pin to b595734 (fix missing tokenRequired imports) --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index d79021bd..e0dbda31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.10.0", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#230bfd105e51cbd27509640c845e0a51dcb6177b", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#b59573408744d228016c02ff6dcfb4b9acdcc934", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.1.0", From 2670cfc9fa49e9741bef5116d1694b22faa02213 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 09:40:12 +0100 Subject: [PATCH 48/59] chore: update AP fork lockfile pin to 6f76ec4 (fix resolveAuthor import) --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index e0dbda31..b603b965 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.10.0", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#b59573408744d228016c02ff6dcfb4b9acdcc934", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#6f76ec45c491fe36e064c738da1b2a1e2d30ba23", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.1.0", From 7cca6a605f031b8372f8d5b52f6b49511562e7ce Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 09:43:27 +0100 Subject: [PATCH 49/59] chore: update AP fork lockfile pin to 69ae731 (fix rate-limit trust proxy error) --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index b603b965..38a3a643 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.10.0", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#6f76ec45c491fe36e064c738da1b2a1e2d30ba23", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#69ae731dab52a8a4aca63e47722f08504647fc23", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.1.0", From 8464e2140cddae6e31a0acc31bedfb3de35bd1b3 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 09:49:18 +0100 Subject: [PATCH 50/59] docs: document 2026-03-27 upstream merge and post-merge fixes --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e57a0a4..833fbc13 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Four packages are installed directly from GitHub forks rather than the npm regis In `package.json` these use the `github:owner/repo[#branch]` syntax so npm fetches them directly from GitHub on install. -> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm install github:svemagie/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `230bfd1` (upstream v3.9.x merged: Fedify 2.1.0, 5 FEPs — Tombstone/soft-delete, Activity Intents, indexable actor, NodeInfo enrichment, Collection Sync; security audit — XSS/CSRF/OAuth scope enforcement, rate limiting, token expiry, secret hashing; architecture refactor — syndicator.js, batch-broadcast.js, init-indexes.js, CSS split into 15 files; plus all fork patches: DM support, pin/unpin status, edit post, favourite/reblog timeout guard, raw signed fetch fallback, timezone-aware status lookup, own Micropub posts mirrored into ap_timeline). +> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm install github:svemagie/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `69ae731` (upstream v3.9.x merged: Fedify 2.1.0, 5 FEPs — Tombstone/soft-delete, Activity Intents, indexable actor, NodeInfo enrichment, Collection Sync; security audit — XSS/CSRF/OAuth scope enforcement, rate limiting, token expiry, secret hashing; architecture refactor — syndicator.js, batch-broadcast.js, init-indexes.js, CSS split into 15 files; plus all fork patches: DM support, pin/unpin status, edit post, favourite/reblog timeout guard, raw signed fetch fallback, timezone-aware status lookup, own Micropub posts mirrored into ap_timeline; trust proxy rate-limit fix). --- @@ -752,6 +752,20 @@ Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config. ## Changelog +### 2026-03-27 + +**merge: upstream v3.9.x — Fedify 2.1.0, 5 FEPs, security/perf audit** (`230bfd1` in svemagie/indiekit-endpoint-activitypub) +14 upstream commits merged (`0820067..c1a6f7e`). Key changes: Fedify upgraded to 2.1.0; 5 FEP implementations added — FEP-4f05 soft-delete with Tombstone (deleted posts serve 410 + JSON-LD Tombstone, new `ap_tombstones` collection), FEP-3b86 Activity Intents (WebFinger links + `authorize_interaction` routes), FEP-5feb indexable/discoverable actor fields, FEP-f1d5/0151 enriched NodeInfo 2.1, FEP-8fcf Collection Sync outbound. Security audit fixes (27 issues): XSS/CSRF on OAuth authorization page, OAuth scope enforcement on all Mastodon API routes, rate limiting on API/auth/app-registration endpoints, access token expiry (1h) + refresh token rotation (90d), client secret hashing, SSRF fix, redirect_uri validation. Architecture refactoring: syndicator extracted to `lib/syndicator.js`, batch broadcast to `lib/batch-broadcast.js`, MongoDB index creation to `lib/init-indexes.js`, federation helpers to `lib/federation-actions.js` (`index.js` reduced by 35%); CSS split from one 3441-line `reader.css` into 15 feature-scoped files. Fork-specific conflict resolutions: `addTimelineItem` mirror moved from inline syndicator in `index.js` to `lib/syndicator.js`; fixed missing `await` on `jf2ToAS2Activity` in upstream's extracted syndicator; DM path, pin/unpin routes, edit post route, and `processStatusContent` retained in `statuses.js`; cache-first avatar approach retained in `enrich-accounts.js`; DM lock icon (🔒) retained in notification card template. + +**fix(accounts): missing tokenRequired/scopeRequired imports** (`b595734` in svemagie/indiekit-endpoint-activitypub) +`accounts.js` started failing with `ReferenceError: tokenRequired is not defined` immediately on startup. During the merge conflict resolution, the upstream-added `tokenRequired`/`scopeRequired` imports in `accounts.js` were incorrectly dropped (they appeared to already exist in the file from a grep of the post-merge state, but in reality they were only referenced via route middleware, not imported). Fix: added the two missing `import` lines. + +**fix(index): missing resolveAuthor import** (`6f76ec4` in svemagie/indiekit-endpoint-activitypub) +`resolveAuthor` from `lib/resolve-author.js` is used in `index.js` for like/boost delivery (within `batchBroadcast` handlers) but its import was dropped when the merge conflict replaced the inline syndicator block with `createSyndicator(this)`. Fix: restored the `import { resolveAuthor }` line. + +**fix(rate-limit): ERR_ERL_PERMISSIVE_TRUST_PROXY on every request** (`69ae731` in svemagie/indiekit-endpoint-activitypub) +The new `express-rate-limit` middleware (from the upstream security audit) threw `ValidationError: ERR_ERL_PERMISSIVE_TRUST_PROXY` on every incoming request because the server sits behind nginx with `trust proxy: true` set in Express, which `express-rate-limit` v7+ treats as a misconfiguration warning by default. The error propagated up the middleware chain and caused Fedify to log spurious "Failed to verify HTTP Signatures" errors for all incoming inbox requests. Fix: added `validate: { trustProxy: false }` to all three rate limiter instances (`apiLimiter`, `authLimiter`, `appRegistrationLimiter`) in `lib/mastodon/router.js`, signalling that the trust proxy configuration is intentional. + ### 2026-03-24 **fix(syndicate): own Micropub posts missing from ap_timeline** (`42f8c2d` in svemagie/indiekit-endpoint-activitypub) From 8e7527ff7fbdc2fd1c6f38f54557f2592e879b24 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 09:52:24 +0100 Subject: [PATCH 51/59] fix(start): webmention poller connects directly to Indiekit, not nginx nginx port 80 returns 444 (no response) for requests with an unrecognised Host header. The poller's curl sends Host: 10.100.0.10 (the IP) which doesn't match any server_name, causing the 180s readiness timeout and "empty reply from server" on every poll. Since livefetch v6 builds synthetic HTML from stored properties and no longer fetches live pages, the poller no longer needs to go through nginx. It now connects directly to Indiekit on INDIEKIT_BIND_HOST:PORT. Co-Authored-By: Claude Opus 4.6 --- start.example.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/start.example.sh b/start.example.sh index 9fb3603f..022344c9 100644 --- a/start.example.sh +++ b/start.example.sh @@ -61,11 +61,11 @@ done INDIEKIT_PID="$!" # Webmention sender — polls every N seconds (see @rmdes/indiekit-endpoint-webmention-sender README) -# Routes through INTERNAL_FETCH_URL (nginx) so the request gets correct Host header -# and X-Forwarded-Proto, avoiding empty-reply issues with direct jail connections. +# Connects directly to Indiekit (not through nginx) so the Host header doesn't +# need to match any nginx server_name. nginx port 80 returns 444 for unknown hosts. WEBMENTION_POLL_INTERVAL="${WEBMENTION_SENDER_POLL_INTERVAL:-300}" -INDIEKIT_INTERNAL_URL="${INTERNAL_FETCH_URL:-http://${INDIEKIT_BIND_HOST:-127.0.0.1}:${PORT:-3000}}" -WEBMENTION_ENDPOINT="${INDIEKIT_INTERNAL_URL}${WEBMENTION_SENDER_MOUNT_PATH:-/webmention-sender}" +INDIEKIT_DIRECT_URL="http://${INDIEKIT_BIND_HOST:-127.0.0.1}:${PORT:-3000}" +WEBMENTION_ENDPOINT="${INDIEKIT_DIRECT_URL}${WEBMENTION_SENDER_MOUNT_PATH:-/webmention-sender}" WEBMENTION_ORIGIN="${PUBLICATION_URL:-${SITE_URL:-}}" ( From ea20d10501abc8c67ba0eb3c2f0394bb2a3974cf Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 09:54:44 +0100 Subject: [PATCH 52/59] chore: build sharp from source for FreeBSD compatibility Co-Authored-By: Claude Opus 4.6 --- .npmrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmrc b/.npmrc index 521a9f7c..d89319d9 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ legacy-peer-deps=true +sharp_from_source=true From 88c988947ba4538aee4994edeea4143f13ed18c7 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 09:57:31 +0100 Subject: [PATCH 53/59] docs: document livefetch v6, poller direct-connect fix, nginx 444 root cause Co-Authored-By: Claude Opus 4.6 --- README.md | 86 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 833fbc13..524c1206 100644 --- a/README.md +++ b/README.md @@ -233,47 +233,43 @@ Post content stored in MongoDB (`post.properties.content.html`) is just the post - `u-repost-of` — same template - `u-bookmark-of` — same template -These links only exist in the live HTML page, so the webmention sender must always fetch the rendered page to discover them. This is what `patch-webmention-sender-livefetch.mjs` does. +These links are **not** stored in MongoDB — only the live rendered page contains them. The livefetch patch (`patch-webmention-sender-livefetch.mjs`) solves this by building synthetic h-entry HTML from stored post properties directly, without fetching the live page. + +### How the livefetch patch works (v6) + +Instead of fetching the live page, v6 reads the stored post properties from MongoDB and builds a minimal synthetic HTML document: + +```html +
+ +
…stored content…
+
+``` + +This avoids all the networking complexity (nginx routing, Host headers, TLS, 502s) and is reliable even during deploys. The `extractLinks` function scopes to `.h-entry` and finds the anchor tags regardless of text content. ### Poller architecture (start.sh) The webmention sender plugin does not have its own scheduling — it exposes an HTTP endpoint that triggers a scan when POSTed to. The `start.sh` script runs a background shell loop: -1. **Readiness check** — polls `GET /webmention-sender/api/status` every 2s until it returns 200 (up to 3 minutes). This ensures MongoDB collections and plugin routes are fully initialised before the first scan. +1. **Readiness check** — polls `GET /webmention-sender/api/status` directly on `INDIEKIT_BIND_HOST:PORT` every 2s until it returns 200 (up to 3 minutes). This ensures MongoDB collections and plugin routes are fully initialised before the first scan. 2. **JWT generation** — mints a short-lived token (`{ me, scope: "update" }`, 5-minute expiry) signed with `SECRET`. 3. **POST trigger** — `curl -X POST /webmention-sender?token=JWT` triggers one scan cycle. 4. **Sleep** — waits `WEBMENTION_SENDER_POLL_INTERVAL` seconds (default 300 = 5 minutes), then repeats. -The poller routes through nginx (`INTERNAL_FETCH_URL`) rather than hitting Indiekit directly, so the request arrives with correct `Host` and `X-Forwarded-Proto` headers. - -### Internal URL rewriting - -When the livefetch patch fetches a post's live page, it rewrites the URL from the public domain to the internal nginx address: - -``` -https://blog.giersig.eu/replies/693e6/ - ↓ rewrite via INTERNAL_FETCH_URL -http://10.100.0.10/replies/693e6/ - ↓ nginx proxies to Indiekit -http://10.100.0.20:3000/replies/693e6/ -``` - -Without this, the node jail cannot reach its own public HTTPS URL (TLS terminates on the web jail). The fallback chain is: - -1. `INTERNAL_FETCH_URL` environment variable (production: `http://10.100.0.10`) -2. `http://localhost:${PORT}` (development) +The poller connects **directly to Indiekit** (`http://INDIEKIT_BIND_HOST:PORT`) — not through nginx. This is essential because nginx's `000-defaults.conf` returns HTTP 444 (connection drop, no response) for any request whose `Host` header doesn't match a known `server_name`. The poller's curl sends `Host: 10.100.0.20` (the jail IP), which matches no `server_name`, so routing through nginx would silently fail. ### Retry behaviour -If the live page fetch fails (e.g. deploy still in progress, 502 from nginx), the post is **not** marked as sent. It stays in the "unsent" queue and is retried on the next poll cycle. This prevents the original upstream bug where a failed fetch would permanently mark the post as sent with zero webmentions. +If a post's stored properties can't produce any external links (e.g. `in-reply-to` is missing), the post is still marked as sent with empty results. This is correct behaviour — if the properties are genuinely empty there's nothing to retry. If the properties were incorrectly stored, bump the `MIGRATION_ID` in `patch-webmention-sender-reset-stale.mjs` to force a re-scan after fixing the data. ### Patches | Patch | Purpose | |---|---| -| `patch-webmention-sender-livefetch.mjs` | **(v2)** Always fetch live HTML; validate it contains `h-entry` (rejects error pages/502s); skip without marking sent on any failure; rewrites URL via `INTERNAL_FETCH_URL` for jailed setups. Upgrades from v1 in-place. | -| `patch-webmention-sender-retry.mjs` | Predecessor to livefetch — superseded by livefetch v2. Silently skips when livefetch v2 marker is present; logs "already applied" otherwise. Kept so it doesn't error if livefetch fails to apply. | -| `patch-webmention-sender-reset-stale.mjs` | One-time MongoDB migration (v9): resets posts incorrectly marked as sent with empty results. Matches both old numeric-zero format and new v1.0.6+ empty-array format. Guarded by `migrations` collection (`webmention-sender-reset-stale-v9`). | +| `patch-webmention-sender-livefetch.mjs` | **(v6)** Builds synthetic h-entry HTML from stored post properties (no live fetch). Logs which property links were found per post. Upgrades from any prior version (v1–v5) in-place. | +| `patch-webmention-sender-retry.mjs` | Superseded by livefetch. Silently skips when any livefetch version marker is present (regex matches `[patched:livefetch]` and `[patched:livefetch:vN]`). Kept as safety fallback. | +| `patch-webmention-sender-reset-stale.mjs` | One-time MongoDB migration (v11): resets posts incorrectly marked as sent with empty results. Matches both old numeric-zero format and new v1.0.6+ empty-array format. Guarded by `migrations` collection (`webmention-sender-reset-stale-v11`). | | `patch-webmention-sender-empty-details.mjs` | UI patch: shows "No external links discovered" in the dashboard when a post was processed but had no outbound links (instead of a blank row). | ### Patch ordering @@ -283,7 +279,7 @@ Patches run alphabetically via `for patch in scripts/patch-*.mjs`. For webmentio 1. `patch-webmention-sender-empty-details.mjs` — targets the `.njk` template (independent) 2. `patch-webmention-sender-livefetch.mjs` — replaces the fetch block in `webmention-sender.js` 3. `patch-webmention-sender-reset-stale.mjs` — MongoDB migration (independent) -4. `patch-webmention-sender-retry.mjs` — detects the livefetch v2 marker and silently skips; logs "already applied" (not a misleading "package updated?" warning) +4. `patch-webmention-sender-retry.mjs` — detects any livefetch version marker via regex and silently skips; logs "already applied" ### Environment variables @@ -299,16 +295,13 @@ Patches run alphabetically via `for patch in scripts/patch-*.mjs`. For webmentio ### Troubleshooting **"No external links discovered in this post"** -The live page was fetched, had a valid `.h-entry`, but no `` tags with external URLs were found. Check that the post's Eleventy template renders the microformat links (`u-like-of`, etc.) correctly. If the post previously processed with 0 results due to an error page (502, redirect), bump the `MIGRATION_ID` in `patch-webmention-sender-reset-stale.mjs` and restart to force a retry. +The livefetch patch built the synthetic h-entry but no external links were found. Check the startup log for the line `[webmention] Built synthetic h-entry for : N prop link(s) [in-reply-to]`. If it says `0 prop link(s) [none]`, the relevant property (`in-reply-to`, `like-of`, etc.) is missing from stored post properties in MongoDB — the data was never saved correctly. If the post was previously processed with 0 results due to the old live-fetch bugs, bump `MIGRATION_ID` in `patch-webmention-sender-reset-stale.mjs` and restart. -**502 Bad Gateway on first poll** -The readiness check (`/webmention-sender/api/status`) should prevent this. If it still happens, the plugin may have registered its routes but MongoDB isn't ready yet. Increase the readiness timeout or check MongoDB connectivity. +**"webmention-sender not ready after 180s" / "Empty reply from server"** +The readiness check or poll is routing through nginx, which returns 444 (connection drop) for requests with an unrecognised `Host` header. The poller must connect directly to `INDIEKIT_BIND_HOST:PORT`, not through `INTERNAL_FETCH_URL`. Check that `start.sh` uses `INDIEKIT_DIRECT_URL` (not `INTERNAL_FETCH_URL`) for `WEBMENTION_ENDPOINT`. -**Posts stuck as "not sent" / retrying every cycle** -The live page fetch is failing every time. Check: -1. `INTERNAL_FETCH_URL` is set and nginx port 80 is reachable from the node jail -2. nginx port 80 has `proxy_set_header X-Forwarded-Proto https` (prevents redirect loop) -3. The post URL actually resolves to a page (not a 404) +**Posts stuck as "not sent" / not appearing in the dashboard** +The post was processed with empty results before the livefetch v6 fix was deployed. Bump `MIGRATION_ID` in `patch-webmention-sender-reset-stale.mjs` to force a re-scan on next restart. **Previously failed posts not retrying** Bump the `MIGRATION_ID` in `scripts/patch-webmention-sender-reset-stale.mjs` to a new version string and restart. The migration resets all posts marked as sent with empty results (both numeric-zero and empty-array formats). It is idempotent per ID — bumping the ID forces it to run once more. @@ -527,14 +520,14 @@ Applies several guards to the listening endpoints: scopes Funkwhale history fetc ### Webmention sender -**`patch-webmention-sender-livefetch.mjs`** (v2) -Forces the webmention sender to always fetch the live published page. Validates the response contains `h-entry` before using it — rejects error pages and 502 responses that would silently produce zero links. Rewrites the fetch URL via `INTERNAL_FETCH_URL` for jailed setups. Does not fall back to stored content (which lacks template-rendered microformat links). Upgrades from v1 in-place; silently skips if already at v2. +**`patch-webmention-sender-livefetch.mjs`** (v6) +Replaces the upstream content-fetching block with a synthetic h-entry builder. Reads stored post properties directly from the MongoDB document (`in-reply-to`, `like-of`, `bookmark-of`, `repost-of`, `syndication`, `content.html`) and constructs a minimal `
` with the appropriate microformat anchor tags. No live page fetch, no nginx dependency, no networking failures. Logs which properties were found per post. Upgrades from any prior version (v1–v5) in-place. **`patch-webmention-sender-retry.mjs`** -Predecessor to livefetch, now fully superseded. Silently skips when livefetch v2 marker is present so it does not log misleading "package updated?" warnings. Kept in case livefetch fails to find its target (acts as a partial fallback). +Predecessor to livefetch, now fully superseded. Silently skips when any livefetch version marker is detected (regex: `/\[patched:livefetch(?::v\d+)?\]/`). Kept as safety fallback in case livefetch fails to find its target. -**`patch-webmention-sender-reset-stale.mjs`** (v9) -One-time migration (guarded by a `migrations` MongoDB collection entry, currently `webmention-sender-reset-stale-v9`) that resets posts incorrectly marked as webmention-sent with empty results. Matches both old numeric-zero format and new v1.0.6+ empty-array format. Bump the `MIGRATION_ID` to re-run after future bugs. +**`patch-webmention-sender-reset-stale.mjs`** (v11) +One-time migration (guarded by a `migrations` MongoDB collection entry, currently `webmention-sender-reset-stale-v11`) that resets posts incorrectly marked as webmention-sent with empty results. Matches both old numeric-zero format and new v1.0.6+ empty-array format. Bump the `MIGRATION_ID` to re-run after future bugs. ### Bluesky syndicator @@ -906,6 +899,23 @@ Removed a stray extra closing quote (`h-entry""`) introduced in the v2 patch, wh --- +### 2026-03-27 + +**fix(webmention): livefetch v6 — synthetic h-entry from stored properties, no live fetch** +Root cause of persistent webmention failures: the livefetch patch was fetching the live page through nginx port 80, which `000-defaults.conf` answered with HTTP 444 (silent connection drop) for any request whose `Host` header didn't match a known `server_name`. The poller sent `Host: 10.100.0.10` (the nginx jail IP), which matched nothing. + +v6 eliminates the live-page fetch entirely. Instead, it reads the stored post properties from MongoDB and builds a minimal synthetic `
` with anchor tags for each microformat property (`in-reply-to`, `like-of`, `bookmark-of`, `repost-of`, `syndication`) plus the stored `content.html`. This is reliable, fast, and requires no networking. + +Additional changes: +- livefetch v6: adds `console.log` per post showing which properties produced links — makes future debugging possible without server access +- livefetch v6: upgrades from any prior version (v1–v5) in-place via per-version end-marker detection +- retry patch: regex now matches `[patched:livefetch]` and `[patched:livefetch:vN]` for all versions +- reset-stale v11: bumped to retry posts stuck before v6 deployment +- start.sh: poller now uses `INDIEKIT_DIRECT_URL=http://INDIEKIT_BIND_HOST:PORT` instead of `INTERNAL_FETCH_URL` (nginx); poller was timing out for 180s every restart due to the 444 responses + +**chore: `sharp_from_source=true` in `.npmrc`** +Builds the `sharp` native module from source for FreeBSD compatibility (no prebuilt binary available). + ### 2026-03-19 **feat: deliver likes as bookmarks, revert announce cc, add OG images** (`45f8ba9` in svemagie/indiekit-endpoint-activitypub) From 02f7db46e1f738480ff1b1fb405aea89d8683ece Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 10:13:30 +0100 Subject: [PATCH 54/59] fix(media): fix image upload size limit and session token bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nginx: add client_max_body_size 20M to blog vhost (default 1MB was silently killing uploads, returning 413 as a JSON parse error in UI) - patch: fix session.token → session.access_token in micropub action controller; caused Bearer undefined on internal /media fetch, giving 500s for Micropub clients that upload files directly (OwnYourSwarm) - npmrc: add sharp_from_source=true to survive future npm install on FreeBSD without breaking sharp's native bindings Co-Authored-By: Claude Sonnet 4.6 --- package.json | 4 +- scripts/patch-micropub-session-token.mjs | 59 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 scripts/patch-micropub-session-token.mjs diff --git a/package.json b/package.json index f4c3f82b..7f3616e8 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "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-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-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.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-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-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.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-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node 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-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-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.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-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-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.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-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.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-micropub-session-token.mjs b/scripts/patch-micropub-session-token.mjs new file mode 100644 index 00000000..c1ce0799 --- /dev/null +++ b/scripts/patch-micropub-session-token.mjs @@ -0,0 +1,59 @@ +/** + * Patch: fix session.token → session.access_token in micropub action controller. + * + * The indieauth authenticate middleware stores the bearer token as + * `session.access_token`, but the micropub action controller destructures it + * as `session.token`. This causes `uploadMedia` to be called with + * `token = undefined`, resulting in `Authorization: Bearer undefined` on the + * internal /media fetch — a 500 for any Micropub client that uploads files + * directly (e.g. OwnYourSwarm). + */ + +import { access, readFile, writeFile } from "node:fs/promises"; + +const candidates = [ + "node_modules/@indiekit/endpoint-micropub/lib/controllers/action.js", +]; + +const oldCode = "const { scope, token } = session;"; +const newCode = "const { scope, access_token: token } = session;"; + +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(newCode)) { + continue; + } + + if (!source.includes(oldCode)) { + console.warn(`[postinstall] micropub-session-token: snippet not found in ${filePath} — skipping`); + continue; + } + + const updated = source.replace(oldCode, newCode); + await writeFile(filePath, updated, "utf8"); + patched += 1; +} + +if (checked === 0) { + console.log("[postinstall] No micropub action controller found"); +} else if (patched === 0) { + console.log("[postinstall] micropub session token patch already applied"); +} else { + console.log(`[postinstall] Patched micropub session token in ${patched} file(s)`); +} From 128ed58e571f0922321a600e93a8c8250e17c098 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 10:15:38 +0100 Subject: [PATCH 55/59] chore: update AP fork lockfile pin to 9b6db98 (suppress inbox signature noise) --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 38a3a643..17f910a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.10.0", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#69ae731dab52a8a4aca63e47722f08504647fc23", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#9b6db9865cf618af261d6aee203ecae92193f1a0", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.1.0", From 0cc51879908f85b2210ebfc304cdd63addb5574f Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 10:29:02 +0100 Subject: [PATCH 56/59] fix(media-browser): fix mixed-content error causing 'Browse media' to fail `getEndpointUrls()` resolved relative endpoint paths (e.g. `/media`) using `getUrl(request)`, which returns `http://` because Express sees HTTP from nginx without trust proxy. This produced `http://blog.giersig.eu/media` as the endpoint attribute in the file-input component, causing Safari to block the fetch as mixed content ('Load failed'). Fix: prefer `application.url` (the configured HTTPS base URL) over `getUrl(request)` when resolving relative endpoint paths. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 4 +- .../patch-indiekit-endpoint-urls-protocol.mjs | 65 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 scripts/patch-indiekit-endpoint-urls-protocol.mjs diff --git a/package.json b/package.json index 7f3616e8..477d1b65 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "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-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-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.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-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-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.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-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node 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-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-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.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-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-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.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-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.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-indiekit-endpoint-urls-protocol.mjs b/scripts/patch-indiekit-endpoint-urls-protocol.mjs new file mode 100644 index 00000000..7a6fcf13 --- /dev/null +++ b/scripts/patch-indiekit-endpoint-urls-protocol.mjs @@ -0,0 +1,65 @@ +/** + * Patch: fix endpoint URL resolution to use application.url (HTTPS) instead + * of getUrl(request) (HTTP) as the base URL for relative endpoint paths. + * + * Indiekit resolves relative endpoint paths (e.g. "/media") to absolute URLs + * using getUrl(request), which returns `http://` because Express sees HTTP + * connections from nginx (no trust proxy set). This results in + * `application.mediaEndpoint = "http://blog.giersig.eu/media"` being passed + * to the frontend, causing mixed-content failures in Safari ("Load failed") + * when the media browser tries to fetch that URL from an HTTPS page. + * + * Fix: prefer application.url (the configured HTTPS base URL) over + * getUrl(request) when resolving relative endpoint paths. + */ + +import { access, readFile, writeFile } from "node:fs/promises"; + +const candidates = [ + "node_modules/@indiekit/indiekit/lib/endpoints.js", +]; + +const oldCode = + ": new URL(application[endpoint], getUrl(request)).href;"; +const newCode = + ": new URL(application[endpoint], application.url || getUrl(request)).href;"; + +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(newCode)) { + continue; + } + + if (!source.includes(oldCode)) { + console.warn(`[postinstall] endpoint-urls-protocol: snippet not found in ${filePath} — skipping`); + continue; + } + + const updated = source.replace(oldCode, newCode); + await writeFile(filePath, updated, "utf8"); + patched += 1; +} + +if (checked === 0) { + console.log("[postinstall] No endpoints.js found"); +} else if (patched === 0) { + console.log("[postinstall] endpoint URL protocol patch already applied"); +} else { + console.log(`[postinstall] Patched endpoint URL protocol in ${patched} file(s)`); +} From 9126058df7c3ab1d489da113d3e262014296e4c1 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 10:53:11 +0100 Subject: [PATCH 57/59] fix(docs): INTERNAL_FETCH_URL must point to Indiekit directly, not nginx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nginx HTTP/80 returns 301 → HTTPS; pf has no hairpin NAT for jail traffic, so following the redirect causes UND_ERR_SOCKET on every internal POST. Correct value: http://10.100.0.20:3000 (direct to node jail). Co-Authored-By: Claude Sonnet 4.6 --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 524c1206..6273c364 100644 --- a/README.md +++ b/README.md @@ -289,7 +289,7 @@ Patches run alphabetically via `for patch in scripts/patch-*.mjs`. For webmentio | `WEBMENTION_SENDER_MOUNT_PATH` | `/webmention-sender` | Plugin mount path in Express | | `WEBMENTION_SENDER_TIMEOUT` | `10000` | Per-endpoint send timeout (ms) | | `WEBMENTION_SENDER_USER_AGENT` | `"Indiekit Webmention Sender"` | User-Agent for outgoing requests | -| `INTERNAL_FETCH_URL` | — | Internal nginx URL for self-fetches (e.g. `http://10.100.0.10`) | +| `INTERNAL_FETCH_URL` | — | Direct Indiekit URL for self-fetches (e.g. `http://10.100.0.20:3000`) | | `SECRET` | _(required)_ | JWT signing secret for poller authentication | ### Troubleshooting @@ -585,12 +585,14 @@ The node jail cannot reach the public HTTPS URL (`https://blog.giersig.eu`) beca - **Bluesky syndicator** — fetches photos for upload, OG metadata/images for link cards - **Micropub/syndicate** — self-fetches for token introspection, post updates -All of these use a shared `_toInternalUrl()` helper (injected by patch scripts) that rewrites the public base URL to `INTERNAL_FETCH_URL`. This should point to the nginx web jail's **HTTP** (port 80) listener, which serves both static files and proxies dynamic routes to Indiekit — without TLS. +All of these use a shared `_toInternalUrl()` helper (injected by patch scripts) that rewrites the public base URL to `INTERNAL_FETCH_URL`. This must point **directly to Indiekit** (node jail IP + port), not to nginx. ``` -INTERNAL_FETCH_URL=http://10.100.0.10 +INTERNAL_FETCH_URL=http://10.100.0.20:3000 ``` +**Why not nginx (`http://10.100.0.10`)?** nginx's HTTP/80 listener for `blog.giersig.eu` returns a `301` redirect to `https://`. Node's fetch follows the redirect to the public HTTPS URL, which the node jail cannot reach: pf's `rdr` rule only fires on the external interface (`vtnet0`), so there is no hairpin NAT for jail-originated traffic. The result is `UND_ERR_SOCKET: other side closed` on every internal POST (editing posts, syndication, token introspection). + ### nginx configuration (`/usr/local/etc/nginx/sites/blog.giersig.eu.conf`) The full vhost config lives in the web jail. Key design points: @@ -726,7 +728,7 @@ server { | Variable | Example | Purpose | |---|---|---| -| `INTERNAL_FETCH_URL` | `http://10.100.0.10` | nginx HTTP endpoint for self-fetches | +| `INTERNAL_FETCH_URL` | `http://10.100.0.20:3000` | Direct Indiekit endpoint for self-fetches (must bypass nginx — see Internal fetch URL) | | `INDIEKIT_BIND_HOST` | `10.100.0.20` | Jail IP (loopback unavailable in jails); used by webmention poller | | `PORT` | `3000` | Indiekit listen port (default 3000) | From 5d7789ead6fc82993185a43a6e16bbff6df5f1c5 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 16:47:57 +0100 Subject: [PATCH 58/59] fix(oauth): update lockfile pin to b54146c (echo OAuth state parameter) --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 17f910a8..374c5609 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,7 +2418,7 @@ }, "node_modules/@rmdes/indiekit-endpoint-activitypub": { "version": "3.10.0", - "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#9b6db9865cf618af261d6aee203ecae92193f1a0", + "resolved": "git+ssh://git@github.com/svemagie/indiekit-endpoint-activitypub.git#b54146ce5b1d1eed1003ca2d046e746e90381b43", "license": "MIT", "dependencies": { "@fedify/debugger": "^2.1.0", From e34decb59ef9d4e28b01b20398a83fa2d4905092 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 27 Mar 2026 16:55:54 +0100 Subject: [PATCH 59/59] docs: document inbox signature suppression and OAuth state fix --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6273c364..24813d4e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Four packages are installed directly from GitHub forks rather than the npm regis In `package.json` these use the `github:owner/repo[#branch]` syntax so npm fetches them directly from GitHub on install. -> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm install github:svemagie/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `69ae731` (upstream v3.9.x merged: Fedify 2.1.0, 5 FEPs — Tombstone/soft-delete, Activity Intents, indexable actor, NodeInfo enrichment, Collection Sync; security audit — XSS/CSRF/OAuth scope enforcement, rate limiting, token expiry, secret hashing; architecture refactor — syndicator.js, batch-broadcast.js, init-indexes.js, CSS split into 15 files; plus all fork patches: DM support, pin/unpin status, edit post, favourite/reblog timeout guard, raw signed fetch fallback, timezone-aware status lookup, own Micropub posts mirrored into ap_timeline; trust proxy rate-limit fix). +> **Lockfile caveat:** The fork dependency is resolved to a specific commit in `package-lock.json`. When fixes are pushed to the fork, run `npm install github:svemagie/indiekit-endpoint-activitypub` to pull the latest commit. The fork HEAD is at `b54146c` (upstream v3.9.x merged: Fedify 2.1.0, 5 FEPs — Tombstone/soft-delete, Activity Intents, indexable actor, NodeInfo enrichment, Collection Sync; security audit — XSS/CSRF/OAuth scope enforcement, rate limiting, token expiry, secret hashing; architecture refactor — syndicator.js, batch-broadcast.js, init-indexes.js, CSS split into 15 files; plus all fork patches: DM support, pin/unpin status, edit post, favourite/reblog timeout guard, raw signed fetch fallback, timezone-aware status lookup, own Micropub posts mirrored into ap_timeline, inbox HTTP Signature noise suppressed, OAuth `state` parameter echo fix). --- @@ -189,7 +189,10 @@ The patch replaces the broken date-from-URL regex with a simple last-path-segmen ### Troubleshooting **`ERR fedify·federation·inbox Failed to verify the request's HTTP Signatures`** -The body buffering patch must preserve raw bytes in `req._rawBody`. If `JSON.stringify(req.body)` is used instead, the Digest header won't match. Check that `patch-inbox-skip-view-activity-parse` applied correctly. +This message is expected at low volume (deleted actors, migrated servers with gone keys) and is suppressed to `fatal` level via a dedicated LogTape logger for `["fedify", "federation", "inbox"]` in `federation-setup.js` (`9b6db98`). If you see it flooding logs, check that the LogTape configuration applied. The body buffering patch must also preserve raw bytes in `req._rawBody` — if `JSON.stringify(req.body)` is used instead, the Digest header won't match. + +**Mastodon client OAuth fails with "OAuth callback failed. Missing parameters."** +The OAuth 2.0 spec requires the server to echo the `state` parameter back in the authorization redirect. Mastodon clients (e.g. murmel.social) send a random `state` value for CSRF protection and fail if it is absent from the callback. Fixed in `b54146c`: `state` is now threaded through GET query → session store (surviving the IndieAuth login redirect) → hidden form field → POST body → callback URL (both approve and deny paths). **Activities appear in outbox but Mastodon doesn't receive them** 1. Check Redis connectivity: `redis-cli -h 10.100.0.20 ping`