Minstrel keeps credentials it has to read back: indexer, Prowlarr and
download-client keys and passwords, the AcoustID key, cover-art and tag
provider keys, the SMTP password, and each user's ListenBrainz token and
Subsonic password. They were stored in the clear; they are now sealed with
AES-256-GCM under a per-column HKDF key derived from the stream secret, each
value bound to its row.
- internal/secret: Seal/Open/Reveal, and Columns, the one list of sealed
columns (a backup will leave them out). Install takes the master key at
startup; SealStored seals any value still in the clear, so an existing
database is brought over in place on the first boot.
- Every write seals and every read opens: acquire (one read path, getService
/ usableServices), acoustid settings, SMTP, ListenBrainz, Subsonic auth and
password generation, cover-art and tag provider settings.
- A value that does not open (the stream secret changed) is logged and reads
as unset, to be entered again in Settings. docs/security.md says so.
- Tests: secret unit tests (wrong owner/purpose/key, malformed, no key), a
SealStored integration test over all eight columns plus a schema check that
each listed column exists, and the existing tests now assert sealed storage.
dbtest installs a test key for integration tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
users.api_token held each user's apiKey in plaintext and was looked up by
equality, so a leaked row or backup handed out working keys. Migration
0063 replaces it with api_token_hash (sha256, hex), computed in place
from the existing keys so every Subsonic client keeps working.
The key can no longer be read back: GET /api/me/api-token is gone, and
POST returns the new key once. Settings shows it right after Regenerate
with a copy button and a "won't be shown again" note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Integration tests previously TRUNCATEd the users table at setup,
which wiped the operator's admin login every time the suite ran
against a Postgres instance shared with their dev environment.
This commit:
- Adds internal/dbtest/reset.go exposing ResetDB(t, pool) and
TestUserPrefix. ResetDB truncates every data table EXCEPT users,
then deletes only users whose username starts with TestUserPrefix.
- Migrates 9 test files (subsonic/scrobble, subsonic/star,
recommendation/candidates, scrobble/queue, similarity/worker,
playevents/writer, playsessions/service, api/auth, api/me) to
call ResetDB instead of issuing TRUNCATE-with-users.
- Renames hardcoded test usernames to use TestUserPrefix so ResetDB
cleans them between runs. seedUser in api/auth_test now prefixes
internally; existing call sites pass bare names.
- Leaves auth/bootstrap_test.go alone — it legitimately tests
first-time admin bootstrap and must wipe users.
Verified locally: full integration suite with -p 1 runs cleanly
under the existing MINSTREL_TEST_DATABASE_URL setup, and the admin
row in the shared dev DB survives the run.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bridges closed play_events to the scrobble_queue: loads the event,
applies the Qualifies() threshold, checks user LB config (enabled + token),
and inserts via ON CONFLICT DO NOTHING for idempotency. Best-effort —
callers (playevents.Writer hooks, Task 8) will log and swallow errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>