Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/dco.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ jobs:
author_email=$(git log -1 --format='%ae' "$sha")
author_name=$(git log -1 --format='%an' "$sha")
expected="Signed-off-by: ${author_name} <${author_email}>"
# GitHub authors Dependabot commits with its noreply identity but
# signs the generated message with its documented support address.
if [ "$author_name" = "dependabot[bot]" ] && [ "$author_email" = "49699333+dependabot[bot]@users.noreply.github.com" ]; then
expected="Signed-off-by: dependabot[bot] <support@github.com>"
fi
if ! git log -1 --format='%B' "$sha" | grep -qFx "$expected"; then
echo "::error::Commit $sha is missing the DCO sign-off line."
echo " Expected trailer: $expected"
Expand Down
111 changes: 108 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ legacy-v1 = []
anyhow = "1"
async-stream = "0.3"
argon2 = { version = "0.5", features = ["std"] }
axum = "0.8"
axum = { version = "0.8", features = ["ws"] }
base64 = "0.22"
blake3 = "1"
bytes = "1"
Expand All @@ -43,7 +43,7 @@ sha2 = "0.10"
sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "sqlite", "macros", "migrate", "uuid"] }
subtle = "2"
thiserror = "2"
tokio = { version = "1", features = ["fs", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time"] }
tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] }
tokio-stream = { version = "0.1", features = ["sync"] }
tokio-util = { version = "0.7", features = ["io"] }
tower = { version = "0.5", features = ["util"] }
Expand All @@ -60,6 +60,7 @@ waveflow-core = { git = "https://github.com/InstaZDLL/WaveFlow", rev = "d4c44eb5
[dev-dependencies]
http-body-util = "0.1"
tempfile = "3"
tokio-tungstenite = "0.29"

[[test]]
name = "v2_foundations"
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,15 @@ The server listens on `127.0.0.1:4533` by default and exposes:
- `GET /health`: process liveness;
- `GET /ready`: SQLite readiness, independent of scan progress;
- `GET /openapi.json` and `GET /reference`: API contract;
- `POST /api/v2/auth/login`, `/refresh`, `/logout`: rotating local sessions.
- `POST /api/v2/auth/login`, `/refresh`, `/logout`: rotating native sessions;
- `/api/v2/web/auth/*`: memory-only browser access token plus HttpOnly rotating
refresh cookie, origin validation and CSRF protection;
- `/api/v2/sync/snapshot`, `/changes`, `/ack`, `/socket`: idempotent user-data
synchronization defined by `docs/rfcs/RFC-003-waveflow-sync-v2.md`;
- `/api/v2/admin/users`, `/libraries`, `/transcode/status`: native server
administration and dedicated Subsonic credential rotation;
- `PUT|DELETE /api/v2/admin/users/{username}/subsonic-credential`: rotate or
revoke the dedicated Subsonic password and API key;
- `POST /api/v2/libraries/{id}/scans`: manual scan trigger;
- `GET /api/v2/scans/{id}` and `/events`: status and SSE progress;
- `GET /api/v2/libraries/{id}/tracks?q=...&offset=...&limit=...`: tenant-scoped catalogue/FTS browsing, paged up to 500 tracks per request.
Expand All @@ -47,7 +55,7 @@ The server listens on `127.0.0.1:4533` by default and exposes:

For browser-hosted clients such as Feishin, list every trusted origin explicitly, for example `WAVEFLOW_ALLOWED_ORIGINS=http://127.0.0.1:9180,https://music.example.com`. Wildcards are rejected so credential-bearing Subsonic requests cannot be opened to arbitrary sites.

Set `WAVEFLOW_PUBLIC_URL=https://music.example.com` behind the reverse proxy so `createShare` returns absolute, externally usable URLs. When it is omitted, share URLs remain relative to the server origin.
Set `WAVEFLOW_PUBLIC_URL=https://music.example.com` behind the reverse proxy so `createShare` returns an absolute, externally usable URL at creation. An authenticated idempotent retry of that creation returns the same URL, but later share reads and sync snapshots omit it because only its hash is persisted. When the setting is absent, the creation response uses a URL relative to the server origin.

Create or restore a coherent database/key bundle:

Expand Down
42 changes: 28 additions & 14 deletions docs/M4-handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,28 @@ la façade Subsonic.

```text
GET /api/v2/albums · /albums/{id} · /artists · /artists/{id} · /search
GET /api/v2/tracks/{id}
GET /api/v2/playlists · POST · PATCH · DELETE
GET /api/v2/favorites · PUT|DELETE /favorites/{kind}/{id}
PUT /api/v2/ratings/{kind}/{id} · POST /scrobbles · GET /now-playing
GET|PUT /api/v2/queue
GET|POST /api/v2/shares · PATCH|DELETE /api/v2/shares/{share_id}
GET|POST /api/v2/libraries
PUT|DELETE /api/v2/libraries/{library_id}/members/{user_id}
GET|POST /api/v2/admin/users · PATCH|DELETE /api/v2/admin/users/{username}
PUT|DELETE /api/v2/admin/users/{username}/subsonic-credential
GET /api/v2/sync/snapshot · /changes · WS /sync/socket · PUT /sync/ack
POST /api/v2/oauth/authorize · POST /api/v2/oauth/token
POST /api/v2/tracks/{id}/stream-ticket · GET /api/v2/stream/{ticket}
GET /api/v2/tracks/{track_id}/stream
POST /api/v2/tracks/{track_id}/stream-ticket · GET /api/v2/stream/{ticket}
```

**Client web embarqué** (`webapp/`, Vite + React + TanStack Router), compilé
dans le binaire par `rust_embed`. Connexion, albums, artistes, recherche,
favoris, lecture, écran de consentement OAuth.
favoris, playlists, file d'attente persistante, partages, lecture, écran de
consentement OAuth et administration des bibliothèques, scans, comptes et
identifiants Subsonic. Le lecteur n'est monté qu'après authentification afin de
charger la file du bon compte à chaque nouvelle session.

**Retrait de la v1** : `src/api/*`, `db.rs`, `apply.rs`, `sync.rs`, les
migrations PostgreSQL et 21 fichiers de tests. Tout était déjà mort (non déclaré
Expand Down Expand Up @@ -104,8 +115,12 @@ régression.

## Ce qui reste

1. **Trancher la sécurité de session navigateur avant `v2.0` stable**, comme
détaillé dans les dettes ci-dessous.
1. **Valider puis fusionner le complément serveur M4.** Il ajoute le journal de
synchronisation documenté par RFC-003, complète l'administration native,
ferme la dette de session navigateur et livre tous les parcours fonctionnels
du client web prévus pour M4. Le workflow DCO accepte désormais l'adresse de
signature réellement émise par Dependabot, sans dérogation manuelle aux
protections de branche.
Comment thread
InstaZDLL marked this conversation as resolved.
2. **Taguer une release uniquement sur demande explicite du user.** M3 et sa
validation Symfonium sont terminés ; aucune action de compatibilité ne reste
ouverte pour cette porte.
Expand All @@ -127,13 +142,12 @@ vérifié.
## Dettes identifiées, non traitées

- `search3` n'exploite pas FTS5 (voir ci-dessus).
- `webapp/` n'a pas de test de composant ni de parcours : la suite couvre les
gardes de redirection et les design tokens. La CI web lint (biome), construit
et lance vitest.
- Les jetons de session vivent en `localStorage`, donc exposés à une XSS. C'est
le compromis SPA habituel ; un cookie éviterait cela mais ajouterait une
authentification ambiante et une surface CSRF à une API sinon purement par
en-tête. **Porte de sortie : trancher avant le tag `v2.0` stable** (pas avant
la beta) — soit adopter un cookie `httpOnly` + protection CSRF, soit acter le
risque par écrit dans ce document avec la justification retenue. Ne pas taguer
la stable tant que l'une des deux branches n'est pas tranchée.
- `webapp/` n'a pas encore de test de composant ou de parcours automatisé. La
suite couvre les gardes de redirection et les design tokens ; un smoke test
navigateur manuel sur installation vide valide setup, session, rôles,
playlists, favoris, queue, partages, bibliothèques, scans, comptes et rotation
d'identifiant Subsonic. La CI web lint (Biome), construit et lance Vitest.
- La dette de session navigateur est fermée par le complément M4 : access token
court en mémoire, refresh rotatif dans un cookie HttpOnly/SameSite, contrôle
d'origine et double-submit CSRF sur refresh/logout. Aucun secret de session
n'est conservé dans `localStorage`.
4 changes: 2 additions & 2 deletions docs/rfcs/RFC-002-waveflow-server-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Audio files are always read-only. Canonical-path and symlink checks apply before

The M3 beta exposes a tested Subsonic/OpenSubsonic façade. GET, form POST, XML and JSON share the same services. Only implemented extensions are advertised. Credentials in query parameters are removed from request logging.

M4 adds `/api/v2`, Authorization Code with PKCE for WaveFlow Desktop, rotating native tokens and user-data-only synchronization. The server catalogue appears in Desktop as a separate remote source. Existing local and server catalogues are not automatically merged.
M4 adds `/api/v2`, Authorization Code with PKCE for WaveFlow Desktop, rotating native tokens and user-data-only synchronization. The server catalogue appears in Desktop as a separate remote source. Existing local and server catalogues are not automatically merged. The cursor, idempotency, ACK and WebSocket contracts are frozen in [RFC-003](RFC-003-waveflow-sync-v2.md).

All web, native and Subsonic writes pass through common services so playlists, favorites, ratings, queue and history converge independent of the calling protocol.

Expand All @@ -62,7 +62,7 @@ Mutation methods whose Subsonic result is empty (`updatePlaylist`, `deletePlayli

Cross-origin access is disabled unless the operator supplies an exact comma-separated allow-list through `WAVEFLOW_ALLOWED_ORIGINS`. Allowed origins may use GET, form POST and OPTIONS and may read the byte-range response headers needed for web playback; wildcard origins are not accepted.

Original downloads and streams use repository authorization and the M2 path guard. They forward valid byte ranges to originals and completed cache entries, including 206/416 response semantics; live transcodes still require temporal `timeOffset` seeking. Requested MP3/Opus transcodes use the same FFmpeg/cache service as `/api/v2`. Without an explicit output format, `maxBitRate` is a ceiling: WaveFlow serves the original when its known bitrate is at or below the ceiling and otherwise transcodes to MP3; unknown source bitrate is conservatively transcoded. `getCoverArt` accepts an authorized track, album, artist or content hash. Public share URLs contain a high-entropy token; its lookup hash and encrypted recoverable form are stored separately so `getShares` can reproduce the URL without storing the token in plaintext. `WAVEFLOW_PUBLIC_URL` supplies the external HTTP(S) origin; otherwise relative URLs are returned. The public metadata response supplies token-scoped per-track stream URLs with the same Range/transcode service, and a share cannot stream a track outside its persisted membership. Share tokens are redacted from request trace paths.
Original downloads and streams use repository authorization and the M2 path guard. They forward valid byte ranges to originals and completed cache entries, including 206/416 response semantics; live transcodes still require temporal `timeOffset` seeking. Requested MP3/Opus transcodes use the same FFmpeg/cache service as `/api/v2`. Without an explicit output format, `maxBitRate` is a ceiling: WaveFlow serves the original when its known bitrate is at or below the ceiling and otherwise transcodes to MP3; unknown source bitrate is conservatively transcoded. `getCoverArt` accepts an authorized track, album, artist or content hash. Public share URLs contain a high-entropy bearer token derived with keyed BLAKE3 from the instance key and immutable share UUID; only its lookup hash is persisted. The URL is returned by the successful creation response and authenticated idempotent replays of that operation, while later share reads, updates and synchronization snapshots omit it. `WAVEFLOW_PUBLIC_URL` supplies the external HTTP(S) origin; otherwise the creation response uses a relative URL. The public metadata response supplies token-scoped per-track stream URLs with the same Range/transcode service, and a share cannot stream a track outside its persisted membership. Share tokens are redacted from request trace paths.

### Reconciliation

Expand Down
Loading
Loading