Patch releases only — no API changes, no patch script updates needed.
2.1.2: fixes CJS build issue with @fedify/fedify/vocab Object export (ESM-only, no runtime impact here)
2.1.3: CLI tooling packaging only (@fedify/init, @fedify/create — not used)
All existing AP patches re-verified: apply cleanly against new install.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause of "Failed to verify the request's HTTP Signatures" errors:
patch-ap-federation-bridge-base-url fixed Fedify URL routing (using the
canonical publicationUrl to build the Request URL) but left the "host"
header in the Headers object untouched.
Fedify's HTTP Signature verifier reads request.headers.get("host") when
reconstructing the signed-string for Cavage-style signatures. If nginx
forwards an internal Host value (e.g. "10.100.0.20") instead of the public
hostname, the reconstructed string differs from what the remote server signed
→ every inbox POST fails with a cryptographic verification error → remote
servers receive 401, exhaust retries, and stop delivering.
Fix (patch-ap-signature-host-header):
After the header-copy loop in fromExpressRequest(), override "host" with
new URL(publicationUrl).host ("blog.giersig.eu") when publicationUrl is
provided. This ensures the signed-string Fedify reconstructs matches what
Mastodon/Pleroma/etc. signed, regardless of what nginx forwards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- patch-ap-inbox-delivery-debug: two fixes for diagnosing missing inbound
AP interactions (likes, boosts, replies not appearing in notifications)
Fix A (federation-setup.js): change ["fedify","federation","inbox"] log
category from lowestLevel "fatal" → "error" so HTTP Signature verification
failures are now visible in server logs instead of being silently swallowed.
The original "fatal" level was hiding real delivery rejections (401s) that
cause remote servers to stop retrying.
Fix B (federation-bridge.js): add a pre-signature-check console.info for
every inbox POST when AP_DEBUG=1 or AP_LOG_LEVEL=debug. Confirms whether
remote servers are reaching our inbox at all (nginx/routing check).
- memory/project_activitypub.md: document full inbound activity pipeline,
_publicationUrl dependency, body buffering, and how to use new diagnostics
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a post is deleted from the web backend (Micropub action=delete),
call each registered syndicator's delete() method so the post is also
removed from the Fediverse (AP Delete/Tombstone) and Bluesky
(com.atproto.repo.deleteRecord).
- patch-bluesky-syndicator-delete: adds Bluesky#deletePost(bskyUrl) to
lib/bluesky.js and BlueskySyndicator#delete(url, syndication) to
index.js; the bsky.app URL is resolved from the syndication array
that postData.delete() preserves in _deletedProperties
- patch-micropub-delete-propagation: patches action.js case "delete"
to iterate publication.syndicationTargets after postContent.delete()
and fire syndicator.delete() fire-and-forget for any syndicator that
exposes the method (errors logged, never break the 200 response)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
collections._publicationUrl was never set, so every pubUrl guard in
handleCreate/handleAnnounce evaluated to undefined:
- Reply notifications were never created (if pubUrl && ...) always false
- Boost notifications for our content never created
- Replies from non-followed accounts never stored in ap_timeline
Fix A: set collections._publicationUrl = publicationUrl before
registerInboxListeners() in federation-setup.js.
Fix B: in handleCreate, add an else-if branch that stores replies to
our own posts in ap_timeline even when the sender is not in ap_following,
so they appear in Mastodon client conversation/notification views.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- status.js: in_reply_to_id was always null (both branches of ternary
returned null — TODO left unfilled). Changed to item.inReplyToId || null.
- statuses.js POST handler: timeline insert now stores inReplyToId from
the in_reply_to_id cursor the client already sent, so own replies are
threaded correctly in Phanpy/Elk.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gitea Contents API DELETE commits fire on:push CI; POST/PUT do not.
delete was triggering both on:push and workflow_dispatch → 2 CI runs.
Now dispatch is skipped for delete; on:push handles the rebuild.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GitHub API infers JSON without Content-Type; Gitea requires it explicitly.
Without the header, Gitea cannot parse the POST/PUT body and returns 422
Unprocessable Entity on all content write operations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gitea's Contents API differs from GitHub's:
POST /contents/{path} = create new file (no SHA)
PUT /contents/{path} = update existing file (SHA required)
store-github used PUT for createFile() because GitHub accepts PUT for
both — Gitea's PUT without SHA returns 422. Also updates the
update-fallback patch to bail to createFile() instead of falling through
to PUT-without-SHA when the file doesn't exist in the store.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Posts that exist in MongoDB but not in Gitea (e.g. due to a previous
failed write) caused HTTP 500 on re-publish: updateFile() tried to read
the file's SHA, got 404, and threw instead of creating. Now detects
Not Found and falls through to a create-style PUT (no sha field).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gitea Contents API commits don't trigger on:push CI workflows.
Patches action.js to fire a workflow_dispatch to giersig.eu/indiekit-blog
after every create/update/delete/undelete so the Eleventy build runs
immediately after a post is published.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
npm fetches git deps before node_modules exist, so git URL rewriting
must happen in preinstall. Detects jail env via INDIEKIT_BIND_HOST /
INTERNAL_FETCH_URL — no-ops on local dev.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hardcoded user: githubUsername (svemagie) and repo: "blog" were wrong
for Gitea where the org is giersig.eu and the repo is indiekit-blog.
Now reads from GITEA_CONTENT_USER / GITEA_CONTENT_REPO env vars with
sensible defaults.
Set in .env on server:
GITEA_CONTENT_USER=giersig.eu
GITEA_CONTENT_REPO=indiekit-blog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Runner is on the internal network — connecting to the public domain
fails due to hairpin NAT, same as the syndication webhook.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- runs-on: freebsd (act_runner host label) instead of ubuntu-latest
- Drop appleboy/ssh-action; use plain ssh in a run step (same pattern as
indiekit-blog deploy.yml)
- Drop actions/setup-node; no build step on runner side
- On deploy: set git remote to internal Gitea URL, fetch, reset --hard
- npm ci --legacy-peer-deps (postinstall applies all patches automatically)
- .env and SECRET preflight checks; preflight-production-security and
preflight-mongo-connection before restart
- Async restart via nohup + poll loop (avoids SSH hanging on open stdout)
- add workflow_dispatch trigger
Required repo secrets: SSH_PRIVATE_KEY, SSH_USER, SSH_HOST
(copy values from giersig.eu/indiekit-blog repo secrets)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rewrote from scratch based on a full read of README.md.
Focus: actionable rules and pitfalls for the AI agent, not human overview.
Covers: patch pattern + rules, two-jail architecture, internal URL
rewriting, nginx/Fedify requirements, MongoDB collections, post-type
discovery table, three compose paths and their checkbox mechanics,
ap_timeline insertion timing, status ID format, JF2→AS2 mapping,
visibility mapping, fork update commands, debugging starting points
(12 symptoms), and full env var reference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs caused replies-to-replies to be posted as 'note' type without
ActivityPub federation:
1. patch-ap-compose-default-checked: The AP reader compose form had
defaultChecked hardcoded to '@rick@rmendes.net' (original dev's handle),
so the AP syndication checkbox was never pre-checked. Fixed to use
target.checked from the Micropub q=config response, which already
carries checked: true for the AP syndicator.
2. patch-ap-mastodon-reply-threading: POST /api/v1/statuses deferred
ap_timeline insertion until the Eleventy build webhook fired (30–120 s).
If the user replied to their own new post before the build finished,
findTimelineItemById returned null → inReplyTo = null → no in-reply-to
in JF2 → post-type-discovery returned 'note' → reply saved at /notes/
and sent without inReplyTo in the AP activity, breaking thread display
on remote servers. Fixed by eagerly inserting the provisional timeline
item immediately after postContent.create() ($setOnInsert — idempotent;
syndicator upsert later is a no-op).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
uploadMedia() had no content-type check, so an HTML login-redirect response
from an auth-protected internal endpoint was uploaded to Bluesky as a blob
with encoding "text/html". uploadBlob() accepts it, but record validation
rejects the post with 'Expected "image/*" (got "text/html")'.
The patch mirrors the guard already present in uploadImageFromUrl() and also
wraps per-photo uploads in try/catch so one bad photo doesn't abort the
entire syndication — other photos and the post text are still published.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without nginx forwarding Host/X-Forwarded-Proto headers, fromExpressRequest()
builds a wrong URL (e.g. http://127.0.0.1:3000/...) that Fedify doesn't
recognise as its own base URL — so it calls next() and requests fall through
to auth middleware, returning 302 to the login page. This breaks webfinger,
actor lookups, and AP inbox delivery.
The patch overrides the URL construction in createFedifyMiddleware() and
fromExpressRequest() to use the configured publicationUrl as the base,
bypassing the dependency on proxy headers entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/og/{slug}.png is only generated by Eleventy for replies, bookmarks, and
articles — not for photo post types. Photo posts now use properties.photo[0]
directly as the ActivityPub image field; all other post types keep the
/og/{slug}.png fallback. Patch handles all known file states (original
upstream, v1 patched, already v2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The OLD_SNIPPET had wrong indentation (8 spaces vs 6) and was missing the
method guard line added in the fork:
if (req.method !== "GET" && req.method !== "HEAD") return next();
Without this fix the patch silently skips patching and webfinger continues
to return 302 → 401 on fediverse delivery.
https://claude.ai/code/session_0124D41vdLYE3DkJxhPqYthX
The Gitea conflict resolution kept main's prom-client/metrics-shim/microsub
changes but dropped our two new AP patch registrations. Re-add them to both
postinstall and serve.
https://claude.ai/code/session_0124D41vdLYE3DkJxhPqYthX
- Add scripts/patch-ap-og-image.mjs to both postinstall and serve so OG
preview images are included in ActivityPub activities (flat URL slug
extraction instead of date-based regex).
- Add scripts/patch-ap-webfinger-before-auth.mjs (new file) to both
postinstall and serve. Extends contentNegotiationRoutes Fedify delegation
to also forward /.well-known/* paths, ensuring webfinger is served by
Fedify before indiekit auth middleware can issue a 302 redirect. Fixes
401 Unauthorized errors from remote fediverse instances.
https://claude.ai/code/session_0124D41vdLYE3DkJxhPqYthX
Preloads metrics-shim.cjs via `node --require` into the Indiekit process
so heap, GC, event loop lag, CPU and handle metrics are exposed at
:9209/metrics for Prometheus scraping. Uses prom-client collectDefaultMetrics.
- Add metrics-shim.cjs (prom-client HTTP server, port 9209)
- Add prom-client ^15.1.3 to dependencies
- Wire --require ./metrics-shim.cjs into start.example.sh and npm serve script
- Grafana: NodeJS Application Dashboard (11159) at console.giersig.eu
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>