From 529fa81cd02ec1fad72c1e0b85a665773010267d Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:10:16 +0100 Subject: [PATCH] Add webmention sender auto-poll for bare-metal --- .env.example | 18 ++++++++ README.md | 25 +++++++++++ start.example.sh | 109 +++++++++++++++++++++++++++++++++++++---------- 3 files changed, 129 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index dea081e5..c2153911 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,24 @@ WEBMENTION_SENDER_TIMEOUT=10000 # User-Agent used for target endpoint discovery and sends WEBMENTION_SENDER_USER_AGENT= +# Enable start script auto-send polling loop (1=enabled, 0=disabled) +WEBMENTION_SENDER_AUTO_POLL=1 + +# Polling interval in seconds +WEBMENTION_SENDER_POLL_INTERVAL=300 + +# Internal host/port used by polling loop +# Defaults in start script: host=127.0.0.1, port=$PORT (or 3000) +WEBMENTION_SENDER_HOST=127.0.0.1 +WEBMENTION_SENDER_PORT=3000 + +# Optional override for JWT me claim (defaults: PUBLICATION_URL -> SITE_URL) +WEBMENTION_SENDER_ORIGIN= + +# Optional full endpoint override (instead of host/port/mount path) +# Example: http://127.0.0.1:3000/webmention-sender +WEBMENTION_SENDER_ENDPOINT= + # Optional webmentions proxy endpoint settings # Default mount path in indiekit.config.mjs is /webmentions-api WEBMENTIONS_PROXY_MOUNT_PATH=/webmentions-api diff --git a/README.md b/README.md index a907df79..73a79767 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,13 @@ - `WEBMENTION_SENDER_MOUNT_PATH` (default `/webmention-sender`) - `WEBMENTION_SENDER_TIMEOUT` (default `10000`, endpoint discovery timeout in milliseconds) - `WEBMENTION_SENDER_USER_AGENT` (default `${SITE_NAME} Webmention Sender`) +- Startup polling loop variables (used by `start.example.sh`): +- `WEBMENTION_SENDER_AUTO_POLL` (default `1`, set `0` to disable) +- `WEBMENTION_SENDER_POLL_INTERVAL` (default `300`, seconds) +- `WEBMENTION_SENDER_HOST` (default `127.0.0.1`) +- `WEBMENTION_SENDER_PORT` (default `${PORT}` or `3000`) +- `WEBMENTION_SENDER_ORIGIN` (optional JWT `me` claim override, defaults `PUBLICATION_URL` -> `SITE_URL`) +- `WEBMENTION_SENDER_ENDPOINT` (optional full URL override) - `POST /webmention-sender` requires authentication (`update` scope) and sends pending webmentions for unpublished targets. ## Webmentions proxy @@ -139,6 +146,24 @@ - `start.sh` is intentionally ignored by Git (`.gitignore`) so server secrets are not committed. - Use `start.example.sh` as the tracked template and keep real credentials in environment variables (or `.env` on the server). - Startup scripts parse `.env` with the `dotenv` parser (not shell `source`), so values containing spaces are handled safely. +- `start.example.sh` includes an optional background webmention sender polling loop for bare-metal deployments (including FreeBSD jails). +- FreeBSD jail env example for auto-send polling: + +```sh +SITE_URL=https://blog.example.net +PORT=3000 + +WEBMENTION_SENDER_AUTO_POLL=1 +WEBMENTION_SENDER_POLL_INTERVAL=300 +WEBMENTION_SENDER_HOST=127.0.0.1 +WEBMENTION_SENDER_PORT=3000 +WEBMENTION_SENDER_MOUNT_PATH=/webmention-sender + +# Optional overrides +# WEBMENTION_SENDER_ORIGIN=https://blog.example.net +# WEBMENTION_SENDER_ENDPOINT=http://127.0.0.1:3000/webmention-sender +``` + - Startup scripts run preflight + patch helpers before boot (`scripts/preflight-production-security.mjs`, `scripts/preflight-mongo-connection.mjs`, `scripts/preflight-activitypub-rsa-key.mjs`, `scripts/preflight-activitypub-profile-urls.mjs`, `scripts/patch-lightningcss.mjs`, `scripts/patch-endpoint-media-scope.mjs`, `scripts/patch-endpoint-media-sharp-runtime.mjs`, `scripts/patch-frontend-sharp-runtime.mjs`, `scripts/patch-endpoint-files-upload-route.mjs`, `scripts/patch-endpoint-files-upload-locales.mjs`, `scripts/patch-endpoint-activitypub-locales.mjs`, `scripts/patch-endpoint-activitypub-docloader-loglevel.mjs`, `scripts/patch-endpoint-activitypub-private-url-docloader.mjs`, `scripts/patch-endpoint-activitypub-migrate-alias-clear.mjs`, `scripts/patch-endpoint-homepage-locales.mjs`, `scripts/patch-frontend-serviceworker-file.mjs`, `scripts/patch-conversations-collection-guards.mjs`, `scripts/patch-indiekit-routes-rate-limits.mjs`, `scripts/patch-indiekit-error-production-stack.mjs`, `scripts/patch-indieauth-devmode-guard.mjs`, `scripts/patch-listening-endpoint-runtime-guards.mjs`). - The production security preflight blocks startup on insecure auth/session configuration and catches empty-password bcrypt hashes. - One-time recovery mode is available with `INDIEKIT_ALLOW_PASSWORD_SETUP=1` to bootstrap/reset `PASSWORD_SECRET` when locked out. Remove this flag after setting a valid hash. diff --git a/start.example.sh b/start.example.sh index d8c73607..2ba8ff3b 100644 --- a/start.example.sh +++ b/start.example.sh @@ -2,11 +2,12 @@ set -eu cd /usr/local/indiekit +NODE_BIN="${NODE_BIN:-/usr/local/bin/node}" # Optional: load environment from local .env file # (dotenv syntax, supports spaces in values). if [ -f .env ]; then - eval "$(${NODE_BIN:-/usr/local/bin/node} -e ' + eval "$("${NODE_BIN}" -e ' const fs = require("node:fs"); const dotenv = require("dotenv"); const parsed = dotenv.parse(fs.readFileSync(".env")); @@ -40,34 +41,96 @@ export INDIEKIT_DEBUG="0" unset DEBUG # Verify production auth/session hardening before launching server. -/usr/local/bin/node scripts/preflight-production-security.mjs +"${NODE_BIN}" scripts/preflight-production-security.mjs # Verify MongoDB credentials/connectivity before launching server. -/usr/local/bin/node scripts/preflight-mongo-connection.mjs +"${NODE_BIN}" scripts/preflight-mongo-connection.mjs # Ensure ActivityPub has an RSA keypair for HTTP Signature delivery. -/usr/local/bin/node scripts/preflight-activitypub-rsa-key.mjs +"${NODE_BIN}" scripts/preflight-activitypub-rsa-key.mjs # Normalize ActivityPub profile URL fields (icon/image/aliases) in MongoDB. -/usr/local/bin/node scripts/preflight-activitypub-profile-urls.mjs +"${NODE_BIN}" scripts/preflight-activitypub-profile-urls.mjs # Ensure runtime dependency patches are applied even if node_modules already exists. -/usr/local/bin/node scripts/patch-lightningcss.mjs -/usr/local/bin/node scripts/patch-endpoint-media-scope.mjs -/usr/local/bin/node scripts/patch-endpoint-media-sharp-runtime.mjs -/usr/local/bin/node scripts/patch-frontend-sharp-runtime.mjs -/usr/local/bin/node scripts/patch-endpoint-files-upload-route.mjs -/usr/local/bin/node scripts/patch-endpoint-files-upload-locales.mjs -/usr/local/bin/node scripts/patch-endpoint-activitypub-locales.mjs -/usr/local/bin/node scripts/patch-endpoint-activitypub-docloader-loglevel.mjs -/usr/local/bin/node scripts/patch-endpoint-activitypub-private-url-docloader.mjs -/usr/local/bin/node scripts/patch-endpoint-activitypub-migrate-alias-clear.mjs -/usr/local/bin/node scripts/patch-endpoint-homepage-locales.mjs -/usr/local/bin/node scripts/patch-frontend-serviceworker-file.mjs -/usr/local/bin/node scripts/patch-conversations-collection-guards.mjs -/usr/local/bin/node scripts/patch-indiekit-routes-rate-limits.mjs -/usr/local/bin/node scripts/patch-indiekit-error-production-stack.mjs -/usr/local/bin/node scripts/patch-indieauth-devmode-guard.mjs -/usr/local/bin/node scripts/patch-listening-endpoint-runtime-guards.mjs +"${NODE_BIN}" scripts/patch-lightningcss.mjs +"${NODE_BIN}" scripts/patch-endpoint-media-scope.mjs +"${NODE_BIN}" scripts/patch-endpoint-media-sharp-runtime.mjs +"${NODE_BIN}" scripts/patch-frontend-sharp-runtime.mjs +"${NODE_BIN}" scripts/patch-endpoint-files-upload-route.mjs +"${NODE_BIN}" scripts/patch-endpoint-files-upload-locales.mjs +"${NODE_BIN}" scripts/patch-endpoint-activitypub-locales.mjs +"${NODE_BIN}" scripts/patch-endpoint-activitypub-docloader-loglevel.mjs +"${NODE_BIN}" scripts/patch-endpoint-activitypub-private-url-docloader.mjs +"${NODE_BIN}" scripts/patch-endpoint-activitypub-migrate-alias-clear.mjs +"${NODE_BIN}" scripts/patch-endpoint-homepage-locales.mjs +"${NODE_BIN}" scripts/patch-frontend-serviceworker-file.mjs +"${NODE_BIN}" scripts/patch-conversations-collection-guards.mjs +"${NODE_BIN}" scripts/patch-indiekit-routes-rate-limits.mjs +"${NODE_BIN}" scripts/patch-indiekit-error-production-stack.mjs +"${NODE_BIN}" scripts/patch-indieauth-devmode-guard.mjs +"${NODE_BIN}" scripts/patch-listening-endpoint-runtime-guards.mjs -exec /usr/local/bin/node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs +# Optional: poll the webmention sender endpoint in the background. +if [ "${WEBMENTION_SENDER_AUTO_POLL:-1}" = "1" ]; then + WEBMENTION_SENDER_HOST="${WEBMENTION_SENDER_HOST:-127.0.0.1}" + WEBMENTION_SENDER_PORT="${WEBMENTION_SENDER_PORT:-${PORT:-3000}}" + WEBMENTION_SENDER_PATH="${WEBMENTION_SENDER_MOUNT_PATH:-/webmention-sender}" + WEBMENTION_SENDER_ORIGIN="${WEBMENTION_SENDER_ORIGIN:-${PUBLICATION_URL:-${SITE_URL:-}}}" + WEBMENTION_SENDER_INTERVAL="${WEBMENTION_SENDER_POLL_INTERVAL:-300}" + + case "$WEBMENTION_SENDER_PATH" in + /*) ;; + *) WEBMENTION_SENDER_PATH="/$WEBMENTION_SENDER_PATH" ;; + esac + + case "$WEBMENTION_SENDER_INTERVAL" in + ''|*[!0-9]*) WEBMENTION_SENDER_INTERVAL=300 ;; + esac + + WEBMENTION_SENDER_ORIGIN="${WEBMENTION_SENDER_ORIGIN%/}" + + if ! command -v curl >/dev/null 2>&1; then + echo "[webmention] curl not found; skipping auto-send polling" >&2 + elif [ -z "$WEBMENTION_SENDER_ORIGIN" ]; then + echo "[webmention] SITE_URL/PUBLICATION_URL missing; skipping auto-send polling" >&2 + else + WEBMENTION_SENDER_ENDPOINT="${WEBMENTION_SENDER_ENDPOINT:-http://${WEBMENTION_SENDER_HOST}:${WEBMENTION_SENDER_PORT}${WEBMENTION_SENDER_PATH}}" + + ( + echo "[webmention] Starting auto-send polling every ${WEBMENTION_SENDER_INTERVAL}s (${WEBMENTION_SENDER_ENDPOINT})" + + while true; do + TOKEN="$({ + WEBMENTION_ORIGIN="$WEBMENTION_SENDER_ORIGIN" \ + WEBMENTION_SECRET="$SECRET" \ + "$NODE_BIN" -e ' + const jwt = require("jsonwebtoken"); + const me = process.env.WEBMENTION_ORIGIN; + const secret = process.env.WEBMENTION_SECRET; + if (!me || !secret) process.exit(1); + process.stdout.write( + jwt.sign({ me, scope: "update" }, secret, { expiresIn: "5m" }), + ); + ' 2>/dev/null; + } || true)" + + if [ -n "$TOKEN" ]; then + RESULT="$(curl -sS -X POST "${WEBMENTION_SENDER_ENDPOINT}?token=${TOKEN}" 2>&1 || true)" + + if [ -n "$RESULT" ]; then + echo "[webmention] $(date '+%Y-%m-%d %H:%M:%S') - $RESULT" + else + echo "[webmention] $(date '+%Y-%m-%d %H:%M:%S') - ok" + fi + else + echo "[webmention] $(date '+%Y-%m-%d %H:%M:%S') - token generation failed" + fi + + sleep "$WEBMENTION_SENDER_INTERVAL" + done + ) & + fi +fi + +exec "${NODE_BIN}" node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs