Skip to content

feat(share): mint + revoke by canonical id (phase 1.g.1b)#27

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/1-g-1b-share-by-canonical
Jun 4, 2026
Merged

feat(share): mint + revoke by canonical id (phase 1.g.1b)#27
InstaZDLL merged 2 commits into
mainfrom
feat/1-g-1b-share-by-canonical

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented Jun 4, 2026

Summary

Closes the API loop for desktop sharing. Without this, the desktop would have to first push a sync_op, wait for the apply pipeline (Phase 1.g.0, PR #26) to materialise the playlist row, then look up its server BIGSERIAL id, then call the classic mint endpoint with that id — three round-trips for what is conceptually one operation.

What lands

Two new JWT-authed routes parallel to the classic mint / revoke:

  • POST /api/v1/share/playlists/by-canonical/{profile_canonical_id}/{playlist_canonical_id}
  • DELETE /api/v1/share/playlists/by-canonical/{profile_canonical_id}/{playlist_canonical_id}

Same idempotent `COALESCE(share_token, $candidate)` SQL pattern under the hood, same no-existence-leak 404 shape. The tenant chain becomes `(user_id, profile.canonical_id, playlist.canonical_id)` — the playlist must already have been materialised by the apply pipeline, so a desktop user can only mint for playlists their own canonical ids point at.

New helpers in `db::share` keep raw SQL out of the handlers per the project's convention.

Tests

4 new integration tests in `tests/share.rs`:

  1. `by_canonical_mint_resolves_via_public_get` — end-to-end (sync_op push → apply materialises → mint by canonical → public GET resolves).
  2. `by_canonical_mint_is_idempotent` — second mint returns the same token.
  3. `by_canonical_revoke_closes_the_link` — public URL 404s after revoke.
  4. `by_canonical_foreign_profile_is_404` — Bob can't mint for Alice's canonical ids; also Bob can't mint with an unknown profile_canonical.

Shared `materialise_playlist_via_sync` helper mirrors what the desktop drain task will do once Phase 1.g.0-desktop ships.

Test plan

  • `cargo fmt --all --check` clean
  • `cargo clippy --all-targets --all-features -- -D warnings` clean
  • `cargo test --test share` → 11/11 (7 existing + 4 new)
  • CI confirms across ubuntu + windows

Summary by CodeRabbit

  • Nouvelles fonctionnalités

    • Ajout d’endpoints pour gérer le partage de playlists via identifiants canoniques : génération/récupération de jetons et révocation.
    • Opérations protégées par authentification JWT et conçues pour être idempotentes et multi-tenant.
  • Tests

    • Scénarios automatisés couvrant mint/get, idempotence, révocation, ré-émission de token et isolation multi-tenant.

Closes the API loop for desktop sharing. Without this, the
desktop would have to first push a sync_op, wait for the apply
pipeline to materialise the playlist row, then look up its
server BIGSERIAL id, then call the classic mint endpoint with
that id — three round-trips for what is conceptually one
operation.

Two new JWT-authed routes:

- POST /api/v1/share/playlists/by-canonical/{profile_canonical_id}/{playlist_canonical_id}
- DELETE /api/v1/share/playlists/by-canonical/{profile_canonical_id}/{playlist_canonical_id}

Same semantics as the classic mint / revoke, same idempotent
COALESCE(share_token, $candidate) under the hood, same no-
existence-leak 404 shape. The tenant chain becomes (user_id,
profile.canonical_id, playlist.canonical_id) — the playlist
must already have been materialised by the apply pipeline, so a
desktop user can only mint for playlists their own canonical
ids point at.

Mirrored helpers in db::share — mint_or_get_token_by_canonical
and revoke_token_by_canonical — keep raw SQL inside the db
layer per the project's no-SQL-in-handlers rule.

Tests (tests/share.rs): 4 new integration tests covering the
end-to-end flow (sync_op push -> apply materialises -> mint by
canonical -> public GET resolves), idempotency, revoke closing
the public URL, and 404 for foreign / unknown canonical ids.
Shared materialise_playlist_via_sync helper mirrors what the
desktop drain task will do once the desktop side of Phase 1.g.0
ships.

Local: cargo fmt + cargo clippy --all-targets --all-features --
-D warnings clean; cargo test --test share = 11/11.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ec97c2e7-7baf-4a79-bf3b-54b6b8fb4d14

📥 Commits

Reviewing files that changed from the base of the PR and between b65cd4c and 10df360.

📒 Files selected for processing (1)
  • tests/share.rs

📝 Walkthrough

Walkthrough

Le PR ajoute POST/DELETE pour mint/revoke de tokens de partage via identifiants canoniques, implémente deux fonctions DB race-free correspondantes, et ajoute des tests e2e vérifiant idempotence, remint après revoke et isolation multi-tenant.

Changes

Partage par identifiants canoniques

Layer / File(s) Summary
Fonctions DB : mint/revoke par canonical ID
src/db.rs
mint_or_get_token_by_canonical et revoke_token_by_canonical implémentent la logique transactionnelle (COALESCE, vérification d'appartenance via profile) pour opérer sur playlist.canonical_id et profile.canonical_id.
Endpoints API : mint/revoke par canonical ID
src/api/share.rs
Documentation mise à jour, enregistrement des routes dans auth_router, deux handlers #[utoipa::path] : POST (200/MintResponse, 404, 500) et DELETE (204, 404, 500) avec logging structuré incluant les canonical ids.
Tests e2e : endpoints par canonical ID
tests/share.rs
Ajout de use uuid::Uuid;, utilitaire materialise_playlist_via_sync, et batterie de sqlx::test : mint, idempotence, revoke + GET public 404, remint produit token différent, et vérifications d'isolation multi-tenant.

Sequence Diagram

sequenceDiagram
  participant User as Client JWT-authed
  participant MintHandler as POST /api/v1/share/playlists/by-canonical/{...}
  participant MintDB as mint_or_get_token_by_canonical
  participant RevokeHandler as DELETE /api/v1/share/playlists/by-canonical/{...}
  participant RevokeDB as revoke_token_by_canonical
  participant DB as PostgreSQL
  participant PublicGet as GET /api/v1/share/playlists/{token}

  User->>MintHandler: POST (profile_canonical_id, playlist_canonical_id)
  MintHandler->>MintDB: call(user_id, canonical_ids)
  MintDB->>DB: COALESCE + ownership check
  DB-->>MintDB: Option<String>
  MintDB-->>MintHandler: Ok(Some(token))
  MintHandler-->>User: 200 { token }

  User->>PublicGet: GET token
  PublicGet->>DB: resolve playlist by token
  DB-->>PublicGet: playlist details
  PublicGet-->>User: 200 PublicPlaylistResponse

  User->>RevokeHandler: DELETE (profile_canonical_id, playlist_canonical_id)
  RevokeHandler->>RevokeDB: call(user_id, canonical_ids)
  RevokeDB->>DB: UPDATE share_token=NULL
  DB-->>RevokeDB: rows_affected > 0
  RevokeDB-->>RevokeHandler: Ok(true)
  RevokeHandler-->>User: 204 No Content

  User->>PublicGet: GET token (revoked)
  PublicGet->>DB: lookup (token cleared)
  DB-->>PublicGet: not found
  PublicGet-->>User: 404
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • InstaZDLL/waveflow-server#25: Introduit la couche share initiale avec mint_or_get_token, revoke_token et GET /api/v1/share/playlists/{token} ; ce PR étend la surface avec variantes canoniques.

Poem

🎵 Sur routes canoniques le partage s'allume,
Jetons forgés, puis détruits quand l'ombre fume.
Tenant gardien, la barrière tient,
Un mint, un revoke — le flux revient.
Petite danse de tokens, claire et brune.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Le titre décrit précisément les deux nouvelles fonctionnalités ajoutées : mint et revoke par identifiants canoniques, avec référence à la phase du projet.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1-g-1b-share-by-canonical

Comment @coderabbitai help to get the list of available commands and usage tips.

@InstaZDLL InstaZDLL self-assigned this Jun 4, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/share.rs`:
- Around line 307-309: Add a missing test that verifies revoke + remint produces
a new token for the by-canonical surface: create a new async sqlx::test named
by_canonical_revoke_then_remint_returns_fresh_token (migrator =
"waveflow_server::db::MIGRATOR") in tests/share.rs that mirrors
revoke_then_remint_returns_a_fresh_token but uses the by-canonical endpoints
(the POST/DELETE to
/api/v1/share/playlists/by-canonical/{PROF_CANON}/{pl_canon}); use
spawn_authenticated, materialise_playlist_via_sync, mint the initial token,
DELETE to revoke, re-POST to remint, extract tokens from JSON responses and
assert_ne!(token1, token2) to ensure mint_or_get_token_by_canonical’s
COALESCE+NULL path produces a fresh token after revocation.
- Around line 56-90: The helper materialise_playlist_via_sync currently uses
fixed device_id ("share-by-canonical-test") and lamport_ts = 1 which can violate
the UNIQUE (user_id, device_id, lamport_ts) constraint if called multiple times;
update materialise_playlist_via_sync to produce unique sync identifiers per
call—either accept a lamport_ts (or device_id) parameter or generate a
colliding-safe value inside the function (e.g., increment lamport_ts, use a
random/UUID device_id, or derive a timestamp-based lamport) and include that
generated value in the JSON payload so repeated calls do not collide.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f4cf3b10-bddf-4bf8-b1b2-cf151f530a35

📥 Commits

Reviewing files that changed from the base of the PR and between f4215df and b65cd4c.

📒 Files selected for processing (3)
  • src/api/share.rs
  • src/db.rs
  • tests/share.rs

Comment thread tests/share.rs
Comment thread tests/share.rs
…e helper

Two findings from CodeRabbit on PR #27:

The materialise_playlist_via_sync helper was using a fixed
device_id ("share-by-canonical-test") and lamport_ts = 1, which
would 409 the second call inside the same test via the
(user_id, device_id, lamport_ts) UNIQUE. No current test hits
this, but the helper is a footgun for any future scenario that
needs more than one playlist. Swapped device_id to a fresh UUID
per call — every materialisation now plays the role of a
distinct device, so the lamport scope is per-call and
lamport_ts = 1 stays valid.

Added by_canonical_revoke_then_remint_returns_fresh_token to
mirror the classic-surface coverage. Pins the COALESCE-on-NULL
path: after a revoke clears share_token, the next mint must
generate a new candidate rather than re-using the prior value.
Same shape as revoke_then_remint_returns_a_fresh_token, just
against the by-canonical endpoints.

cargo fmt + cargo clippy --all-targets --all-features -- -D
warnings clean; cargo test --test share = 12/12.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@InstaZDLL
Copy link
Copy Markdown
Owner Author

@coderabbitai both findings addressed in 10df360.

1. Missing revoke + remint coverage — added by_canonical_revoke_then_remint_returns_fresh_token mirroring revoke_then_remint_returns_a_fresh_token for the by-canonical surface. Pins the COALESCE-on-NULL path: after a revoke clears share_token, the next mint must generate a fresh candidate. assert_ne!(t1, t2) keeps it honest.

2. Collision-risk in materialise_playlist_via_sync — sharp catch. Fixed by minting a fresh UUID for device_id per call. Every materialisation now plays a distinct device, so the (user_id, device_id, lamport_ts) UNIQUE stays per-call and lamport_ts = 1 keeps working. Simpler than threading a counter through callers.

Local: cargo fmt + clippy strict + 12/12 share tests.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Parfait — les deux points sont bien couverts.

  • Revoke + remint : assert_ne!(t1, t2) est exactement le bon guard ; ça force l'exécution du chemin COALESCE(NULL, $candidate) et évite un faux-positif si le token précédent était réutilisé par accident.
  • UUID par appel : choix propre. Un compteur externe aurait fuité dans les signatures de test ; un Uuid::new_v4() inline est autonome et suffit.

12/12 ✅ — bon pour la suite.

(◕‿◕✿)🐇

@InstaZDLL InstaZDLL merged commit 06c7485 into main Jun 4, 2026
8 checks passed
@InstaZDLL InstaZDLL deleted the feat/1-g-1b-share-by-canonical branch June 4, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant