Commit Graph

382 Commits

Author SHA1 Message Date
Sven
55923be69c Fix AP inbox HTTP Signature verification failures by normalising host header
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m15s
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>
2026-04-01 16:40:26 +02:00
Sven
8b1b5d990a Add AP inbox diagnostics: surface signature errors and request logging
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m20s
- 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
Sven
e791c06b79 feat: propagate Micropub deletes to ActivityPub and Bluesky
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m13s
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>
2026-04-01 15:41:00 +02:00
Sven
63bc41ebb5 fix(activitypub): inbound replies/notifications broken — publicationUrl missing in inbox handlers
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m14s
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>
2026-04-01 15:28:24 +02:00
Sven
12ee80f4a4 fix(activitypub): populate in_reply_to_id in Mastodon status serializer
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m11s
- 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>
2026-04-01 15:01:55 +02:00
Sven
71a8b60d96 fix(activitypub) delete request - Added broadcastDelete to mastodonPluginOptions
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m13s
2026-04-01 14:46:56 +02:00
Sven
c0f847c96f fix(activitypub) dedup: query ap_activities for an existing outbound Create/Announce/Update
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m18s
2026-04-01 14:38:12 +02:00
Sven
2211b1d6f7 fix: activitypun fav/boost
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m14s
2026-04-01 14:27:45 +02:00
Sven
f28552a5b2 fix: activitypub -> time confusion created / published times
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m17s
2026-04-01 14:17:42 +02:00
Sven
720487edb3 fix: activitypub/mastodon posting reenabled
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m19s
2026-04-01 13:51:28 +02:00
Sven
bc661282c7 fix: del gitea dispatch
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 3m17s
2026-03-31 18:24:49 +02:00
Sven
5342cd1ff0 fix: skip workflow_dispatch for delete — Gitea DELETE commits trigger on:push
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m11s
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>
2026-03-31 17:19:34 +02:00
Sven
40ec8dbce3 fix: add Content-Type: application/json to store-github requests
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m15s
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>
2026-03-31 17:09:15 +02:00
Sven
302316c8d0 fix: use POST for file create in Gitea Contents API
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m14s
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>
2026-03-31 16:58:14 +02:00
Sven
323db7c3e2 chore: trigger server deploy
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m20s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 16:45:18 +02:00
Sven
53102a03b0 fix: fall back to create when updateFile gets 404 from Gitea
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m15s
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>
2026-03-31 16:37:53 +02:00
Sven
263e6e081a feat: dispatch Gitea workflow_dispatch after each Micropub action
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m15s
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>
2026-03-31 16:24:39 +02:00
373e0c4be8 docs: add Gitea store, dispatch, and server push technique from blog CLAUDE.md
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m16s
2026-03-31 16:12:45 +02:00
78fbd8de8a fix: wire GITEA_BASE_URL into store-github config 2026-03-31 16:06:43 +02:00
Sven
0bac69090d fix: rewrite gitea.giersig.eu to internal IP before npm install
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m15s
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>
2026-03-31 15:09:14 +02:00
Sven
2d5b713b7d chore: point svemagie fork deps and docs at Gitea
Some checks failed
Deploy Indiekit Server / deploy (push) Failing after 7s
- Switch 4 svemagie fork deps from github: shorthand to git+https://gitea.giersig.eu/svemagie/...
- Add patch-store-github-error-message.mjs to replace hardcoded github.com token URL with gitea.giersig.eu
- Update CLAUDE.md and README.md fork dependency docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 15:05:49 +02:00
Sven
18c1b3aca1 fix: read store-github user/repo from env for Gitea
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m14s
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>
2026-03-31 13:33:01 +02:00
Sven
3f63e84cd6 ci: use internal host IP 10.100.0.1 for SSH (hairpin NAT fix)
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m11s
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>
2026-03-31 13:21:41 +02:00
Sven
9cbf574b15 ci: use SSH_PORT secret instead of hardcoded port 222
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m16s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 13:20:07 +02:00
Sven
59ea628595 ci: replace GitHub Actions deploy with Gitea FreeBSD runner
Some checks failed
Deploy Indiekit Server / deploy (push) Failing after 3s
- 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>
2026-03-31 13:17:33 +02:00
Sven
6372a9f422 fix: startup and funkwhale listening 2026-03-31 13:01:53 +02:00
a55868be52 README.md aktualisiert 2026-03-31 10:21:46 +02:00
Sven
7ce1112c4e docs(readme): document all patch scripts, production deployment, and env vars
Adds documentation for 14 previously undocumented patch scripts:
- AP threading patches (federation-bridge-base-url, compose-default-checked,
  mastodon-reply-threading)
- Conversations Bluesky patches (cursor-fix, self-filter)
- Micropub session token fix
- Microsub compose draft guard and AP dispatch (fixed // comment prefix)
- Indiekit endpoint URLs protocol fix
- Webmention sender hentry syntax fix
- Endpoint posts fetch diagnostic

Adds Production deployment section covering:
- start.example.sh structure and webmention poller architecture
- FreeBSD rc.d service script (indiekit.rcd.example)
- Prometheus metrics shim (metrics-shim.cjs, port 9209)

Extends Setup section with required and key optional env var tables.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 09:05:16 +02:00
Sven
1628b47cc8 docs(claude): rewrite CLAUDE.md as agent working guide
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>
2026-03-30 08:55:31 +02:00
Sven
5e50d7aceb docs: add CLAUDE.md and memory files for AP threading context
CLAUDE.md covers patch authoring rules, post-type discovery, the two
reply compose paths, ap_timeline insertion timing, fork dependencies,
and common debugging entry points.

memory/ contains three files:
- project_activitypub.md — data flows, syndicator config, all AP patches
- project_architecture.md — FreeBSD jails, MongoDB collections, actor URLs
- feedback_patches.md — patch pattern, known fragile points, threading gotchas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 08:30:32 +02:00
Sven
97d99976ea fix(ap): fix reply threading — pre-check AP syndication and resolve in_reply_to_id immediately
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>
2026-03-30 08:12:43 +02:00
Sven
b4fc7ffb4f fix(bluesky): guard uploadMedia() against non-image HTTP responses
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>
2026-03-29 09:53:46 +02:00
Sven
4aa1554f3a fix(ap): patch federation-bridge to use publication URL as Fedify base URL
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>
2026-03-29 09:51:00 +02:00
Sven
bc76e25361 fix(ap): use photo attachment URL for OG image on photo posts (v2)
/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>
2026-03-29 09:05:37 +02:00
Sven
0886b4b01e Merge remote-tracking branch 'origin/claude/fix-activitypub-og-image-CrCGI' 2026-03-28 19:52:53 +01:00
Claude
f19f7e1741 fix(ap): correct webfinger patch snippet to match current fork (318720c)
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
2026-03-28 18:51:16 +00:00
svemagie
7dab6d6ed8 Merge pull request #3 from svemagie/claude/fix-activitypub-og-image-CrCGI
fix(ap): wire og-image and webfinger-before-auth patches into postins…
2026-03-28 19:40:53 +01:00
Claude
7c404a19ed fix(ap): add og-image and webfinger patches dropped during conflict resolution
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
2026-03-28 18:35:29 +00:00
svemagie
bf9af6ff30 Merge branch 'main' into claude/fix-activitypub-og-image-CrCGI 2026-03-28 19:30:46 +01:00
Claude
ebf17341ee fix(ap): wire og-image and webfinger-before-auth patches into postinstall/serve
- 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
2026-03-28 18:25:59 +00:00
Sven
fc77e72a97 chore: add prom-client to package-lock.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 16:12:51 +01:00
Sven
c9084f0586 feat: add Prometheus metrics shim for Indiekit process monitoring
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>
2026-03-28 16:06:21 +01:00
Sven
78f0b80dbd chore: update AP fork lockfile pin to 318720c (upstream timeline content synthesis) 2026-03-27 20:33:55 +01:00
svemagie
54d0e46ff8 Merge pull request #2 from svemagie/claude/fix-activitypub-og-image-CrCGI
Claude/fix activitypub og image cr cgi
2026-03-27 20:00:34 +01:00
svemagie
034e944c9d Merge branch 'main' into claude/fix-activitypub-og-image-CrCGI 2026-03-27 20:00:25 +01:00
Sven
e34decb59e docs: document inbox signature suppression and OAuth state fix 2026-03-27 16:55:54 +01:00
Sven
5d7789ead6 fix(oauth): update lockfile pin to b54146c (echo OAuth state parameter) 2026-03-27 16:47:57 +01:00
Sven
9126058df7 fix(docs): INTERNAL_FETCH_URL must point to Indiekit directly, not nginx
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>
2026-03-27 10:53:11 +01:00
Sven
0cc5187990 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 <noreply@anthropic.com>
2026-03-27 10:29:02 +01:00
Sven
128ed58e57 chore: update AP fork lockfile pin to 9b6db98 (suppress inbox signature noise) 2026-03-27 10:15:38 +01:00