Appearance
@libris/web
2.0.0
Major Changes
1b6c424: Multi-user authentication, and a security pass over everything it touched.
Libris now has real user accounts. Authentication moves from a single shared API key to Better Auth: people sign in with an email and password, admins manage other accounts, and e-readers and scripts authenticate with per-user app passwords instead of one install-wide key. Books, reading progress, inbox uploads and third-party credentials all belong to a person now, so an install can be shared without everyone seeing everyone else's things.
Breaking
Read the "Upgrading to the Better Auth Release" runbook in
docs/deployment.mdbefore deploying. In short:BETTER_AUTH_SECRETis required. Generate it withopenssl rand -base64 32. Published placeholders and low-diversity values are rejected at startup, so a copied.env.examplewill not boot.BETTER_AUTH_URLis required whenNODE_ENV=production, and must be the public origin the browser actually reaches — scheme and host only, no path. Better Auth does not infer an HTTPS origin behind a TLS-terminating proxy; it reads the container's plain-HTTP socket, so leaving this unset makes every browser sign-in fail with403 INVALID_ORIGIN.COOKIE_DOMAINis gone. The session cookie is host-only, so Libris must be served from a single origin. The variable is now ignored rather than rejected, so a compose file that still sets it gets no error — delete it.- Everyone is signed out by the upgrade, and API keys stop working. Re-pair e-readers with app passwords minted under Settings → Connections.
- KoSync and OPDS credentials must be regenerated. OPDS now authenticates with an account email plus an app password.
- Rotating
API_SECRET_KEYnow invalidates every KoSync credential, because stored secrets are peppered with a key derived from it. Every paired reader has to be paired again after a rotation. - Unbanning does not restore app passwords. Banning disables the user's app passwords; lifting the ban leaves them disabled and the user must mint new ones.
- Upload API: a file already in the library is reported in a new
skipped[]array rather thanerrors[], and a batch in which every file was already present now answers200instead of400. - OpenAPI: the
BookApprovedResponsecomponent is replaced byBookUpdated(a superset of its fields — no client loses anything).
Upgrading an install that predates this release does not need SQL: the first-run form adopts the existing account, so an operator signs in through it and sets a password.
Added
- Sign-in, first-run admin setup, and an Account tab for changing your own name and password, listing the devices you are signed in on, and revoking them.
- A Users tab for admins, with the last admin protected from demotion, ban and deletion through every path that could otherwise strip them.
- Per-user Hardcover accounts. Previously only one user per install could connect one, and a scheduled sync spent whichever token sorted first; the install-wide phase now uses an admin's token and skips, loudly, if no admin has connected.
GET /api/health/live, an I/O-free liveness probe for container health checks.GET /api/healthkeeps its existing readiness semantics and its response shape.- The realtime event socket now follows the session: it closes when the session behind it is revoked, and re-dials to be re-scoped when your role changes rather than signing you out.
Fixed and hardened
Ownership is now enforced on every read surface — inbox, dashboard counts, search suggestions, uploader attribution and library facets no longer leak other users' pre-approval uploads or raw user ids. Rate limiting buckets IPv4 clients individually, applies a per-credential budget to KoSync and sign-in, and bounds the request body before it is parsed. EPUB parsing runs in linear time and refuses archives crafted to exhaust memory. Redis is treated as a cache rather than the authority on who is signed in, so an outage degrades instead of signing everyone out. Remote cover fetching, ingestion paths, WebSocket upgrades and proxy-derived client addresses are all validated at their boundaries.
Patch Changes
- Updated dependencies [1b6c424]
- @libris/api-hono@2.0.0
1.1.2
Patch Changes
11664da: Make the unit-test tasks cacheable again.
Both
@libris/api-hono#testand@libris/web#testwere reported by Vite Task asnot cached because they modified their inputson every single run — locally and in CI. The cause: loading a TypeScriptvite.config.tsmakes Vite write a transient.mjsintonode_modules/.vite-temp/, import it, then delete it. Because both tasks declareinput: [{ auto: true }], which tracks the whole package directory, that write lands inside the tracked input set and the task fingerprint changes mid-run.Excluding
node_modules/.vite-temp/**from both tasks fixes it. A repeatvp run -r testwith no changes now reports 3/3 cache hit and finishes in 0.19s instead of re-running the full suite (~73s locally, ~5 minutes on a 2-core CI runner, since the api-hono suite is 481 tests each bootstrapping a PGlite instance).75bb9af: Port CI/CD from Forgejo Actions to GitHub Actions, and publish images to GHCR.
.github/workflows/ci.ymlreplaces.forgejo/workflows/ci.yml: jobs move toubuntu-latest,setup-vploses its Forgejo-only full-URL form, and the artifact actions go to v4 (v3 was shut down on github.com in January 2025). The separatee2e-prande2e-mainjobs are merged into onee2ejob that selects the@smokesubset on pull requests and the full suite on pushes, so the service, container, and env setup can no longer drift between them. Shards go from 2 to 3, and aconcurrencygroup cancels superseded runs. The bespoke PR-comment step that called the Forgejo issues API is dropped — GitHub's native checks UI already reports per-shard status..github/workflows/release.ymlreplacespublish-images.ymland switches the release strategy tochangesets/action@v1. Releases are now PR-gated: merging to main opens a "chore: version packages" PR, and merging that is what triggers the release. The action runs without apublishinput, since no workspace goes to npm.The old two-job split existed only because the Forgejo runners were split — one had Node without a Docker daemon, the other a daemon without Node. A GitHub runner has both, so the
ci/release-<run_id>staging branch, the re-clone, and the rebase-onto-main are all gone. Publishing is idempotent via the existing composite-tag registry check, so no commit-message sniffing is needed to detect the version merge. Registry auth uses the built-inGITHUB_TOKEN, retiring theREGISTRY_TOKENsecret, and the build gains buildx with a GitHub Actions layer cache.Docs and agent instructions follow:
docs/ci-cd.mdis rewritten for the new workflows and documents the release flow,docs/deployment.mdpoints atghcr.io/razorsim/libris, andfjis replaced byghthroughout AGENTS.md, CLAUDE.md, README.md, anddocs/contributing.md.75bb9af: Upgrade Vite+ from 0.1.24 to 0.2.6 and move the Node toolchain to 26.5.0.
Vite+ 0.2.0 dropped
@voidzero-dev/vite-plus-test(its rebundled Vitest copy) and now runs upstream Vitest, pulled in transitively byvite-plus. The workspace catalog no longer aliasesvitestto the removed wrapper, and thevitestoverride / peer-dependency rules that existed only to serve that alias are gone.import ... from "vite-plus/test"is unaffected.Toolchain moves with it: Vite 8.0.16 -> 8.1.5, Rolldown 1.0.3 -> 1.2.0, Vitest 4.1.8 -> 4.1.10 (includes the GHSA-p63j-vcc4-9vmv fix), oxfmt 0.52 -> 0.60, oxlint 1.67 -> 1.75, oxlint-tsgolint 0.23 -> 7.0.2001 (stable tsgolint 7), tsdown 0.22.1 -> 0.22.13.
Node 26 pins brought in step with the runtime:
.node-versionpinned to26.5.0(was the floating major26)- Docker images pinned to
node:26.5.0-slim, withpnpm@11.5.0matchingpackageManagerandvite-plus@0.2.6pinned in the builder stage @types/nodecatalog bumped^25->^26to match the runtime major- api-hono's tsdown
targetcorrectednode24->node26, so the bundle is no longer down-levelled below the runtime it actually ships on
Also fixes the local
test:e2e:dockerpath, which could not boot:- the Playwright image ships Node 24, below the
engines.nodefloor, so the compose entrypoint now provisions the pinned Node vianinto a cached volume and installs pnpm explicitly (Node 25+ no longer bundles corepack) playwright.config.tsinvokedpnpm --filter @libris/web run dev, a script the Vite+ task migration had removed; it now goes throughvp run -F @libris/web dev
75bb9af: Convert the user-guide screenshots from PNG to WebP.
All 29 images in
docs/guide/images/are now WebP at quality 90 (webp:method=6), and every reference inREADME.mdanddocs/guide/*.mdwas rewritten to match.This was the dominant cost in the repo: the PNGs were 7.9MB of a 12MB tracked tree, all of them unoptimized retina-scale captures at 1376x1403. WebP brings that to 1.8MB, a 77% reduction, taking the whole tracked tree from 12MB to 5.4MB — a cost every clone was paying.
Quality 90 was chosen after measuring: PSNR 41.6dB, and a 1:1 crop of a text-dense region is visually indistinguishable from the original, which matters because these are UI screenshots where readers need to make out interface text.
Application images are deliberately untouched —
apps/web/public/favicons and PWA icons must stay PNG/ICO for manifest andapple-touch-iconcompatibility, and the logos are already SVG.Updated dependencies [11664da]
Updated dependencies [75bb9af]
Updated dependencies [75bb9af]
Updated dependencies [75bb9af]
- @libris/api-hono@1.1.2
1.1.1
Patch Changes
- @libris/api-hono@1.1.1
1.1.0
Minor Changes
4b86d47: Make book language a canonical ISO 639-1 code everywhere so language filtering is reliable.
- Add a shared, dependency-free
normalizeLanguage/languageLabel/LANGUAGESmodule (@libris/api-hono/languages) used by both the API and the web app. - Predict language at ingestion: normalize the embedded EPUB
<dc:language>tag (en-GB/English/eng→en,it-IT/Italian→it). When the tag is missing or unrecognized, detect the language (tinyld) from a sample of the book's body prose (spine-ordered, skipping short front matter), falling back to the title + description. The approve, PATCH, and apply-metadata routes re-normalize on write as a safety net. - Replace the free-text language inputs with a searchable language select in the edit modal and the inbox review picker; the library filter, chips, table, and badges now display full language names while filtering by code.
- Add a
db:normalize-languagesbackfill script (dry-run by default,--applyto write) to clean up existing inconsistent values. - Add a "Browse by Language" OPDS catalog: a navigation feed listing only the languages present in the library (as full names) plus per-language acquisition feeds (
/opds/languagesand/opds/languages/{code}).
- Add a shared, dependency-free
Patch Changes
- Updated dependencies [4b86d47]
- @libris/api-hono@1.1.0
1.0.1
Patch Changes
- Updated dependencies [1442ee7]
- @libris/api-hono@1.0.1
1.0.0
Initial public release.