What changed
Kobo subsystem audit follow-up plus a tranche of CI dependency bumps.
The two Kobo fixes close two distinct classes of bug that the v4.0.44 audit surfaced but didn't address: a race condition in reading-state writes that could 500 a device into an infinite retry loop, and a policy gap where the Kobo blueprint could surface books the user's web UI would have hidden.
Kobo reading-state race fix
Two concurrent PUTs to /v1/library/<uuid>/state from the same Kobo — which legitimately happen on wake-from-sleep — could race past the read-then-write check in get_or_create_reading_state and both INSERT, leaving duplicate (user_id, book_id) rows in book_read_link and kobo_reading_state. The next read uses .one_or_none(), which raises MultipleResultsFound on duplicates → 500 to the device. The device then retries forever and the sync queue fills with the same failing call.
Two-part fix:
-
UniqueConstraint(user_id, book_id)added toReadBook,KoboReadingState,KoboSyncedBooks, andArchivedBookat the model layer, plus amigrate_kobo_unique_constraintsmigration that creates the matchingCREATE UNIQUE INDEXon existing installs. The migration first dedupes any existing duplicate rows with sensible winner rules — newestlast_modifiedwins forKoboReadingState, status-level (FINISHED>IN_PROGRESS>UNREAD) + LM forReadBook, archived-wins forArchivedBook. ForKoboReadingStateit also reparents the winner's bookmark + statistics children, merging non-null fields from the losers so position and reading-time totals are preserved. ForReadBookit sumstimes_started_readingacross losers so the user's read counter doesn't reset. The migration is gated by a marker file inCONFIG_DIR/.cwa_migrations/so it runs at most once per install. -
get_or_create_reading_staterewritten toINSERT ... ON CONFLICT(user_id, book_id) DO NOTHINGvia the sqlite dialect'ssqlite_insertupsert. The race itself can't produce a duplicate even before the DB constraint trips. The subsequent SELECT then sees exactly one row regardless of which writer won.
29 regression tests cover the model constraints, migration idempotence, dedupe rules per table, child reparenting, and a real 16-thread concurrent-insert test that asserts exactly one row exists after a thundering-herd PUT.
Kobo common_filters policy fix
Upstream's calibre_db.get_book_by_uuid skips common_filters, so the Kobo blueprint was handing back books that the user's web UI would have hidden — denied-tagged, language-filtered, custom-column-restricted, or per-user-hidden via the fork's hide-books feature. Five callsites in cps/kobo.py audited; new helper get_book_by_uuid_for_kobo(uuid, *, enforce_policy) on CalibreDB makes the policy explicit at each callsite.
- ENFORCE filters on the two policy-boundary surfaces —
HandleMetadataRequest(GET/v1/library/<uuid>/metadata) andadd_items_to_shelf(POST shelf-add). Denied or hidden books shouldn't expose metadata via Kobo, and adding such a book to a Kobo-synced shelf would leak it to every other Kobo on the account. - DO NOT ENFORCE on the three device-trailing or destructive user-initiated surfaces —
HandleStateRequest(GET/PUT state),HandleTagRemoveItem(POST shelf-remove),HandleBookDeletionRequest(DELETE book). Reading-state must keep syncing for books already on the device even if they later become denied (otherwise the device retries forever and the user sees sync failures). Destructive cleanup operations must never be blocked by policy.
allow_show_archived=True on the enforced path because the ArchivedBook table doubles as the Kobo's device-deletion track — filtering on it would 404 the device's own deletions and break sync. Decision matrix in notes/KOBO-B4-COMMON-FILTERS-POLICY.md.
12 regression tests pin the helper contract, the per-callsite policy decisions, and an audit invariant that no Kobo callsite still uses the unfiltered helper.
CI: third-party Actions on their current majors
Five dependabot bumps batched in this release. All were reviewed for breaking changes against the actual workflow usage; nothing in our use of any action depends on a removed input or a deprecated codepath. All are on Node 24 runtimes (GitHub-hosted runners support it).
actions/attest-build-provenance2.4.0 → 4.1.0 — used for SLSA build attestation; samesubject-name/subject-digest/push-to-registryinputs.actions/github-script7 → 9 — only used to post a PR comment on failed tests; the script doesn'trequire('@actions/github')or redeclaregetOctokit, so the v9 ESM-only break isn't hit.docker/setup-buildx-action3 → 4 —driver-opts: network=hostis preserved.sigstore/cosign-installer3.10.1 → 4.1.2 — bumps default Cosign to v3; we usecosign sign(notsign-blob), so the v3--bundlerequirement doesn't apply.peter-evans/dockerhub-description4 → 5 — only the runtime changed.
To get the update
docker pull ghcr.io/new-usemame/calibre-web-nextgen:v4.0.45
Or :latest. No data migration step for the user — the Kobo unique-constraint migration runs automatically on first boot of v4.0.45, idempotent, gated by a marker file. Existing duplicate rows (if any) are deduped before the index is created.
Test verification
- 41 new Kobo regression tests (12 policy + 29 race/migration/dedupe), all green.
- 118 kobo-adjacent unit tests green on merged main (
de2bd2e), 0 regressions. - CI on merged main: Fast Tests (Smoke + Unit) green, Integration Tests (Docker) green — full tier-2 gate.
- Concurrent-insert threaded test: 16 threads hit the upsert simultaneously, exactly one row remains.
Credit
- The 5 Action bumps are dependabot-generated.
- The 2 Kobo fixes are audit-originals from the v4.0.44 follow-up; no upstream PRs map to either.
The Kobo subsystem audit doc is committed at notes/KOBO-AUDIT-2026-05-10.md; #132 closes item B2 and #133 closes item B4.