Feature release adding configurable import modes, an end-to-end smoke-test suite, and a full security hardening pass (SSRF guards, security headers, SLSA provenance, OpenSSF Scorecard). First release with all GitHub Actions pinned to SHA digests.
Added
- Import mode — move / copy / hardlink (closes #54) — Settings → General → Import Mode controls how completed downloads are placed in the library. Move (default) removes the source after import. Copy duplicates to the library and leaves the source intact so torrent clients continue seeding. Hardlink links both paths to the same inode (zero extra disk, seeding preserved) — requires download dir and library on the same filesystem. The setting is respected for both ebook files and audiobook folders. Migration
013_import_mode.sqlseeds the default. - Log viewer in Settings → Logs (closes #93) — the last 1 000 log entries are held in an in-process ring buffer and exposed at
GET /api/v1/system/logs. The Settings → Logs tab shows the 200 most recent entries colour-coded by severity, auto-refreshes every 5 s, and lets you filter by WARN/ERROR without leaving the UI. A runtime Level selector (PUT /api/v1/system/loglevel) switches between DEBUG/INFO/WARN/ERROR without restarting the process — useful for capturing verbose output while investigating a problem. - UI localization — English, French, German, Dutch — the entire web UI is now internationalised with
react-i18next. All labels, button text, error messages, and toasts are translation-keyed. Language is auto-detected from the browser'sAccept-Languagesetting and can be overridden in Settings → General → Language (persists tolocalStorageso the first paint is always in the right language). The language switcher includes a System (auto) option that delegates back to the browser. - Root folders — multiple root library paths can now be configured under Settings → Root Folders. Each author can be assigned to a specific root folder; unassigned authors continue to use the startup default path. Free disk space is shown next to each path.
- Language propagation into indexer queries — search queries now include the author's metadata-profile language filter. For Prowlarr/Jackett, the allowed-language codes are appended to the query so foreign-language releases can be excluded on the indexer side as well as during metadata ingestion. The outgoing query string is visible at DEBUG level in the new log viewer.
- Language field from metadata providers — Google Books and Hardcover now populate the
languagefield on book records. Hardcover exposes language via theeditionsGraphQL node; Google Books via thevolumeInfo.languageJSON field. Language pills are surfaced in the Wanted page result rows when the indexer returns<newznab:attr name="language">. - End-to-end smoke test suite (closes #97) —
tests/smoke/smoke_test.goboots the real binary against a scratch data directory and exercises the golden-path HTTP endpoints (health, auth, authors, books, settings, history, OPDS). Runs on every PR and main/development push via themake smoketarget in CI. Catches wiring regressions (broken route registration, missing migration, bad frontend embed) that unit tests miss.
Fixed
- Dark mode not applied on first load — dark mode preference was only activated after visiting Settings, leaving the app in light mode on a cold start for users whose preference was already saved.
useTheme()is now called at theShelllevel inApp.tsxso the correct theme is applied before any route renders. - Circular checkboxes on Books page (closes #75, #76) — monitor/auto-grab checkboxes on the Books and Authors pages were rendered as squares; they are now circular and visually consistent with the rest of the UI.
- 500 on add author (closes #91) — when a concurrent add-author request caused a UNIQUE-constraint violation at the database layer, the handler returned a raw 500 and leaked the SQLite error message. It now returns 409 with
"author already exists"and logs the underlying error at ERROR level with full context. A regression test covers this path. - Log viewer defects (closes #98) — repaired column alignment, light-mode palette, timestamp formatting, attribute rendering, word-boundary wrapping, the DEBUG filter option, and the stale-ref refresh toggle. The log table is now readable in both themes and honours the selected severity filter.
Security
- GitHub Actions pinned to SHA digests — all actions in
ci.ymlandsecurity.ymlare pinned to their commit SHA (with the version tag as a comment). Satisfies the OpenSSF Scorecard Pinned-Dependencies check; eliminates the tag-mutable supply-chain risk on every workflow step. - SSRF validation for outbound URLs — webhooks, indexers, and download-client endpoints now pass through
internal/httpsec.ValidateOutboundURLbefore any request is issued. The validator blocks loopback, link-local, cloud-metadata (169.254.169.254,metadata.google.internal, AWS IPv6), and (for webhooks) RFC1918 ranges; DNS results are re-checked to defeat rebinding attacks. Escape hatch:BINDERY_NOTIFICATIONS_ALLOW_PRIVATE=1flips webhooks to the LAN policy for on-network ntfy / Home Assistant installs. - Security headers middleware — every response emits
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: strict-origin-when-cross-origin, and a locked-downContent-Security-Policy(nounsafe-eval, no foreign origins,frame-ancestors 'none'). HSTS is emitted only when TLS is detected (direct or viaX-Forwarded-Proto: https) so plain-HTTP homelab setups don't get locked out. - Session cookie
Secureauto-detect — the cookie'sSecureattribute now flips on automatically behind TLS or a TLS-terminating proxy. Override withBINDERY_COOKIE_SECURE=auto|always|never. - Upload hardening —
/api/v1/migrate/*now validates the multipart Content-Type against an allowlist and spools uploads to$BINDERY_DB_PATH/../tmp(mode0700) instead of the world-writable/tmp. The database file is chmod'd to0600on boot. - Container / supply chain — Docker base images are digest-pinned (node 22, Go 1.25.9, distroless static); Dependabot keeps them fresh. A
bindery healthchecksubcommand drives theHEALTHCHECKdirective. GoReleaser now emits Syft SBOMs (SPDX) alongside release archives.actions/attest-build-provenancemints SLSA provenance on every image push, verifiable withgh attestation verify. - Helm chart — dedicated ServiceAccount with
automountServiceAccountToken: false, managed or externally-referenced Secret forBINDERY_API_KEY(no more plain env rendering), opt-in NetworkPolicy, and an opt-in ArgoCD PostSync smoke-test hook against/api/v1/health.helm-unittestcases guard the posture against regressions. - CI security pipeline —
.github/workflows/security.ymlruns gosec, govulncheck, golangci-lint, Semgrep, gitleaks, Trivy, Grype, Dockle, Syft, ZAP baseline, hadolint, Helm lint, and kubesec on every push / PR / weekly cron..github/workflows/scorecard.ymltracks OpenSSF Scorecard. All findings upload to the Security tab as SARIF.SECURITY.mddocuments the disclosure policy.
Upgrade notes
- Schema: migration
013_import_mode.sqlinsertsimport.mode = moveas a new setting row. The default is backward-compatible — existing installs behave identically (files are moved as before). Change the setting in Settings → General → Import Mode if you want copy or hardlink behaviour.