feat(subsonic): auth + envelope + ping.view + getLicense.view (Fable #294) #7

Merged
bvandeusen merged 262 commits from dev into main 2026-04-19 14:10:13 -04:00
bvandeusen commented 2026-04-19 13:41:00 -04:00 (Migrated from git.fabledsword.com)

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

  • 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)
  • CI on Forgejo Actions (pending merge)

🤖 Generated with Claude Code

## 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)
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/minstrel#7