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
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.
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).
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.
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.
## 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)
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
The Audiobookshelf phone app could not sign in over the public name. Frank's phone reported:
and it was not the app, and nothing inside Audiobookshelf was broken.
Root cause (measured, not inferred)
:8000—/ping,/healthcheck,/socket.io/{"success":true},OK, a real Engine.IO400 Transport unknown— the app's three machine endpoints are healthyhttps://audiobooks.franksdank.cloud/audiobookshelf/ping,/healthcheck,/socket.io/with no browser session200+ "Making sure you're not a bot!" HTML (Anubis v1.27.0 challenge)SocketAuthority.js:361emitsinit;Server.js:390builds that payload withisInit: Database.hasRootUser— the error is the app'sinitpayload check receiving HTML/pingreturned200 text/html— the router forwards 443 and that 200 is the challenge pageThe 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*arrstack:exempt by construction.
The change — four artifacts, and the order matters
dankclaw/Caddyfile— the public site block dials192.168.0.235:8000directly, keepingthe
header_up X-Forwarded-Proto httpspin that ABS's same-origin OIDC check depends on, andthe
@audiobookshandler is removed from thehttp://:8017post-auth listener so one host hasone owner. Root
/redirects onto/audiobookshelf/as before.14-guard.spec.tsdropsaudiobooksfromGUARDED(and its leak marker) and gainsthe exemption assertion in
plex's style: an anonymous raw client gets the app's{"success":true}on/ping, a machine-format socket handshake (nevertext/html, never thechallenge marker), and the app's own 401 on
/audiobookshelf/api/libraries; an anonymousbrowser gets the app's login screen with Sign in with Authentik and zero IdP gate form.
tests/anubis.batspins the route shape (neveranubis:8923, no stale@audiobookshandle).scripts/authentik-audiobookshelf-sso.sh— themobile-redirectURI is now registered forevery origin in
BASES, not the LAN origin only. The app sends the mobile URI belonging tothe 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.
AGENTS.mdrecords the three exposure postures and the browser-only law;Infrastructure/Caddy.mdgains Unguard a vhost (route first, IdP rows last, audit listsafter); 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-proxywhile traffic still enters the:8016auth listener leaves the outpost matchingzero 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 arenot in this diff.
Accepted exposure, stated plainly
/audiobookshelf/api/loginbecomes internet-visible with no rate limiter on either side: thisCaddy build (v2.11.7) carries no
rate_limitmodule, and ABS 2.37.2 ships no login lockout — bothmeasured, not remembered. That is the identical exposure
jelly.,plex.and the*arrloginsalready live with, and the mitigation is credential hygiene: long unique passwords per listener.
Unauthenticated
/api/*answers401, and/ping+/healthcheckleak only{"success":true}and
OK.Verification
./run.sh --project gueston this branch against the liveguarded 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../tests/run.sh→ 824 tests, ALL TESTS PASSED on the code commits. Thedocs 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, andtests/docs_completeness.batsis green after it (index row regenerated, OKF conformance and link integrity intact). A final
whole-suite run on the tip is appended below.
/ping, the socket handshake and anunauthenticated
/api/*, plus the full Playwright gate thatdeploy.shruns.Known drift I did not chase
My Caddyfile edit shifts line numbers, so
Caddyfile:N-Mcitations on four unrelated pages(
Network Architecture,Hermes-WebUI,Marreta,SearXNG) are off by +3/+12/-6. None points atdeleted 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)