Skip to content

feat(sdk): sign preview generation requests - #14552

Closed
rickyrombo wants to merge 1 commit into
feat/signed-audio-uploadsfrom
feat/signed-preview-generation
Closed

feat(sdk): sign preview generation requests#14552
rickyrombo wants to merge 1 commit into
feat/signed-audio-uploadsfrom
feat/signed-preview-generation

Conversation

@rickyrombo

Copy link
Copy Markdown
Contributor

Stacked on #14550. Review that first — this branch contains only the commit on top of it.

Why previews need signing

generatePreview asks 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's preview_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

generatePreview takes an optional userId and, when a wallet is configured, signs with signUpload from #14550 — the same EIP-712 payload audio uploads use, passed as signature/userId/timestamp query 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 TracksApi pass the acting user: the uploadTrack fallback that generates a preview when the upload did not, and updateTrack with generatePreview: 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.ts and signUpload.test.ts both fail at collection with TypeError: 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

@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7c6f6d9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@audius/sdk Minor
@audius/sdk-legacy Patch
@audius/protocol-dashboard Patch
@audius/sp-actions Patch

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
rickyrombo force-pushed the feat/signed-preview-generation branch from 033a340 to 7c6f6d9 Compare August 8, 2026 07:10
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>
@rickyrombo

Copy link
Copy Markdown
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.

@rickyrombo rickyrombo closed this Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant