From 77442ec83788d2974d3c35baa5b561b8b2380d6b Mon Sep 17 00:00:00 2001 From: Sven Date: Tue, 24 Mar 2026 20:46:14 +0100 Subject: [PATCH] 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",