fix(audiobookshelf): serve under /audiobookshelf, send the OIDC redirect subfolder #128

Merged
dank merged 5 commits from fix/abs-base-path-sso into main 2026-11-02 20:02:58 -08:00
Owner

What was broken

Two faults, both invisible to every gate we had.

The web UI could never boot. A fresh Audiobookshelf answers /healthcheck 200 and serves its API perfectly, but index.html inside the pinned image is built with routerBasePath:"/audiobookshelf" — so the SPA asks for its assets under that prefix whatever ROUTER_BASE_PATH says. Mounted at /, every script request fell through to the SPA fallback, came back text/html, and Chrome refused to execute it: the page sat at "Loading…". The health gate, the API and the phone app were all healthy, which is exactly how an API-only deploy check misses it.

The SSO round trip died at the IdP with authentik's Redirect URI Error. ABS builds redirect_uri as <origin><authOpenIDSubfolderForRedirectURLs>/auth/openid/callback, and that key is unset on a database created before OIDC was enabled — the 2.17.4 migration only fills a DB that already had OIDC on. The URL carried the literal string undefined.

What changes

File Change
dankserver/audiobookshelf/quadlet/audiobookshelf.container ROUTER_BASE_PATH=/audiobookshelf; header records why the value is not a free choice; /healthcheck measured to answer at the root under either base path, so the health gate is unchanged
scripts/deploy-remote.sh the handler reads the mounted base path out of the unit and passes it to the bootstrapper as ABS_URL — the number is never restated
scripts/audiobookshelf-init.sh base-path-aware default URL; sends authOpenIDSubfolderForRedirectURLs, derived from ABS_URL's path instead of becoming a second knob, and verifies it came back
scripts/authentik-audiobookshelf-sso.sh (new) the IdP half in git: provider, groups, the abs_groups scope mapping, the redirect URIs. That state lives in authentik's database and a blueprint re-import reverts it silently, so it needs a re-runnable door. HS256: no provider on the fleet carries a signing key, so the client secret is the signature key
tests/dankserver_quadlet.bats the base-path assertion moves from / to /audiobookshelf — the pinned value was the bug
tests/audiobookshelf_init.bats stub gained PATCH /api/auth-settings and base-path routing; two new seams: the subfolder that reaches the wire follows ABS_URL (prefixed and root arms), and the two scripts build the same redirect path
vault the base-path law, the SSO contract (which script owns which half), the phone app's new server address

Verification

  • ./tests/run.sh — 824 tests, all pass; the two new seams confirmed running by name.
  • Throwaway container (base /audiobookshelf, its own empty config, no shared state): asset …/_nuxt/c7f3e29.js → 200 application/javascript, /audiobookshelf/healthcheck → OK.
  • Provider side already applied to the live IdP (the script is idempotent): uris ['http://192.168.0.235:8000/audiobookshelf/auth/openid/callback', '…/audiobookshelf/auth/openid/mobile-redirect'], claim for akadmin → {'abs_groups': ['admin', 'user']}.
  • Pending on merge: deploy.sh dankserver --service audiobookshelf (base path + subfolder), then the browser round trip into ABS as admin, then the phone app against http://192.168.0.235:8000/audiobookshelf.

Rollback

Revert cfdd343..3138e78 and redeploy the service. The local frank account stays enabled in authActiveAuthMethods throughout, so the box is reachable either way.


cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb

## What was broken Two faults, both invisible to every gate we had. **The web UI could never boot.** A fresh Audiobookshelf answers `/healthcheck` 200 and serves its API perfectly, but `index.html` inside the pinned image is built with `routerBasePath:"/audiobookshelf"` — so the SPA asks for its assets under that prefix whatever `ROUTER_BASE_PATH` says. Mounted at `/`, every script request fell through to the SPA fallback, came back `text/html`, and Chrome refused to execute it: the page sat at "Loading…". The health gate, the API and the phone app were all healthy, which is exactly how an API-only deploy check misses it. **The SSO round trip died at the IdP** with authentik's *Redirect URI Error*. ABS builds `redirect_uri` as `<origin><authOpenIDSubfolderForRedirectURLs>/auth/openid/callback`, and that key is **unset** on a database created before OIDC was enabled — the 2.17.4 migration only fills a DB that *already* had OIDC on. The URL carried the literal string `undefined`. ## What changes | File | Change | |---|---| | `dankserver/audiobookshelf/quadlet/audiobookshelf.container` | `ROUTER_BASE_PATH=/audiobookshelf`; header records why the value is not a free choice; `/healthcheck` measured to answer at the root under **either** base path, so the health gate is unchanged | | `scripts/deploy-remote.sh` | the handler reads the mounted base path out of the unit and passes it to the bootstrapper as `ABS_URL` — the number is never restated | | `scripts/audiobookshelf-init.sh` | base-path-aware default URL; sends `authOpenIDSubfolderForRedirectURLs`, derived from `ABS_URL`'s path instead of becoming a second knob, and verifies it came back | | `scripts/authentik-audiobookshelf-sso.sh` (new) | the IdP half in git: provider, groups, the `abs_groups` scope mapping, the redirect URIs. That state lives in authentik's database and a blueprint re-import reverts it silently, so it needs a re-runnable door. HS256: no provider on the fleet carries a signing key, so the client secret *is* the signature key | | `tests/dankserver_quadlet.bats` | the base-path assertion moves from `/` to `/audiobookshelf` — the pinned value **was** the bug | | `tests/audiobookshelf_init.bats` | stub gained `PATCH /api/auth-settings` and base-path routing; two new seams: the subfolder that reaches the wire follows `ABS_URL` (prefixed and root arms), and the two scripts build the same redirect path | | vault | the base-path law, the SSO contract (which script owns which half), the phone app's new server address | ## Verification - `./tests/run.sh` — 824 tests, all pass; the two new seams confirmed running by name. - Throwaway container (base `/audiobookshelf`, its own empty config, no shared state): asset `…/_nuxt/c7f3e29.js` → `200 application/javascript`, `/audiobookshelf/healthcheck` → `OK`. - Provider side already applied to the live IdP (the script is idempotent): `uris ['http://192.168.0.235:8000/audiobookshelf/auth/openid/callback', '…/audiobookshelf/auth/openid/mobile-redirect']`, claim for `akadmin` → `{'abs_groups': ['admin', 'user']}`. - Pending on merge: `deploy.sh dankserver --service audiobookshelf` (base path + subfolder), then the browser round trip into ABS as admin, then the phone app against `http://192.168.0.235:8000/audiobookshelf`. ## Rollback Revert `cfdd343..3138e78` and redeploy the service. The local `frank` account stays enabled in `authActiveAuthMethods` throughout, so the box is reachable either way. --- cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
dank added 4 commits 2026-11-02 19:51:39 -08:00
ROUTER_BASE_PATH=/ served a healthy API and a web UI that could never boot. The index.html in the pinned image is built with routerBasePath:/audiobookshelf, so every script request fell through to the SPA fallback and came back as text/html, which Chrome refuses to execute. healthcheck, the API and the phone app all stayed fine, which is exactly how this hid behind an API-only deploy check.

The deployer now reads the mounted base path out of the unit and hands it to the bootstrapper as ABS_URL, so the API door, the login route and the OIDC redirect subfolder cannot drift from the unit. healthcheck answers at the root under either base path, so the health gate is unchanged.
The live round trip died on authentik with Redirect URI Error. ABS builds redirect_uri as <origin><authOpenIDSubfolderForRedirectURLs>/auth/openid/callback, and that key is unset on a database created before SSO (the 2.17.4 migration only fills a DB that already had OIDC enabled), so the URL carried the literal string undefined. Arming through the API therefore has to send the value; it is the mounted base path, so it is derived from ABS_URL rather than becoming a second knob an operator has to keep in sync.

authentik-audiobookshelf-sso.sh is the IdP half kept in git: provider, groups, the abs_groups scope mapping and the redirect URIs. That state lives in authentik's database, and a blueprint re-import reverts hand-made rows silently, so it needs a re-runnable door. Tokens are HS256 because no provider on the fleet carries a signing key, which makes the client secret the signature key.

The stub server now records the auth-settings PATCH body, so the seam asserts the subfolder that reaches the wire follows ABS_URL for both the prefixed and root shapes, and a second seam pins that the provider script and the bootstrapper build the same redirect path.
The page owned the URL as a plain host:port; every client now carries the /audiobookshelf prefix. Added the two laws a next operator needs (why the base path is not a choice, why the redirect subfolder must be sent) and the Single sign-on section naming which script owns which half.
dank added 1 commit 2026-11-02 19:53:16 -08:00
Measured on both shapes: ping returns success:true and healthcheck returns OK at the root whether or not the app is mounted under a prefix, which is why the phone app's connectivity check works before it ever resolves the base path. The paragraph now says which routes were probed instead of asserting 200s in general, and the frontmatter description fits the OKF 120-char budget the linter enforces.
dank merged commit 7e9bfbc2be into main 2026-11-02 20:02:58 -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#128