Daniel Chong 602289a0f6 fix(kobo): forward x-kobo-userkey and decode the composite sync token in the catch-all proxy (#1138)
* fix(kobo): forward x-kobo-userkey and decode the composite sync token in the catch-all proxy

The catch-all proxy in kobo-device.controller.ts forwards device requests to
storeapi.kobo.com through KoboProxyService, which built its outgoing headers from
a static allow-list (FORWARD_HEADERS). The list covered accept, accept-language,
authorization, content-type, user-agent, and a curated set of x-kobo-* headers,
but two of the headers that matter for store-API authentication and cursor
tracking were missing: x-kobo-userkey, the device's per-account secret that
authorizes most Kobo store endpoints, and x-kobo-synctoken, the upstream cursor
the device carries on every store call.

Without x-kobo-userkey, every store interaction proxied through the catch-all
shipped without credentials, so Kobo either rejected the call or treated the
device as unauthenticated. The "Connecting to OverDrive" dialog that issue #738
describes is the borrow endpoint, /v1/library/borrow, hanging because Kobo
cannot reconcile a borrow into a user's library without the user key. The same
gap also left library books a user borrowed on libbyapp.com (which is just
OverDrive pushed to kobo.com) unreachable from the device, because every
proxy-bound sync the device makes lands at Kobo without the credentials it
needs to enumerate the user's entitlements.

The Kobo Plus / Kobo Store entitlement fix in 2.6.0 (closes #789) only patched
the one explicit library/sync call. That call already decodes the BookOrbit
composite sync token via kobo-sync-token.ts and overrides the outgoing header
with the Kobo cursor Kobo understands, so the store page works. Every other
store call still went through the catch-all proxy with the device's
PX.<base64> composite on it, which Kobo treats as a fresh cursor and answers
for. The borrow endpoint in particular was the most visible casualty, but
anything else proxied through @All('*') was affected the same way.

Switch the proxy to a deny-list (DROP_HEADERS) for the headers RFC 7230 names
hop-by-hop or otherwise scoped to a single connection (host, connection,
content-length, content-encoding, transfer-encoding, keep-alive,
proxy-authenticate, proxy-authorization, te, trailers, upgrade), and forward
everything else the device sends. x-kobo-userkey now reaches Kobo on every
store call, so the borrow endpoint can authorize the user's library and the
libby-arrived entitlements surface on the next /v1/library/sync.

Add a forward() path that decodes the device's composite sync token before
relaying, and forwards the Kobo cursor Kobo can use instead. A raw Kobo token
(no PX. prefix) is forwarded verbatim, the way devices that synced with Kobo
directly before BookOrbit was put in the path already do. A composite token
whose cursor is undefined has no Kobo-side value, so omit the header entirely
on the way out rather than leak our composite to Kobo.

The extraHeaders and omitHeaders escape hatches stay in place, so the explicit
library/sync call can still pin the upstream cursor (and the no-cursor path
still drops the header) without going through the new decode-on-forward path.
The librarySync controller in kobo-sync.controller.ts already uses those
exacts knobs for the store page, so this change does not touch it; this
commit is the catch-all side of the same fix.

Closes #738

* fix(kobo): honor RFC 7230 Trailer and Connection fields, and emit sanitized proxy logs

Three small corrections surfaced by code review on the catch-all proxy that
delivers Kobo store entitlements.

The deny-list shipped with the wrong name for one of the RFC 7230 hop-by-hop
fields. `trailers` (plural) does not match the standard `Trailer` header that
arrives in mixed case, so a Trailer field set by an upstream could leak through
on the way to the device. Replace it with `trailer`.

RFC 7230 says the fields listed in `Connection` are also hop-specific, and a
proxy must strip them before forwarding. The two header-copy paths only
checked DROP_HEADERS, so a `Connection: x-kobo-tracking` header from the device
or from upstream could let a nominated field slip through. Parse the
Connection header in both directions, normalize case-insensitively, and add
its fields to the ignore set alongside DROP_HEADERS. The new
readConnectionFieldNames helper does the splitting once and is reused on both
sides of the relay.

Add a structured start/end/fail log around the relay, replacing the previous
single-line warn that interpolated a request-derived targetUrl and a raw
err.message. The repo's logging conventions in AGENTS.md name an external
HTTP call as a non-trivial operation that warrants start/end/fail logs, and
require sanitizeLogValue() for every dynamic value that lands inside a quoted
log field. The fail line now carries durationMs, errorClass, sanitized error,
and sanitized targetUrl. The 502 response with the "Upstream Kobo API
unavailable" body is preserved.

Tests cover the new paths: Trailer is stripped, Connection-nominated fields
are stripped on the request side, and Connection-nominated fields are stripped
on the response side. All 10,486 server tests pass; lint and typecheck clean.

CodeRabbit also asked to instantiate KoboProxyService through
Test.createTestingModule. AGENTS.md says the same thing, but every sibling
test in the kobo module uses direct construction with no testing module, so
changing only this file would invert the local pattern. The convention is
already inconsistent across the module and is not enforced by any lint rule;
that is a separate cleanup better suited to its own PR.

* test(kobo): drop unused makeHeaders helper in proxy service tests

makeHeaders was a placeholder identity function that returned its argument
unchanged. Inline the headers object literals at the 9 call sites.

* fix(kobo): scrub BookOrbit credentials and the composite token in every relay

Move the sync-token substitution into request() so forwardTagDelete, which
relays with no options, stops handing Kobo a PX. token it cannot read. Apply it
before omitHeaders and extraHeaders so store sync keeps both escape hatches.

Strip BookOrbit's session cookies and the reverse proxy forwarding headers:
these routes sit on the BookOrbit origin, so a browser carrying a session
reached them with our auth cookies attached and relayed them to Kobo.

Restore the SSRF regression case that covers the allowed hostname in userinfo.

* test(maintenance): wait for the cover sweep instead of a fixed turn budget

The detached sweep does real filesystem I/O, so 50 setImmediate turns settled it
locally but not on a loaded runner. The drain loop then gave up silently and the
next call failed as "The cover sweep is not complete", pointing at the wrong
place. Poll the store to a deadline and fail loudly when it truly hangs.

---------

Co-authored-by: Daniel Chong <daniel@chong.nz>
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-08-25 09:52:40 -06:00
2026-07-08 01:34:42 +00:00
2026-07-08 01:34:42 +00:00

BookOrbit

A self-hosted library and reading platform for ebooks, PDFs, audiobooks, and comics.

Latest release Stars CI Release Coverage Crowdin

Website Demo GHCR Pulls Contributing License: AGPL v3

BookOrbit dashboard showing reading stats, widgets, and book shelves


What is BookOrbit?

BookOrbit organizes your books and reads them back to you anywhere: the web reader, a Kobo, or KOReader. Progress, highlights, and reading status move between all three, so you can start a chapter in one place and finish it in another.

Around that core sit 14 metadata providers, reading statistics and achievements, OPDS and Send-to-Kindle delivery, multi-user accounts with OIDC/SSO, and automatic sync out to Hardcover, Readwise, and StoryGraph. All of it runs on infrastructure you control.

Visit Website

Live Demo

Try the live instance before you install. No account required.

Launch Live Demo

Note: The demo includes a sample library of public domain books. Some administrative features are limited in the public demo. Self-hosting BookOrbit provides the full experience.

Features

Reading Experience & Sync

  • Built-in Web Readers: Ebooks (EPUB, KEPUB, MOBI, AZW3, AZW, FB2), PDFs, comics (CBZ, CBR, CB7), and audiobooks (M4B, MP3, M4A, OPUS, OGG, FLAC), with no extra plugins required.
  • Three-Way Sync (Kobo + KOReader + BookOrbit): Progress and annotations flow bidirectionally between Kobo devices, KOReader, and the BookOrbit web reader. Pick up on any surface where you left off on another, including highlights and deletions.
  • KOReader Plugin: An on-device catalog browser with search, download, and status and rating management, alongside full progress and annotation sync.
  • Annotations & Highlights: Highlights from the web reader, KOReader, and Kobo merge into one searchable hub. Filter by color, style, and source; export as Markdown, CSV, or JSON.
  • Hardcover, Readwise & StoryGraph Sync: Push status, progress, reading dates, and ratings to Hardcover on configurable triggers; status and progress to The StoryGraph; and new highlights and notes to Readwise as you create them, from both the web reader and synced devices. Hardcover read history can be pulled back to backfill blank BookOrbit entries.
  • Statistics, Goals & Achievements: Daily reading time, heatmaps, streaks, and library health, plus yearly goals, monthly challenges, and 50+ achievements across five categories. Reading DNA profiles your reading style from your actual session history.

Library Management

  • Multiple Libraries: Isolate content with per-library folders, custom scan rules, and format priorities.
  • 14 Metadata Providers: Google Books, Open Library, Amazon, Goodreads, Kobo, Hardcover, Audible, Audnexus, Libro.fm, and iTunes, plus ComicVine for comics, RanobeDB for light novels, and Aladin and Lubimyczytać for Korean and Polish catalogs. Cover art is sourced separately from iTunes, DuckDuckGo, and AudiobookCovers.
  • Smart Scopes & Collections: Organize your collection with curated lists and dynamic, rule-based saved filters.

Platform & Delivery

  • Multi-User & SSO: Granular per-user permissions and isolated reading data, with native support for Authentik, Keycloak, and Authelia via OIDC.
  • Multilingual Interface: Community translations are managed on Crowdin. See the localization guide for current language support and contributor instructions.
  • Content Delivery: OPDS support for compatible apps, Send-to-Kindle via email, and browser drag-and-drop uploads.
  • Automated Ingestion: Configure a Book Dock drop folder for hands-free importing.

Quick Start (Docker)

mkdir bookorbit && cd bookorbit
mkdir -p books data/app data/postgres
curl -fsSLo .env https://raw.githubusercontent.com/bookorbit/bookorbit/main/.env.example
curl -fsSLo docker-compose.yml https://raw.githubusercontent.com/bookorbit/bookorbit/main/docker-compose.yml

Edit .env and set these required values:

APP_URL=http://your-server-ip:3000   # the URL you'll open in your browser
BOOKS_HOST_PATH=./books              # folder on your server where your book files live

POSTGRES_PASSWORD=         # database password           - openssl rand -hex 24
JWT_SECRET=                # signs login tokens          - openssl rand -hex 32
SETUP_BOOTSTRAP_TOKEN=     # one-time setup wizard token - openssl rand -hex 16

On a NAS, or any host where your book folder is owned by a user other than UID 1000, also set PUID and PGID to match that owner. Run id -u and id -g as the owning user to find them. Getting these wrong is the most common cause of permission errors on first scan.

Optionally set LIBRARY_BROWSE_ROOT=/books to start the library folder picker at /books instead of /.

Then start:

docker compose up -d

Open http://your-server-ip:3000 and complete setup using your SETUP_BOOTSTRAP_TOKEN.

For the full installation guide including reverse proxy setup, file permissions on NAS, external databases, and environment variable reference, see bookorbit.app/installation.

KOReader Plugin

The BookOrbit plugin for KOReader adds progress sync, two-way annotation sync, and a native catalog browser: navigate, search, and download books from your library without leaving the device.

BookOrbit KOReader Plugin showing dashboard, catalog search, and book details

  1. In BookOrbit, go to Settings > KOReader, create credentials if prompted, and click Download Plugin.
  2. Unzip bookorbit.koplugin.zip.
  3. Copy bookorbit.koplugin to koreader/plugins/ on the device.
  4. Restart KOReader and open a book.
  5. Use Tools > BookOrbit Sync to connect.

The download is pre-configured with your server URL and credentials, so there is no manual entry on the device. For full setup and sync options, see bookorbit.app/koreader-plugin.

Documentation and Contributing

Full documentation is at bookorbit.app, covering libraries, metadata, readers, Kobo sync, OPDS, users and permissions, OIDC setup, and more.

For a one-time import from Audiobookshelf, see the Audiobookshelf migration guide. For a stopped-snapshot import from Calibre-Web Automated, see the Calibre-Web Automated migration guide. For local development, see docs/DEVELOPMENT.md. To contribute, see docs/CONTRIBUTING.md for the full workflow: branch naming, test expectations, PR checklist, and commit format.

Repository Activity

BookOrbit repository activity: commits, issues, and pull requests over the last 30 days

Translations

Help translate BookOrbit into your language on Crowdin.

When adding user-facing text in code, add the Vue I18n key only to client/src/locales/en.json. Do not edit non-English catalogs in a feature pull request; untranslated keys fall back to English until Crowdin provides a translation. See docs/LOCALIZATION.md for the complete workflow.

Translation progress

Star History

BookOrbit GitHub star history

Support

License

BookOrbit is licensed under the GNU Affero General Public License v3.0.

Languages
TypeScript 74.3%
Vue 16%
Lua 7.6%
JavaScript 1.5%
CSS 0.3%
Other 0.2%