Skip to content
Draft
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/blob-client-upload-access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vercel/blob': minor
---

Allow client uploads to set `access`. `handleUpload`'s `onBeforeGenerateToken` callback can now return `access: 'public' | 'private'`, and it is baked into the signed client token — bringing client uploads to parity with the server-side `put({ access })` API. When omitted, behavior is unchanged (public). Fixes #1079.
56 changes: 56 additions & 0 deletions packages/blob/src/client.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,31 @@ describe('client uploads', () => {
});
});

it('bakes `access` into the client token payload', async () => {
const uploadToken = await generateClientTokenFromReadWriteToken({
pathname: 'foo.txt',
access: 'private',
token: 'vercel_blob_rw_12345fakeStoreId_30FakeRandomCharacters12345678',
});

expect(getPayloadFromClientToken(uploadToken)).toEqual({
pathname: 'foo.txt',
access: 'private',
validUntil: 1672531230000,
});
});

it('omits `access` from the payload when not provided (unchanged default)', async () => {
const uploadToken = await generateClientTokenFromReadWriteToken({
pathname: 'foo.txt',
token: 'vercel_blob_rw_12345fakeStoreId_30FakeRandomCharacters12345678',
});

expect(getPayloadFromClientToken(uploadToken)).not.toHaveProperty(
'access',
);
});

it('accepts a tokenPayload property', async () => {
const uploadToken = await generateClientTokenFromReadWriteToken({
pathname: 'foo.txt',
Expand Down Expand Up @@ -153,6 +178,37 @@ describe('client uploads', () => {
});
});

it('threads `access` from onBeforeGenerateToken into the client token', async () => {
const token =
'vercel_blob_rw_12345fakeStoreId_30FakeRandomCharacters12345678';
const jsonResponse = await handleUpload({
token,
request: {
headers: { 'x-vercel-signature': '123' },
} as unknown as IncomingMessage,
body: {
type: 'blob.generate-client-token',
payload: {
pathname: 'newfile.txt',
multipart: false,
clientPayload: null,
},
},
onBeforeGenerateToken: async () => {
await Promise.resolve();
return { access: 'private' };
},
});

expect(
getPayloadFromClientToken((jsonResponse as any).clientToken),
).toEqual({
access: 'private',
pathname: 'newfile.txt',
validUntil: 1672534800000,
});
});

it('should run onCompleted when called with blob.upload-completed', async () => {
const token = 'vercel_blob_client_123456789_TEST_TOKEN';
const spy = jest.fn();
Expand Down
11 changes: 11 additions & 0 deletions packages/blob/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ export interface HandleUploadOptions {
) => Promise<
Pick<
GenerateClientTokenOptions,
| 'access'
| 'allowedContentTypes'
| 'maximumSizeInBytes'
| 'validUntil'
Expand Down Expand Up @@ -1231,6 +1232,16 @@ export interface GenerateClientTokenOptions
* The destination path for the blob
*/
pathname: string;
/**
* The access level the client upload should be created with.
* - 'public': The blob will be publicly accessible via its URL.
* - 'private': The blob will require authentication to access.
*
* Baked into the signed client token so `handleUpload` / `onBeforeGenerateToken`
* can mint private client uploads, mirroring the server-side `put({ access })`.
* When omitted, existing behavior is unchanged (public).
*/
access?: BlobAccessType;
}

/**
Expand Down