feat(sdk): sign audio uploads and send the uploading user id - #14550
Open
rickyrombo wants to merge 1 commit into
Open
feat(sdk): sign audio uploads and send the uploading user id#14550rickyrombo wants to merge 1 commit into
rickyrombo wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: 8ec31ef 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 |
rickyrombo
force-pushed
the
feat/signed-audio-uploads
branch
2 times, most recently
from
August 7, 2026 06:35
0dee8e0 to
ed0ecb3
Compare
rickyrombo
force-pushed
the
feat/signed-audio-uploads
branch
from
August 7, 2026 07:29
ed0ecb3 to
590911e
Compare
Storage nodes are moving to attesting on chain which wallet uploaded a file, because that attestation is what will entitle the uploader to name the resulting cids on a track. A node can only attest to an uploader it can identify, and today it cannot: tus metadata carried no wallet, no user id and no signature, so Upload.UserWallet was read from an unsigned metadata key the SDK never sent and sat NULL on every SDK upload. Audio uploads now carry an EIP-712 signature over the user id and a timestamp, with both fields sent alongside so the validator can rebuild the typed data. They are reproduced inside the signed payload, so tampering with either in transit only breaks recovery — it cannot redirect the upload to another user. Typed data rather than a hashed JSON payload for two reasons. It removes JSON canonicalization from the protocol entirely: the type definition is the encoding, so there is no key ordering or number formatting for client and server to agree on and later drift apart over. And it provides domain separation, which a bare hashed payload does not — without a domain, a signature produced for any other purpose over the same two fields would be replayable as an upload authorization. It also matches how the SDK already signs entity-manager writes, so both sides use standard calls rather than a hand-rolled hashing scheme. The payload deliberately does not name the content. At upload-create time the bytes have not been sent and nobody knows the cid yet; binding to content happens on the node's side, once transcoding produces cids it can attest to. There is a matching fixture test on the validator side pinning a real signature produced here, so the two implementations cannot drift apart silently. Image uploads are untouched. They are served unauthenticated so there is no claim to protect, and requiring a signature would break signup, which uploads a profile picture before the account has a user id — the wallet exists by then, which is why the payload is keyed on the wallet rather than the user id. Signing is best effort: without a wallet client or user id the upload proceeds unsigned and simply never earns an attestation, failing later at the point of claiming rather than blocking the upload itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rickyrombo
force-pushed
the
feat/signed-audio-uploads
branch
from
August 7, 2026 09:13
590911e to
8ec31ef
Compare
rickyrombo
added a commit
that referenced
this pull request
Aug 7, 2026
Found while adding EIP-712 upload signing in #14550. Independent of that stack. `packages/sdk/src/sdk/utils/signatureSchemas.ts` exported `domains`, `types`, and `generators` alongside `getNonce`, but **only `getNonce` has ever been imported.** `EntityManagerClient` builds its typed data from the contract package's `EntityManager.types` instead, and `sdk/index.ts` re-exports only `parseParams`, `rendezvous`, `errors`, and `hashId` — not this module. So the rest is unreachable, and removing it is not a breaking change. ## Why bother Not the line count. It is an attractive nuisance: it is the first thing you find when adding EIP-712 signing to the SDK, and what you find is the wrong definition — contract-shaped with `chainId` and `verifyingContract`, and typing `userId` as `uint` rather than `uint256`. That last one is what the `// @ts-ignore Need to update this type to "uint32" instead of "uint"` in `EntityManagerClient` is working around. I reached for it myself before checking whether anything used it. ## Renamed to `nonce.ts` Two reasons. It matches what is left, and there is a **live** `packages/libs/src/data-contracts/signatureSchemas.js` — used by `identity-service/rateLimiter`, `AudiusABIDecoder`, and libs' own `EntityManagerClient`. That one is untouched here. Two files with the same name where one is dead invites editing the wrong one; I conflated them myself at first. ## Verification `packages/sdk` and `packages/common` typecheck clean, the SDK builds, and the EntityManager tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Marcus Pasell <marcus@audius.co> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Client half of a change to close a content-gating bypass. Server side is OpenAudio/go-openaudio#459 and OpenAudio/go-openaudio#460 — this can merge independently and is inert until those activate.
Why
Storage nodes are moving to attesting on chain which wallet uploaded a file, because that attestation is what will entitle the uploader to name the resulting cids on a track. Today anyone can read a gated track's
track_cidoff the public API and assert it on their own ungated track, and nothing can tell the difference.A node can only attest to an uploader it can identify, and right now it cannot: tus metadata carries no wallet, no user id and no signature, so
Upload.UserWalletis read from an unsigned metadata key the SDK never sends and sits NULL on every SDK upload.What changes
uploadTrackFilesacceptsuserIdandStorageaccepts anaudiusWalletClient. When both are present, audio uploads carry an EIP-712 signature over the user id and a timestamp, with both fields sent alongside so the validator can rebuild the typed data. They are reproduced inside the signed payload, so tampering with either in transit only breaks recovery — it cannot redirect the upload to another user.Why typed data rather than signed JSON
An earlier revision hashed canonical JSON and used
personal_sign. EIP-712 is better on two counts:It also matches how this SDK already signs entity-manager writes, so both sides use standard calls rather than a hand-rolled hashing scheme.
The payload deliberately does not name the content: at upload-create time the bytes have not been sent and nobody knows the cid yet. Binding to content happens on the node's side, once transcoding produces cids it can attest to.
What is deliberately unchanged
Image uploads. They are served unauthenticated so there is no claim to protect, and requiring a signature would break signup, which uploads a profile picture before the account has a user id. The ordering nuance is that the Hedgehog wallet does exist by then — what does not is the user id, minted later inside
createUserWithEntityManager. That is why the payload is keyed on the wallet.Unsigned uploads still succeed. Without a wallet client or user id the upload proceeds and simply never earns an attestation, failing later at the point of claiming rather than blocking the upload.
Testing
5 tests covering the returned shape, that the signature verifies against the agreed domain and types, binding to both the user id and the timestamp, and the no-wallet error.
There is a matching fixture test on the validator side pinning a real signature produced by this code, so the two independently-written implementations cannot drift apart silently. I confirmed a viem-signed payload recovers to the expected address in Go before wiring it up.
Both
packages/sdkandpackages/commontypecheck clean.🤖 Generated with Claude Code