Skip to content

feat(api): migrate POST /api/sandbox/upload to /api/sandboxes/staged-files#541

Merged
sweetmantech merged 7 commits into
testfrom
feature/sandbox-staged-files
May 11, 2026
Merged

feat(api): migrate POST /api/sandbox/upload to /api/sandboxes/staged-files#541
sweetmantech merged 7 commits into
testfrom
feature/sandbox-staged-files

Conversation

@arpitgupta1214
Copy link
Copy Markdown
Collaborator

@arpitgupta1214 arpitgupta1214 commented May 9, 2026

Summary

Adds POST /api/sandboxes/staged-files — the Vercel Blob client-upload token-handshake endpoint, ported from chat's /api/sandbox/upload. Closes Group 6 of the chat→api migration once paired with the chat-side cutover PR.

Auth is minimum-port: validates clientPayload.token (the Privy access token) inside onBeforeGenerateToken. @vercel/blob/client.upload() does not allow setting an Authorization header on the handshake POST, so token transport rides on clientPayload. The downstream commit (POST /api/sandboxes/files) re-authenticates with a real Bearer token.

CORS preflight is wired through getCorsHeaders() so cross-origin calls from chat.recoupable.com work the same way the existing /api/sandboxes/files route does.

Test plan

  • pnpm test lib/sandbox/__tests__/postSandboxesUploadTokensHandler.test.ts — 7/7 pass
  • pnpm lint, pnpm format clean
  • Preview: hit OPTIONS and POST /api/sandboxes/staged-files from a browser at chat.recoupable.com; confirm token-generation phase + completion callback both succeed
  • Preview: end-to-end sandbox file upload via lib/sandboxes/uploadSandboxFiles.ts after the chat PR points at this route

Summary by cubic

Adds POST /api/sandboxes/staged-file to issue Vercel Blob client-upload tokens, replacing chat’s /api/sandbox/upload. Adds CORS, Bearer auth, and safer error handling.

  • New Features

    • Handshake endpoint for @vercel/blob/client uploads.
    • Auth via Authorization Bearer header with validateAuthContext; upload-completed callbacks skip auth and are verified by handleUpload().
    • CORS preflight and responses via getCorsHeaders().
    • 100 MB limit and random filename suffix.
    • Generic 500s with server-side logging; no internal details leaked.
  • Migration

    • Point chat’s handleUploadUrl to /api/sandboxes/staged-file.
    • Ensure @vercel/blob/client.upload() forwards the Authorization header via its headers option.

Written for commit 27163a9. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • New API endpoint for generating file upload tokens with CORS support and an OPTIONS preflight handler.
    • Enforces a 100MB maximum upload size and adds randomized suffixes to uploads.
    • Includes authentication validation for upload requests and returns structured JSON responses on success or error.

Review Change Stack

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 9, 2026

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

Project Deployment Actions Updated (UTC)
api Ready Ready Preview May 11, 2026 10:57pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

Warning

Rate limit exceeded

@sweetmantech has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 33 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2796040d-3034-4aa9-808b-1eb3bab4eb7f

📥 Commits

Reviewing files that changed from the base of the PR and between 4c55c39 and 27163a9.

⛔ Files ignored due to path filters (1)
  • lib/sandbox/__tests__/postSandboxesUploadTokensHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • app/api/sandboxes/staged-file/route.ts
  • lib/sandbox/postSandboxesUploadTokensHandler.ts
📝 Walkthrough

Walkthrough

Adds a Next.js API route and handler to issue Vercel Blob client upload tokens: parses request JSON, validates auth for token generation, enforces a 100MB max with random suffix option, returns token JSON with CORS headers, and handles errors with logged 500 responses.

Changes

Upload Token Endpoint Implementation

Layer / File(s) Summary
Configuration
lib/sandbox/postSandboxesUploadTokensHandler.ts
Defines MAX_UPLOAD_SIZE_BYTES constant set to 100MB.
Authentication & Parsing
lib/sandbox/postSandboxesUploadTokensHandler.ts
Parses HandleUploadBody from request; validates auth context for blob.generate-client-token and returns early on validation failure.
Token Generation
lib/sandbox/postSandboxesUploadTokensHandler.ts
Invokes handleUpload with onBeforeGenerateToken callback enforcing 100MB size limit and enabling random suffix; returns handleUpload JSON with CORS headers.
Error Handling
lib/sandbox/postSandboxesUploadTokensHandler.ts
Wraps handler in try/catch; logs errors and returns HTTP 500 JSON error response with CORS headers on failure.
Route Handlers
app/api/sandboxes/staged-file/route.ts
OPTIONS returns 200 with CORS headers for preflight; POST delegates to postSandboxesUploadTokensHandler and returns its response.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A token seeds the sandbox sky,
Signed, restrained to a hundred megs,
A suffix sprinkled, sly and spry,
Wrapped safe in CORS like tiny kegs,
Uploads set sail — cloud-bound legs.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Solid & Clean Code ✅ Passed SRP met: one function per file. Complexity good: ~35 LOC, <3 nesting. Clear names, proper constants, error handling, DRY composition, OCP compliant.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/sandbox-staged-files

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
lib/sandbox/postSandboxesUploadTokensHandler.ts (2)

20-48: ⚡ Quick win

Consider extracting sub-functions to reduce length.

This function is 27 lines, exceeding the 20-line guideline. While the logic is straightforward, consider extracting the token validation logic and error response construction into separate functions for better testability and readability.

Example structure:

  • validateClientPayload(clientPayload) → validates and returns parsed payload
  • createErrorResponse(error) → determines status code and formats error response


As per coding guidelines: "Flag functions longer than 20 lines" and "Keep functions small and focused."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/sandbox/postSandboxesUploadTokensHandler.ts` around lines 20 - 48, The
handler postSandboxesUploadTokensHandler is over the 20-line guideline—extract
the client payload parsing/token validation and the error response creation into
small helpers: implement validateClientPayload(clientPayload) used inside the
onBeforeGenerateToken callback to parse JSON and throw a clear Authentication
error when payload?.token is missing, and implement createErrorResponse(error)
to return the object and status used in the catch block (message and appropriate
status); replace the inline parsing/throw and the catch return with calls to
these helpers to shorten and improve testability of
postSandboxesUploadTokensHandler.

42-46: ⚡ Quick win

Return appropriate HTTP status codes for different error types.

The catch block returns 400 Bad Request for all errors, but different error types warrant different status codes:

  • Authentication errors → 401 Unauthorized
  • File size limit exceeded → 413 Payload Too Large
  • Server/Vercel Blob errors → 500 Internal Server Error
  • Malformed request → 400 Bad Request

Differentiate error types to provide clearer signals to clients and improve observability.



♻️ Proposed error handling improvement
 } catch (error) {
+  const message = error instanceof Error ? error.message : "Upload failed";
+  let status = 400;
+  
+  if (message.includes("Authentication") || message.includes("Unauthorized")) {
+    status = 401;
+  } else if (message.includes("size") || message.includes("too large")) {
+    status = 413;
+  } else if (message.includes("internal") || message.includes("server")) {
+    status = 500;
+  }
+  
   return NextResponse.json(
-    { error: error instanceof Error ? error.message : "Upload failed" },
-    { status: 400, headers: getCorsHeaders() },
+    { error: message },
+    { status, headers: getCorsHeaders() },
   );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/sandbox/postSandboxesUploadTokensHandler.ts` around lines 42 - 46, The
catch in postSandboxesUploadTokensHandler currently always returns 400; update
it to map specific error types to appropriate HTTP statuses by inspecting the
thrown error (e.g., if error is an instance of AuthenticationError or contains
auth-related marker -> 401 Unauthorized; if error is a PayloadTooLargeError or
message indicates file size -> 413 Payload Too Large; if error is a
VercelBlobError/server-side error -> 500 Internal Server Error; otherwise keep
400 for malformed requests), and return NextResponse.json with the error message
and the selected status while preserving getCorsHeaders(); use the existing
function name postSandboxesUploadTokensHandler and the current catch block as
the location to implement this mapping.
app/api/sandboxes/staged-files/route.ts (1)

42-44: 🏗️ Heavy lift

Consider authenticating before delegating to the handler.

While delegation keeps the route thin, the coding guidelines require API routes to use validateAuthContext(). Consider authenticating the request here before calling postSandboxesUploadTokensHandler, then passing the validated account context through:

export async function POST(request: Request): Promise<Response> {
  const authContext = await validateAuthContext(request);
  return postSandboxesUploadTokensHandler(request, authContext);
}

This would:

  1. Comply with the guideline "Always use validateAuthContext() for authentication in API routes"
  2. Allow the handler to receive pre-validated authentication context
  3. Eliminate the need for the non-standard clientPayload.token workaround

If the Vercel Blob library constraint prevents this, document why validateAuthContext() cannot be used and ensure the handler implements equivalent validation.



As per coding guidelines: "Always use validateAuthContext() for authentication in API routes; it supports both x-api-key header and Authorization: Bearer token authentication."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/sandboxes/staged-files/route.ts` around lines 42 - 44, The route
currently delegates to postSandboxesUploadTokensHandler without authenticating;
call validateAuthContext(request) inside POST to get the authContext and pass it
into postSandboxesUploadTokensHandler (update postSandboxesUploadTokensHandler
signature to accept the authContext and remove the clientPayload.token
workaround), or if Vercel Blob truly prevents using validateAuthContext(), add a
short comment explaining why and implement equivalent validation inside
postSandboxesUploadTokensHandler (or a shared helper) so authentication is
always performed before upload token logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/sandbox/postSandboxesUploadTokensHandler.ts`:
- Around line 27-31: The onBeforeGenerateToken handler currently only checks
payload?.token presence; replace that with real server-side validation: call a
verifyPrivyToken(token) helper (implement using Privy server SDK or JWT public
key verification) from onBeforeGenerateToken in
postSandboxesUploadTokensHandler.ts, ensure verifyPrivyToken returns a decoded
payload and extract the account/user id (e.g., decoded.sub or decoded.accountId)
and reject (throw Error) if verification fails or token is expired, then use the
extracted user id for downstream audit/authorization; if full verification
cannot be implemented immediately, at minimum log the incoming token and client
context for audit and wire up token/IP rate-limiting keyed by that token.

---

Nitpick comments:
In `@app/api/sandboxes/staged-files/route.ts`:
- Around line 42-44: The route currently delegates to
postSandboxesUploadTokensHandler without authenticating; call
validateAuthContext(request) inside POST to get the authContext and pass it into
postSandboxesUploadTokensHandler (update postSandboxesUploadTokensHandler
signature to accept the authContext and remove the clientPayload.token
workaround), or if Vercel Blob truly prevents using validateAuthContext(), add a
short comment explaining why and implement equivalent validation inside
postSandboxesUploadTokensHandler (or a shared helper) so authentication is
always performed before upload token logic.

In `@lib/sandbox/postSandboxesUploadTokensHandler.ts`:
- Around line 20-48: The handler postSandboxesUploadTokensHandler is over the
20-line guideline—extract the client payload parsing/token validation and the
error response creation into small helpers: implement
validateClientPayload(clientPayload) used inside the onBeforeGenerateToken
callback to parse JSON and throw a clear Authentication error when
payload?.token is missing, and implement createErrorResponse(error) to return
the object and status used in the catch block (message and appropriate status);
replace the inline parsing/throw and the catch return with calls to these
helpers to shorten and improve testability of postSandboxesUploadTokensHandler.
- Around line 42-46: The catch in postSandboxesUploadTokensHandler currently
always returns 400; update it to map specific error types to appropriate HTTP
statuses by inspecting the thrown error (e.g., if error is an instance of
AuthenticationError or contains auth-related marker -> 401 Unauthorized; if
error is a PayloadTooLargeError or message indicates file size -> 413 Payload
Too Large; if error is a VercelBlobError/server-side error -> 500 Internal
Server Error; otherwise keep 400 for malformed requests), and return
NextResponse.json with the error message and the selected status while
preserving getCorsHeaders(); use the existing function name
postSandboxesUploadTokensHandler and the current catch block as the location to
implement this mapping.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 699570e0-1317-4c01-a4a7-5d2ff0f4deba

📥 Commits

Reviewing files that changed from the base of the PR and between f677145 and 70bae64.

⛔ Files ignored due to path filters (1)
  • lib/sandbox/__tests__/postSandboxesUploadTokensHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • app/api/sandboxes/staged-files/route.ts
  • lib/sandbox/postSandboxesUploadTokensHandler.ts

Comment thread lib/sandbox/postSandboxesUploadTokensHandler.ts Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 3 files

Confidence score: 2/5

  • Two high-severity, high-confidence findings in lib/sandbox/postSandboxesUploadTokensHandler.ts create meaningful runtime and security risk, so this is not a low-risk merge as-is.
  • The catch block currently returns raw exception details and HTTP 400 for all failures, which can leak internal error information and misclassify unexpected server faults that should be handled as generic 500 responses.
  • onBeforeGenerateToken only checks for token presence and does not validate that payload.token is a valid Privy access token, which could allow invalid or unauthorized token use in upload token generation flows.
  • Pay close attention to lib/sandbox/postSandboxesUploadTokensHandler.ts and lib/sandbox/__tests__/postSandboxesUploadTokensHandler.test.ts - harden auth/error handling behavior and then align the new test file with repository length/style limits.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/sandbox/postSandboxesUploadTokensHandler.ts">

<violation number="1" location="lib/sandbox/postSandboxesUploadTokensHandler.ts:29">
P1: The `onBeforeGenerateToken` callback only performs a presence check on `payload.token` without actually validating it as a Privy access token. Per the [Vercel Blob docs](https://vercel.com/docs/storage/vercel-blob/client-upload), you must authenticate and authorize users in this callback — without proper validation, anyone can upload files to your Blob store by passing any truthy string as `token`. Consider verifying the JWT signature here (e.g., using Privy's server SDK) to prevent unauthorized uploads to the staging area.</violation>

<violation number="2" location="lib/sandbox/postSandboxesUploadTokensHandler.ts:43">
P1: Do not return raw exception messages and HTTP 400 for all failures in this catch block; unexpected errors should be logged server-side and returned as a generic 500 response.

(Based on your team's feedback about hiding internal exception details in API error responses.) [FEEDBACK_USED]</violation>
</file>

<file name="lib/sandbox/__tests__/postSandboxesUploadTokensHandler.test.ts">

<violation number="1" location="lib/sandbox/__tests__/postSandboxesUploadTokensHandler.test.ts:1">
P2: Custom agent: **Enforce Clear Code Style and Maintainability Practices**

New test file exceeds the repository’s 100-line file-length limit.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant UI as Browser (chat.recoupable.com)
    participant Next as Next.js API Route
    participant Handler as postSandboxesUploadTokensHandler
    participant Cors as getCorsHeaders
    participant BlobSDK as @vercel/blob/client
    participant VercelBlob as Vercel Blob Service

    Note over UI,VercelBlob: NEW: POST /api/sandboxes/staged-files

    alt OPTIONS preflight
        UI->>Next: OPTIONS /api/sandboxes/staged-files
        Next->>Cors: get empty CORS headers
        Cors-->>Next: Access-Control-Allow-Origin: *
        Next-->>UI: 200 (empty body) + CORS headers
    end

    Note over UI,VercelBlob: Phase 1: Token Handshake (from @vercel/blob/client.upload())
    UI->>Next: POST /api/sandboxes/staged-files
    Note over UI,Next: Body: HandleUploadBody<br/>No Authorization header allowed by SDK

    Next->>Handler: postSandboxesUploadTokensHandler(request)

    Handler->>Handler: Parse request body as HandleUploadBody

    Handler->>BlobSDK: handleUpload({ body, request, onBeforeGenerateToken, onUploadCompleted })

    BlobSDK->>Handler: Invoke onBeforeGenerateToken(pathname, clientPayload)
    Handler->>Handler: Parse clientPayload JSON

    alt clientPayload has token field
        Handler-->>BlobSDK: Return { maximumSizeInBytes: 104857600, addRandomSuffix: true }
    else clientPayload missing or no token
        Handler-->>BlobSDK: Throw Error("Authentication required")
        BlobSDK-->>Handler: Reject
        Handler->>Handler: Catch error
        Handler->>Cors: getCorsHeaders()
        Cors-->>Handler: CORS headers
        Handler-->>Next: 400 { error: "Authentication required" }
        Next-->>UI: 400 + CORS headers
    end

    Note over VercelBlob,BlobSDK: Phase 1a: Blob generates client token
    BlobSDK->>VercelBlob: Request presigned upload token
    VercelBlob-->>BlobSDK: { clientToken: "tkn_xxx" }

    BlobSDK-->>Handler: { type: "blob.generate-client-token", ... }
    Handler->>Cors: getCorsHeaders()
    Cors-->>Handler: CORS headers
    Handler-->>Next: 200 JSON response + CORS headers
    Next-->>UI: 200 { clientToken: "tkn_xxx" }

    Note over UI,VercelBlob: Phase 2: Direct Upload (browser to Blob)
    UI->>VercelBlob: PUT file with clientToken
    VercelBlob-->>UI: 200 { url: "https://blob.vercel-storage.com/..." }

    Note over VercelBlob,BlobSDK: Phase 2a: Blob calls back with completion
    VercelBlob->>BlobSDK: POST callback (completion event)
    BlobSDK->>Handler: Invoke onUploadCompleted({ blob: { url, pathname } })
    Handler->>Handler: Currently no-op (awaiting downstream commit)

    Note over UI,Next: Phase 3: Commit (separate request)
    UI->>Next: POST /api/sandboxes/files (with Bearer token)
    Note over UI,Next: Downstream route re-authenticates via Authorization header
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread lib/sandbox/postSandboxesUploadTokensHandler.ts
Comment thread lib/sandbox/postSandboxesUploadTokensHandler.ts Outdated
@@ -0,0 +1,110 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Custom agent: Enforce Clear Code Style and Maintainability Practices

New test file exceeds the repository’s 100-line file-length limit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sandbox/__tests__/postSandboxesUploadTokensHandler.test.ts, line 1:

<comment>New test file exceeds the repository’s 100-line file-length limit.</comment>

<file context>
@@ -0,0 +1,110 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { handleUpload } from "@vercel/blob/client";
+
</file context>

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

0 issues found across 2 files (changes from recent commits).

Requires human review: Auto-approval blocked by 3 unresolved issues from previous reviews.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

0 issues found across 2 files (changes from recent commits).

Requires human review: Auto-approval blocked by 3 unresolved issues from previous reviews.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

0 issues found across 3 files (changes from recent commits).

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

0 issues found across 3 files (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

@arpitgupta1214
Copy link
Copy Markdown
Collaborator Author

Preview smoke test — https://api-git-feature-sandbox-staged-files-recoup.vercel.app

Endpoint matrix

Test Expected Actual
OPTIONS preflight (origin: chat.recoupable.com) 200 + permissive CORS ✅ 200; allow-origin: *, allow-headers: Content-Type, Authorization, X-Requested-With, x-api-key
POST handshake, no auth 401 from validateAuthContext 401 "Exactly one of x-api-key or Authorization must be provided"
POST handshake, with x-api-key 200 + clientToken ✅ 200, returned a real vercel_blob_client_… token
POST callback, empty body (no auth) 500 generic — signature verify fails 500 "Failed to issue upload token" (raw exception hidden, server-side console.error per review fix)
POST junk body.type 500 generic 500 "Failed to issue upload token"

End-to-end via @vercel/blob/client.upload()

Ran from the api worktree pointed at the preview, with x-api-key forwarded via the library's headers option:

starting upload,  25 bytes
blob.url: https://dxfamqbi5zyezrs5.public.blob.vercel-storage.com/smoke-1778349079998-…txt
blob.pathname: smoke-1778349079998-…txt
download status: 200
download body: "smoke test 1778349079998\n"
match: true

Confirms the full handshake → presigned token → direct PUT to Vercel Blob → upload-completed callback path works end-to-end against this preview, with the auth surface aligned to api convention.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
lib/sandbox/postSandboxesUploadTokensHandler.ts (2)

30-30: ⚡ Quick win

Remove the empty callback or document why it's needed.

The onUploadCompleted callback is empty and serves no purpose. If the Vercel Blob handleUpload API doesn't require this callback, consider removing it entirely to reduce noise.

♻️ Proposed cleanup
 const jsonResponse = await handleUpload({
   body,
   request,
   onBeforeGenerateToken: async () => ({
     maximumSizeInBytes: MAX_UPLOAD_SIZE_BYTES,
     addRandomSuffix: true,
   }),
-  onUploadCompleted: async () => {},
 });

If the callback is required by the API signature but intentionally does nothing (because signature verification is sufficient), add a comment:

-  onUploadCompleted: async () => {},
+  onUploadCompleted: async () => {
+    // No-op: signature verification by handleUpload is sufficient
+  },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/sandbox/postSandboxesUploadTokensHandler.ts` at line 30, The
onUploadCompleted: async () => {} callback in
postSandboxesUploadTokensHandler.ts is a no-op and should be removed to reduce
noise; either drop the onUploadCompleted property from the options passed to
handleUpload() if the Vercel Blob handleUpload API does not require it, or if
the API signature mandates the property, replace the empty function with a short
explanatory comment (e.g., "// intentionally no-op: signature required,
verification handled elsewhere") and keep the minimal async stub to satisfy
TypeScript types in the same place where handleUpload is invoked.

10-41: 💤 Low value

Consider extracting auth validation into a helper function.

The function is 32 lines, exceeding the 20-line guideline. While the logic is straightforward, you could improve readability by extracting the auth validation block (lines 16-21) into a separate helper like validateTokenGenerationAuth(body, request).

♻️ Optional refactor to reduce function length

Create a new file lib/sandbox/validateTokenGenerationAuth.ts:

import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import type { HandleUploadBody } from "@vercel/blob/client";
import { validateAuthContext } from "@/lib/auth/validateAuthContext";

export async function validateTokenGenerationAuth(
  body: HandleUploadBody,
  request: NextRequest,
): Promise<NextResponse | null> {
  if (body.type === "blob.generate-client-token") {
    const auth = await validateAuthContext(request);
    if (auth instanceof NextResponse) {
      return auth;
    }
  }
  return null;
}

Then update the handler:

+import { validateTokenGenerationAuth } from "@/lib/sandbox/validateTokenGenerationAuth";
+
 export async function postSandboxesUploadTokensHandler(
   request: NextRequest,
 ): Promise<NextResponse> {
   try {
     const body = (await request.json()) as HandleUploadBody;
 
-    if (body.type === "blob.generate-client-token") {
-      const auth = await validateAuthContext(request);
-      if (auth instanceof NextResponse) {
-        return auth;
-      }
-    }
+    const authError = await validateTokenGenerationAuth(body, request);
+    if (authError) {
+      return authError;
+    }
 
     const jsonResponse = await handleUpload({

As per coding guidelines: "Flag functions longer than 20 lines".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/sandbox/postSandboxesUploadTokensHandler.ts` around lines 10 - 41,
Extract the auth validation block inside postSandboxesUploadTokensHandler into a
helper named validateTokenGenerationAuth that accepts (body: HandleUploadBody,
request: NextRequest) and returns Promise<NextResponse | null>; move the
conditional that checks body.type === "blob.generate-client-token" and calls
validateAuthContext(request) into that helper, returning the NextResponse when
auth fails and null otherwise, then replace the inline block in
postSandboxesUploadTokensHandler with a call to validateTokenGenerationAuth and
early-return if it yields a NextResponse; keep the rest of handleUpload
invocation unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/sandboxes/staged-file/route.ts`:
- Around line 11-14: Add a complete JSDoc block above the exported POST function
that documents the request parameter and the returned Response: include a brief
description, an `@param` tag for the request parameter (type NextRequest) and an
`@returns` tag indicating a Promise<Response> (or Response) returned by
postSandboxesUploadTokensHandler; place it directly above the POST function
declaration so the linter picks it up and reference the handler name
postSandboxesUploadTokensHandler in the description for clarity.
- Around line 6-9: Add a complete JSDoc block above the exported OPTIONS
function describing its purpose and include a `@returns` tag specifying it returns
a NextResponse (CORS preflight response) so the linter is satisfied; locate the
OPTIONS function and reference NextResponse and getCorsHeaders in the
description to make clear it returns a NextResponse with CORS headers for
preflight requests.

---

Nitpick comments:
In `@lib/sandbox/postSandboxesUploadTokensHandler.ts`:
- Line 30: The onUploadCompleted: async () => {} callback in
postSandboxesUploadTokensHandler.ts is a no-op and should be removed to reduce
noise; either drop the onUploadCompleted property from the options passed to
handleUpload() if the Vercel Blob handleUpload API does not require it, or if
the API signature mandates the property, replace the empty function with a short
explanatory comment (e.g., "// intentionally no-op: signature required,
verification handled elsewhere") and keep the minimal async stub to satisfy
TypeScript types in the same place where handleUpload is invoked.
- Around line 10-41: Extract the auth validation block inside
postSandboxesUploadTokensHandler into a helper named validateTokenGenerationAuth
that accepts (body: HandleUploadBody, request: NextRequest) and returns
Promise<NextResponse | null>; move the conditional that checks body.type ===
"blob.generate-client-token" and calls validateAuthContext(request) into that
helper, returning the NextResponse when auth fails and null otherwise, then
replace the inline block in postSandboxesUploadTokensHandler with a call to
validateTokenGenerationAuth and early-return if it yields a NextResponse; keep
the rest of handleUpload invocation unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: da23ad0c-8f1a-433e-8c49-6d58a537ee87

📥 Commits

Reviewing files that changed from the base of the PR and between 70bae64 and 87e9350.

⛔ Files ignored due to path filters (1)
  • lib/sandbox/__tests__/postSandboxesUploadTokensHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • app/api/sandboxes/staged-file/route.ts
  • lib/sandbox/postSandboxesUploadTokensHandler.ts

Comment thread app/api/sandboxes/staged-file/route.ts Outdated
Comment thread app/api/sandboxes/staged-file/route.ts Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

0 issues found across 2 files (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

arpitgupta1214 and others added 7 commits May 11, 2026 17:55
Adds POST /api/sandboxes/staged-files — the Vercel Blob client-upload
handshake half of the sandbox upload flow. Mirrors chat's existing
/api/sandbox/upload handler so the chat side can flip its
handleUploadUrl to api and delete the local route.

Auth follows the minimum-port shape: validates clientPayload.token
because @vercel/blob/client.upload() does not allow setting an
Authorization header on the handshake POST. The downstream commit
(POST /api/sandboxes/files) re-authenticates with a real Bearer token.

OPTIONS preflight is wired with getCorsHeaders() so cross-origin
calls from chat.recoupable.com work the same way the existing
/api/sandboxes/files route does.

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

Verb form matches /api/sandboxes/files (commit) and /api/sandboxes/file (get).

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

Noun-shaped resource matching /api/sandboxes/file (singular getter)
and /api/sandboxes/files (collection commit). Each call generates a
token to stage one file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops clientPayload.token in favor of validateAuthContext() so the
endpoint matches the auth surface of every other api route. The
chat-side upload() now forwards an Authorization Bearer header via
the library's headers option (which I missed earlier — that field
is documented for exactly this use case).

Branches on body.type to skip auth on the upload-completed callback;
that POST comes from Vercel Blob's backend without the user's auth
header, and handleUpload() verifies its signature internally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Log unexpected handleUpload errors server-side via console.error and
return a generic 500 — matches the createSandboxHandler pattern and
avoids leaking internal context in error responses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the multi-paragraph commentary; keep a one-liner on the handler
explaining the asymmetric auth, and minimal JSDoc on the route per
project convention.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sweetmantech sweetmantech force-pushed the feature/sandbox-staged-files branch from 4c55c39 to 27163a9 Compare May 11, 2026 22:56
@sweetmantech
Copy link
Copy Markdown
Contributor

Manual verification on the preview deployment

Preview URL: https://api-git-feature-sandbox-staged-files-recoup.vercel.app

Note: I rebased the branch onto current origin/test before testing (the branch had drifted ~12 commits behind, and was about to silently revert merged credits work from #547 / #549). The diff against test is now clean — only the 3 new files for this PR (route, handler, test). Force-pushed.

Setup

Used an existing fresh agent's API key (account cef5eb58-..., free tier) to exercise auth on the rebased preview build.

Results

# Scenario Expected Actual Body
1 OPTIONS /api/sandboxes/staged-file 200 + CORS 200 (CORS header Access-Control-Allow-Origin: *)
2 POST with no auth + blob.generate-client-token body 401 401 {"status":"error","error":"Exactly one of x-api-key or Authorization must be provided"}
3 POST with valid key + unknown body type 500 (handleUpload throws) 500 {"status":"error","error":"Failed to issue upload token"}
4 POST with valid key + well-formed blob.generate-client-token body 200 + Vercel Blob handshake 200 {"type":"blob.generate-client-token","clientToken":"vercel_blob_client_DXfAmqBi5ZyEzRs5_NzY..."}

Highlights

  • Auth enforcement (row 2)validateAuthContext is reached and rejects unauthenticated handshake requests with the standard { error } shape used elsewhere in the api.
  • Error masking (row 3) — the bogus-body path goes through the catch block and returns the generic "Failed to issue upload token" message, hiding internal exception details from the client. Good defense-in-depth.
  • End-to-end happy path works (row 4)BLOB_READ_WRITE_TOKEN is configured on the preview env and handleUpload successfully returns a real clientToken. This is the contract the browser SDK consumes, so the wire format is intact.
  • CORS preflight (row 1) — OPTIONS handler responds as expected so cross-origin browser uploads will work.

Note on the URL itself: documented as /api/sandboxes/staged-file (singular). The docs PR (#198) shipped with this exact path, so they're aligned. Flagged earlier as not quite REST — the resource being created is really an upload token, not a "staged-file" — but the contract is consistent across api + docs which is what matters for merging.

🤖 Generated with Claude Code

@sweetmantech sweetmantech merged commit 329c589 into test May 11, 2026
5 checks passed
@sweetmantech sweetmantech deleted the feature/sandbox-staged-files branch May 11, 2026 23:05
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.

2 participants