diff --git a/.changeset/sdk-sign-preview-generation.md b/.changeset/sdk-sign-preview-generation.md new file mode 100644 index 00000000000..396a0b1a2b1 --- /dev/null +++ b/.changeset/sdk-sign-preview-generation.md @@ -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. diff --git a/packages/sdk/src/sdk/api/tracks/TracksApi.ts b/packages/sdk/src/sdk/api/tracks/TracksApi.ts index 3a02df4091b..1798bd618d0 100644 --- a/packages/sdk/src/sdk/api/tracks/TracksApi.ts +++ b/packages/sdk/src/sdk/api/tracks/TracksApi.ts @@ -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) @@ -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) diff --git a/packages/sdk/src/sdk/services/Storage/Storage.ts b/packages/sdk/src/sdk/services/Storage/Storage.ts index 94d14ec5fad..0ad9df4d24e 100644 --- a/packages/sdk/src/sdk/services/Storage/Storage.ts +++ b/packages/sdk/src/sdk/services/Storage/Storage.ts @@ -220,17 +220,27 @@ 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} The CID of the generated preview */ async generatePreview({ cid, - secondOffset + secondOffset, + userId }: { cid: string secondOffset: number + userId?: number }) { const contentNodeEndpoint = await this.storageNodeSelector.getSelectedNode() @@ -238,12 +248,24 @@ export class Storage implements StorageService { 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}` diff --git a/packages/sdk/src/sdk/services/Storage/types.ts b/packages/sdk/src/sdk/services/Storage/types.ts index 9417c6b603a..84e8abb9c25 100644 --- a/packages/sdk/src/sdk/services/Storage/types.ts +++ b/packages/sdk/src/sdk/services/Storage/types.ts @@ -86,10 +86,12 @@ export type StorageService = { getUploadStatus: (uploadId: string) => Promise generatePreview: ({ cid, - secondOffset + secondOffset, + userId }: { cid: string secondOffset: number + userId?: number }) => Promise }