diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 88f37261..394a39e3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,7 +24,7 @@ jobs: restart_log=/tmp/indiekit-restart.log # Update code as indiekit user; point remote at internal Gitea (no auth needed — public read). - doas bastille cmd node sh -lc 'su -l indiekit -c "cd /usr/local/indiekit && git remote set-url origin https://git.wildwuchs.work/giersig.eu/indiekit-server.git && git fetch origin && git reset --hard origin/main"' + doas bastille cmd node sh -lc 'su -l indiekit -c "cd /usr/local/indiekit && git remote set-url origin https://git.wildwuchs.work/svemagie.net/indiekit-server.git && git fetch origin && git reset --hard origin/main"' # Install dependencies (postinstall runs all patches automatically). doas bastille cmd node sh -lc 'su -l indiekit -c "cd /usr/local/indiekit && npm ci --legacy-peer-deps"' diff --git a/CLAUDE.md b/CLAUDE.md index c343832b..c7a179a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # CLAUDE.md — indiekit-blog -Personal [Indiekit](https://getindiekit.com/) deployment for [blog.giersig.eu](https://blog.giersig.eu). +Personal [Indiekit](https://getindiekit.com/) deployment for [svemagie.net](https://svemagie.net). ## Always read memory files first @@ -63,7 +63,7 @@ The node jail **cannot reach its own public HTTPS URL**. Internal self-fetches m ### nginx / Fedify -nginx must forward `Host: blog.giersig.eu` and `X-Forwarded-Proto: https` or AP lookups 302-redirect to the login page. See `patch-ap-federation-bridge-base-url`. +nginx must forward `Host: svemagie.net` and `X-Forwarded-Proto: https` or AP lookups 302-redirect to the login page. See `patch-ap-federation-bridge-base-url`. `createFederation()` requires `allowPrivateAddress: true` (blog resolves to a LAN IP) and `signatureTimeWindow: { hours: 12 }` (Mastodon retries with old signatures). @@ -182,7 +182,7 @@ npm install git+https://git.wildwuchs.work/svemagie/indiekit-endpoint-activitypu | Favourited/reblogged state wrong on account statuses timeline | `accounts.js` added `ix.objectUrl` to the Sets instead of `item.uid` — fixed by `patch-ap-interactions-accounts-uid` | | Liked posts show as not-liked in thread context (ancestors/descendants) | Context endpoint used empty interaction Sets — fixed by `patch-ap-interactions-context-state` | | "Empty reply from server" on webmention poller | Poller routing through nginx (returns 444 for wrong Host) — must use `INDIEKIT_DIRECT_URL` | -| HTTP Signature 401 errors on all inbound activities | nginx forwarding wrong `Host` header — fixed by `patch-ap-signature-host-header` (overrides to `blog.giersig.eu`) | +| HTTP Signature 401 errors on all inbound activities | nginx forwarding wrong `Host` header — fixed by `patch-ap-signature-host-header` (overrides to `svemagie.net`) | | HTTP Signature verify errors flooding logs for deleted/migrated actors | Expected noise — `patch-ap-inbox-delivery-debug` (in `patch-ap-federation-infra.mjs`) suppresses both `["fedify","federation","inbox"]` and `["fedify","runtime","docloader"]` to `lowestLevel: "fatal"`. Current marker: `ap-inbox-delivery-debug-A-fatal` | | Mastodon client (Phanpy, etc.) gets 401 on all authenticated endpoints ~10 min after login | OAuth access token inherited the auth code's 10-min `expiresAt` — fixed by `patch-ap-oauth-token-expiry-fix` (`$unset: { expiresAt }` during code exchange) | | Mastodon client gets 401 on all requests immediately (not just after 10 min) | "Autorisiertes Abrufen erfordern" (authorized fetch / secure mode) is enabled — unsigned GET requests to actor/collections are rejected. Error message "access token is invalid" is misleading; it comes from the authorized-fetch layer, not OAuth. Fix: disable authorized fetch in AP admin settings. Trade-off: blocked servers can still fetch public posts, but this is acceptable for a public blog. | @@ -229,16 +229,16 @@ npm install git+https://git.wildwuchs.work/svemagie/indiekit-endpoint-activitypu **`GITEA_BASE_URL`** must end with a trailing slash: `http://10.100.0.90:3000/api/v1/` Without it, `new URL(apiPath, baseUrl)` silently strips the `v1` segment → 404 on all writes. -**`GH_CONTENT_TOKEN`** — the Gitea PAT for `svemagie`. `start.sh` rejects startup if neither `GH_CONTENT_TOKEN` nor `GITHUB_TOKEN` is present. The token must have repo read/write scope on `giersig.eu/indiekit-blog`. +**`GH_CONTENT_TOKEN`** — the Gitea PAT for `svemagie`. `start.sh` rejects startup if neither `GH_CONTENT_TOKEN` nor `GITHUB_TOKEN` is present. The token must have repo read/write scope on `svemagie.net/indiekit-blog`. -**`GITEA_CONTENT_USER`** = `giersig.eu` (the org, not the personal username) +**`GITEA_CONTENT_USER` = `svemagie.net` (the org, not the personal username) **`GITEA_CONTENT_REPO`** = `indiekit-blog` --- ## Micropub → Gitea build dispatch -Gitea Contents API commits (what `store-github` does) do **not** trigger `on: push` CI workflows. `patch-micropub-gitea-dispatch-conditional.mjs` patches the Micropub endpoint to fire a `workflow_dispatch` event to `giersig.eu/indiekit-blog` after each create/update, so the blog rebuilds immediately after a post is published. +Gitea Contents API commits (what `store-github` does) do **not** trigger `on: push` CI workflows. `patch-micropub-gitea-dispatch-conditional.mjs` patches the Micropub endpoint to fire a `workflow_dispatch` event to `svemagie.net/indiekit-blog` after each create/update, so the blog rebuilds immediately after a post is published. --- @@ -251,7 +251,7 @@ python3 << 'PYEOF' import urllib.request, json, base64 TOKEN = "your-gitea-pat" -REPO = "giersig.eu/indiekit-blog" +REPO = "svemagie.net/indiekit-blog" PATH = ".github/workflows/deploy.yml" BASE = "http://10.100.0.90:3000/api/v1" diff --git a/README.md b/README.md index ff856e7e..575025ee 100644 --- a/README.md +++ b/README.md @@ -25,16 +25,16 @@ In `package.json` these use the `git+https://git.wildwuchs.work/svemagie/repo` s ## ActivityPub federation -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`. +The blog is a native ActivityPub actor (`@svemagie@svemagie.net`) 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 | Field | Value | |---|---| | Handle | `svemagie` (`AP_HANDLE` env var) | -| Actor URL | `https://blog.giersig.eu/activitypub/users/svemagie` | +| Actor URL | `https://svemagie.net/activitypub/users/svemagie` | | Actor type | `Person` | -| WebFinger | `acct:svemagie@blog.giersig.eu` | +| WebFinger | `acct:svemagie@svemagie.net` | | Migration alias | `https://troet.cafe/users/svemagie` (`AP_ALSO_KNOWN_AS`) | ### Key management @@ -89,7 +89,7 @@ createFederation({ ``` - **`signatureTimeWindow: { hours: 12 }`** — Mastodon retries failed deliveries with the original signature, which can be hours old. Without this, retries are rejected. -- **`allowPrivateAddress: true`** — blog.giersig.eu resolves to a private IP (10.100.0.10) on the home LAN. Without this, Fedify's SSRF guard blocks WebFinger and `lookupObject()` for own-site URLs, breaking federation. +- **`allowPrivateAddress: true`** — svemagie.net resolves to a private IP (10.100.0.10) on the home LAN. Without this, Fedify's SSRF guard blocks WebFinger and `lookupObject()` for own-site URLs, breaking federation. ### Inbox handling @@ -189,7 +189,7 @@ The patch replaces the broken date-from-URL regex with a simple last-path-segmen ### Troubleshooting **All inbound AP activities return 401 / remote servers stop delivering** -The root cause is usually the `host` header being forwarded as the nginx upstream IP instead of the canonical `blog.giersig.eu`. Fedify includes `host` in the signed-string for Cavage HTTP Signatures; if it doesn't match what the remote server signed, every inbox POST fails verification. Fixed by `patch-ap-signature-host-header`: overrides `"host"` with `new URL(publicationUrl).host` in `fromExpressRequest()` after copying headers from the Express request. +The root cause is usually the `host` header being forwarded as the nginx upstream IP instead of the canonical `svemagie.net`. Fedify includes `host` in the signed-string for Cavage HTTP Signatures; if it doesn't match what the remote server signed, every inbox POST fails verification. Fixed by `patch-ap-signature-host-header`: overrides `"host"` with `new URL(publicationUrl).host` in `fromExpressRequest()` after copying headers from the Express request. **`ERR fedify·federation·inbox Failed to verify the request's HTTP Signatures`** At low volume this is expected (deleted actors, migrated servers with stale keys). `patch-ap-inbox-delivery-debug` changes the log level for `["fedify","federation","inbox"]` from `"fatal"` to `"error"` so real delivery failures are visible. If you see it flooding, the most common cause is the `host` header mismatch above — check that `patch-ap-signature-host-header` is 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. @@ -344,7 +344,7 @@ Like posts are created as **drafts** (`post-status: draft` → `draft: true` in The YouTube Data API requires OAuth 2.0 (not just an API key) to access a user's liked videos. 1. Create an **OAuth 2.0 Client ID** (Web application) in [Google Cloud Console](https://console.cloud.google.com/apis/credentials) -2. Add authorized redirect URI: `https://blog.giersig.eu/youtube/likes/callback` +2. Add authorized redirect URI: `https://svemagie.net/youtube/likes/callback` 3. Ensure **YouTube Data API v3** is enabled for the project 4. Set environment variables: @@ -444,7 +444,7 @@ Two bugs in the Mastodon API delete route. Bug 1 (ReferenceError): the route use The LogTape logger for `["fedify","federation","inbox"]` was hardcoded to `"fatal"` in `federation-setup.js`, suppressing all inbox errors including genuine delivery failures. Fix: changes the log level to `"error"` so real failures are visible. Expected noise from deleted/migrated actors (whose keys no longer resolve) still floods at `"error"` but can be filtered at the log aggregator level. **`patch-ap-signature-host-header.mjs`** -`patch-ap-federation-bridge-base-url` fixed Fedify URL routing to use the canonical `publicationUrl`, but left the `host` header in the copied Headers object untouched. nginx forwards an internal host (e.g. `10.100.0.20`) which Fedify reads from `request.headers.get("host")` when reconstructing the signed-string for Cavage HTTP Signatures. Signed-string mismatch → every inbox POST returns 401 → remote servers exhaust retries and stop delivering. Fix: after the header-copy loop in `fromExpressRequest()`, overrides `"host"` with `new URL(publicationUrl).host` (`"blog.giersig.eu"`) when `publicationUrl` is provided. +`patch-ap-federation-bridge-base-url` fixed Fedify URL routing to use the canonical `publicationUrl`, but left the `host` header in the copied Headers object untouched. nginx forwards an internal host (e.g. `10.100.0.20`) which Fedify reads from `request.headers.get("host")` when reconstructing the signed-string for Cavage HTTP Signatures. Signed-string mismatch → every inbox POST returns 401 → remote servers exhaust retries and stop delivering. Fix: after the header-copy loop in `fromExpressRequest()`, overrides `"host"` with `new URL(publicationUrl).host` (`"svemagie.net"`) when `publicationUrl` is provided. ### Conversations @@ -510,7 +510,7 @@ Adds AI disclosure field UI (text level, code level, etc.) to the post creation/ Removes AI disclosure fields from the post form submission before saving, delegating persistence to the AI block sidecar system. **`patch-endpoint-posts-fetch-diagnostic.mjs`** -In the two-jail setup the node jail cannot reach `https://blog.giersig.eu` directly; self-referential fetches in `@indiekit/endpoint-posts` fail with `ECONNREFUSED`. Fix: rewrites self-referential fetch URLs to `http://localhost:` using `INTERNAL_FETCH_URL` (or an automatic fallback), and wraps the fetch in a try-catch that logs the URL and response status on failure to make networking problems easier to diagnose. +In the two-jail setup the node jail cannot reach `https://svemagie.net` directly; self-referential fetches in `@indiekit/endpoint-posts` fail with `ECONNREFUSED`. Fix: rewrites self-referential fetch URLs to `http://localhost:` using `INTERNAL_FETCH_URL` (or an automatic fallback), and wraps the fetch in a try-catch that logs the URL and response status on failure to make networking problems easier to diagnose. **`patch-endpoint-posts-uid-lookup.mjs`** Fixes post editing 404s by adding `uid`-based lookup to the micropub source query. Without this, posts older than the first 40 results could not be opened for editing. @@ -645,7 +645,7 @@ The production setup uses two FreeBSD jails managed by [Bastille](https://bastil ### Internal fetch URL -The node jail cannot reach the public HTTPS URL (`https://blog.giersig.eu`) because TLS terminates on the web jail. Several features need to fetch their own pages or static assets: +The node jail cannot reach the public HTTPS URL (`https://svemagie.net`) because TLS terminates on the web jail. Several features need to fetch their own pages or static assets: - **Webmention sender** — fetches live page HTML for link extraction - **Bluesky syndicator** — fetches photos for upload, OG metadata/images for link cards @@ -657,9 +657,9 @@ All of these use a shared `_toInternalUrl()` helper (injected by patch scripts) 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). +**Why not nginx (`http://10.100.0.10`)?** nginx's HTTP/80 listener for `svemagie.net` 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`) +### nginx configuration (`/usr/local/etc/nginx/sites/svemagie.net.conf`) The full vhost config lives in the web jail. Key design points: @@ -681,11 +681,11 @@ map $http_accept $is_activitypub { # Passes responses through unmodified — no error interception. server { listen 10.100.0.10:80; - server_name blog.giersig.eu; + server_name svemagie.net; # 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 Host svemagie.net; 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; @@ -698,27 +698,27 @@ server { } } -# ── 2. HTTP: giersig.eu + www → blog.giersig.eu ───────────────────────────── +# ── 2. HTTP: giersig.eu + www → svemagie.net ───────────────────────────── server { listen 80; server_name giersig.eu www.giersig.eu; - return 301 https://blog.giersig.eu$request_uri; + return 301 https://svemagie.net$request_uri; } -# ── 3. HTTP: blog.giersig.eu (ACME challenge + HTTPS redirect) ────────────── +# ── 3. HTTP: svemagie.net (ACME challenge + HTTPS redirect) ────────────── server { listen 80; - server_name blog.giersig.eu; + server_name svemagie.net; location /.well-known/acme-challenge/ { root /usr/local/www/letsencrypt; } location / { - return 301 https://blog.giersig.eu$request_uri; + return 301 https://svemagie.net$request_uri; } } -# ── 4. HTTPS: giersig.eu + www → blog.giersig.eu ──────────────────────────── +# ── 4. HTTPS: giersig.eu + www → svemagie.net ──────────────────────────── server { listen 443 ssl; server_name giersig.eu www.giersig.eu; @@ -726,16 +726,16 @@ server { 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; + return 301 https://svemagie.net$request_uri; } -# ── 5. HTTPS: blog.giersig.eu (main) ──────────────────────────────────────── +# ── 5. HTTPS: svemagie.net (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; + server_name svemagie.net; + ssl_certificate /usr/local/etc/letsencrypt/live/svemagie.net/fullchain.pem; + ssl_certificate_key /usr/local/etc/letsencrypt/live/svemagie.net/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; @@ -822,7 +822,7 @@ Environment variables are loaded from `.env` via `dotenv`. Copy `.env.example` t | Variable | Default | Purpose | |---|---|---| -| `PUBLICATION_URL` | `https://blog.giersig.eu` | Canonical blog URL | +| `PUBLICATION_URL` | `https://svemagie.net` | Canonical blog URL | | `INDIEKIT_URL` | same as `PUBLICATION_URL` | Application base URL | | `REDIS_URL` | — | Redis for AP message queue + KV store (production-required for persistence) | | `INTERNAL_FETCH_URL` | `http://localhost:PORT` | Direct Indiekit URL for self-fetches, bypassing nginx | @@ -1074,7 +1074,7 @@ Also bumped `@rmdes/indiekit-endpoint-posts` beta.25→beta.44 and removed `came **fix(webmention): livefetch evolution v3→v5** (`11d600058`, `7f9f02bc3`, `17b93b3a2`) Three successive fixes to the webmention sender livefetch patch, driven by split-DNS and jail networking constraints: -- **v3** (`11d600058`): Send `Host: blog.giersig.eu` on internal fetches so nginx routes to the correct vhost; add `fetchUrl` diagnostics and response body preview on h-entry check failure +- **v3** (`11d600058`): Send `Host: svemagie.net` on internal fetches so nginx routes to the correct vhost; add `fetchUrl` diagnostics and response body preview on h-entry check failure - **v4** (`7f9f02bc3`): Remove `INTERNAL_FETCH_URL` rewrite for live page fetches — post URLs require authentication on the internal nginx vhost (returns login page). Fetch from `postUrl` (public URL) directly. Add `WEBMENTION_LIVEFETCH_URL` as an opt-in override - **v5** (`17b93b3a2`): Replace live page fetch entirely with a synthetic h-entry HTML snippet built from `post.properties` stored in MongoDB (`in-reply-to`, `like-of`, `bookmark-of`, `repost-of`, `content.html`). No network fetch required — eliminates all split-DNS / auth reliability issues