Files
indiekit-server/memory/project_activitypub.md
Sven 8b1b5d990a
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m20s
Add AP inbox diagnostics: surface signature errors and request logging
- 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>
2026-04-01 16:32:06 +02:00

8.7 KiB
Raw Blame History

ActivityPub Federation — Architecture & Patch Chain

Key Files

File Role
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/controllers/compose.js AP reader compose form: GET builds the form with syndicationTargets, POST submits to Micropub
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/statuses.js Mastodon-compatible API: POST /api/v1/statuses creates posts via Micropub pipeline
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js AP syndicator: federates posts to followers, adds own posts to ap_timeline
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js Converts JF2 properties to ActivityPub Note/Create activity
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/storage/timeline.js addTimelineItem() — atomic upsert ($setOnInsert) to ap_timeline
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/entities/status.js Serializes ap_timeline documents to Mastodon Status JSON
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/helpers/pagination.js encodeCursor(date) / decodeCursor(id) — ms-since-epoch as status ID
node_modules/@indiekit/endpoint-micropub/lib/post-type-discovery.js getPostType() — returns "reply" if in-reply-to key present, else "note"
node_modules/@indiekit/endpoint-micropub/lib/post-data.js Micropub create pipeline: normalise → getPostType → save
node_modules/@rmdes/indiekit-endpoint-microsub/lib/controllers/reader.js Microsub reader compose: auto-selects syndication targets via detectProtocol()
node_modules/@rmdes/indiekit-endpoint-activitypub/views/activitypub-compose.njk AP reader compose template: uses target.defaultChecked for checkbox state
node_modules/@rmdes/indiekit-endpoint-activitypub/views/partials/ap-item-card.njk Timeline card: reply button passes item.url as replyTo query param

Data Flow: Reply via AP Reader UI

User clicks reply on timeline item
  → /activitypub/admin/reader/compose?replyTo=<url>
  → composeController() fetches syndicationTargets from Micropub q=config
  → sets target.defaultChecked = target.checked === true  [patch: ap-compose-default-checked]
  → renders activitypub-compose.njk with hidden in-reply-to field
  → user submits form
  → submitComposeController() POSTs to Micropub with in-reply-to
  → Micropub: formEncodedToJf2 → normaliseProperties → getPostType("reply") → save to /replies/{slug}/
  → syndication webhook fires → AP syndicator.syndicate(properties)
  → jf2ToAS2Activity creates Note with inReplyTo
  → delivered to followers + original author's inbox

Data Flow: Reply via Mastodon Client API (Phanpy/Elk)

Client sends POST /api/v1/statuses { status, in_reply_to_id }
  → findTimelineItemById(ap_timeline, in_reply_to_id)
    → decodes cursor (ms-since-epoch) → looks up by published date
  → inReplyTo = replyItem.uid || replyItem.url
  → jf2["in-reply-to"] = inReplyTo  (if resolved)
  → jf2["mp-syndicate-to"] = [publicationUrl]  (always set — AP-only)
  → postData.create → postContent.create
  → addTimelineItem immediately [patch: ap-mastodon-reply-threading]
  → returns minimal status JSON to client
  → build webhook → syndicator → AP delivery

Own Post in ap_timeline

The AP syndicator stores outbound posts with:

  • uid = url = properties.url (blog URL, e.g. https://blog.giersig.eu/replies/slug/)
  • published = properties.published (ISO 8601 with TZ offset, e.g. "2026-03-21T16:33:50+01:00")
  • inReplyTo = properties["in-reply-to"] (original Mastodon URL or own blog URL)

The Mastodon API encodes status IDs as encodeCursor(published) = ms-since-epoch string. findTimelineItemById uses a ±1 s range query with $dateFromString to handle TZ-offset strings.

Syndication Target Config

The AP syndicator's info object:

{
  checked: true,                        // from indiekit.config.mjs: checked: true
  name: `@${handle}@${hostname}`,       // e.g. "@svemagie@blog.giersig.eu"
  uid: publicationUrl,                  // e.g. "https://blog.giersig.eu/"
  service: { name: "ActivityPub (Fediverse)", ... }
}

The Micropub q=config endpoint returns this with checked: true. Both compose forms read this value:

  • Microsub reader: uses target.checked directly (always pre-checked if checked: true in config)
  • AP reader: uses target.defaultChecked set by composeController() — was broken, now fixed

Patches Applied (AP threading)

patch-ap-compose-default-checked

File: lib/controllers/compose.js Problem: target.defaultChecked was hardcoded to name === "@rick@rmendes.net" — never matches. Fix: target.defaultChecked = target.checked === true Effect: AP syndication checkbox is pre-checked in the AP reader compose form, matching the checked: true config — replies via the AP reader UI are now federated.

patch-ap-mastodon-reply-threading

File: lib/mastodon/routes/statuses.js Problem: After POST /api/v1/statuses, own post was NOT inserted into ap_timeline until the Eleventy build webhook fired (30120 s later). Follow-up replies during that window would fail findTimelineItemByIdinReplyTo = null → no in-reply-to in JF2 → getPostType returned "note" → reply saved at /notes/ with no inReplyTo in the AP activity. Fix: Eagerly insert a provisional timeline item via addTimelineItem() immediately after postContent.create(). Uses $setOnInsert (idempotent); syndicator's later upsert is a no-op. Effect: in_reply_to_id can be resolved immediately → correct "reply" post type → proper inReplyTo in the AP Note → thread displays correctly on Mastodon.

Post Type Discovery

getPostType(postTypes, properties) in post-type-discovery.js:

  • Checks propertiesMap.has("in-reply-to")"reply"
  • Checks propertiesMap.has("like-of")"like"
  • Checks propertiesMap.has("repost-of")"repost"
  • Falls through to "note" if none match

The "reply" post type in indiekit.config.mjs has no discovery field — standard PTD spec applies.

Inbound AP Activity Pipeline

Activities from remote servers follow this path:

Remote server → nginx → Express (body buffered in createFedifyMiddleware)
  → Fedify signature check (uses req._rawBody for digest)
  → Fedify Redis message queue (if Redis configured)
  → Fedify queue worker → inbox listener (inbox-listeners.js)
  → enqueueActivity() → ap_inbox_queue (MongoDB)
  → startInboxProcessor() (1s poll) → routeToHandler()
  → handleLike / handleAnnounce / handleCreate
  → addNotification() → ap_notifications

Critical: collections._publicationUrl is set in index.js (_publicationUrl: this._publicationUrl) AND by patch-ap-inbox-publication-url in federation-setup.js. Both set "https://blog.giersig.eu/".

Notification conditions gate on pubUrl && objectId.startsWith(pubUrl):

  • handleLike: only notifies for likes of our own content
  • handleAnnounce PATH 1: only notifies for boosts of our content
  • handleCreate: only notifies for replies to our posts (inReplyTo.startsWith(pubUrl))

Body buffering (createFedifyMiddleware): application/activity+json bodies are buffered into req._rawBody before express.json() (which only handles application/json) touches them. fromExpressRequest passes req._rawBody verbatim to the Fedify Request object so the HTTP Signature Digest check passes.

Fedify inbox log suppression: ["fedify","federation","inbox"] was hardcoded to "fatal" (patch-ap-inbox-delivery-debug fixes this to "error" so real failures are visible).

Diagnosing inbox delivery issues:

  • Set AP_DEBUG=1 → logs [AP-inbox] POST /activitypub/users/svemagie/inbox ct=... body=...B BEFORE Fedify's signature check. If this doesn't appear, activities aren't reaching our server.
  • With inbox log level now "error": signature failures show as Fedify error logs.
  • Queue processing failures: [inbox-queue] Failed processing ... — always logged.

detectProtocol() in Microsub Reader

detectProtocol(url) in reader.js classifies URLs for syndication auto-selection:

  • "atmosphere" — bsky.app / bluesky
  • "fediverse" — mastodon., mstdn., fosstodon., troet., social., misskey., pixelfed., hachyderm., infosec.exchange, chaos.social (extended by patch-microsub-reader-ap-dispatch)
  • "web" — everything else, including own blog URLs

Own blog URLs return "web", so auto-selection doesn't trigger for reply-to-own-reply in the microsub reader. This is harmless because checked: true in the config already pre-checks the AP target in the microsub reader's target.checked field.