apiKey (OpenSubsonic) is preferred.u+t+s token auth is per-user opt-in via a new users.subsonic_password column. Plaintext p= is disabled by default and gated by SMARTMUSIC_SUBSONIC_ALLOW_PLAINTEXT_PASSWORD.
Reason subsonic_password is a separate column: bcrypt password_hash isn't reversible, but Subsonic's t = md5(password + salt) needs the plaintext. Storing it separately, opt-in, means the default posture is secure and legacy clients can still be supported without compromising the web-login password.
Endpoints:
/rest/ping(.view) — envelope-only ok.
/rest/getLicense(.view) — always-valid license (required by many clients before streaming).
Envelope:
JSON wrapped in {"subsonic-response": {…}}, XML with xmlns=http://subsonic.org/restapi, JSONP wrapped in callback(…) (falls back to plain JSON if callback is empty).
Each endpoint exposed at both /rest/name and /rest/name.view, GET + POST.
Docs:
config.example.yaml refreshed to include auth.admin_bootstrap, library, and subsonic sections (first two were stale from #292/#293).
Verification
go test ./... — green, including integration tests against ephemeral Postgres (migration 0003 applies clean).
go build ./... / go vet ./... / gofmt -l . — clean.
Live smoke against cmd/minstrel:
/healthz → 200 ok
/rest/ping.view no creds → error code 10
u+t+s (correct) → ok
u+t+s (wrong) → error 40
apiKey → ok
getLicense.view?apiKey=… → license.valid=true
JSONP wrap confirmed
XML with xmlns confirmed
p= with plaintext disabled → error 41
p=hunter2 with plaintext enabled → ok
p=enc:68756e74657232 (hex) with plaintext enabled → ok
Test plan
Unit tests for envelope JSON/XML/JSONP and WriteFail
Unit tests for decodePassword enc/plain/bad-hex
MD5 derivation test documenting t = md5(password + salt)
## Summary
Foundation of the `/rest/*` Subsonic-compatible surface. Covers Fable task #294.
**Architecture — hybrid auth posture:**
- Minstrel's native `/api/*` surface gets modern auth (apiKey bearer + OIDC later).
- `/rest/*` ships Subsonic-compatible auth for third-party clients (Symfonium, Substreamer, DSub, Tempo, play:Sub).
- **apiKey (OpenSubsonic) is preferred.** `u+t+s` token auth is per-user opt-in via a new `users.subsonic_password` column. Plaintext `p=` is disabled by default and gated by `SMARTMUSIC_SUBSONIC_ALLOW_PLAINTEXT_PASSWORD`.
- Reason `subsonic_password` is a separate column: bcrypt `password_hash` isn't reversible, but Subsonic's `t = md5(password + salt)` needs the plaintext. Storing it separately, opt-in, means the default posture is secure and legacy clients can still be supported without compromising the web-login password.
**Endpoints:**
- `/rest/ping(.view)` — envelope-only ok.
- `/rest/getLicense(.view)` — always-valid license (required by many clients before streaming).
**Envelope:**
- JSON wrapped in `{"subsonic-response": {…}}`, XML with `xmlns=http://subsonic.org/restapi`, JSONP wrapped in `callback(…)` (falls back to plain JSON if callback is empty).
- Each endpoint exposed at both `/rest/name` and `/rest/name.view`, GET + POST.
**Docs:**
- `config.example.yaml` refreshed to include `auth.admin_bootstrap`, `library`, and `subsonic` sections (first two were stale from #292/#293).
## Verification
- `go test ./...` — green, including integration tests against ephemeral Postgres (migration 0003 applies clean).
- `go build ./...` / `go vet ./...` / `gofmt -l .` — clean.
- Live smoke against `cmd/minstrel`:
- `/healthz` → 200 ok
- `/rest/ping.view` no creds → error code 10
- `u+t+s` (correct) → ok
- `u+t+s` (wrong) → error 40
- `apiKey` → ok
- `getLicense.view?apiKey=…` → `license.valid=true`
- JSONP wrap confirmed
- XML with xmlns confirmed
- `p=` with plaintext disabled → error 41
- `p=hunter2` with plaintext enabled → ok
- `p=enc:68756e74657232` (hex) with plaintext enabled → ok
## Test plan
- [x] Unit tests for envelope JSON/XML/JSONP and WriteFail
- [x] Unit tests for `decodePassword` enc/plain/bad-hex
- [x] MD5 derivation test documenting `t = md5(password + salt)`
- [x] CI on Forgejo Actions (pending merge)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Foundation of the
/rest/*Subsonic-compatible surface. Covers Fable task #294.Architecture — hybrid auth posture:
/api/*surface gets modern auth (apiKey bearer + OIDC later)./rest/*ships Subsonic-compatible auth for third-party clients (Symfonium, Substreamer, DSub, Tempo, play:Sub).u+t+stoken auth is per-user opt-in via a newusers.subsonic_passwordcolumn. Plaintextp=is disabled by default and gated bySMARTMUSIC_SUBSONIC_ALLOW_PLAINTEXT_PASSWORD.subsonic_passwordis a separate column: bcryptpassword_hashisn't reversible, but Subsonic'st = md5(password + salt)needs the plaintext. Storing it separately, opt-in, means the default posture is secure and legacy clients can still be supported without compromising the web-login password.Endpoints:
/rest/ping(.view)— envelope-only ok./rest/getLicense(.view)— always-valid license (required by many clients before streaming).Envelope:
{"subsonic-response": {…}}, XML withxmlns=http://subsonic.org/restapi, JSONP wrapped incallback(…)(falls back to plain JSON if callback is empty)./rest/nameand/rest/name.view, GET + POST.Docs:
config.example.yamlrefreshed to includeauth.admin_bootstrap,library, andsubsonicsections (first two were stale from #292/#293).Verification
go test ./...— green, including integration tests against ephemeral Postgres (migration 0003 applies clean).go build ./.../go vet ./.../gofmt -l .— clean.cmd/minstrel:/healthz→ 200 ok/rest/ping.viewno creds → error code 10u+t+s(correct) → oku+t+s(wrong) → error 40apiKey→ okgetLicense.view?apiKey=…→license.valid=truep=with plaintext disabled → error 41p=hunter2with plaintext enabled → okp=enc:68756e74657232(hex) with plaintext enabled → okTest plan
decodePasswordenc/plain/bad-hext = md5(password + salt)🤖 Generated with Claude Code