Skip to content

[wip] presigned url version of handleUpload#1059

Merged
elliotdauber merged 12 commits into
elliot/presigned-urlsfrom
elliot/presigned-handle-upload
May 4, 2026
Merged

[wip] presigned url version of handleUpload#1059
elliotdauber merged 12 commits into
elliot/presigned-urlsfrom
elliot/presigned-handle-upload

Conversation

@elliotdauber
Copy link
Copy Markdown
Collaborator

No description provided.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vercel-storage-next-integration-test-suite Ready Ready Preview May 4, 2026 10:35pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 30, 2026

⚠️ No Changeset found

Latest commit: fa32df8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

options.ifMatch !== undefined
) {
throw new BlobError(
"client/`upload` doesn't allow `addRandomSuffix`, `cacheControlMaxAge`, `allowOverwrite` or `ifMatch`. Configure these options at the server side when generating client tokens.",
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i need to update messaging here

signature: string;
body: string;
}): Promise<boolean> {
console.warn('verifyCallbackSignaturePresigned is not yet implemented');
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo

const resolvedWebhookPublicKey =
webhookPublicKey ?? process.env.BLOB_WEBHOOK_PUBLIC_KEY;
if (!resolvedWebhookPublicKey) {
throw new BlobError('Missing webhook public key');
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we error here? or only when doing webhook callback, if onUploadCompleted is provided

const BLOB_PRESIGN_QUERY_DELEGATION = 'vercel-blob-delegation' as const;

function normalizeDelegationStoreId(storeId: string): string {
const lowercase = storeId.toLowerCase();
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im not actually sure if we should be lowercasing store ids. Idk if they are case sensitive

@elliotdauber elliotdauber force-pushed the elliot/presigned-handle-upload branch from ef94e63 to 1750415 Compare May 4, 2026 22:06
@elliotdauber elliotdauber marked this pull request as ready for review May 4, 2026 22:34
@elliotdauber elliotdauber merged commit 41de657 into elliot/presigned-urls May 4, 2026
3 checks passed
@elliotdauber elliotdauber deleted the elliot/presigned-handle-upload branch May 4, 2026 22:35
@elliotdauber
Copy link
Copy Markdown
Collaborator Author

merged into elliot/presigned-urls to make branch management easier

elliotdauber added a commit that referenced this pull request May 15, 2026
* [wip] presigned url version of handleUpload

* up

* up

* up

* add presigned urls for mpu

* update options shape

* webhook signature

* update example

* up

* up

* up

* change to operation
elliotdauber added a commit that referenced this pull request May 18, 2026
* [wip] presigned url version of handleUpload

* up

* up

* up

* add presigned urls for mpu

* update options shape

* webhook signature

* update example

* up

* up

* up

* change to operation
elliotdauber added a commit that referenced this pull request May 18, 2026
* presigned urls

* up

* normalize

* fix

* update to new signing string

* [wip] presigned url version of handleUpload (#1059)

* [wip] presigned url version of handleUpload

* up

* up

* up

* add presigned urls for mpu

* update options shape

* webhook signature

* update example

* up

* up

* up

* change to operation

* up

* up

* verify webhook signature

* up

* callbackUrl

* non-null

* presigned url opts

* new delegation token + url opts

* dont lowercase store id

* validUntil instead of ttlSeconds

* up

* up

* Add 'delete' to DelegationOperation for presigned DELETE URLs (#1061)

Mirrors the API-side change that adds `delete` to the issue_signed_token
allowed operations. `presignUrl` now signs canonical
`operation=delete\npathname=...` against the public blob object URL
(same host shape as `get`/`head`, just used with HTTP DELETE).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* change upload to put

* delete

* up

* presigned url payload

* update app

* fix

* docs

* refactor presign url

* up

* up

* up

* add head

* up

* add params to url

* add keys first

* token payload

* add buildPresignedGetUrl

* up

* presignUrl returns url, not accepted in sdk methods

* up

* up

* feat(blob): presigned HEAD & DELETE URLs (#1064)

* feat(blob): presigned DELETE URLs

Wires the `'delete'` DelegationOperation (added in #1061, since rebased
out of elliot/presigned-urls) through `presignUrl`. A token issued with
`operations: ['delete']` can now mint a presigned `DELETE /?pathname=…`
against the control-plane API.

- `PresignDeleteUrlOptions` accepts `pathname`, optional `validUntil`,
  optional `ifMatch`. Upload-only fields are rejected at the type level.
- `presign()` gates on the delegation scope including `'delete'`.
- `buildPresignedDeleteUrl()` mirrors the PUT URL shape; the HTTP method
  is the discriminator (canonical signing string carries `operation=delete`).
- `buildPresignCanonicalQueryEntries()` for `delete` emits only
  `validUntil` (when below the delegation ceiling) and `ifMatch`.
- E2E test route + delete button on the presigned-upload demo page.

Based on `elliot/presigned-urls`, not `main`.

* feat(blob): presigned HEAD URLs (#1065)

`HEAD` mirrors `GET` against the blob object host
(`<storeId>.<access>.blob.vercel-storage.com/<pathname>`); the URL shape
is identical and the HTTP method is the discriminator. `operation=head`
goes into the canonical signing string so a GET-signed URL cannot be
replayed as a HEAD (and vice versa).

- `'head'` added to `DelegationOperation`.
- `PresignHeadUrlOptions` — same shape as `PresignGetUrlOptions`
  (`pathname`, optional `validUntil`).
- `presign()` gates on the delegation scope including `'head'`.
- `presignUrl()` reuses `buildPresignedGetUrl()` for `operation: 'head'`.
- `buildPresignCanonicalQueryEntries()` for `head` emits only
  `validUntil` (when below the delegation ceiling) — same as `get`.
- E2E test route + HEAD button on the presigned-upload demo page.

Stacked on `falcoagustin/presigned-delete-impl` (#1064).

* environment error

* up

* cleanup

---------

Co-authored-by: Agustin Falco <agusfalco_11@hotmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
elliotdauber added a commit that referenced this pull request May 18, 2026
* Add Vercel OIDC auth

* up

* header

* up

* prioritize token

* normalize storeId in OIDC auth resolution

BLOB_STORE_ID and the storeId option are accepted in either store_<id>
or <id> form (Vercel env pull writes the prefixed form), and may be
mixed-case. resolveBlobAuth was passing those through verbatim, so the
storeId in API headers and CDN host subdomains could be malformed —
e.g. blob.get against a private store with `store_WdsHBk1w9fDO4vPW`
built `https://store_WdsHBk1w9fDO4vPW.private.blob.vercel-storage.com/...`
and 404'd. The RW path was unaffected because parseStoreIdFromReadWriteToken
yields a bare lowercase id from the token's structure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* preserve case when normalizing storeId

The first pass also lowercased — that breaks API requests, since the
Vercel Blob API is case-sensitive on the storeId (header and bearer
parsing). The CDN host accepts either case, so prefix-strip alone is
sufficient and works for both consumers. Verified end-to-end against
a private store: blob get and blob list both succeed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* add oidcToken option

* minor version

* Vercel Presigned URLs (#1057)

* presigned urls

* up

* normalize

* fix

* update to new signing string

* [wip] presigned url version of handleUpload (#1059)

* [wip] presigned url version of handleUpload

* up

* up

* up

* add presigned urls for mpu

* update options shape

* webhook signature

* update example

* up

* up

* up

* change to operation

* up

* up

* verify webhook signature

* up

* callbackUrl

* non-null

* presigned url opts

* new delegation token + url opts

* dont lowercase store id

* validUntil instead of ttlSeconds

* up

* up

* Add 'delete' to DelegationOperation for presigned DELETE URLs (#1061)

Mirrors the API-side change that adds `delete` to the issue_signed_token
allowed operations. `presignUrl` now signs canonical
`operation=delete\npathname=...` against the public blob object URL
(same host shape as `get`/`head`, just used with HTTP DELETE).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* change upload to put

* delete

* up

* presigned url payload

* update app

* fix

* docs

* refactor presign url

* up

* up

* up

* add head

* up

* add params to url

* add keys first

* token payload

* add buildPresignedGetUrl

* up

* presignUrl returns url, not accepted in sdk methods

* up

* up

* feat(blob): presigned HEAD & DELETE URLs (#1064)

* feat(blob): presigned DELETE URLs

Wires the `'delete'` DelegationOperation (added in #1061, since rebased
out of elliot/presigned-urls) through `presignUrl`. A token issued with
`operations: ['delete']` can now mint a presigned `DELETE /?pathname=…`
against the control-plane API.

- `PresignDeleteUrlOptions` accepts `pathname`, optional `validUntil`,
  optional `ifMatch`. Upload-only fields are rejected at the type level.
- `presign()` gates on the delegation scope including `'delete'`.
- `buildPresignedDeleteUrl()` mirrors the PUT URL shape; the HTTP method
  is the discriminator (canonical signing string carries `operation=delete`).
- `buildPresignCanonicalQueryEntries()` for `delete` emits only
  `validUntil` (when below the delegation ceiling) and `ifMatch`.
- E2E test route + delete button on the presigned-upload demo page.

Based on `elliot/presigned-urls`, not `main`.

* feat(blob): presigned HEAD URLs (#1065)

`HEAD` mirrors `GET` against the blob object host
(`<storeId>.<access>.blob.vercel-storage.com/<pathname>`); the URL shape
is identical and the HTTP method is the discriminator. `operation=head`
goes into the canonical signing string so a GET-signed URL cannot be
replayed as a HEAD (and vice versa).

- `'head'` added to `DelegationOperation`.
- `PresignHeadUrlOptions` — same shape as `PresignGetUrlOptions`
  (`pathname`, optional `validUntil`).
- `presign()` gates on the delegation scope including `'head'`.
- `presignUrl()` reuses `buildPresignedGetUrl()` for `operation: 'head'`.
- `buildPresignCanonicalQueryEntries()` for `head` emits only
  `validUntil` (when below the delegation ceiling) — same as `get`.
- E2E test route + HEAD button on the presigned-upload demo page.

Stacked on `falcoagustin/presigned-delete-impl` (#1064).

* environment error

* up

* cleanup

---------

Co-authored-by: Agustin Falco <agusfalco_11@hotmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* up

* rm dead code

* throw on manual token

* up

* up

---------

Co-authored-by: Agustin Falco <agusfalco_11@hotmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant