Skip to content

feat(contracts,api): access model v2 P1a - Free mode, artist mode changes, free keys#76

Merged
knzeng-e merged 9 commits into
mainfrom
feat/access-v2-p1
Jul 9, 2026
Merged

feat(contracts,api): access model v2 P1a - Free mode, artist mode changes, free keys#76
knzeng-e merged 9 commits into
mainfrom
feat/access-v2-p1

Conversation

@knzeng-e

@knzeng-e knzeng-e commented Jul 8, 2026

Copy link
Copy Markdown
Owner

First implementation slice of the approved v2 design (#75, docs/design/dotify-v2-access-and-streaming.md, ticket 24). This is the chain + key-service half of P1; the web half (UI modes + preview-machinery deletion) follows as P1b on top.

Contracts (artist SmartRuntime pallets)

  • AccessMode.Free (=2, appended - storage-safe for the enum layout). Free tracks carry no price and no personhood requirement; registration normalizes both to zero.
  • musicRegSetAccessMode (artist-only, active tracks): change a track's access policy without re-uploading or re-pinning - ciphertext and CID never change, only the key-release policy. Emits TrackAccessModeChanged. Past Classic buyers keep access through paidAccess across mode flips (tested both directions).
  • musicAccCanAccess grants Free tracks to everyone; musicRoyPayAccess still rejects non-Classic tracks (no paying for free music by accident).
  • New selector routed in ArtistRuntimeFactory; ABIs regenerated into web/src/generated.
  • 8 new hardhat tests (34 passing): Free registration + normalization + payment rejection, the Classic->Free->Classic flip matrix including buyer survival, artist-only guard, policy validation on change, inactive guard + event assertion.

Key service

  • checkPublicAccess(contentHash): probes musicAccCanAccess(hash, address(0)) - the zero address is never artist/owner/buyer/verified, so this returns true iff the track's current mode grants access to everyone. No new ABI, chain-authoritative, fail-closed (RPC down / not found / ambiguous -> denied).
  • POST /api/tracks/:contentHash/free-key: releases the content key for Free tracks with no signature, no wallet, no session - free must feel free. Covered by the API-wide rate limit; a track flipped back to paid stops being served on the very next request.
  • Denials lose the 42% preview framing: no more playbackMode: 'preview' / previewRatio - a denial is a reason + unlock CTA. Runtime-compatible with the current frontend (it only reads access/contentKey); client types update in P1b.
  • 27 API tests passing (4 new free-key cases), typecheck clean.

Notes for review

  • The enum append is deliberate: existing deployed runtimes are immutable diamonds per artist; new runtimes get the new pallets on next deploy. Existing tracks keep decoding correctly (0/1 unchanged).
  • Deployment required to exercise on testnet: redeploy pallets + factory (the frozen Hub TestNet deployment noted in the repo memory is unaffected until you choose to redeploy).
  • Pre-existing uncommitted contracts/ working-tree changes (README, hardhat.config, deploy scripts) are excluded, as in previous PRs.

Verification: hardhat 34/34, API 27/27 + tsc --noEmit clean, web npm run build clean against the regenerated ABI. ASCII-only additions.

🤖 Generated with Claude Code

…ree keys

Ticket 24 P1, chain + key-service half of the approved v2 pivot:

Contracts (artist SmartRuntime pallets):
- AccessMode gains Free (=2, appended: storage-safe). Free tracks have no
  price and no personhood requirement; registration normalizes both to zero.
- musicRegSetAccessMode (artist-only, active tracks): change a track's access
  policy without re-uploading or re-pinning - the ciphertext and CID never
  change, only the key-release policy. Emits TrackAccessModeChanged. Past
  Classic buyers keep access through paidAccess across mode flips.
- musicAccCanAccess grants Free tracks to everyone; payAccess still rejects
  non-Classic tracks. New selector routed in ArtistRuntimeFactory.
- 8 new hardhat tests (Free registration/normalization/no-payment, mode-flip
  matrix incl. buyer survival, artist-only guard, policy validation on
  change, inactive guard + event). 34 passing. ABIs regenerated.

Key service:
- checkPublicAccess(contentHash): probes musicAccCanAccess with the zero
  address - true iff the track's CURRENT mode grants access to everyone.
  Chain-authoritative, fail-closed like checkTrackAccess.
- POST /api/tracks/:contentHash/free-key: releases the key for Free tracks
  with no signature, wallet, or session (free must feel free); covered by
  the API-wide rate limit. A track flipped back to paid stops being served
  on the next request.
- Denials lose the 42% preview framing (playbackMode/previewRatio retired):
  a denial is a reason plus an unlock CTA, never a degraded playback mode.
  Runtime-compatible with the current frontend (it reads only access and
  contentKey); client types follow in the P1b web PR.
- 27 API tests passing (4 new free-key cases), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@netlify

netlify Bot commented Jul 8, 2026

Copy link
Copy Markdown

Deploy Preview for muzinga ready!

Name Link
🔨 Latest commit 354410d
🔍 Latest deploy log https://app.netlify.com/projects/muzinga/deploys/6a501725493211000844346a
😎 Deploy Preview https://deploy-preview-76--muzinga.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

…deleted

The web half of ticket 24 P1, on top of P1a (Free mode contracts + free-key
endpoint):

Free mode, end to end:
- AccessMode gains 'free' in the app types, chain encoding (free=2), publish
  UI (third "Free for everyone" door + review copy), and catalog/release
  labels.
- Guests probe access with the zero address, so Free tracks resolve playable
  with no wallet; requestFreeContentKey fetches their key with no signature.
  Gated tracks keep the signed path unchanged.

Preview machinery deleted (the doctrine is retired, not hidden):
- useCatalog: 42% slicing, preview cutoff/limit, preview-asset resolution,
  and the preview branches of fetch/decrypt are gone. Access is binary:
  authorized plays full, unauthorized gets the unlock gate and no audio.
- PlaybackProvider/PersistentAudio/ListenerShell lose the cutoff wiring;
  playbackModeForAccess removed from accessPolicy; the WAV preview encoders
  (shared/utils/audio.ts + test) are deleted outright.
- Publish no longer generates preview assets (uploadPreviewToBackend gone);
  the API /api/uploads/preview route and its tests are removed. previewCID
  stays optional in the manifest schema so already-pinned manifests validate;
  nothing produces or reads it.
- Room hosts without access stream nothing and move the room to a playable
  track. The room playback-mode wire protocol stays (always 'full') for
  compatibility; removal is scheduled with the signaling cleanup.
- UI copy de-previewed (locked state, unlock CTA, "Free to discover") and the
  e2e specs rewritten to v2 expectations (classic: locked-not-preview;
  room-join: no-stream-for-unauthorized-host, manual advance). Playwright not
  run here - needs a local pass.

Docs: threat model (access modes + retired preview boundary), web README
access policy, backlog doctrine + ticket rows, ticket 18 retirement note,
ticket 24 P1 delivery notes, and docs/index.html positioning (same-PR Pages
alignment rule).

Web: 90 unit tests, lint 0 errors, build clean. API: 24 tests, typecheck
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One SIWE-style signature opens a ~24h session; every later key request rides
a bearer token instead of a fresh wallet signature. The token proves identity
only - the on-chain access check still runs per track on every request.

Key service:
- sessionTokens.ts: HMAC-SHA256 tokens over a strict claim shape, key
  HKDF-derived from CONTENT_KEY_MASTER_SECRET with a dedicated info label (no
  new secret, no shared bytes with content keys), constant-time verify, 24h
  TTL, jti revocation (in-memory, process-lifetime - documented boundary).
- signatures.ts: canonical SIGN_IN message + verifySignInRequest (expiry ->
  signature -> nonce consumption, same fail-closed order).
- POST /api/auth/session (401 on bad signature, 503 when unconfigured so the
  per-request path remains) and idempotent POST /api/auth/logout.
- key-request route accepts {sessionToken, purpose}; the chain check runs
  against the token's address, never client input. 13 new tests (37 total).

Web:
- ensureDotifySession: reuse the stored per-address token (refresh margin),
  else one sign-in signature; requestContentKey rides the token, retries once
  with a fresh sign-in on 401, and falls back to per-request signing when the
  backend lacks session support (404/503).
- Wallet disconnect also signs the session out server-side and forgets the
  stored token.

Threat model gains the session-auth boundary section; ticket 24 delivery
notes updated. API 37/37 + typecheck; web 90 unit, lint 0 errors, build ok.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@knzeng-e
knzeng-e merged commit a5addcf into main Jul 9, 2026
10 checks passed
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