feat(sdk): sign preview generation requests - #14552
Closed
rickyrombo wants to merge 1 commit into
Closed
Conversation
🦋 Changeset detectedLatest commit: 7c6f6d9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
generatePreview asks a storage node to produce new bytes from a cid, and the node now attests the resulting cid on chain so it can be named as a track's preview. It will only do that for a user who already owns the source audio, so the request has to say who is asking. Reuses the upload request signature rather than inventing a second scheme, so both audio paths recover a signer the same way. Unsigned when there is no wallet or no user id, matching how uploadFile behaves: nodes accept those where content authorization is not yet enforced, and the preview simply never earns a claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rickyrombo
force-pushed
the
feat/signed-preview-generation
branch
from
August 8, 2026 07:10
033a340 to
7c6f6d9
Compare
rickyrombo
added a commit
that referenced
this pull request
Aug 8, 2026
#14556) Follow-up bug fix from the content-auth review of #14550/#14552 — independent of the redesign in #14555, works against today's servers. ## The bug `useUpdateTrack` never passes `generatePreview` to `sdk.tracks.updateTrack`, and the SDK's `updateTrack` only calls `generate_preview` when that flag is set. So editing a gated track's preview start point updates `preview_start_seconds` in the track metadata while the track keeps streaming the old `preview_cid`'s clip — the preview is never re-sliced at the new offset. As far as I can trace, nothing else regenerates it (`populateTrackMetadataWithUploadResponseV2` only touches `previewCid` when a new audio file was uploaded). ## The fix Pass `generatePreview: true` when the preview start is set and differs from the cached previous track's value: - The SDK already guards the rest: it skips regeneration when a new `audioFile` is present (transcoding produces the preview then) and when `previewStartSeconds` is undefined. - A cache miss for the previous track errs toward regenerating, which is idempotent for an unchanged offset. Once #14555 lands, this call site inherits the attributed `generate_preview` request automatically — `updateTrack` threads its `userId` internally. ## Testing `packages/common` typechecks clean; eslint clean on the touched file. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Marcus Pasell <marcus@audius.co> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
rickyrombo
added a commit
that referenced
this pull request
Aug 8, 2026
… (no signatures) (#14555) Fresh implementation of upload/preview attribution on the unsigned design path. **Supersedes #14550, #14552 and #14554** — the client-side EIP-712 signing approach — which can be closed in favor of this. Server counterpart: `OpenAudio/go-openaudio` branch `feat/content-auth-unsigned` (PR opening alongside), stacked on OpenAudio/go-openaudio#459. ## Design Audio uploads and `generate_preview` carry the id of the user they are made for — plain tus metadata / query parameter, **no signature and no wallet client**. The id is an assertion; what makes the scheme safe is unchanged from the analysis on the server PR: - Asserting someone else's id only ever credits *them* — exercising a claim (naming the cid on a track) happens in a signed, grant-checked entity-manager write. - Upload claims derive from bytes the node received; preview claims require the asserted user to already claim the source cid. - A minted preview cid is useless to the requester: audio never serves by bare cid, and discovery only signs track-resolved cidstream URLs. Dropping the signature is what keeps every SDK flow working — including OAuth apps with no client-side wallet — with nothing but this parameter. No proxy, no wallet plumbing, no timestamp windows. ## BREAKING (major changeset included) - `tracks.uploadTrackFiles` requires `userId` (encoded) - `uploads.createAudioUpload` requires `userId` (encoded) - `Storage.generatePreview` requires `userId` (decoded) and sends it as a query param High-level methods (`createTrack`, `updateTrack`, `uploadTrack`, `publishTrack`) already required `userId` and now thread it through — their callers need no changes. Required rather than optional so an upgraded integrator cannot silently produce unclaimable uploads that fail later at publish. Always explicit, never derived from auth state — a manager or developer-app session can act for more than one user (same convention as `ChatsApi.currentUserId`). ## Client updates - `useUpload`: current account's id on track/stem/cover-art/collection-artwork uploads, `requireUserId()` guard so a missing account fails loudly at the call site, and the id is in every dependency array that reads it - Web stems saga passes the id it already looked up - All four upload examples (web upload / gated-upload / upload-server, mobile upload) + READMEs - Docs site: `sdk/uploads` and `sdk/tracks` pages document the parameter and why it exists ## Testing - sdk: vitest 20/20 (`Storage.test.ts` pins the `userId` query param on `generatePreview`), typecheck clean - common, web: typecheck clean; eslint clean on touched files (2 pre-existing warnings in examples untouched) - Example-app tsconfigs resolve the *published* npm SDK, so their pre-existing typecheck errors are unrelated and unchanged 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Marcus Pasell <marcus@audius.co> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Closing in favor of #14555, which carries preview attribution as an asserted userId query param with the ownership check server-side (OpenAudio/go-openaudio#476). Decision record on OpenAudio/go-openaudio#471. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #14550. Review that first — this branch contains only the commit on top of it.
Why previews need signing
generatePreviewasks a storage node to slice new bytes out of a cid. The node attests the resulting cid on chain so it can be named as a track'spreview_cid— and it will only do that for a user who already claims the source audio. So the request has to say who is asking.Without that check the endpoint is a gating bypass. Previews stream publicly, and they are 30 seconds long, so an attacker could generate previews of a gated track at offsets 0/30/60/…, name each on a throwaway track of their own, and reassemble the full 320. Requiring a signature and refusing unless the caller owns the source kills that at the door.
What changed
generatePreviewtakes an optionaluserIdand, when a wallet is configured, signs withsignUploadfrom #14550 — the same EIP-712 payload audio uploads use, passed assignature/userId/timestampquery parameters. Reusing it rather than inventing a second scheme means both audio paths recover a signer through one code path on the node side too.Both call sites in
TracksApipass the acting user: theuploadTrackfallback that generates a preview when the upload did not, andupdateTrackwithgeneratePreview: true. The second is the one that matters most — it has no audio upload at all, so this endpoint is its only route to a preview cid.Unsigned still works
No wallet or no user id sends the request unsigned, matching
uploadFile. Nodes accept those where content authorization is not enforced; the preview just never earns a claim, so it cannot be named on a track once enforcement is on. That makes this safe to land ahead of the gate but means it has to ship before the gate opens.Testing
Typechecks clean. The SDK's vitest suites do not run in my environment —
StorageNodeSelector.test.tsandsignUpload.test.tsboth fail at collection withTypeError: Object.defineProperty called on non-object, and they fail identically on a clean tree with none of these changes, so it is environmental rather than caused by this branch. These changes have not been exercised by a running test.🤖 Generated with Claude Code