From 44ae1db443f05ef3d4d11ecfd351a9a59675f58a Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Mon, 23 Mar 2026 13:10:08 -0400 Subject: [PATCH 1/2] feat: enhance Vercel Blob adapter to accept structured request body and improve request handling --- .../media/__tests__/storage-adapters.test.ts | 56 +++++++++++++++++-- .../plugins/media/api/adapters/vercel-blob.ts | 8 +-- .../stack/src/plugins/media/api/plugin.ts | 2 +- .../src/plugins/media/api/storage-adapter.ts | 32 +++++++++++ 4 files changed, 87 insertions(+), 11 deletions(-) diff --git a/packages/stack/src/plugins/media/__tests__/storage-adapters.test.ts b/packages/stack/src/plugins/media/__tests__/storage-adapters.test.ts index 804c100e..dbc0d080 100644 --- a/packages/stack/src/plugins/media/__tests__/storage-adapters.test.ts +++ b/packages/stack/src/plugins/media/__tests__/storage-adapters.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, afterEach } from "vitest"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import * as os from "node:os"; +import type { VercelBlobHandleUploadBody } from "../api/storage-adapter"; // Top-level vi.mock calls are hoisted by Vitest before any imports. // Factories are used so the packages do not need to be installed as devDependencies. @@ -271,9 +272,13 @@ describe("vercelBlobAdapter", () => { const { vercelBlobAdapter } = await import("../api/adapters/vercel-blob"); const adapter = vercelBlobAdapter(); - const body = { + const body: VercelBlobHandleUploadBody = { type: "blob.generate-client-token", - payload: { pathname: "photo.jpg" }, + payload: { + pathname: "photo.jpg", + multipart: false, + clientPayload: null, + }, }; const request = new Request("https://example.com/api/upload", { method: "POST", @@ -281,7 +286,7 @@ describe("vercelBlobAdapter", () => { headers: { "Content-Type": "application/json" }, }); - const result = await adapter.handleRequest(request, {}); + const result = await adapter.handleRequest(request, body, {}); expect(mockHandleUpload).toHaveBeenCalledWith( expect.objectContaining({ @@ -300,9 +305,13 @@ describe("vercelBlobAdapter", () => { const adapter = vercelBlobAdapter(); const onBeforeGenerateToken = vi.fn().mockResolvedValue(undefined); - const body = { + const body: VercelBlobHandleUploadBody = { type: "blob.generate-client-token", - payload: { pathname: "test.jpg" }, + payload: { + pathname: "test.jpg", + multipart: false, + clientPayload: null, + }, }; const request = new Request("https://example.com/api/upload", { method: "POST", @@ -310,7 +319,7 @@ describe("vercelBlobAdapter", () => { headers: { "Content-Type": "application/json" }, }); - await adapter.handleRequest(request, { onBeforeGenerateToken }); + await adapter.handleRequest(request, body, { onBeforeGenerateToken }); // Verify that handleUpload received an onBeforeGenerateToken callback const callArgs = mockHandleUpload.mock.calls[0]![0] as Record< @@ -325,6 +334,41 @@ describe("vercelBlobAdapter", () => { expect(onBeforeGenerateToken).toHaveBeenCalledWith("test.jpg", null); }); + it("reuses the already-parsed request body without reading the request again", async () => { + const { vercelBlobAdapter } = await import("../api/adapters/vercel-blob"); + const adapter = vercelBlobAdapter(); + + const body: VercelBlobHandleUploadBody = { + type: "blob.generate-client-token", + payload: { + pathname: "consumed.jpg", + multipart: false, + clientPayload: null, + }, + }; + const request = new Request("https://example.com/api/upload", { + method: "POST", + body: JSON.stringify(body), + headers: { "Content-Type": "application/json" }, + }); + + // Simulate the BTST route layer parsing the request before the adapter runs. + const parsedBody = await request.json(); + + const result = await adapter.handleRequest(request, parsedBody, {}); + + expect(mockHandleUpload).toHaveBeenCalledWith( + expect.objectContaining({ + body: parsedBody, + request, + }), + ); + expect(result).toEqual({ + type: "blob.generate-client-token", + clientToken: "tok123", + }); + }); + it("calls del when deleting a blob by URL", async () => { const { vercelBlobAdapter } = await import("../api/adapters/vercel-blob"); const adapter = vercelBlobAdapter(); diff --git a/packages/stack/src/plugins/media/api/adapters/vercel-blob.ts b/packages/stack/src/plugins/media/api/adapters/vercel-blob.ts index a93cf6bf..02c61051 100644 --- a/packages/stack/src/plugins/media/api/adapters/vercel-blob.ts +++ b/packages/stack/src/plugins/media/api/adapters/vercel-blob.ts @@ -1,6 +1,7 @@ import type { VercelBlobStorageAdapter, VercelBlobHandlerCallbacks, + VercelBlobHandleUploadBody, } from "../storage-adapter"; export interface VercelBlobStorageAdapterOptions { @@ -17,7 +18,7 @@ export interface VercelBlobStorageAdapterOptions { * Defined inline so we do not hard-depend on a specific `@vercel/blob` release. */ interface HandleUploadOptions { - body: unknown; + body: VercelBlobHandleUploadBody; request: Request; token?: string; onBeforeGenerateToken: ( @@ -73,13 +74,14 @@ export function vercelBlobAdapter( async handleRequest( request: Request, + body: VercelBlobHandleUploadBody, callbacks: VercelBlobHandlerCallbacks, ): Promise { let handleUpload: HandleUploadFn; try { const vercelBlobClient = /* @vite-ignore */ - (await import("@vercel/blob/client")) as { + (await import("@vercel/blob/client")) as unknown as { handleUpload: HandleUploadFn; }; ({ handleUpload } = vercelBlobClient); @@ -91,8 +93,6 @@ export function vercelBlobAdapter( ); } - const body = await request.json(); - return handleUpload({ body, request, diff --git a/packages/stack/src/plugins/media/api/plugin.ts b/packages/stack/src/plugins/media/api/plugin.ts index 29471015..a8fc0d03 100644 --- a/packages/stack/src/plugins/media/api/plugin.ts +++ b/packages/stack/src/plugins/media/api/plugin.ts @@ -788,7 +788,7 @@ export const mediaBackendPlugin = (config: MediaBackendConfig) => }); } - return storageAdapter.handleRequest(ctx.request, { + return storageAdapter.handleRequest(ctx.request, ctx.body, { onBeforeGenerateToken: async (pathname, clientPayload) => { const filename = pathname.split("/").pop() ?? pathname; let parsed: Record = {}; diff --git a/packages/stack/src/plugins/media/api/storage-adapter.ts b/packages/stack/src/plugins/media/api/storage-adapter.ts index f30bfe8d..3027fd34 100644 --- a/packages/stack/src/plugins/media/api/storage-adapter.ts +++ b/packages/stack/src/plugins/media/api/storage-adapter.ts @@ -72,6 +72,37 @@ export interface VercelBlobTokenOptions { maximumSizeInBytes?: number; } +/** + * Minimal blob metadata sent back by Vercel Blob's upload completion callback. + * Keep this intentionally small so BTST does not hard-depend on a specific SDK type. + */ +export interface VercelBlobCallbackBlob { + url: string; + pathname: string; + [key: string]: unknown; +} + +export interface VercelBlobGenerateClientTokenBody { + type: "blob.generate-client-token"; + payload: { + pathname: string; + multipart: boolean; + clientPayload: string | null; + }; +} + +export interface VercelBlobUploadCompletedBody { + type: "blob.upload-completed"; + payload: { + blob: VercelBlobCallbackBlob; + tokenPayload?: string | null; + }; +} + +export type VercelBlobHandleUploadBody = + | VercelBlobGenerateClientTokenBody + | VercelBlobUploadCompletedBody; + /** * Callbacks provided to the Vercel Blob adapter when handling a request. */ @@ -107,6 +138,7 @@ export interface VercelBlobStorageAdapter { */ handleRequest( request: Request, + body: VercelBlobHandleUploadBody, callbacks: VercelBlobHandlerCallbacks, ): Promise; /** From 08f0a0e35e0f5b37f0f039965e05156cb5e7f5d2 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Mon, 23 Mar 2026 13:10:21 -0400 Subject: [PATCH 2/2] chore: bump version to 2.9.2 in package.json for @btst/stack --- packages/stack/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stack/package.json b/packages/stack/package.json index b0d6ac8f..c90130f7 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -1,6 +1,6 @@ { "name": "@btst/stack", - "version": "2.9.1", + "version": "2.9.2", "description": "A composable, plugin-based library for building full-stack applications.", "repository": { "type": "git",