Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sdk-sign-preview-generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@audius/sdk': minor
---

Sign preview generation requests. `Storage.generatePreview` accepts a `userId` and, when a wallet client is configured, signs the request with the same EIP-712 payload audio uploads use. Storage nodes attest the resulting cid on chain so it can be named as a track's `preview_cid`, and they only do so for a user who already owns the source audio. Requests without a wallet or user id still succeed unsigned; the preview simply never earns a claim.
6 changes: 4 additions & 2 deletions packages/sdk/src/sdk/api/tracks/TracksApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,8 @@ export class TracksApi extends GeneratedTracksApi {
async () =>
await this.storage.generatePreview({
cid: populatedMetadata.trackCid!,
secondOffset: populatedMetadata.previewStartSeconds!
secondOffset: populatedMetadata.previewStartSeconds!,
userId: decodeHashId(params.userId) ?? undefined
}),
(e) => {
this.logger.info('Retrying generatePreview', e)
Expand Down Expand Up @@ -512,7 +513,8 @@ export class TracksApi extends GeneratedTracksApi {
async () =>
await this.storage.generatePreview({
cid: metadata.trackCid!,
secondOffset: metadata.previewStartSeconds!
secondOffset: metadata.previewStartSeconds!,
userId: decodeHashId(params.userId) ?? undefined
}),
(e) => {
this.logger.info('Retrying generatePreview', e)
Expand Down
34 changes: 28 additions & 6 deletions packages/sdk/src/sdk/services/Storage/Storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,30 +220,52 @@ export class Storage implements StorageService {

/**
* Generates a preview for a track at the given second offset
*
* Signed for the same reason audio uploads are: the node attests the
* resulting cid on chain so it can be named as a track's preview, and it
* will only do that for a user who already owns the source audio. Previews
* stream publicly, so a preview cid anyone could claim would let an attacker
* slice a gated track into 30-second windows and reassemble it.
*
* @param {Object} params
* @param {string} params.cid - The CID of the track to generate a preview for
* @param {number} params.secondOffset - The offset in seconds to start the preview from
* @param {number} params.userId - Decoded id of the user the source audio belongs to
* @returns {Promise<string>} The CID of the generated preview
*/
async generatePreview({
cid,
secondOffset
secondOffset,
userId
}: {
cid: string
secondOffset: number
userId?: number
}) {
const contentNodeEndpoint = await this.storageNodeSelector.getSelectedNode()

if (!contentNodeEndpoint) {
throw new Error('No content node available')
}

const response = await fetch(
`${contentNodeEndpoint}/generate_preview/${cid}/${secondOffset}`,
{
method: 'POST'
}
const url = new URL(
`${contentNodeEndpoint}/generate_preview/${cid}/${secondOffset}`
)

// Unsigned when there is no wallet or no user to sign for. The node
// accepts those where content authorization is not yet enforced, and the
// preview simply never earns a claim.
if (this.audiusWalletClient && userId !== undefined) {
const signed = await signUpload({
audiusWalletClient: this.audiusWalletClient,
userId
})
url.searchParams.set('signature', signed.signature)
url.searchParams.set('userId', String(signed.userId))
url.searchParams.set('timestamp', String(signed.timestamp))
}

const response = await fetch(url, { method: 'POST' })
if (!response.ok) {
throw new Error(
`Failed to generate preview for cid ${cid} at offset ${secondOffset}, status: ${response.status}`
Expand Down
4 changes: 3 additions & 1 deletion packages/sdk/src/sdk/services/Storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,12 @@ export type StorageService = {
getUploadStatus: (uploadId: string) => Promise<UploadResponse>
generatePreview: ({
cid,
secondOffset
secondOffset,
userId
}: {
cid: string
secondOffset: number
userId?: number
}) => Promise<string>
}

Expand Down
Loading