Skip to content

@vercel/blob: client uploads can't set accessonBeforeGenerateToken omits it, so client uploads are locked to public #1079

Description

@kars85

Summary

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:

onBeforeGenerateToken: (
  pathname: string,
  clientPayload: string | null,
  multipart: boolean,
) => Promise<
  Pick<
    GenerateClientTokenOptions,
    | 'allowedContentTypes'
    | 'maximumSizeInBytes'
    | 'validUntil'
    | 'addRandomSuffix'
    | 'allowOverwrite'
    | 'cacheControlMaxAge'
    | 'ifMatch'
  > & { tokenPayload?: string | null; callbackUrl?: string }
>;

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.ts
import { handleUpload } from '@vercel/blob/client';

export async function POST(req: Request) {
  return Response.json(
    await handleUpload({
      request: req,
      body: await req.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)

  1. Add an optional access?: BlobAccessType to the client-token options (GenerateClientTokenOptions).
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions