feat(caddy): Audiobookshelf outside the guard chain, so the phone app works from anywhere #133

Merged
dank merged 4 commits from feat/audiobookshelf-public-app-access into main 2026-11-03 01:56:51 -08:00
Owner

Problem

The Audiobookshelf phone app could not sign in over the public name. Frank's phone reported:

Failed to contact server. (TypeError: e.data is not an Object. (evaluating '"isInit" in e.data')

and it was not the app, and nothing inside Audiobookshelf was broken.

Root cause (measured, not inferred)

Probe Result
LAN door :8000 — /ping, /healthcheck, /socket.io/ {"success":true}, OK, a real Engine.IO 400 Transport unknown — the app's three machine endpoints are healthy
https://audiobooks.franksdank.cloud/audiobookshelf/ping, /healthcheck, /socket.io/ with no browser session all 200 + "Making sure you're not a bot!" HTML (Anubis v1.27.0 challenge)
ABS server code SocketAuthority.js:361 emits init; Server.js:390 builds that payload with isInit: Database.hasRootUser — the error is the app's init payload check receiving HTML
ABS request log (last 40 min at diagnosis) only deploy-gate traffic; the app's requests never reached the container
Inbound 443 from the internet a datacenter-side fetch of /ping returned 200 text/html — the router forwards 443 and that 200 is the challenge page

The guard chain is a browser-only device: Anubis demands a JavaScript proof-of-work and the
authentik embedded outpost works from a session cookie. A native app solves neither, so it is
answered with HTML wherever it expects JSON or a socket payload. Podcast RSS feeds and OPDS
readers die the same death, and the outpost would have intercepted the app's API calls even
with Anubis removed — so no path-scoped exemption inside the chain can serve a native client.

Audiobookshelf authenticates itself (local accounts plus authentik OIDC — authActiveAuthMethods = ["local","openid"]), which puts it in the same category as Jellyfin, Plex and the *arr stack:
exempt by construction.

The change — four artifacts, and the order matters

  1. dankclaw/Caddyfile — the public site block dials 192.168.0.235:8000 directly, keeping
    the header_up X-Forwarded-Proto https pin that ABS's same-origin OIDC check depends on, and
    the @audiobooks handler is removed from the http://:8017 post-auth listener so one host has
    one owner. Root / redirects onto /audiobookshelf/ as before.
  2. Tests — 14-guard.spec.ts drops audiobooks from GUARDED (and its leak marker) and gains
    the exemption assertion in plex's style: an anonymous raw client gets the app's
    {"success":true} on /ping, a machine-format socket handshake (never text/html, never the
    challenge marker), and the app's own 401 on /audiobookshelf/api/libraries; an anonymous
    browser gets the app's login screen with Sign in with Authentik and zero IdP gate form.
    tests/anubis.bats pins the route shape (never anubis:8923, no stale @audiobooks handle).
  3. scripts/authentik-audiobookshelf-sso.sh — the mobile-redirect URI is now registered for
    every origin in BASES, not the LAN origin only. The app sends the mobile URI belonging to
    the door it was configured with; STRICT matching answers an unregistered one with "Redirect URI
    Error", which would have locked phones out at the last step of a login that looked healthy.
  4. Docs — AGENTS.md records the three exposure postures and the browser-only law;
    Infrastructure/Caddy.md gains Unguard a vhost (route first, IdP rows last, audit lists
    after); the Audiobookshelf page retires its two-doors/VPN law for one address every client
    carries, adds how a listener gets an account, and says plainly what the exemption costs.

Deploy order: this Caddy change first, then the IdP rows. Deleting proxy provider
audiobooks-proxy while traffic still enters the :8016 auth listener leaves the outpost matching
zero providers and serves authentik's error page to every browser. The IdP deletion and the outpost
re-claim are run by hand immediately after this deploys (procedure now in Caddy.md), so they are
not in this diff.

Accepted exposure, stated plainly

/audiobookshelf/api/login becomes internet-visible with no rate limiter on either side: this
Caddy build (v2.11.7) carries no rate_limit module, and ABS 2.37.2 ships no login lockout — both
measured, not remembered. That is the identical exposure jelly., plex. and the *arr logins
already live with, and the mitigation is credential hygiene: long unique passwords per listener.
Unauthenticated /api/* answers 401, and /ping + /healthcheck leak only {"success":true}
and OK.

Verification

  • Red before the route flips: ./run.sh --project guest on this branch against the live
    guarded fleet → 2 failed, 14 passed, and the failure body is the Anubis challenge page
    (This website is running Anubis version v1.27.0) where the app's bytes belong.
  • Static suite: ./tests/run.sh → 824 tests, ALL TESTS PASSED on the code commits. The
    docs seam then failed its own rule once — the generated vault index still carried the page's old
    description — fixed by re-running scripts/gen-vault-index.py, and tests/docs_completeness.bats
    is green after it (index row regenerated, OKF conformance and link integrity intact). A final
    whole-suite run on the tip is appended below.
  • After merge and deploy: LAN + datacenter-side probes of /ping, the socket handshake and an
    unauthenticated /api/*, plus the full Playwright gate that deploy.sh runs.

Known drift I did not chase

My Caddyfile edit shifts line numbers, so Caddyfile:N-M citations on four unrelated pages
(Network Architecture, Hermes-WebUI, Marreta, SearXNG) are off by +3/+12/-6. None points at
deleted content. Say the word and I will convert those citations to symbol anchors instead of line
numbers, which is the fix that stops recurring.


cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac993669f8bcc (build worktree /tmp/wt-absopen)

## Problem The Audiobookshelf phone app could not sign in over the public name. Frank's phone reported: > Failed to contact server. (TypeError: e.data is not an Object. (evaluating '"isInit" in e.data') and it was not the app, and nothing inside Audiobookshelf was broken. ## Root cause (measured, not inferred) | Probe | Result | |---|---| | LAN door `:8000` — `/ping`, `/healthcheck`, `/socket.io/` | `{"success":true}`, `OK`, a real Engine.IO `400 Transport unknown` — the app's three machine endpoints are healthy | | `https://audiobooks.franksdank.cloud/audiobookshelf/ping`, `/healthcheck`, `/socket.io/` with no browser session | all `200` + **"Making sure you're not a bot!"** HTML (Anubis v1.27.0 challenge) | | ABS server code | `SocketAuthority.js:361` emits `init`; `Server.js:390` builds that payload with `isInit: Database.hasRootUser` — the error is the app's **`init` payload check** receiving HTML | | ABS request log (last 40 min at diagnosis) | only deploy-gate traffic; **the app's requests never reached the container** | | Inbound 443 from the internet | a datacenter-side fetch of `/ping` returned `200 text/html` — the router forwards 443 and that 200 **is** the challenge page | The guard chain is a browser-only device: Anubis demands a JavaScript proof-of-work and the authentik embedded outpost works from a session cookie. A native app solves neither, so it is answered with HTML wherever it expects JSON or a socket payload. Podcast RSS feeds and OPDS readers die the same death, and the outpost would have intercepted the app's API calls even with Anubis removed — so no path-scoped exemption inside the chain can serve a native client. Audiobookshelf authenticates itself (local accounts plus authentik OIDC — `authActiveAuthMethods = ["local","openid"]`), which puts it in the same category as Jellyfin, Plex and the `*arr` stack: **exempt by construction**. ## The change — four artifacts, and the order matters 1. **`dankclaw/Caddyfile`** — the public site block dials `192.168.0.235:8000` directly, keeping the `header_up X-Forwarded-Proto https` pin that ABS's same-origin OIDC check depends on, and the `@audiobooks` handler is removed from the `http://:8017` post-auth listener so one host has one owner. Root `/` redirects onto `/audiobookshelf/` as before. 2. **Tests** — `14-guard.spec.ts` drops `audiobooks` from `GUARDED` (and its leak marker) and gains the exemption assertion in `plex`'s style: an anonymous raw client gets the app's `{"success":true}` on `/ping`, a machine-format socket handshake (never `text/html`, never the challenge marker), and the app's **own 401** on `/audiobookshelf/api/libraries`; an anonymous browser gets the app's login screen with **Sign in with Authentik** and zero IdP gate form. `tests/anubis.bats` pins the route shape (never `anubis:8923`, no stale `@audiobooks` handle). 3. **`scripts/authentik-audiobookshelf-sso.sh`** — the `mobile-redirect` URI is now registered for **every** origin in `BASES`, not the LAN origin only. The app sends the mobile URI belonging to the door it was configured with; STRICT matching answers an unregistered one with "Redirect URI Error", which would have locked phones out at the last step of a login that looked healthy. 4. **Docs** — `AGENTS.md` records the three exposure postures and the browser-only law; `Infrastructure/Caddy.md` gains **Unguard a vhost** (route first, IdP rows last, audit lists after); the Audiobookshelf page retires its two-doors/VPN law for one address every client carries, adds how a listener gets an account, and says plainly what the exemption costs. **Deploy order:** this Caddy change first, then the IdP rows. Deleting proxy provider `audiobooks-proxy` while traffic still enters the `:8016` auth listener leaves the outpost matching zero providers and serves authentik's error page to every browser. The IdP deletion and the outpost re-claim are run by hand immediately after this deploys (procedure now in `Caddy.md`), so they are not in this diff. ## Accepted exposure, stated plainly `/audiobookshelf/api/login` becomes internet-visible with **no rate limiter on either side**: this Caddy build (v2.11.7) carries no `rate_limit` module, and ABS 2.37.2 ships no login lockout — both measured, not remembered. That is the identical exposure `jelly.`, `plex.` and the `*arr` logins already live with, and the mitigation is credential hygiene: long unique passwords per listener. Unauthenticated `/api/*` answers `401`, and `/ping` + `/healthcheck` leak only `{"success":true}` and `OK`. ## Verification - **Red before the route flips:** `./run.sh --project guest` on this branch against the live guarded fleet → **2 failed, 14 passed**, and the failure body is the Anubis challenge page (`This website is running Anubis version v1.27.0`) where the app's bytes belong. - **Static suite:** `./tests/run.sh` → **824 tests, ALL TESTS PASSED** on the code commits. The docs seam then failed its own rule once — the generated vault index still carried the page's old description — fixed by re-running `scripts/gen-vault-index.py`, and `tests/docs_completeness.bats` is green after it (index row regenerated, OKF conformance and link integrity intact). A final whole-suite run on the tip is appended below. - After merge and deploy: LAN + datacenter-side probes of `/ping`, the socket handshake and an unauthenticated `/api/*`, plus the full Playwright gate that `deploy.sh` runs. ## Known drift I did not chase My Caddyfile edit shifts line numbers, so `Caddyfile:N-M` citations on four unrelated pages (`Network Architecture`, `Hermes-WebUI`, `Marreta`, `SearXNG`) are off by +3/+12/-6. None points at deleted content. Say the word and I will convert those citations to symbol anchors instead of line numbers, which is the fix that stops recurring. --- cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac993669f8bcc (build worktree /tmp/wt-absopen)
dank added 4 commits 2026-11-03 01:55:06 -08:00
The Audiobookshelf phone app could never sign in over the public name. The guard
chain answered its /ping and /socket.io/ handshake with Anubis challenge HTML, so
the app evaluated 'isInit' in e.data on a non-object and reported "Failed to
contact server" while the container's request log recorded nothing at all. Anubis
demands a JavaScript proof-of-work and the authentik outpost works from a session
cookie: the chain is browser-only by construction, and a native client, a podcast
RSS reader or an OPDS client cannot pass it.

Audiobookshelf authenticates itself (local accounts plus authentik OIDC), so it
joins jelly/plex/*arr in the exempt set: the public site block dials the container
directly, keeping the X-Forwarded-Proto pin its OIDC origin check depends on, and
the @audiobooks route leaves the post-auth listener so one host has one owner. The
IdP rows that guarded the name go after this deploys, never before.

tests/e2e/specs/14-guard.spec.ts loses audiobooks as a guarded target and gains the
exemption assertion (app bytes to an anonymous client, the app's own 401, never
challenge HTML); tests/anubis.bats pins the route shape.
The mobile-redirect URI was registered on the LAN origin only, because the phone
had to be configured with the LAN address while the vhost was guarded. Now that the
app rides the public name it sends the public mobile-redirect URI, and the RP's
STRICT matching answers an unregistered one with "Redirect URI Error" — a phone
locked out at the last step of a login that looked healthy. Both callback URIs are
now emitted for every entry in BASES.
AGENTS.md states the three exposure postures and why the guard chain cannot front a
non-browser client. Infrastructure/Caddy.md gains *Unguard a vhost* — route first,
IdP rows last, audit lists after — since deleting the proxy provider while traffic
still enters :8016 hands browsers authentik's error page. The Audiobookshelf page
retires its two-doors law (the guarded vhost as a browser-only door, remote
listening via VPN) for one address every client carries, adds how a listener gets an
account, and says plainly what the exemption costs.
dank merged commit 3a6f7c7280 into main 2026-11-03 01:56:51 -08:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dank/serverAppConfigs#133