You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There is no way to request private access for a client upload. handleUpload's onBeforeGenerateToken callback — the only server-side hook where a client-upload token is minted — cannot return access, so every client-uploaded blob is forced to public, even though private Blob shipped (#816).
This is a gap left over from the private-storage rollout: server put({ access: 'private' }) works, but the client-upload path (upload() → handleUpload() → generateClientTokenFromReadWriteToken()) was never given an access knob.
Where it's blocked (in packages/blob/src/client.ts)
onBeforeGenerateToken's return type is a Pick that omits access:
And GenerateClientTokenOptions has no access field to Pick from — it extends BlobCommandOptions (token/oidcToken/storeId/abortSignal) and BlobClientTokenConstraintOptions (size/content-types/validUntil/suffix/overwrite/cache/ifMatch). access lives only on CommonCreateBlobOptions (packages/blob/src/helpers.ts), which the token options do not extend.
Net effect: returning { access: 'private', ... } from onBeforeGenerateToken is a TypeScript error, and there is no runtime channel to carry the intended access level into the signed client token.
Reproduction
// app/api/upload/route.tsimport{handleUpload}from'@vercel/blob/client';exportasyncfunctionPOST(req: Request){returnResponse.json(awaithandleUpload({request: req,body: awaitreq.json(),onBeforeGenerateToken: async()=>({access: 'private',// ❌ TS2353: 'access' does not exist in type Pick<GenerateClientTokenOptions, ...>allowedContentTypes: ['application/pdf'],}),}),);}
Confirmed on @vercel/blob@2.5.0 (and 2.3.3) — the Pick and GenerateClientTokenOptions shape are identical in both.
Expected
Symmetry with the server-side API: a client upload should be able to be minted as private, e.g. onBeforeGenerateToken returning { access: 'private' }, so the resulting blob is created with private access.
Proposed fix (SDK side)
Add an optional access?: BlobAccessType to the client-token options (GenerateClientTokenOptions).
Add 'access' to the Pick in onBeforeGenerateToken's return type.
Because generateClientTokenFromReadWriteToken already serializes all of its non-token args into the signed token payload, and handleUpload already spreads the onBeforeGenerateToken result into it, access would flow end-to-end into the token payload with no other runtime change. A PR implementing exactly this (with a unit test asserting the decoded token payload carries access, plus a changeset) is open — see below.
Open question for maintainers
Does the control-plane / api-storage side already honor an access field embedded in a vercel_blob_client_* token payload, or is additional backend work required for it to take effect? The SDK change is safe and minimal, but if the backend ignores token-level access, the type change alone would let developers express private client uploads without them actually being private — so I've flagged this explicitly rather than assume. Happy to adjust the PR (e.g. gate it, or wire it differently) based on how the backend consumes the token.
Context
Follow-up to Blob access control #816 (private Blob storage) — that issue delivered private access for server-side operations but did not cover the client-upload token path.
Real-world motivation: uploading receipts/documents that contain PII/PHI directly from the browser; these should be private, but client uploads currently can only be public.
Summary
There is no way to request private access for a client upload.
handleUpload'sonBeforeGenerateTokencallback — the only server-side hook where a client-upload token is minted — cannot returnaccess, so every client-uploaded blob is forced topublic, even though private Blob shipped (#816).This is a gap left over from the private-storage rollout: server
put({ access: 'private' })works, but the client-upload path (upload()→handleUpload()→generateClientTokenFromReadWriteToken()) was never given anaccessknob.Where it's blocked (in
packages/blob/src/client.ts)onBeforeGenerateToken's return type is aPickthat omitsaccess:And
GenerateClientTokenOptionshas noaccessfield to Pick from — it extendsBlobCommandOptions(token/oidcToken/storeId/abortSignal) andBlobClientTokenConstraintOptions(size/content-types/validUntil/suffix/overwrite/cache/ifMatch).accesslives only onCommonCreateBlobOptions(packages/blob/src/helpers.ts), which the token options do not extend.Net effect: returning
{ access: 'private', ... }fromonBeforeGenerateTokenis a TypeScript error, and there is no runtime channel to carry the intended access level into the signed client token.Reproduction
Confirmed on
@vercel/blob@2.5.0(and2.3.3) — thePickandGenerateClientTokenOptionsshape are identical in both.Expected
Symmetry with the server-side API: a client upload should be able to be minted as private, e.g.
onBeforeGenerateTokenreturning{ access: 'private' }, so the resulting blob is created with private access.Proposed fix (SDK side)
access?: BlobAccessTypeto the client-token options (GenerateClientTokenOptions).'access'to thePickinonBeforeGenerateToken's return type.Because
generateClientTokenFromReadWriteTokenalready serializes all of its non-tokenargs into the signed token payload, andhandleUploadalready spreads theonBeforeGenerateTokenresult into it,accesswould flow end-to-end into the token payload with no other runtime change. A PR implementing exactly this (with a unit test asserting the decoded token payload carriesaccess, plus a changeset) is open — see below.Open question for maintainers
Does the control-plane / api-storage side already honor an
accessfield embedded in avercel_blob_client_*token payload, or is additional backend work required for it to take effect? The SDK change is safe and minimal, but if the backend ignores token-levelaccess, the type change alone would let developers express private client uploads without them actually being private — so I've flagged this explicitly rather than assume. Happy to adjust the PR (e.g. gate it, or wire it differently) based on how the backend consumes the token.Context