Skip to content

Conversation

IslamIhab
Copy link

@IslamIhab IslamIhab commented Sep 2, 2025

Summary

  • This PR adds Convex adapter.
  • This repo is an example for how to implement it.
  • It was highly inspired from PR feat: add Convex adapter #929.
  • New Convex adapter entrypoints
    • packages/uploadthing/src/convex.ts
    • packages/uploadthing/src/convex-helpers.ts

Design decisions

  • Internal action boundary: Keep UploadThing’s core handler pure over Request/Response, and adapt at the Convex boundary using internalActionGeneric. Mirrors existing framework adapters and avoids Convex-specific leakage.
  • Request envelope: Convex internal actions require serializable args; we pass { url, method, headers, body? } and reconstruct a Request inside the action.
  • CORS handling: Uses convex-helpers corsRouter to handle cors

Developer experience

Usage sketch:

// convex/http.ts
import corsRouter from "convex-helpers/server/cors";
import { httpRouter } from "convex/server";

import { createRouteHandler } from "uploadthing/convex-helpers";

import { internal } from "./_generated/api";

const http = httpRouter();

const cors = corsRouter(http, {
  allowedOrigins: ["http://localhost:3000"],
});

createRouteHandler({
  http: cors,
  internalAction: internal.uploadthing.handler,
  path: "/api/uploadthing",
});

export default http;
// convex/uploadthing.ts
"use node";

import { createInternalAction, createUploadthing, FileRouter } from "uploadthing/convex";
import { api } from "./_generated/api";
import crypto from "node:crypto";
// globalThis.crypto is not defined in convex
globalThis.crypto = crypto as Crypto;

const f = createUploadthing();

const router = {
  imageUploader: f({ image: { maxFileSize: "4MB" } })
    .middleware(async (opts) => {
      const identity = await opts.ctx.auth.getUserIdentity();

      return { userId: identity?.subject };
    })
    .onUploadComplete(async (opts) => {
      await opts.ctx.runMutation(api.media.add, { url: opts.file.ufsUrl });
      return { uploadedBy: opts.metadata.userId };
    }),
} satisfies FileRouter;

export type OurFileRouter = typeof router;

export const handler = createInternalAction({ router });

Required env on convex:

  • UPLOADTHING_TOKEN

Thanks for taking a look! I’m happy to iterate on naming, paths, or API surface to fit project.

Summary by CodeRabbit

  • New Features

    • Convex backend adapter: upload builders, internal action bridge, and HTTP route helper; optional Convex peer support.
  • Documentation

    • New Convex integration guide, docs page, navigation entry, and Convex logo/icon added.
  • Examples

    • Minimal Next.js + Convex example with upload routes, handlers, schema, and client wiring.
  • Chores

    • Build and publish config updated to include Convex artifacts and example assets.

Copy link

changeset-bot bot commented Sep 2, 2025

🦋 Changeset detected

Latest commit: 75f4383

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
uploadthing Minor

Not sure what this means? Click here to learn what changesets are.

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

Copy link
Contributor

coderabbitai bot commented Sep 2, 2025

Walkthrough

Adds a Convex backend adapter for UploadThing: new source modules (convex, convex-helpers), package exports and optional peer dependencies for convex/convex-helpers, build and declaration config updates, docs, and a minimal Convex example app wiring UploadThing through a Convex internal action and HTTP router.

Changes

Cohort / File(s) Summary
Package exports & publishing
packages/uploadthing/package.json
Adds exports entries ./convex and ./convex-helpers (ESM/CJS + types), includes convex and convex-helpers in files, and declares them as optional peerDependencies.
Build / declaration config
packages/uploadthing/tsdown.config.ts,
packages/uploadthing/turbo.json
Adds tsdown entries mapping ../convex/indexsrc/convex.ts and ../convex-helpers/indexsrc/convex-helpers.ts; adds convex/** and convex-helpers/** to turbo build outputs.
Convex integration source
packages/uploadthing/src/convex.ts,
packages/uploadthing/src/convex-helpers.ts
New Convex adapter implementation: createUploadthing builder, createInternalAction (adapts UploadThing handlers to Convex internal actions), and createRouteHandler HTTP bridge registering OPTIONS/GET/POST and forwarding requests to Convex actions.
Documentation page
docs/src/app/(docs)/backend-adapters/convex/page.mdx
New docs page describing Convex adapter setup, FileRouter example, server wiring, HTTP mounting, and client usage.
Docs UI & nav
docs/src/components/icons.tsx, docs/src/components/Libraries.tsx, docs/src/site-config.ts
Adds ConvexIcon, inserts Convex into Backend Adapters list and site navigation, and adjusts the adapters grid layout.
Changeset
.changeset/swift-beds-explain.md
Adds changeset noting a minor version bump and Convex adapter addition.
Example project metadata
examples/minimal-convex/package.json, examples/minimal-convex/next.config.js, examples/minimal-convex/tsconfig.json, examples/minimal-convex/next-env.d.ts, examples/minimal-convex/.env.example, examples/minimal-convex/.gitignore, examples/minimal-convex/README.md
New Next.js + Convex example project with scripts, env example, gitignore, README, and TS/Next config.
Example Convex backend
examples/minimal-convex/convex/*
Adds Convex schema, http router (http.ts) using createRouteHandler, media mutation, UploadThing internal action/router (uploadthing.ts), Convex tsconfig, and example README.
Example Next.js UI
examples/minimal-convex/src/app/layout.tsx, examples/minimal-convex/src/app/page.tsx, examples/minimal-convex/src/utils/uploadthing.ts
Adds RootLayout with UploadThing SSR plugin, example page demonstrating UploadButton/Dropzone and manual upload flows, and typed helper exports (UploadButton, UploadDropzone, useUploadThing) configured to Convex site URL.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as Client
  participant Browser as Browser
  participant ConvexHTTP as Convex HTTP Router
  participant Bridge as createRouteHandler
  participant InternalAction as Convex internalAction
  participant Adapter as adapter handler (makeAdapterHandler)
  participant UT as UploadThing router

  User->>Browser: Start upload
  Browser->>ConvexHTTP: POST /api/uploadthing
  Note right of ConvexHTTP #f3f3f3: OPTIONS route returns 204 for preflight
  ConvexHTTP->>Bridge: matched route
  Bridge->>InternalAction: ctx.runAction(internalAction, { request })
  InternalAction->>Adapter: construct Request and invoke adapter handler
  Adapter->>UT: Dispatch to UploadThing router
  UT-->>Adapter: Response { status, statusText, headers, body }
  Adapter-->>InternalAction: return Response-like object
  InternalAction-->>Bridge: plain result forwarded
  Bridge-->>ConvexHTTP: respond with action-derived status, headers, body
  ConvexHTTP-->>Browser: HTTP response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

uploadthing/client

Suggested reviewers

  • markflorkowski
  • juliusmarminge

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f840666 and 75f4383.

📒 Files selected for processing (1)
  • examples/minimal-convex/convex/http.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/minimal-convex/convex/http.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vade Review
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

vercel bot commented Sep 2, 2025

@IslamIhab is attempting to deploy a commit to the Ping Labs Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

vercel bot commented Sep 2, 2025

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

1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
legacy-docs-uploadthing Ignored Ignored Sep 4, 2025 0:03am

@markflorkowski
Copy link
Collaborator

Needs docs and examples.

Copy link
Contributor

@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 (7)
packages/uploadthing/src/convex-helpers.ts (3)

4-15: CORS helper: add credential support and mirror requested headers

If clients send cookies or Authorization, you’ll need Access-Control-Allow-Credentials and you should mirror Access-Control-Request-Headers instead of "*".

Apply:

-const addCorsHeaders = (headers?: Record<string, string>) => {
+const addCorsHeaders = (
+  headers?: Record<string, string>,
+  requestAllowHeaders?: string,
+  allowCredentials = false,
+) => {
@@
   return new Headers({
     ...headers,
     "Access-Control-Allow-Origin": process.env.CLIENT_ORIGIN,
     "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
-    "Access-Control-Allow-Headers": "*",
+    "Access-Control-Allow-Headers": requestAllowHeaders ?? "*",
+    "Access-Control-Allow-Credentials": allowCredentials ? "true" : "false",
     "Access-Control-Max-Age": "86400",
   });

62-66: OPTIONS handler: simplify and reflect requested headers

No need for Promise.resolve. Also reflect Access-Control-Request-Headers from the preflight.

-    handler: httpActionGeneric(async () =>
-      Promise.resolve(
-        new Response(null, { status: 204, headers: addCorsHeaders() }),
-      ),
-    ),
+    handler: httpActionGeneric(async (_ctx, req) =>
+      new Response(null, {
+        status: 204,
+        headers: addCorsHeaders(
+          undefined,
+          req.headers.get("access-control-request-headers") ?? undefined,
+        ),
+      }),
+    ),

40-49: Verify: req.text() will corrupt binary bodies (multipart/form-data)

If this endpoint ever carries file bytes (multipart/form-data), reading as text and re-encoding into a Blob will mangle data. If only JSON/urlencoded ever flows here, you’re fine.

Potential binary-safe approach (requires adjusting the action schema and the reconstruction in packages/uploadthing/src/convex.ts):

  • In convex-helpers.ts (send base64):
const ab = await req.arrayBuffer();
const bodyBase64 =
  ab.byteLength > 0 ? Buffer.from(ab).toString("base64") : undefined;
// send { bodyBase64 } instead of { body }
  • In convex.ts (rebuild body):
const body =
  args.request.bodyBase64 != null
    ? new Blob([Uint8Array.from(Buffer.from(args.request.bodyBase64, "base64"))])
    : args.request.body ?? null;

Note: Large bodies through internal actions may hit payload limits; confirm UploadThing’s Convex flow doesn’t require proxying raw file bytes.

packages/uploadthing/src/convex.ts (2)

55-59: Optional: accept base64 body for binary-safe reconstruction

If you adopt a base64 envelope (see helpers comment), reconstruct from base64; otherwise this Blob-from-string path is fine for textual payloads.

Example:

-      const request = new Request(args.request.url, {
-        method: args.request.method,
-        headers: new Headers(args.request.headers),
-        body: args.request.body ? new Blob([args.request.body]) : null,
-      });
+      const body =
+        // prefer bodyBase64 when present; fallback to textual body
+        (args as any).request.bodyBase64
+          ? new Blob([
+              Uint8Array.from(
+                Buffer.from((args as any).request.bodyBase64, "base64"),
+              ),
+            ])
+          : args.request.body
+            ? new Blob([args.request.body])
+            : null;
+      const request = new Request(args.request.url, {
+        method: args.request.method,
+        headers: new Headers(args.request.headers),
+        body,
+      });

28-31: Minor: add explicit return types for exported APIs

Helps keep the public surface stable across refactors/tsdown inference.

Example:

export const createUploadthing = <TErrorShape extends Json>(
  opts?: CreateBuilderOptions<TErrorShape>,
): ReturnType<typeof createBuilder<AdapterArgs, TErrorShape>> => ...

Also applies to: 32-71

packages/uploadthing/package.json (2)

157-158: Added to published files: good; consider updating clean script to avoid stale artifacts.

Add the new dirs (and missing client-future) to "scripts.clean" so local builds don’t leave outdated outputs.

Apply:

   "scripts": {
     "lint": "eslint src test --max-warnings 0",
     "build": "tsdown",
-    "clean": "git clean -xdf client express fastify h3 internal next next-legacy server tw node_modules",
+    "clean": "git clean -xdf client client-future convex convex-helpers express fastify h3 internal next next-legacy server tw node_modules",
     "dev": "tsdown --no-clean",
     "prepack": "bun ../../.github/replace-workspace-protocol.ts",
     "test": "vitest run",
     "typecheck": "tsc --noEmit"
   },

219-219: Peer dep "convex" in packages/uploadthing/package.json: replace "*" with ">=1 <2" to pin to Convex v1 (latest 1.26.2).
This bounds to all v1 releases and prevents unintentional v2 upgrades.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 18f98a7 and 50e71a9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • packages/uploadthing/package.json (4 hunks)
  • packages/uploadthing/src/convex-helpers.ts (1 hunks)
  • packages/uploadthing/src/convex.ts (1 hunks)
  • packages/uploadthing/tsdown.config.ts (1 hunks)
  • packages/uploadthing/turbo.json (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-10-09T22:39:50.896Z
Learnt from: riordanpawley
PR: pingdotgg/uploadthing#1005
File: packages/uploadthing/src/internal/upload.server.ts:35-35
Timestamp: 2024-10-09T22:39:50.896Z
Learning: In `packages/uploadthing/src/internal/upload.server.ts`, using `Effect.andThen(unsafeCoerce<unknown, UploadPutResult>)` when processing the JSON response is appropriate and should not be replaced with a type assertion.

Applied to files:

  • packages/uploadthing/src/convex.ts
🧬 Code graph analysis (1)
packages/uploadthing/src/convex.ts (3)
packages/shared/src/types.ts (1)
  • Json (8-8)
packages/uploadthing/src/_internal/upload-builder.ts (1)
  • CreateBuilderOptions (89-91)
packages/uploadthing/src/types.ts (1)
  • RouteHandlerOptions (89-92)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vade Review
🔇 Additional comments (5)
packages/uploadthing/turbo.json (1)

9-10: LGTM: convex and convex-helpers outputs correctly reflected in package.json exports and tsdown.config.ts entries.

packages/uploadthing/tsdown.config.ts (1)

18-19: Unable to automatically verify build outputs due to script errors. Manually confirm before approving:

  • Ensure npm run build (tsdown) in packages/uploadthing produces under dist/convex and dist/convex-helpers:
    • index.js (ESM)
    • index.cjs (CJS)
    • index.d.ts
    • index.d.cts
  • Verify src/convex.ts uses only type‐only imports from “convex” (no runtime code) so that at runtime it still imports “convex” rather than bundling it.
  • Add an explicit external: ['convex'] in tsdown.config.ts if tsdown’s defaults don’t already externalize dependencies.

Once confirmed, remove `` and merge.

packages/uploadthing/package.json (3)

129-138: Export "./convex" mapping looks correct.

Matches tsdown entry and dual ESM/CJS + d.ts/d.cts pattern.

After building, ensure these files exist:

  • convex/index.js, convex/index.cjs
  • convex/index.d.ts, convex/index.d.cts

139-148: Export "./convex-helpers" mapping looks correct.

Consistent with tsdown entry and other framework adapters.

Post-build, verify:

  • convex-helpers/index.js, convex-helpers/index.cjs
  • convex-helpers/index.d.ts, convex-helpers/index.d.cts

237-239: Marking "convex" as optional: LGTM.

Prevents forcing install for users who don’t consume the adapter.

url: req.url,
method: req.method,
headers,
body: await req.text(),
Copy link

Choose a reason for hiding this comment

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

The request body is converted to text with await req.text(), which will corrupt binary file data during uploads.

View Details
📝 Patch Details
diff --git a/packages/uploadthing/src/convex-helpers.ts b/packages/uploadthing/src/convex-helpers.ts
index 444ab16..5065c24 100644
--- a/packages/uploadthing/src/convex-helpers.ts
+++ b/packages/uploadthing/src/convex-helpers.ts
@@ -45,7 +45,12 @@ export const createRouteHandler = (
       url: req.url,
       method: req.method,
       headers,
-      body: await req.text(),
+      ...(await req.arrayBuffer().then(buffer => {
+        // Convert binary data to base64 to preserve it through Convex serialization
+        return buffer.byteLength > 0 ? 
+          { body: btoa(String.fromCharCode(...new Uint8Array(buffer))) } : 
+          {};
+      })),
     };
     const response = await ctx.runAction(internalAction, { request });
 
diff --git a/packages/uploadthing/src/convex.ts b/packages/uploadthing/src/convex.ts
index a536096..2e8ec63 100644
--- a/packages/uploadthing/src/convex.ts
+++ b/packages/uploadthing/src/convex.ts
@@ -55,7 +55,15 @@ export const createInternalAction = <TRouter extends FileRouter>(
       const request = new Request(args.request.url, {
         method: args.request.method,
         headers: new Headers(args.request.headers),
-        body: args.request.body ? new Blob([args.request.body]) : null,
+        body: args.request.body ? (() => {
+          // Convert base64 back to binary data
+          const binaryString = atob(args.request.body);
+          const bytes = new Uint8Array(binaryString.length);
+          for (let i = 0; i < binaryString.length; i++) {
+            bytes[i] = binaryString.charCodeAt(i);
+          }
+          return new Blob([bytes]);
+        })() : null,
       });
 
       const response = await handler(ctx, request);

Analysis

The Convex adapter attempts to serialize HTTP request bodies as strings using await req.text() on line 48 in convex-helpers.ts. This approach will corrupt binary data when users upload files, as converting binary data to text and back to binary (via new Blob([args.request.body]) in convex.ts line 58) does not preserve the original bytes.

File uploads typically contain multipart/form-data with binary file content that cannot be safely represented as UTF-8 text. When req.text() is called on binary data, it attempts to decode the bytes as UTF-8, replacing invalid byte sequences with replacement characters (�). This corrupted data is then serialized to Convex, and when reconstructed as a Blob, the original file data cannot be recovered.

This will cause uploaded files to be corrupted and unusable. The adapter needs to handle binary data properly, potentially using base64 encoding or a similar approach to safely serialize binary content through Convex's action system.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (1)
packages/uploadthing/src/convex.ts (1)

63-66: Good fix on header extraction; past issue addressed.

Using Headers.forEach avoids the earlier Object.entries/Headers pitfall.

🧹 Nitpick comments (3)
packages/uploadthing/src/convex.ts (3)

45-53: Optional: narrow accepted HTTP methods at the validator.

If this action only ever handles GET/POST/OPTIONS, pre-validate to fail fast.

Apply if desired:

       request: v.object({
         url: v.string(),
-        method: v.string(),
+        // Limit to the methods your helper supports
+        method: v.union(v.literal("GET"), v.literal("POST"), v.literal("OPTIONS")),
         headers: v.record(v.string(), v.string()),
         body: v.optional(v.string()),
       }),

55-59: Use the raw string body instead of wrapping in a Blob.

Blob offers no benefit here and can muddle content-type expectations; strings are valid BodyInit.

-      const request = new Request(args.request.url, {
-        method: args.request.method,
-        headers: new Headers(args.request.headers),
-        body: args.request.body ? new Blob([args.request.body]) : null,
-      });
+      const request = new Request(args.request.url, {
+        method: args.request.method,
+        headers: new Headers(args.request.headers),
+        body: args.request.body ?? null,
+      });

63-73: Preserve multi-value headers (e.g., Set-Cookie).

Serializing Headers to Record<string, string> collapses duplicates; Set-Cookie cannot be comma-joined safely. Consider transporting headers as an array of [key, value] pairs end-to-end to preserve multiplicity and order, and only down-convert at the very edge if you’re certain no multi-value headers are used.

If you choose to keep Record<string, string>, at minimum document that multi-value headers aren’t supported by this adapter.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 50e71a9 and e20ad2b.

📒 Files selected for processing (2)
  • .changeset/swift-beds-explain.md (1 hunks)
  • packages/uploadthing/src/convex.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .changeset/swift-beds-explain.md
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-10-09T22:39:50.896Z
Learnt from: riordanpawley
PR: pingdotgg/uploadthing#1005
File: packages/uploadthing/src/internal/upload.server.ts:35-35
Timestamp: 2024-10-09T22:39:50.896Z
Learning: In `packages/uploadthing/src/internal/upload.server.ts`, using `Effect.andThen(unsafeCoerce<unknown, UploadPutResult>)` when processing the JSON response is appropriate and should not be replaced with a type assertion.

Applied to files:

  • packages/uploadthing/src/convex.ts
🧬 Code graph analysis (1)
packages/uploadthing/src/convex.ts (3)
packages/shared/src/types.ts (1)
  • Json (8-8)
packages/uploadthing/src/_internal/upload-builder.ts (1)
  • CreateBuilderOptions (89-91)
packages/uploadthing/src/types.ts (1)
  • RouteHandlerOptions (89-92)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vade Review
🔇 Additional comments (4)
packages/uploadthing/src/convex.ts (4)

13-21: Re-exports look good and match public surface intent.

Keeping UTRegion behind an experimental alias is consistent with prior patterns.


28-31: createUploadthing builder wiring is correct.

Generic args and error shape flow align with createBuilder’s expectations.


32-44: Adapter glue is clean and side-effect free.

Passing (ctx, req) as a tuple and projecting to AdapterArgs via Effect.succeed matches other adapters.


55-56: Script to inspect both files has been executed. The outputs didn’t appear, suggesting either file paths differ or the sandbox environment didn't print them. Requesting clarification:

  • Please confirm the paths for convex-helpers.ts and convex.ts within packages/uploadthing/src/.
  • Alternatively, provide the relevant sections around where req.url is defined in convex-helpers.ts and where args.request.url is used in convex.ts.

@IslamIhab IslamIhab changed the title Add Convex Adapter feat: Add Convex Adapter Sep 2, 2025
Copy link
Contributor

@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

♻️ Duplicate comments (1)
packages/uploadthing/src/convex-helpers.ts (1)

50-55: Critical: req.text() corrupts binary bodies; base64-encode arrayBuffer instead.

Multipart/form-data with binary content will be mangled by text(). Encode the raw bytes and decode in the internal action.

Apply this diff here:

-    const request = {
+    const request = {
       url: req.url,
       method: req.method,
       headers,
-      body: await req.text(),
+      ...(await req.arrayBuffer().then((buffer) => {
+        // Preserve binary payload through Convex serialization
+        return buffer.byteLength > 0
+          ? { body: btoa(String.fromCharCode(...new Uint8Array(buffer))) }
+          : {};
+      })),
     };

And in packages/uploadthing/src/convex.ts, reconstruct the body (shown for reference):

// Inside createInternalAction(...)
const request = new Request(args.request.url, {
  method: args.request.method,
  headers: new Headers(args.request.headers),
  body: args.request.body
    ? (() => {
        const bin = atob(args.request.body);
        const bytes = new Uint8Array(bin.length);
        for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
        return new Blob([bytes]);
      })()
    : null,
});
🧹 Nitpick comments (4)
docs/src/components/icons.tsx (1)

731-739: Add aria-hidden for consistency and accessibility.

Most icons here set aria-hidden="true". Mirror that on ConvexIcon.

 export function ConvexIcon(props: IconProps) {
   return (
     <svg
       viewBox="28 28 128 132"
       xmlns="http://www.w3.org/2000/svg"
       fill="none"
+      aria-hidden="true"
       {...props}
     >
docs/src/app/(docs)/backend-adapters/convex/page.mdx (2)

23-27: Tiny copy fix: article + brand casing.

-  If you don't already have a uploadthing secret key, [sign
-  up](https://uploadthing.com/sign-in) and create one from the
-  [dashboard!](https://uploadthing.com/dashboard)
+  If you don't already have an UploadThing secret key, [sign
+  up](https://uploadthing.com/sign-in) and create one from the
+  [dashboard!](https://uploadthing.com/dashboard)

42-47: Minor punctuation nit.

Use “etc.” per house style elsewhere.

-- Permitted types ["image", "video", etc]
+- Permitted types ["image", "video", etc.]
packages/uploadthing/src/convex-helpers.ts (1)

44-62: Optional: ensure CORS on failures.

If internalAction throws, clients may get a CORS-less 500. Consider try/catch and return a JSON error with addCorsHeaders applied.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e20ad2b and 6b8145d.

📒 Files selected for processing (5)
  • docs/src/app/(docs)/backend-adapters/convex/page.mdx (1 hunks)
  • docs/src/components/Libraries.tsx (3 hunks)
  • docs/src/components/icons.tsx (1 hunks)
  • docs/src/site-config.ts (1 hunks)
  • packages/uploadthing/src/convex-helpers.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
docs/src/components/Libraries.tsx (1)
docs/src/components/icons.tsx (1)
  • ConvexIcon (731-753)
🪛 LanguageTool
docs/src/app/(docs)/backend-adapters/convex/page.mdx

[grammar] ~3-~3: There might be a mistake here.
Context: ... export const metadata = docsMetadata({ title: "Convex", description: "Adapter...

(QB_NEW_EN)


[grammar] ~6-~6: There might be a mistake here.
Context: ...cation", category: "Backend Adapters", }); # Getting started with Convex > Ad...

(QB_NEW_EN)


[grammar] ~38-~38: There might be a mistake here.
Context: ...sociated with a FileRoute. The following is a very minimalistic example, with a s...

(QB_NEW_EN)


[grammar] ~39-~39: There might be a mistake here.
Context: ... single FileRoute "imageUploader". Think of a FileRoute similar to an endpoint, i...

(QB_NEW_EN)


[style] ~42-~42: In American English, abbreviations like “etc.” require a period.
Context: ...: - Permitted types ["image", "video", etc] - Max file size - How many files are a...

(ETC_PERIOD)


[grammar] ~42-~42: There might be a mistake here.
Context: ... Permitted types ["image", "video", etc] - Max file size - How many files are allow...

(QB_NEW_EN)


[grammar] ~43-~43: There might be a mistake here.
Context: ... ["image", "video", etc] - Max file size - How many files are allowed to be uploade...

(QB_NEW_EN)


[grammar] ~44-~44: There might be a mistake here.
Context: ...ow many files are allowed to be uploaded - (Optional) input validation to validat...

(QB_NEW_EN)


[grammar] ~45-~45: There might be a mistake here.
Context: ...idate client-side data sent to the route - (Optional) middleware to authenticate ...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ... do with the FileRoutes, please refer to the File Router API. ``...

(QB_NEW_EN)


[grammar] ~115-~115: There might be a mistake here.
Context: ...ttp; ``` > See configuration options in > [server API reference](/api-reference/...

(QB_NEW_EN)


[grammar] ~120-~120: There might be a mistake here.
Context: ...htly from the fullstack framework setups when using a separate backend server. Yo...

(QB_NEW_EN)


[grammar] ~121-~121: There might be a mistake here.
Context: ...ou'll need to set the URL of your server when you generate the components and hel...

(QB_NEW_EN)


[grammar] ~136-~136: There might be a mistake here.
Context: ... fullstack framework guides: - Next.js - [Solid.js](/getting-started/solid#creatin...

(QB_NEW_EN)


[grammar] ~137-~137: There might be a mistake here.
Context: ...the-upload-thing-components) - Solid.js - [Vue](https://github.com/pingdotgg/upload...

(QB_NEW_EN)


[grammar] ~138-~138: There might be a mistake here.
Context: ...ting-the-upload-thing-components) - Vue - [Svelte](/getting-started/svelte#creating...

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vade Review
🔇 Additional comments (5)
docs/src/site-config.ts (1)

46-46: Nav entry LGTM.

Route matches the new docs page and placement is consistent.

docs/src/components/Libraries.tsx (3)

5-5: Import LGTM.

Consistent with other logo imports.


89-94: Backend card LGTM.

Copy and href align with the new docs route; icon renders via ConvexIcon.


176-176: Grid expansion LGTM.

xl:grid-cols-3 matches Frameworks/Frontend sections for visual consistency.

docs/src/app/(docs)/backend-adapters/convex/page.mdx (1)

11-11: Version callout: please verify before release.

Confirm v7.8 is the actual tag when publishing; otherwise update before merge.

Comment on lines 4 to 16
const addCorsHeaders = (headers?: Record<string, string>) => {
if (!process.env.CLIENT_ORIGIN) {
throw new Error("Convex deployment doesn't have CLIENT_ORIGIN set");
}

return new Headers({
...headers,
"Access-Control-Allow-Origin": process.env.CLIENT_ORIGIN,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "*",
"Access-Control-Max-Age": "86400",
});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

CORS: avoid "*" in Allow-Headers; add Vary and echo requested headers.

Using "*" in Access-Control-Allow-Headers is not consistently honored. Echo Access-Control-Request-Headers and set Vary for cache correctness.

-const addCorsHeaders = (headers?: Record<string, string>) => {
+const addCorsHeaders = (
+  headers?: Record<string, string>,
+  reqHeaders?: Headers,
+) => {
   if (!process.env.CLIENT_ORIGIN) {
     throw new Error("Convex deployment doesn't have CLIENT_ORIGIN set");
   }
 
-  return new Headers({
-    ...headers,
-    "Access-Control-Allow-Origin": process.env.CLIENT_ORIGIN,
-    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
-    "Access-Control-Allow-Headers": "*",
-    "Access-Control-Max-Age": "86400",
-  });
+  const acrh =
+    reqHeaders?.get("access-control-request-headers") ??
+    "content-type, authorization, x-uploadthing-*";
+  return new Headers({
+    ...headers,
+    "Access-Control-Allow-Origin": process.env.CLIENT_ORIGIN,
+    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
+    "Access-Control-Allow-Headers": acrh,
+    "Access-Control-Max-Age": "86400",
+    Vary: "Origin, Access-Control-Request-Headers, Access-Control-Request-Method",
+  });
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const addCorsHeaders = (headers?: Record<string, string>) => {
if (!process.env.CLIENT_ORIGIN) {
throw new Error("Convex deployment doesn't have CLIENT_ORIGIN set");
}
return new Headers({
...headers,
"Access-Control-Allow-Origin": process.env.CLIENT_ORIGIN,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "*",
"Access-Control-Max-Age": "86400",
});
};
const addCorsHeaders = (
headers?: Record<string, string>,
reqHeaders?: Headers,
) => {
if (!process.env.CLIENT_ORIGIN) {
throw new Error("Convex deployment doesn't have CLIENT_ORIGIN set");
}
const acrh =
reqHeaders?.get("access-control-request-headers") ??
"content-type, authorization, x-uploadthing-*";
return new Headers({
...headers,
"Access-Control-Allow-Origin": process.env.CLIENT_ORIGIN,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": acrh,
"Access-Control-Max-Age": "86400",
Vary: "Origin, Access-Control-Request-Headers, Access-Control-Request-Method",
});
};
🤖 Prompt for AI Agents
In packages/uploadthing/src/convex-helpers.ts around lines 4-16, the function
currently sets "Access-Control-Allow-Headers" to "*", which is not reliably
honored; change it to echo the incoming "Access-Control-Request-Headers" value
(if present) and add a Vary header for cache correctness. Specifically, read the
incoming headers argument for "Access-Control-Request-Headers"
(case-insensitive), set "Access-Control-Allow-Headers" to that value when
present (fallback to a sensible default list if absent), and add a "Vary" header
that includes "Access-Control-Request-Headers" (and "Origin") so caches treat
responses correctly.

Comment on lines 65 to 71
method: "OPTIONS",
path,
handler: httpActionGeneric(async () =>
Promise.resolve(
new Response(null, { status: 204, headers: addCorsHeaders() }),
),
),
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Preflight: pass request headers to CORS helper.

Echo Access-Control-Request-Headers on OPTIONS.

   http.route({
     method: "OPTIONS",
     path,
-    handler: httpActionGeneric(async () =>
-      Promise.resolve(
-        new Response(null, { status: 204, headers: addCorsHeaders() }),
-      ),
-    ),
+    handler: httpActionGeneric(async (_ctx, req) => {
+      return new Response(null, {
+        status: 204,
+        headers: addCorsHeaders(undefined, req.headers),
+      });
+    }),
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
method: "OPTIONS",
path,
handler: httpActionGeneric(async () =>
Promise.resolve(
new Response(null, { status: 204, headers: addCorsHeaders() }),
),
),
http.route({
method: "OPTIONS",
path,
handler: httpActionGeneric(async (_ctx, req) => {
return new Response(null, {
status: 204,
headers: addCorsHeaders(undefined, req.headers),
});
}),
});
🤖 Prompt for AI Agents
In packages/uploadthing/src/convex-helpers.ts around lines 65 to 71, the OPTIONS
handler currently returns CORS headers without echoing the client's requested
headers; update the handler to read the incoming request's
"Access-Control-Request-Headers" header and pass that value into the CORS helper
(or include it in the response headers) so the response echoes that header on
preflight; implement by extracting
req.headers.get('access-control-request-headers') in the httpActionGeneric
callback and supplying it to addCorsHeaders (or appending
'Access-Control-Allow-Headers' with that value) when constructing the Response.

@IslamIhab
Copy link
Author

IslamIhab commented Sep 3, 2025

@markflorkowski Added docs and example

Copy link
Contributor

@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: 6

🧹 Nitpick comments (22)
examples/minimal-convex/CHANGELOG.md (2)

1-1: Header name doesn’t match directory/package; likely copy/paste.

The file path is examples/minimal-convex but the H1 says @example/appdir-minimal. Suggest aligning with the example's actual name.

Apply:

-# @example/appdir-minimal
+# @example/minimal-convex

3-5: Invalid and duplicate version headings (“## null”); breaks markdownlint and confuses readers.

“## null” suggests the package has no version (or a Changesets config mismatch). Also, repeating the same heading triggers MD024. If this example isn’t published, consider ignoring it in Changesets; otherwise, give each section a real version.

Two options:

  • Preferred: mark the example as ignored by Changesets (and delete this file).
  • If keeping the changelog: replace “null” with real versions so each heading is unique.

If keeping this file, minimally fix headings:

-## null
+## 0.0.3
@@
-## null
+## 0.0.2
@@
-## null
+## 0.0.1

If ignoring in Changesets, add the package to .changeset/config.json “ignore” array and remove this changelog. I can draft that change if you want.

Also applies to: 11-13, 19-21

examples/minimal-convex/.env.example (1)

4-5: Order NEXT_PUBLIC_ keys to satisfy dotenv-linter.

Place NEXT_PUBLIC_CONVEX_SITE_URL before NEXT_PUBLIC_CONVEX_URL.

Apply:

-NEXT_PUBLIC_CONVEX_URL=
 NEXT_PUBLIC_CONVEX_SITE_URL=
+NEXT_PUBLIC_CONVEX_URL=
examples/minimal-convex/next.config.js (3)

1-3: Align with Next docs naming and accept ANALYZE=1|true

Minor readability polish and friendlier env handling for scripts across shells.

-const analyze = require("@next/bundle-analyzer")({
-  enabled: process.env.ANALYZE === "true",
-});
+const withBundleAnalyzer = require("@next/bundle-analyzer")({
+  enabled: /^(1|true)$/i.test(process.env.ANALYZE ?? ""),
+});
@@
-module.exports = analyze(nextConfig);
+module.exports = withBundleAnalyzer(nextConfig);

Also applies to: 11-11


6-9: Enable React Strict Mode in the example

Catches side effects and legacy patterns early without affecting behavior.

 const nextConfig = {
+  reactStrictMode: true,
   eslint: { ignoreDuringBuilds: true },
   typescript: { ignoreBuildErrors: true },
 };

5-9: Gate ESLint/TS ignore flags behind an opt-in env for all examples
Search reveals multiple next.config.* under examples/* unconditionally disabling lint/type checks. Replace hardcoded ignoreDuringBuilds: true and ignoreBuildErrors: true with an env-gated flag, e.g.:

const lax = process.env.UT_EXAMPLE_LAX === 'true';

module.exports = {
  eslint: { ignoreDuringBuilds: lax },
  typescript: { ignoreBuildErrors: lax },
  // …other settings
};

This preserves a local “lax mode” without masking real issues in CI.

examples/minimal-convex/tsconfig.json (1)

4-4: Optional: drop allowJs if not needed.

If the example is TypeScript-only, removing "allowJs" prevents accidental JS drift.

-    "allowJs": true,
+    // "allowJs": true,
examples/minimal-convex/package.json (3)

4-12: Make typecheck target the Convex tsconfig (or add a root tsconfig).

As-is, tsc --noEmit will try to typecheck the whole app without a root tsconfig.json, which is brittle for this example. Point it at convex/tsconfig.json or add a root config.

Apply this diff to scope typechecking to the Convex functions:

   "scripts": {
     "dev": "next dev",
     "dev:setup": "convex dev --once  --configure=new",
     "dev:convex": "convex dev",
     "clean": "git clean -xdf .next node_modules",
     "build": "next build",
     "start": "next start",
-    "typecheck": "tsc --noEmit"
+    "typecheck": "tsc -p convex/tsconfig.json --noEmit"
   },

If you prefer to typecheck the whole Next app, add a root tsconfig.json (e.g., {"extends":"next/core-web-vitals"}) and keep tsc -p tsconfig.json.


6-6: Remove the double space in dev:setup.

Minor cleanup.

-    "dev:setup": "convex dev --once  --configure=new",
+    "dev:setup": "convex dev --once --configure=new",

13-27: Consider pin compatibility and Node engine hint.

Pinned versions are fine for examples, but Next 15 + React 19 + TS 5.8 can have tight peer/engine constraints. Add an engines.node hint to reduce “unsupported engine” surprises.

Proposed diff:

   "devDependencies": {
     "@next/bundle-analyzer": "15.1.3",
     "@types/node": "^22.10.0",
     "@types/react": "19.1.2",
     "@types/react-dom": "19.1.2",
     "typescript": "5.8.3"
-  }
+  },
+  "engines": {
+    "node": ">=18.18.0"
+  }

Please verify that the pinned versions satisfy peer deps in a fresh install on Node 18/20.

examples/minimal-convex/README.md (3)

3-5: Add alt text and prefer Markdown image/link to satisfy MD045 and readability.

-<a href="https://stackblitz.com/github/pingdotgg/uploadthing/tree/main/examples/minimal-convex">
-  <img height="64" src="https://github.com/pingdotgg/uploadthing/assets/51714798/45907a4e-aa64-401a-afb3-b6c6df6eb71f" />
-</a>
+[![Open in StackBlitz](https://github.com/pingdotgg/uploadthing/assets/51714798/45907a4e-aa64-401a-afb3-b6c6df6eb71f)](https://stackblitz.com/github/pingdotgg/uploadthing/tree/main/examples/minimal-convex)

12-14: Replace bare URLs with links to satisfy MD034.

-4. Grab an API key from the UploadThing dashboard:
-   https://uploadthing.com/dashboard
+4. Grab an API key from the UploadThing dashboard:
+   <https://uploadthing.com/dashboard>
-Check out the docs at: https://docs.uploadthing.com/backend-adapters/convex
+Check out the docs at: <https://docs.uploadthing.com/backend-adapters/convex>

Also applies to: 22-22


9-18: Clarify concurrent dev processes and env file.

  • Mention that pnpm dev:convex and pnpm dev should run in separate terminals (or add a combined script with concurrently).
  • Consider adding a .env.example with NEXT_PUBLIC_CONVEX_* to smooth onboarding.

Happy to submit a follow-up PR to add .env.example and a dev:all script.

examples/minimal-convex/convex/tsconfig.json (1)

6-22: Solid Convex tsconfig; consider stricter ESM typing.

Looks good. Optionally add verbatimModuleSyntax: true for clearer ESM import/export semantics.

   "compilerOptions": {
@@
-    "noEmit": true
+    "noEmit": true,
+    "verbatimModuleSyntax": true
   },
examples/minimal-convex/convex/schema.ts (1)

4-8: Schema is minimal and fine; consider indexing if you’ll query by url.

If you’ll look up media by URL or enforce uniqueness, add an index/constraint accordingly. For the minimal demo this is fine as-is.

examples/minimal-convex/convex/media.ts (1)

4-9: Add basic auth guard and URL validation to the mutation.

Even for examples, preventing unauthenticated writes and validating URLs is low-effort hardening.

Apply this diff:

 export const add = mutation({
   args: { url: v.string() },
   handler: async (ctx, args) => {
-    return await ctx.db.insert("media", args);
+    // Optional auth guard (adjust to your app's auth model)
+    const identity = await ctx.auth.getUserIdentity?.();
+    if (!identity) {
+      throw new Error("Unauthorized");
+    }
+    // Basic URL validation
+    try {
+      // Will throw on invalid URLs
+      new URL(args.url);
+    } catch {
+      throw new Error("Invalid URL");
+    }
+    return await ctx.db.insert("media", args);
   },
 });
examples/minimal-convex/convex/README.md (1)

4-4: Fix markdownlint MD034 (bare URL).

Wrap the URL to satisfy markdownlint and improve readability.

-See https://docs.convex.dev/functions for more.
+See <https://docs.convex.dev/functions> for more.
examples/minimal-convex/convex/http.ts (1)

9-9: Make the route path explicit.
Add a path property so the endpoint is clear and insulated from future default changes:

-createRouteHandler({ http, internalAction: internal.uploadthing.handler });
+createRouteHandler({
+  http,
+  internalAction: internal.uploadthing.handler,
+  path: "/api/uploadthing",
+});

CORS env var CLIENT_ORIGIN is already documented in the example README/docs and enforced by the helper.

examples/minimal-convex/src/server/uploadthing.ts (3)

3-5: Use framework-agnostic server import for router creation.

If you keep this file, prefer import from "uploadthing/server" to avoid coupling to Next internals in a Convex-focused example.

-import { createUploadthing, UTFiles } from "uploadthing/next";
-import type { FileRouter } from "uploadthing/next";
+import { createUploadthing, UTFiles } from "uploadthing/server";
+import type { FileRouter } from "uploadthing/server";

38-41: Fix typo in comment.

"requrest" → "request".

-      // Check some condition based on the incoming requrest
+      // Check some condition based on the incoming request

44-47: Make randomUUID cross-runtime safe (Node/Browser).

Convex/Next can run in different runtimes. Prefer globalThis.crypto.randomUUID when available, falling back to Node’s crypto.

-import { randomUUID } from "crypto";
+import { randomUUID as nodeRandomUUID } from "crypto";

 // ...
-      const filesWithMyIds = files.map((file, idx) => ({
+      const filesWithMyIds = files.map((file, idx) => ({
         ...file,
-        customId: `${idx}-${randomUUID()}`,
+        customId: `${idx}-${(globalThis.crypto?.randomUUID?.() ?? nodeRandomUUID())}`,
       }));
examples/minimal-convex/src/utils/uploadthing.ts (1)

9-18: DRY the URL and add a safe fallback/guard.

Avoid repeating the env var, and provide a browser fallback for local dev. Warn early if missing.

+const RESOLVED_URL =
+  process.env.NEXT_PUBLIC_CONVEX_SITE_URL ??
+  (typeof window !== "undefined" ? window.location.origin : undefined);
+
+if (process.env.NODE_ENV !== "production" && !RESOLVED_URL) {
+  // Helps developers spot misconfig quickly in the example
+  // eslint-disable-next-line no-console
+  console.warn("UploadThing: NEXT_PUBLIC_CONVEX_SITE_URL is not set; falling back may fail on SSR.");
+}
+
 export const UploadButton = generateUploadButton<OurFileRouter>({
-  url: process.env.NEXT_PUBLIC_CONVEX_SITE_URL,
+  url: RESOLVED_URL,
 });
 export const UploadDropzone = generateUploadDropzone<OurFileRouter>({
-  url: process.env.NEXT_PUBLIC_CONVEX_SITE_URL,
+  url: RESOLVED_URL,
 });
 
 export const { useUploadThing } = generateReactHelpers<OurFileRouter>({
-  url: process.env.NEXT_PUBLIC_CONVEX_SITE_URL,
+  url: RESOLVED_URL,
 });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6b8145d and 45a1f12.

⛔ Files ignored due to path filters (6)
  • examples/minimal-convex/bun.lock is excluded by !**/*.lock
  • examples/minimal-convex/convex/_generated/api.d.ts is excluded by !**/_generated/**
  • examples/minimal-convex/convex/_generated/api.js is excluded by !**/_generated/**
  • examples/minimal-convex/convex/_generated/dataModel.d.ts is excluded by !**/_generated/**
  • examples/minimal-convex/convex/_generated/server.d.ts is excluded by !**/_generated/**
  • examples/minimal-convex/convex/_generated/server.js is excluded by !**/_generated/**
📒 Files selected for processing (18)
  • examples/minimal-convex/.env.example (1 hunks)
  • examples/minimal-convex/.gitignore (1 hunks)
  • examples/minimal-convex/CHANGELOG.md (1 hunks)
  • examples/minimal-convex/README.md (1 hunks)
  • examples/minimal-convex/convex/README.md (1 hunks)
  • examples/minimal-convex/convex/http.ts (1 hunks)
  • examples/minimal-convex/convex/media.ts (1 hunks)
  • examples/minimal-convex/convex/schema.ts (1 hunks)
  • examples/minimal-convex/convex/tsconfig.json (1 hunks)
  • examples/minimal-convex/convex/uploadthing.ts (1 hunks)
  • examples/minimal-convex/next-env.d.ts (1 hunks)
  • examples/minimal-convex/next.config.js (1 hunks)
  • examples/minimal-convex/package.json (1 hunks)
  • examples/minimal-convex/src/app/layout.tsx (1 hunks)
  • examples/minimal-convex/src/app/page.tsx (1 hunks)
  • examples/minimal-convex/src/server/uploadthing.ts (1 hunks)
  • examples/minimal-convex/src/utils/uploadthing.ts (1 hunks)
  • examples/minimal-convex/tsconfig.json (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • examples/minimal-convex/next-env.d.ts
  • examples/minimal-convex/.gitignore
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-08T16:10:15.532Z
Learnt from: markflorkowski
PR: pingdotgg/uploadthing#978
File: examples/minimal-appdir/src/server/uploadthing.ts:62-62
Timestamp: 2024-10-08T16:10:15.532Z
Learning: In the `onUploadComplete` handler in `examples/minimal-appdir/src/server/uploadthing.ts`, the return value `{ foo: "bar" }` is intentionally a placeholder for user-defined logic and may represent database returns or other computed values.

Applied to files:

  • examples/minimal-convex/convex/uploadthing.ts
  • examples/minimal-convex/src/server/uploadthing.ts
📚 Learning: 2024-10-03T20:04:00.113Z
Learnt from: markflorkowski
PR: pingdotgg/uploadthing#978
File: examples/minimal-appdir/src/server/uploadthing.ts:41-43
Timestamp: 2024-10-03T20:04:00.113Z
Learning: In the `examples/minimal-appdir/src/server/uploadthing.ts` and other example files, the commented-out header validation code is intentional and serves as an example for users.

Applied to files:

  • examples/minimal-convex/convex/uploadthing.ts
  • examples/minimal-convex/src/server/uploadthing.ts
🧬 Code graph analysis (7)
examples/minimal-convex/src/utils/uploadthing.ts (1)
examples/minimal-convex/convex/uploadthing.ts (1)
  • OurFileRouter (30-30)
examples/minimal-convex/convex/http.ts (1)
examples/minimal-convex/convex/_generated/api.js (2)
  • internal (22-22)
  • internal (22-22)
examples/minimal-convex/convex/uploadthing.ts (2)
examples/minimal-convex/convex/_generated/api.js (2)
  • api (21-21)
  • api (21-21)
packages/uploadthing/src/convex.ts (1)
  • createInternalAction (32-76)
examples/minimal-convex/src/server/uploadthing.ts (1)
examples/minimal-convex/convex/uploadthing.ts (1)
  • OurFileRouter (30-30)
examples/minimal-convex/convex/media.ts (1)
examples/minimal-convex/convex/_generated/server.js (2)
  • mutation (49-49)
  • mutation (49-49)
examples/minimal-convex/src/app/page.tsx (1)
examples/minimal-convex/src/utils/uploadthing.ts (2)
  • UploadButton (9-11)
  • UploadDropzone (12-14)
examples/minimal-convex/src/app/layout.tsx (2)
packages/react/src/next-ssr-plugin.tsx (1)
  • NextSSRPlugin (12-30)
examples/minimal-convex/src/server/uploadthing.ts (1)
  • uploadRouter (23-59)
🪛 dotenv-linter (3.3.0)
examples/minimal-convex/.env.example

[warning] 5-5: [UnorderedKey] The NEXT_PUBLIC_CONVEX_SITE_URL key should go before the NEXT_PUBLIC_CONVEX_URL key

(UnorderedKey)

🪛 LanguageTool
examples/minimal-convex/CHANGELOG.md

[grammar] ~7-~7: There might be a mistake here.
Context: ...## Patch Changes - Updated dependencies [[2e8b410](https://github.com/pingdotg...

(QB_NEW_EN)


[grammar] ~8-~8: There might be a mistake here.
Context: ...b410bb15c2688e9b6938c4a2cd17cf6110289)]: - [email protected] ## null ### Patch Ch...

(QB_NEW_EN)


[grammar] ~16-~16: There might be a mistake here.
Context: ...dade9fee61a636fa1c88bc9380d4ac77e91d9)]: - [email protected] ## null ### Patch Ch...

(QB_NEW_EN)


[grammar] ~24-~24: There might be a mistake here.
Context: ...f6d026fbee7480573d735d0406477dcb9e0bc)]: - [email protected]

(QB_NEW_EN)


[grammar] ~25-~25: There might be a mistake here.
Context: ...0406477dcb9e0bc)]: - [email protected]

(QB_NEW_EN)

examples/minimal-convex/README.md

[grammar] ~12-~12: There might be a mistake here.
Context: ... API key from the UploadThing dashboard: https://uploadthing.com/dashboard 5. `pn...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
examples/minimal-convex/CHANGELOG.md

11-11: Multiple headings with the same content

(MD024, no-duplicate-heading)


19-19: Multiple headings with the same content

(MD024, no-duplicate-heading)

examples/minimal-convex/README.md

4-4: Images should have alternate text (alt text)

(MD045, no-alt-text)


13-13: Bare URL used

(MD034, no-bare-urls)


22-22: Bare URL used

(MD034, no-bare-urls)

examples/minimal-convex/convex/README.md

4-4: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vade Review
🔇 Additional comments (8)
examples/minimal-convex/tsconfig.json (2)

4-7: Good defaults for strictness and Next integration.

Strict mode, noEmit, ES2022 target, isolatedModules, incremental, JSX preserve, and the Next plugin look solid.

Also applies to: 9-13


8-8: Fix invalid "module" setting; align with Next/modern TS bundling.

"module": "Preserve" is not a valid TypeScript target and will break compilation. Use ES modules and bundler resolution; also enable verbatimModuleSyntax for cleaner ESM output.

-    "module": "Preserve",
+    "module": "esnext",
+    "moduleResolution": "bundler",
+    "verbatimModuleSyntax": true,

Likely an incorrect or invalid review comment.

examples/minimal-convex/README.md (1)

11-11: No changes needed; README correctly uses NEXT_PUBLIC_CONVEX_SITE_URL. The example’s uploadthing.ts files all read process.env.NEXT_PUBLIC_CONVEX_SITE_URL, matching the README instruction.

examples/minimal-convex/src/app/layout.tsx (1)

18-23: LGTM: SSR config wiring is correct.

Injecting NextSSRPlugin with extractRouterConfig(uploadRouter) in the root layout is the intended pattern.

examples/minimal-convex/convex/uploadthing.ts (1)

25-26: Ignore outdated url suggestion: ufsUrl is correct

The OnUploadComplete payload and all current examples, tests, types, and docs use file.ufsUrl as the canonical URL property; the url field is a deprecated alias that will be removed in v9. No change needed here.

Likely an incorrect or invalid review comment.

examples/minimal-convex/src/server/uploadthing.ts (2)

24-36: Public ACL check.

"public-read" makes uploaded images world-readable. If that’s not intended for this example, drop the ACL or document the implication.


23-59: Keep both Uploadthing routers separate
The src/server/uploadthing.ts router uses the Next.js integration (uploadthing/next) and the convex/uploadthing.ts router uses the Convex integration (uploadthing/convex); they serve different runtimes and must both remain.

Likely an incorrect or invalid review comment.

examples/minimal-convex/src/utils/uploadthing.ts (1)

7-7: Type import path consistency looks good.

Using OurFileRouter from "~/convex/uploadthing" aligns the UI with the Convex router.

Comment on lines 7 to 10
- Updated dependencies
[[`2e8b410`](https://github.com/pingdotgg/uploadthing/commit/2e8b410bb15c2688e9b6938c4a2cd17cf6110289)]:
- [email protected]

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Changelog generation signal looks auto-derived; verify example package metadata.

These entries only note dependency bumps for [email protected]/5.5.2/5.5.1. That’s fine, but “null” headings usually come from missing "version" or misconfigured "private"/"publishConfig". Please confirm:

  • examples/minimal-convex/package.json has a name like @example/minimal-convex, private: true, and a version (e.g., 0.0.0).
  • .changeset/config.json either ignores this package or is intended to produce a changelog.

I can run a quick repo scan script to verify and propose exact edits.

Example verification script:

Also applies to: 15-18, 23-26


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check example package metadata
jq -r '.name, .private, .version' examples/minimal-convex/package.json 2>/dev/null || echo "package.json missing?"

# Check if Changesets ignores the example
if [ -f ".changeset/config.json" ]; then
  echo "Changesets ignore list:"
  jq '.ignore // []' .changeset/config.json
else
  echo ".changeset/config.json missing"
fi

Length of output: 292


Fix example package metadata
examples/minimal-convex/package.json has "name": "@example/minimal-appdir" and no "version" (null), causing null headings in CHANGELOG.md. Update it to "name": "@example/minimal-convex" and add a "version" (e.g., "0.0.0"). Note that .changeset/config.json ignores @example/*, so this example uses a manual changelog.

🧰 Tools
🪛 LanguageTool

[grammar] ~7-~7: There might be a mistake here.
Context: ...## Patch Changes - Updated dependencies [[2e8b410](https://github.com/pingdotg...

(QB_NEW_EN)


[grammar] ~8-~8: There might be a mistake here.
Context: ...b410bb15c2688e9b6938c4a2cd17cf6110289)]: - [email protected] ## null ### Patch Ch...

(QB_NEW_EN)

🤖 Prompt for AI Agents
In examples/minimal-convex/CHANGELOG.md around lines 7 to 10, the changelog
shows null headings because examples/minimal-convex/package.json currently has
an incorrect "name" ("@example/minimal-appdir") and no "version"; update
package.json to set "name": "@example/minimal-convex" and add a "version" field
(for example "0.0.0"), then regenerate or manually update the CHANGELOG entry so
it reflects the correct package name and version instead of null.


import { api } from "./_generated/api";

globalThis.crypto = crypto as unknown as Crypto;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix Web Crypto polyfill: assign crypto.webcrypto, not the crypto module.

globalThis.crypto = crypto as Crypto is incorrect; Node’s module isn’t the Web Crypto interface. Use crypto.webcrypto and only set it if missing.

-globalThis.crypto = crypto as unknown as Crypto;
+// Prefer the Web Crypto API where available
+// @ts-expect-error - global may not have crypto in Convex node runtime
+if (!globalThis.crypto && 'webcrypto' in crypto) {
+  // @ts-expect-error - assign Node's WebCrypto to global
+  globalThis.crypto = (crypto as any).webcrypto as Crypto;
+}
🤖 Prompt for AI Agents
In examples/minimal-convex/convex/uploadthing.ts around line 13, the code
incorrectly assigns the Node crypto module to globalThis.crypto; instead, set
globalThis.crypto to crypto.webcrypto (casting to the Web Crypto Crypto type)
and only do this assignment when globalThis.crypto is undefined to avoid
overwriting existing Web Crypto implementations.

} from "~/utils/uploadthing";

export default function Home() {
const { startUpload } = useUploadThing("videoAndImage", {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Route name mismatch: UI targets videoAndImage but router defines imageUploader.

This won’t type-check/run with OurFileRouter unless the server/router exposes videoAndImage. Either rename here to imageUploader or add a videoAndImage route to the router.

-  const { startUpload } = useUploadThing("videoAndImage", {
+  const { startUpload } = useUploadThing("imageUploader", {
-        endpoint={(routeRegistry) => routeRegistry.videoAndImage}
+        endpoint={(routeRegistry) => routeRegistry.imageUploader}
-        endpoint={(routeRegistry) => routeRegistry.videoAndImage}
+        endpoint={(routeRegistry) => routeRegistry.imageUploader}

Also applies to: 35-35, 49-49

🤖 Prompt for AI Agents
In examples/minimal-convex/src/app/page.tsx around lines 10, 35 and 49, the
client calls useUploadThing with route name "videoAndImage" but the
server/router exposes "imageUploader"; update the client to use "imageUploader"
(or alternatively add a matching "videoAndImage" route to the server router) so
the route names match OurFileRouter; ensure all occurrences at the listed lines
are changed consistently to the router's route name and re-run type checks.

Comment on lines +19 to +20
"include": [".next/types/**/*.ts", "next-env.d.ts", "src", "*.js"],
"exclude": ["node_modules"]
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Ensure Convex files are type-checked and JS includes are correct.

Currently only root-level *.js files are included and the convex directory is omitted. Include convex sources (and generated d.ts) and, if needed, widen JS patterns within src.

-  "include": [".next/types/**/*.ts", "next-env.d.ts", "src", "*.js"],
+  "include": [
+    "next-env.d.ts",
+    ".next/types/**/*.ts",
+    "src",
+    "src/**/*.js",
+    "convex",
+    "convex/_generated/**/*.d.ts"
+  ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"include": [".next/types/**/*.ts", "next-env.d.ts", "src", "*.js"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"src",
"src/**/*.js",
"convex",
"convex/_generated/**/*.d.ts"
],
"exclude": ["node_modules"]
🤖 Prompt for AI Agents
In examples/minimal-convex/tsconfig.json around lines 19-20, the "include" array
omits the Convex source and generated type files and only matches root-level JS;
update the include list to add the convex sources and generated .d.ts files
(e.g., include "convex/**/*" and the generated declarations) and broaden JS
matching to include JS under src (e.g., "src/**/*.js" or "**/*.js") so
TypeScript will type-check Convex files and any JS inside src.

} from "~/utils/uploadthing";

export default function Home() {
const { startUpload } = useUploadThing("videoAndImage", {
Copy link

@vercel vercel bot Sep 3, 2025

Choose a reason for hiding this comment

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

The page component references a non-existent videoAndImage endpoint. The imported OurFileRouter from ~/convex/uploadthing only defines an imageUploader endpoint.

View Details
📝 Patch Details
diff --git a/examples/minimal-convex/package-lock.json b/examples/minimal-convex/package-lock.json
new file mode 100644
index 0000000..b36aa1e
--- /dev/null
+++ b/examples/minimal-convex/package-lock.json
@@ -0,0 +1,2033 @@
+{
+  "name": "@example/minimal-appdir",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "@example/minimal-appdir",
+      "dependencies": {
+        "@uploadthing/react": "7.3.3",
+        "convex": "1.26.2",
+        "next": "15.3.1",
+        "react": "19.1.0",
+        "react-dom": "19.1.0",
+        "uploadthing": "7.7.4"
+      },
+      "devDependencies": {
+        "@next/bundle-analyzer": "15.1.3",
+        "@types/node": "^22.10.0",
+        "@types/react": "19.1.2",
+        "@types/react-dom": "19.1.2",
+        "typescript": "5.8.3"
+      }
+    },
+    "node_modules/@discoveryjs/json-ext": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
+      "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/@effect/platform": {
+      "version": "0.90.3",
+      "resolved": "https://registry.npmjs.org/@effect/platform/-/platform-0.90.3.tgz",
+      "integrity": "sha512-XvQ37yzWQKih4Du2CYladd1i/MzqtgkTPNCaN6Ku6No4CK83hDtXIV/rP03nEoBg2R3Pqgz6gGWmE2id2G81HA==",
+      "license": "MIT",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "^1.33.0",
+        "find-my-way-ts": "^0.1.6",
+        "msgpackr": "^1.11.4",
+        "multipasta": "^0.2.7"
+      },
+      "peerDependencies": {
+        "effect": "^3.17.7"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
+      "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
+      "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
+      "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
+      "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
+      "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
+      "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
+      "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
+      "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
+      "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
+      "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
+      "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
+      "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
+      "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
+      "cpu": [
+        "loong64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
+      "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
+      "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
+      "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
+      "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
+      "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
+      "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
+      "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
+      "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
+      "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
+      "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
+      "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
+      "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
+      "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@img/sharp-darwin-arm64": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
+      "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-arm64": "1.2.0"
+      }
+    },
+    "node_modules/@img/sharp-darwin-x64": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz",
+      "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-x64": "1.2.0"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-arm64": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz",
+      "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-x64": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz",
+      "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz",
+      "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm64": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz",
+      "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-ppc64": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz",
+      "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-s390x": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz",
+      "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-x64": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz",
+      "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz",
+      "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz",
+      "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz",
+      "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm": "1.2.0"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm64": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz",
+      "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm64": "1.2.0"
+      }
+    },
+    "node_modules/@img/sharp-linux-ppc64": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz",
+      "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-ppc64": "1.2.0"
+      }
+    },
+    "node_modules/@img/sharp-linux-s390x": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz",
+      "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-s390x": "1.2.0"
+      }
+    },
+    "node_modules/@img/sharp-linux-x64": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz",
+      "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-x64": "1.2.0"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-arm64": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz",
+      "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.0"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-x64": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz",
+      "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.0"
+      }
+    },
+    "node_modules/@img/sharp-wasm32": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz",
+      "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==",
+      "cpu": [
+        "wasm32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/runtime": "^1.4.4"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-arm64": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz",
+      "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-ia32": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz",
+      "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-x64": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz",
+      "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
+      "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
+      "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
+      "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
+      "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
+      "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
+      "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@next/bundle-analyzer": {
+      "version": "15.1.3",
+      "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.1.3.tgz",
+      "integrity": "sha512-dh5i2KBONWVhQzJnL10sv9+ImsKgGtOHHeA1dWp/H3MXphWBt1uGjXCwPCcitwimvNncHBmxaOyTm2FwfOLRSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "webpack-bundle-analyzer": "4.10.1"
+      }
+    },
+    "node_modules/@next/env": {
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz",
+      "integrity": "sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==",
+      "license": "MIT"
+    },
+    "node_modules/@next/swc-darwin-arm64": {
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz",
+      "integrity": "sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-darwin-x64": {
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz",
+      "integrity": "sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-gnu": {
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz",
+      "integrity": "sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-musl": {
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz",
+      "integrity": "sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-gnu": {
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz",
+      "integrity": "sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-musl": {
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz",
+      "integrity": "sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-arm64-msvc": {
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz",
+      "integrity": "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-x64-msvc": {
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.1.tgz",
+      "integrity": "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.37.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz",
+      "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@polka/url": {
+      "version": "1.0.0-next.29",
+      "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+      "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+      "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+      "license": "MIT"
+    },
+    "node_modules/@swc/counter": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+      "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/@swc/helpers": {
+      "version": "0.5.15",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
+      "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^2.8.0"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "22.18.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz",
+      "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@types/react": {
+      "version": "19.1.2",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
+      "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/@types/react-dom": {
+      "version": "19.1.2",
+      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz",
+      "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "^19.0.0"
+      }
+    },
+    "node_modules/@uploadthing/mime-types": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/@uploadthing/mime-types/-/mime-types-0.3.6.tgz",
+      "integrity": "sha512-t3tTzgwFV9+1D7lNDYc7Lr7kBwotHaX0ZsvoCGe7xGnXKo9z0jG2Sjl/msll12FeoLj77nyhsxevXyGpQDBvLg==",
+      "license": "MIT"
+    },
+    "node_modules/@uploadthing/react": {
+      "version": "7.3.3",
+      "resolved": "https://registry.npmjs.org/@uploadthing/react/-/react-7.3.3.tgz",
+      "integrity": "sha512-GhKbK42jL2Qs7OhRd2Z6j0zTLsnJTRJH31nR7RZnUYVoRh2aS/NabMAnHBNqfunIAGXVaA717Pvzq7vtxuPTmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@uploadthing/shared": "7.1.10",
+        "file-selector": "0.6.0"
+      },
+      "peerDependencies": {
+        "next": "*",
+        "react": "^17.0.2 || ^18.0.0 || ^19.0.0",
+        "uploadthing": "^7.2.0"
+      },
+      "peerDependenciesMeta": {
+        "next": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@uploadthing/shared": {
+      "version": "7.1.10",
+      "resolved": "https://registry.npmjs.org/@uploadthing/shared/-/shared-7.1.10.tgz",
+      "integrity": "sha512-R/XSA3SfCVnLIzFpXyGaKPfbwlYlWYSTuGjTFHuJhdAomuBuhopAHLh2Ois5fJibAHzi02uP1QCKbgTAdmArqg==",
+      "license": "MIT",
+      "dependencies": {
+        "@uploadthing/mime-types": "0.3.6",
+        "effect": "3.17.7",
+        "sqids": "^0.3.0"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-walk": {
+      "version": "8.3.4",
+      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+      "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.11.0"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/busboy": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+      "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+      "dependencies": {
+        "streamsearch": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.16.0"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001739",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
+      "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/client-only": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+      "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+      "license": "MIT"
+    },
+    "node_modules/color": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+      "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "color-convert": "^2.0.1",
+        "color-string": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=12.5.0"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/color-string": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+      "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "color-name": "^1.0.0",
+        "simple-swizzle": "^0.2.2"
+      }
+    },
+    "node_modules/commander": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+      "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/convex": {
+      "version": "1.26.2",
+      "resolved": "https://registry.npmjs.org/convex/-/convex-1.26.2.tgz",
+      "integrity": "sha512-QG3zvZ9GTTeeBS+N5PJj73TULHYu99eVSOI3KU2hMnB9q40fHxkMFJ+AYox+K81roTakciBvNTvGlhfz+M10fQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "esbuild": "0.25.4",
+        "jwt-decode": "^4.0.0",
+        "prettier": "^3.0.0"
+      },
+      "bin": {
+        "convex": "bin/main.js"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=7.0.0"
+      },
+      "peerDependencies": {
+        "@auth0/auth0-react": "^2.0.1",
+        "@clerk/clerk-react": "^4.12.8 || ^5.0.0",
+        "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@auth0/auth0-react": {
+          "optional": true
+        },
+        "@clerk/clerk-react": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/debounce": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
+      "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/detect-libc": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+      "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+      "license": "Apache-2.0",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/duplexer": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
+      "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/effect": {
+      "version": "3.17.7",
+      "resolved": "https://registry.npmjs.org/effect/-/effect-3.17.7.tgz",
+      "integrity": "sha512-dpt0ONUn3zzAuul6k4nC/coTTw27AL5nhkORXgTi6NfMPzqWYa1M05oKmOMTxpVSTKepqXVcW9vIwkuaaqx9zA==",
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.0.0",
+        "fast-check": "^3.23.1"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.4",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
+      "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.4",
+        "@esbuild/android-arm": "0.25.4",
+        "@esbuild/android-arm64": "0.25.4",
+        "@esbuild/android-x64": "0.25.4",
+        "@esbuild/darwin-arm64": "0.25.4",
+        "@esbuild/darwin-x64": "0.25.4",
+        "@esbuild/freebsd-arm64": "0.25.4",
+        "@esbuild/freebsd-x64": "0.25.4",
+        "@esbuild/linux-arm": "0.25.4",
+        "@esbuild/linux-arm64": "0.25.4",
+        "@esbuild/linux-ia32": "0.25.4",
+        "@esbuild/linux-loong64": "0.25.4",
+        "@esbuild/linux-mips64el": "0.25.4",
+        "@esbuild/linux-ppc64": "0.25.4",
+        "@esbuild/linux-riscv64": "0.25.4",
+        "@esbuild/linux-s390x": "0.25.4",
+        "@esbuild/linux-x64": "0.25.4",
+        "@esbuild/netbsd-arm64": "0.25.4",
+        "@esbuild/netbsd-x64": "0.25.4",
+        "@esbuild/openbsd-arm64": "0.25.4",
+        "@esbuild/openbsd-x64": "0.25.4",
+        "@esbuild/sunos-x64": "0.25.4",
+        "@esbuild/win32-arm64": "0.25.4",
+        "@esbuild/win32-ia32": "0.25.4",
+        "@esbuild/win32-x64": "0.25.4"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/fast-check": {
+      "version": "3.23.2",
+      "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
+      "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/dubzzz"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fast-check"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "pure-rand": "^6.1.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/file-selector": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
+      "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/find-my-way-ts": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
+      "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
+      "license": "MIT"
+    },
+    "node_modules/gzip-size": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
+      "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "duplexer": "^0.1.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/html-escaper": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+      "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+      "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/is-plain-object": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/jwt-decode": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+      "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/mrmime": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+      "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/msgpackr": {
+      "version": "1.11.5",
+      "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
+      "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
+      "license": "MIT",
+      "optionalDependencies": {
+        "msgpackr-extract": "^3.0.2"
+      }
+    },
+    "node_modules/msgpackr-extract": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
+      "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "node-gyp-build-optional-packages": "5.2.2"
+      },
+      "bin": {
+        "download-msgpackr-prebuilds": "bin/download-prebuilds.js"
+      },
+      "optionalDependencies": {
+        "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
+      }
+    },
+    "node_modules/multipasta": {
+      "version": "0.2.7",
+      "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz",
+      "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==",
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/next": {
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz",
+      "integrity": "sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==",
+      "license": "MIT",
+      "dependencies": {
+        "@next/env": "15.3.1",
+        "@swc/counter": "0.1.3",
+        "@swc/helpers": "0.5.15",
+        "busboy": "1.6.0",
+        "caniuse-lite": "^1.0.30001579",
+        "postcss": "8.4.31",
+        "styled-jsx": "5.1.6"
+      },
+      "bin": {
+        "next": "dist/bin/next"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
+      },
+      "optionalDependencies": {
+        "@next/swc-darwin-arm64": "15.3.1",
+        "@next/swc-darwin-x64": "15.3.1",
+        "@next/swc-linux-arm64-gnu": "15.3.1",
+        "@next/swc-linux-arm64-musl": "15.3.1",
+        "@next/swc-linux-x64-gnu": "15.3.1",
+        "@next/swc-linux-x64-musl": "15.3.1",
+        "@next/swc-win32-arm64-msvc": "15.3.1",
+        "@next/swc-win32-x64-msvc": "15.3.1",
+        "sharp": "^0.34.1"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.1.0",
+        "@playwright/test": "^1.41.2",
+        "babel-plugin-react-compiler": "*",
+        "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+        "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+        "sass": "^1.3.0"
+      },
+      "peerDependenciesMeta": {
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@playwright/test": {
+          "optional": true
+        },
+        "babel-plugin-react-compiler": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/node-gyp-build-optional-packages": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
+      "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.1"
+      },
+      "bin": {
+        "node-gyp-build-optional-packages": "bin.js",
+        "node-gyp-build-optional-packages-optional": "optional.js",
+        "node-gyp-build-optional-packages-test": "build-test.js"
+      }
+    },
+    "node_modules/opener": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
+      "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
+      "dev": true,
+      "license": "(WTFPL OR MIT)",
+      "bin": {
+        "opener": "bin/opener-bin.js"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/postcss": {
+      "version": "8.4.31",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.6",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+      "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/pure-rand": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
+      "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/dubzzz"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fast-check"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/react": {
+      "version": "19.1.0",
+      "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
+      "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "19.1.0",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
+      "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
+      "license": "MIT",
+      "dependencies": {
+        "scheduler": "^0.26.0"
+      },
+      "peerDependencies": {
+        "react": "^19.1.0"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.26.0",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
+      "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+      "license": "MIT"
+    },
+    "node_modules/semver": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+      "license": "ISC",
+      "optional": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/sharp": {
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
+      "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "dependencies": {
+        "color": "^4.2.3",
+        "detect-libc": "^2.0.4",
+        "semver": "^7.7.2"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-darwin-arm64": "0.34.3",
+        "@img/sharp-darwin-x64": "0.34.3",
+        "@img/sharp-libvips-darwin-arm64": "1.2.0",
+        "@img/sharp-libvips-darwin-x64": "1.2.0",
+        "@img/sharp-libvips-linux-arm": "1.2.0",
+        "@img/sharp-libvips-linux-arm64": "1.2.0",
+        "@img/sharp-libvips-linux-ppc64": "1.2.0",
+        "@img/sharp-libvips-linux-s390x": "1.2.0",
+        "@img/sharp-libvips-linux-x64": "1.2.0",
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.0",
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.0",
+        "@img/sharp-linux-arm": "0.34.3",
+        "@img/sharp-linux-arm64": "0.34.3",
+        "@img/sharp-linux-ppc64": "0.34.3",
+        "@img/sharp-linux-s390x": "0.34.3",
+        "@img/sharp-linux-x64": "0.34.3",
+        "@img/sharp-linuxmusl-arm64": "0.34.3",
+        "@img/sharp-linuxmusl-x64": "0.34.3",
+        "@img/sharp-wasm32": "0.34.3",
+        "@img/sharp-win32-arm64": "0.34.3",
+        "@img/sharp-win32-ia32": "0.34.3",
+        "@img/sharp-win32-x64": "0.34.3"
+      }
+    },
+    "node_modules/simple-swizzle": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+      "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "is-arrayish": "^0.3.1"
+      }
+    },
+    "node_modules/sirv": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
+      "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@polka/url": "^1.0.0-next.24",
+        "mrmime": "^2.0.0",
+        "totalist": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/sqids": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/sqids/-/sqids-0.3.0.tgz",
+      "integrity": "sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==",
+      "license": "MIT"
+    },
+    "node_modules/streamsearch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+      "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/styled-jsx": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
+      "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
+      "license": "MIT",
+      "dependencies": {
+        "client-only": "0.0.1"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "peerDependencies": {
+        "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "@babel/core": {
+          "optional": true
+        },
+        "babel-plugin-macros": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/totalist": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+      "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/typescript": {
+      "version": "5.8.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/uploadthing": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/uploadthing/-/uploadthing-7.7.4.tgz",
+      "integrity": "sha512-rlK/4JWHW5jP30syzWGBFDDXv3WJDdT8gn9OoxRJmXLoXi94hBmyyjxihGlNrKhBc81czyv8TkzMioe/OuKGfA==",
+      "license": "MIT",
+      "dependencies": {
+        "@effect/platform": "0.90.3",
+        "@standard-schema/spec": "1.0.0-beta.4",
+        "@uploadthing/mime-types": "0.3.6",
+        "@uploadthing/shared": "7.1.10",
+        "effect": "3.17.7"
+      },
+      "engines": {
+        "node": ">=18.13.0"
+      },
+      "peerDependencies": {
+        "express": "*",
+        "h3": "*",
+        "tailwindcss": "^3.0.0 || ^4.0.0-beta.0"
+      },
+      "peerDependenciesMeta": {
+        "express": {
+          "optional": true
+        },
+        "fastify": {
+          "optional": true
+        },
+        "h3": {
+          "optional": true
+        },
+        "next": {
+          "optional": true
+        },
+        "tailwindcss": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/uploadthing/node_modules/@standard-schema/spec": {
+      "version": "1.0.0-beta.4",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0-beta.4.tgz",
+      "integrity": "sha512-d3IxtzLo7P1oZ8s8YNvxzBUXRXojSut8pbPrTYtzsc5sn4+53jVqbk66pQerSZbZSJZQux6LkclB/+8IDordHg==",
+      "license": "MIT"
+    },
+    "node_modules/webpack-bundle-analyzer": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz",
+      "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@discoveryjs/json-ext": "0.5.7",
+        "acorn": "^8.0.4",
+        "acorn-walk": "^8.0.0",
+        "commander": "^7.2.0",
+        "debounce": "^1.2.1",
+        "escape-string-regexp": "^4.0.0",
+        "gzip-size": "^6.0.0",
+        "html-escaper": "^2.0.2",
+        "is-plain-object": "^5.0.0",
+        "opener": "^1.5.2",
+        "picocolors": "^1.0.0",
+        "sirv": "^2.0.3",
+        "ws": "^7.3.1"
+      },
+      "bin": {
+        "webpack-bundle-analyzer": "lib/bin/analyzer.js"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      }
+    },
+    "node_modules/ws": {
+      "version": "7.5.10",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+      "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.3.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    }
+  }
+}
diff --git a/examples/minimal-convex/src/app/page.tsx b/examples/minimal-convex/src/app/page.tsx
index 6a6a87c..41d02b9 100644
--- a/examples/minimal-convex/src/app/page.tsx
+++ b/examples/minimal-convex/src/app/page.tsx
@@ -7,7 +7,7 @@ import {
 } from "~/utils/uploadthing";
 
 export default function Home() {
-  const { startUpload } = useUploadThing("videoAndImage", {
+  const { startUpload } = useUploadThing("imageUploader", {
     /**
      * @see https://docs.uploadthing.com/api-reference/react#useuploadthing
      */
@@ -32,7 +32,7 @@ export default function Home() {
         /**
          * @see https://docs.uploadthing.com/api-reference/react#uploadbutton
          */
-        endpoint={(routeRegistry) => routeRegistry.videoAndImage}
+        endpoint={(routeRegistry) => routeRegistry.imageUploader}
         onClientUploadComplete={(res) => {
           console.log(`onClientUploadComplete`, res);
           alert("Upload Completed");
@@ -46,7 +46,7 @@ export default function Home() {
         /**
          * @see https://docs.uploadthing.com/api-reference/react#uploaddropzone
          */
-        endpoint={(routeRegistry) => routeRegistry.videoAndImage}
+        endpoint={(routeRegistry) => routeRegistry.imageUploader}
         onUploadAborted={() => {
           alert("Upload Aborted");
         }}

Analysis

The page component imports useUploadThing and other utilities typed with OurFileRouter from ~/convex/uploadthing, which defines a router with only an imageUploader endpoint (line 18 in convex/uploadthing.ts). However, the page component attempts to use a videoAndImage endpoint in three places:

  1. Line 10: useUploadThing("videoAndImage", ...)
  2. Line 35: endpoint={(routeRegistry) => routeRegistry.videoAndImage}
  3. Line 49: endpoint={(routeRegistry) => routeRegistry.videoAndImage}

This will cause TypeScript compilation errors since videoAndImage doesn't exist on the router type, and runtime errors if the code somehow executes. The example should either:

  1. Change all references from videoAndImage to imageUploader to match the actual Convex router definition, or
  2. Update the Convex router in convex/uploadthing.ts to use videoAndImage instead of imageUploader

Option 1 is recommended since imageUploader better describes the single image upload configuration defined in the Convex router.

Copy link
Contributor

@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

♻️ Duplicate comments (1)
examples/minimal-convex/convex/uploadthing.ts (1)

13-13: Fix Web Crypto polyfill: assign crypto.webcrypto, and don’t overwrite if present.

-globalThis.crypto = crypto as unknown as Crypto;
+// Prefer Web Crypto and avoid clobbering an existing implementation
+// @ts-expect-error - Convex node runtime may lack global crypto
+if (!globalThis.crypto && "webcrypto" in crypto) {
+  // @ts-expect-error - assign Node's WebCrypto to global
+  globalThis.crypto = (crypto as any).webcrypto as Crypto;
+}
🧹 Nitpick comments (5)
examples/minimal-convex/package.json (4)

22-22: Align the analyzer with Next’s version to avoid subtle plugin skew.

-    "@next/bundle-analyzer": "15.1.3",
+    "@next/bundle-analyzer": "15.3.1",

6-6: Remove extra space in dev:setup.

-    "dev:setup": "convex dev --once  --configure=new",
+    "dev:setup": "convex dev --once --configure=new",

8-8: Safer git clean pathspec.

Add “--” to disambiguate paths and keep the scope local.

-    "clean": "git clean -xdf .next node_modules",
+    "clean": "git clean -xdf -- .next node_modules",

1-28: (Optional) Declare Node engine to match Next/Convex requirements.

 {
   "name": "@example/minimal-convex",
   "private": true,
+  "engines": {
+    "node": ">=18.18"
+  },
   "scripts": {
examples/minimal-convex/convex/uploadthing.ts (1)

47-47: Fix typo in comment (“requrest” → “request”).

-      // Check some condition based on the incoming requrest
+      // Check some condition based on the incoming request
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 45a1f12 and 2d4bb76.

📒 Files selected for processing (3)
  • examples/minimal-convex/convex/uploadthing.ts (1 hunks)
  • examples/minimal-convex/package.json (1 hunks)
  • examples/minimal-convex/tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/minimal-convex/tsconfig.json
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-08T16:10:15.532Z
Learnt from: markflorkowski
PR: pingdotgg/uploadthing#978
File: examples/minimal-appdir/src/server/uploadthing.ts:62-62
Timestamp: 2024-10-08T16:10:15.532Z
Learning: In the `onUploadComplete` handler in `examples/minimal-appdir/src/server/uploadthing.ts`, the return value `{ foo: "bar" }` is intentionally a placeholder for user-defined logic and may represent database returns or other computed values.

Applied to files:

  • examples/minimal-convex/convex/uploadthing.ts
📚 Learning: 2024-10-03T20:04:00.113Z
Learnt from: markflorkowski
PR: pingdotgg/uploadthing#978
File: examples/minimal-appdir/src/server/uploadthing.ts:41-43
Timestamp: 2024-10-03T20:04:00.113Z
Learning: In the `examples/minimal-appdir/src/server/uploadthing.ts` and other example files, the commented-out header validation code is intentional and serves as an example for users.

Applied to files:

  • examples/minimal-convex/convex/uploadthing.ts
🧬 Code graph analysis (1)
examples/minimal-convex/convex/uploadthing.ts (1)
packages/uploadthing/src/convex.ts (1)
  • createInternalAction (32-76)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vade Review
🔇 Additional comments (2)
examples/minimal-convex/convex/uploadthing.ts (2)

1-1: “use node” directive is correct for Convex internal actions.


59-59: Import missing UTFiles

UTFiles is used on line 59 but not imported; add:

+import { UTFiles } from "uploadthing/convex-helpers";

Adjust the source if UTFiles is re-exported from another entrypoint (e.g. "uploadthing/server").

Comment on lines 53 to 56
const filesWithMyIds = files.map((file, idx) => ({
...file,
customId: `${idx}-${randomUUID()}`,
}));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

randomUUID is not defined; qualify the call.

Use Node’s crypto.randomUUID() (or import { randomUUID }).

-        customId: `${idx}-${randomUUID()}`,
+        customId: `${idx}-${crypto.randomUUID()}`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const filesWithMyIds = files.map((file, idx) => ({
...file,
customId: `${idx}-${randomUUID()}`,
}));
const filesWithMyIds = files.map((file, idx) => ({
...file,
customId: `${idx}-${crypto.randomUUID()}`,
}));
🤖 Prompt for AI Agents
In examples/minimal-convex/convex/uploadthing.ts around lines 53 to 56, the call
to randomUUID() is undefined; fix by qualifying or importing it — either prefix
with Node's crypto.randomUUID() or add an import like import { randomUUID } from
'crypto' at the top of the file and use randomUUID() so the identifier is
defined.

Comment on lines 14 to 20
"@uploadthing/react": "7.3.3",
"convex": "1.26.2",
"next": "15.3.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"uploadthing": "7.7.4"
},
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Point the example to the in-repo packages (workspace:*) so it works before publish.

Otherwise, the example may fetch a registry version without the new convex export.

Apply:

   "dependencies": {
-    "@uploadthing/react": "7.3.3",
+    "@uploadthing/react": "workspace:*",
     "convex": "1.26.2",
     "next": "15.3.1",
     "react": "19.1.0",
     "react-dom": "19.1.0",
-    "uploadthing": "7.7.4"
+    "uploadthing": "workspace:*"
   },

To verify locally linked deps:


🏁 Script executed:

#!/bin/bash
# Expect "link:packages/uploadthing" or "workspace:*" resolution
rg -n '"uploadthing"\s*:\s*"'
rg -n '"@uploadthing/react"\s*:\s*"'

Length of output: 5025


Use workspace: for uploadthing packages in examples/minimal-convex*

In examples/minimal-convex/package.json dependencies, update to point at in-repo packages:

   "dependencies": {
-    "@uploadthing/react": "7.3.3",
+    "@uploadthing/react": "workspace:*",
     "convex": "1.26.2",
     "next": "15.3.1",
     "react": "19.1.0",
     "react-dom": "19.1.0",
-    "uploadthing": "7.7.4"
+    "uploadthing": "workspace:*"
   },

Verify the change locally:

rg -n '"uploadthing": "workspace:\*"' examples/minimal-convex/package.json
rg -n '"@uploadthing/react": "workspace:\*"' examples/minimal-convex/package.json
🤖 Prompt for AI Agents
In examples/minimal-convex/package.json around lines 14 to 20, the uploadthing
packages are pinned to published versions; change the "uploadthing" and
"@uploadthing/react" dependency entries to use "workspace:*" so they point at
the in-repo packages, save the file, and then verify the change locally by
running: rg -n '"uploadthing": "workspace:*"'
examples/minimal-convex/package.json and rg -n '"@uploadthing/react":
"workspace:*"' examples/minimal-convex/package.json to confirm both entries were
updated.

Copy link
Contributor

@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 (4)
examples/minimal-convex/README.md (4)

3-5: Add alt text to image for accessibility (MD045).

Provide descriptive alt text for the StackBlitz image.

-<a href="https://stackblitz.com/github/pingdotgg/uploadthing/tree/main/examples/minimal-convex">
-  <img height="64" src="https://github.com/pingdotgg/uploadthing/assets/51714798/45907a4e-aa64-401a-afb3-b6c6df6eb71f" />
-</a>
+<a href="https://stackblitz.com/github/pingdotgg/uploadthing/tree/main/examples/minimal-convex">
+  <img
+    height="64"
+    src="https://github.com/pingdotgg/uploadthing/assets/51714798/45907a4e-aa64-401a-afb3-b6c6df6eb71f"
+    alt="Open minimal Convex example in StackBlitz"
+  />
+</a>

12-13: Avoid bare URL; use a markdown link (MD034) and tighten wording.

This also resolves the formatting glitch flagged by the grammar linter.

-4. Grab an API key from the UploadThing dashboard:
-   https://uploadthing.com/dashboard
+4. Grab an API key from the [UploadThing dashboard](https://uploadthing.com/dashboard).

22-22: Avoid bare URL in “Further reference” (MD034).

-Check out the docs at: https://docs.uploadthing.com/backend-adapters/convex
+Check out the docs at: <https://docs.uploadthing.com/backend-adapters/convex>

9-19: Polish QuickStart formatting and add small clarity notes.

  • Use fenced shell blocks for command steps.
  • Give an example value for NEXT_PUBLIC_CONVEX_SITE_URL.
  • Note that CLIENT_ORIGIN should match the site URL to avoid CORS issues.
-1. `pnpm i`
-2. `pnpm dev:setup`
-3. Add the `NEXT_PUBLIC_CONVEX_SITE_URL` to the .env file
-4. Grab an API key from the UploadThing dashboard:
-   https://uploadthing.com/dashboard
-5. `pnpm dev:convex`
-6. `pnpx convex env set UPLOADTHING_TOKEN=<your-token>`
-7. `pnpx convex env set CLIENT_ORIGIN=http://localhost:3000`
-8. `pnpm dev`
-9. Upload files!
+1. Install deps:
+   ```sh
+   pnpm i
+   ```
+2. Project setup:
+   ```sh
+   pnpm dev:setup
+   ```
+3. Add to `.env`:
+   ```sh
+   NEXT_PUBLIC_CONVEX_SITE_URL=http://localhost:3000
+   ```
+4. Grab an API key from the [UploadThing dashboard](https://uploadthing.com/dashboard).
+5. Start Convex (in a separate terminal):
+   ```sh
+   pnpm dev:convex
+   ```
+6. Set Convex environment variables:
+   ```sh
+   pnpx convex env set UPLOADTHING_TOKEN=<your-token>
+   pnpx convex env set CLIENT_ORIGIN=http://localhost:3000
+   ```
+   Tip: CLIENT_ORIGIN should match NEXT_PUBLIC_CONVEX_SITE_URL.
+7. Start the app:
+   ```sh
+   pnpm dev
+   ```
+8. Upload files!
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2d4bb76 and de178e7.

📒 Files selected for processing (3)
  • examples/minimal-convex/README.md (1 hunks)
  • examples/minimal-convex/convex/http.ts (1 hunks)
  • examples/minimal-convex/convex/uploadthing.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/minimal-convex/convex/uploadthing.ts
  • examples/minimal-convex/convex/http.ts
🧰 Additional context used
🪛 LanguageTool
examples/minimal-convex/README.md

[grammar] ~12-~12: There might be a mistake here.
Context: ... API key from the UploadThing dashboard: https://uploadthing.com/dashboard 5. `pn...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
examples/minimal-convex/README.md

4-4: Images should have alternate text (alt text)

(MD045, no-alt-text)


13-13: Bare URL used

(MD034, no-bare-urls)


22-22: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vade Review

url: req.url,
method: req.method,
headers,
body: await req.text(),
Copy link

Choose a reason for hiding this comment

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

Request bodies are always serialized as strings, but GET requests typically have empty bodies which will result in an empty string being passed instead of undefined.

View Details

Analysis

In the createRouteHandler function, the request body is always serialized using await req.text() on line 53, which means GET requests (which typically have no body) will result in an empty string "" being passed to the internal action. However, in the convex.ts file on line 51, the body is defined as v.optional(v.string()), and on line 58, the reconstruction logic checks args.request.body ? new Blob([args.request.body]) : null.

This creates an inconsistency: GET requests will have body: "" (empty string), which is truthy, so the reconstructed Request will have a Blob with empty content instead of null. This could potentially cause issues with UploadThing's internal handler logic, which may expect GET requests to have no body (null) rather than an empty body.

The fix is to check if the request method is GET or if the body is empty, and conditionally set the body to undefined:

body: req.method === "GET" || !(await req.text()) ? undefined : await req.text(),

However, since req.text() can only be called once, the implementation should read the body once and then check if it's empty.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (1)
examples/minimal-convex/.env.example (1)

7-10: Good callout of Convex-side env vars (addressed prior feedback).

Clear guidance to set CLIENT_ORIGIN and UPLOADTHING_TOKEN in Convex’s env (not this .env) reduces setup friction. Nicely done.

🧹 Nitpick comments (2)
examples/minimal-convex/.env.example (2)

4-5: Fix dotenv key order to satisfy dotenv-linter.

Swap the two public keys per the linter hint.

-NEXT_PUBLIC_CONVEX_URL=
-NEXT_PUBLIC_CONVEX_SITE_URL=
+NEXT_PUBLIC_CONVEX_SITE_URL=
+NEXT_PUBLIC_CONVEX_URL=

1-1: Standardize on npx vs pnpx in this file.

Line 1 uses npx while the examples use pnpx. Pick one to avoid confusion; below aligns the examples to npx.

-#   pnpx convex env set CLIENT_ORIGIN='http://localhost:3000'
-#   pnpx convex env set UPLOADTHING_TOKEN='<your-uploadthing-token>'
+#   npx convex env set CLIENT_ORIGIN='http://localhost:3000'
+#   npx convex env set UPLOADTHING_TOKEN='<your-uploadthing-token>'

Also applies to: 8-9

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between de178e7 and 121f21a.

📒 Files selected for processing (1)
  • examples/minimal-convex/.env.example (1 hunks)
🧰 Additional context used
🪛 dotenv-linter (3.3.0)
examples/minimal-convex/.env.example

[warning] 5-5: [UnorderedKey] The NEXT_PUBLIC_CONVEX_SITE_URL key should go before the NEXT_PUBLIC_CONVEX_URL key

(UnorderedKey)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vade Review
🔇 Additional comments (1)
examples/minimal-convex/.env.example (1)

10-10: Use neutral wording instead of specifying HTTP status code
examples/minimal-convex/.env.example:10

-# The adapter will 500/throw early if these are missing.
+# The adapter will error/throw early if these are missing.

url: req.url,
method: req.method,
headers,
body: await req.text(),
Copy link

Choose a reason for hiding this comment

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

The code uses req.text() to read the request body, which will corrupt binary file upload data by converting it to a UTF-8 string, causing uploads to fail.

View Details
📝 Patch Details
diff --git a/packages/uploadthing/src/convex-helpers.ts b/packages/uploadthing/src/convex-helpers.ts
index daebba6..56e3480 100644
--- a/packages/uploadthing/src/convex-helpers.ts
+++ b/packages/uploadthing/src/convex-helpers.ts
@@ -46,11 +46,12 @@ export const createRouteHandler = ({
     req.headers.forEach((value, key) => {
       headers[key] = value;
     });
+    const arrayBuffer = await req.arrayBuffer();
     const request = {
       url: req.url,
       method: req.method,
       headers,
-      body: await req.text(),
+      ...(arrayBuffer.byteLength > 0 && { body: Buffer.from(arrayBuffer).toString('base64') }),
     };
     const response = await ctx.runAction(internalAction, { request });
 
diff --git a/packages/uploadthing/src/convex.ts b/packages/uploadthing/src/convex.ts
index 75f60db..3dd4375 100644
--- a/packages/uploadthing/src/convex.ts
+++ b/packages/uploadthing/src/convex.ts
@@ -55,7 +55,7 @@ export const createInternalAction = <TRouter extends FileRouter>(
       const request = new Request(args.request.url, {
         method: args.request.method,
         headers: new Headers(args.request.headers),
-        body: args.request.body ? new Blob([args.request.body]) : null,
+        body: args.request.body ? new Blob([Buffer.from(args.request.body, 'base64')]) : null,
       });
 
       const response = await handler(ctx, request);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 286ae0b..ff748ef 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -130,7 +130,7 @@ importers:
         version: 12.0.6([email protected]([email protected]))([email protected])
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       next-sitemap:
         specifier: ^4.2.3
         version: 4.2.3([email protected](@playwright/[email protected])([email protected]([email protected]))([email protected]))
@@ -368,7 +368,7 @@ importers:
         version: link:../../packages/react
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react:
         specifier: 19.1.0
         version: 19.1.0
@@ -432,6 +432,43 @@ importers:
         specifier: 5.8.3
         version: 5.8.3
 
+  examples/minimal-convex:
+    dependencies:
+      '@uploadthing/react':
+        specifier: 7.3.3
+        version: link:../../packages/react
+      convex:
+        specifier: 1.26.2
+        version: 1.26.2(@clerk/[email protected]([email protected]([email protected]))([email protected]))([email protected])
+      next:
+        specifier: 15.3.1
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
+      react:
+        specifier: 19.1.0
+        version: 19.1.0
+      react-dom:
+        specifier: 19.1.0
+        version: 19.1.0([email protected])
+      uploadthing:
+        specifier: 7.7.4
+        version: link:../../packages/uploadthing
+    devDependencies:
+      '@next/bundle-analyzer':
+        specifier: 15.1.3
+        version: 15.1.3
+      '@types/node':
+        specifier: ^22.10.0
+        version: 22.12.0
+      '@types/react':
+        specifier: 19.1.2
+        version: 19.1.2
+      '@types/react-dom':
+        specifier: 19.1.2
+        version: 19.1.2(@types/[email protected])
+      typescript:
+        specifier: 5.8.3
+        version: 5.8.3
+
   examples/minimal-expo:
     dependencies:
       '@bacons/text-decoder':
@@ -572,7 +609,7 @@ importers:
         version: link:../../packages/react
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react:
         specifier: 19.1.0
         version: 19.1.0
@@ -723,7 +760,7 @@ importers:
         version: 0.469.0([email protected])
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       next-auth:
         specifier: 5.0.0-beta.25
         version: 5.0.0-beta.25([email protected](@playwright/[email protected])([email protected]([email protected]))([email protected]))([email protected])
@@ -799,7 +836,7 @@ importers:
         version: link:../../packages/react
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react:
         specifier: 19.1.0
         version: 19.1.0
@@ -848,7 +885,7 @@ importers:
         version: link:../../packages/react
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react:
         specifier: 19.1.0
         version: 19.1.0
@@ -1013,7 +1050,7 @@ importers:
         version: 0.38.3(@cloudflare/[email protected])(@libsql/[email protected])(@types/[email protected])([email protected])([email protected])
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react:
         specifier: 19.1.0
         version: 19.1.0
@@ -1065,7 +1102,7 @@ importers:
         version: 0.38.3(@cloudflare/[email protected])(@libsql/[email protected])(@types/[email protected])([email protected])([email protected])
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react:
         specifier: 19.1.0
         version: 19.1.0
@@ -1123,7 +1160,7 @@ importers:
         version: 0.469.0([email protected])
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       next-themes:
         specifier: ^0.4.6
         version: 0.4.6([email protected]([email protected]))([email protected])
@@ -1178,7 +1215,7 @@ importers:
         version: link:../../packages/react
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react:
         specifier: 19.1.0
         version: 19.1.0
@@ -1209,7 +1246,7 @@ importers:
     dependencies:
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react:
         specifier: 19.1.0
         version: 19.1.0
@@ -1240,7 +1277,7 @@ importers:
         version: link:../../packages/react
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react:
         specifier: 19.1.0
         version: 19.1.0
@@ -1436,7 +1473,7 @@ importers:
         version: 2.7.5(@types/[email protected])([email protected])
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react:
         specifier: 19.1.0
         version: 19.1.0
@@ -1689,7 +1726,7 @@ importers:
         version: 2.7.5(@types/[email protected])([email protected])
       next:
         specifier: 15.3.1
-        version: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+        version: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       solid-js:
         specifier: ^1.9.3
         version: 1.9.3
@@ -4883,12 +4920,6 @@ packages:
     cpu: [arm64]
     os: [darwin]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-    cpu: [arm64]
-    os: [darwin]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -4901,12 +4932,6 @@ packages:
     cpu: [x64]
     os: [darwin]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-    cpu: [x64]
-    os: [darwin]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -4918,11 +4943,6 @@ packages:
     cpu: [arm64]
     os: [darwin]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==}
-    cpu: [arm64]
-    os: [darwin]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==}
     cpu: [arm64]
@@ -4933,11 +4953,6 @@ packages:
     cpu: [x64]
     os: [darwin]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==}
-    cpu: [x64]
-    os: [darwin]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==}
     cpu: [x64]
@@ -4948,11 +4963,6 @@ packages:
     cpu: [arm64]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
-    cpu: [arm64]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==}
     cpu: [arm64]
@@ -4963,21 +4973,11 @@ packages:
     cpu: [arm]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==}
-    cpu: [arm]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==}
     cpu: [arm]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==}
-    cpu: [ppc64]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==}
     cpu: [ppc64]
@@ -4988,11 +4988,6 @@ packages:
     cpu: [s390x]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
-    cpu: [s390x]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==}
     cpu: [s390x]
@@ -5003,11 +4998,6 @@ packages:
     cpu: [x64]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
-    cpu: [x64]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==}
     cpu: [x64]
@@ -5018,11 +5008,6 @@ packages:
     cpu: [arm64]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==}
-    cpu: [arm64]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==}
     cpu: [arm64]
@@ -5033,11 +5018,6 @@ packages:
     cpu: [x64]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==}
-    cpu: [x64]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==}
     cpu: [x64]
@@ -5049,12 +5029,6 @@ packages:
     cpu: [arm64]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-    cpu: [arm64]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -5067,12 +5041,6 @@ packages:
     cpu: [arm]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-    cpu: [arm]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -5091,12 +5059,6 @@ packages:
     cpu: [s390x]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-    cpu: [s390x]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -5109,12 +5071,6 @@ packages:
     cpu: [x64]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-    cpu: [x64]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -5127,12 +5083,6 @@ packages:
     cpu: [arm64]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-    cpu: [arm64]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -5145,12 +5095,6 @@ packages:
     cpu: [x64]
     os: [linux]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-    cpu: [x64]
-    os: [linux]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -5162,11 +5106,6 @@ packages:
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [wasm32]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-    cpu: [wasm32]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -5184,12 +5123,6 @@ packages:
     cpu: [ia32]
     os: [win32]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-    cpu: [ia32]
-    os: [win32]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -5202,12 +5135,6 @@ packages:
     cpu: [x64]
     os: [win32]
 
-  '@img/[email protected]':
-    resolution: {integrity: sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-    cpu: [x64]
-    os: [win32]
-
   '@img/[email protected]':
     resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -16552,10 +16479,6 @@ packages:
     resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
 
-  [email protected]:
-    resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-
   [email protected]:
     resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -20235,7 +20158,7 @@ snapshots:
       '@clerk/shared': 2.20.10([email protected]([email protected]))([email protected])
       '@clerk/types': 4.41.1
       crypto-js: 4.2.0
-      next: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+      next: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react: 19.1.0
       react-dom: 19.1.0([email protected])
       server-only: 0.0.1
@@ -21979,11 +21902,6 @@ snapshots:
       '@img/sharp-libvips-darwin-arm64': 1.0.4
     optional: true
 
-  '@img/[email protected]':
-    optionalDependencies:
-      '@img/sharp-libvips-darwin-arm64': 1.1.0
-    optional: true
-
   '@img/[email protected]':
     optionalDependencies:
       '@img/sharp-libvips-darwin-arm64': 1.2.0
@@ -21994,11 +21912,6 @@ snapshots:
       '@img/sharp-libvips-darwin-x64': 1.0.4
     optional: true
 
-  '@img/[email protected]':
-    optionalDependencies:
-      '@img/sharp-libvips-darwin-x64': 1.1.0
-    optional: true
-
   '@img/[email protected]':
     optionalDependencies:
       '@img/sharp-libvips-darwin-x64': 1.2.0
@@ -22007,78 +21920,51 @@ snapshots:
   '@img/[email protected]':
     optional: true
 
-  '@img/[email protected]':
-    optional: true
-
   '@img/[email protected]':
     optional: true
 
   '@img/[email protected]':
     optional: true
 
-  '@img/[email protected]':
-    optional: true
-
   '@img/[email protected]':
     optional: true
 
   '@img/[email protected]':
     optional: true
 
-  '@img/[email protected]':
-    optional: true
-
   '@img/[email protected]':
     optional: true
 
   '@img/[email protected]':
     optional: true
 
-  '@img/[email protected]':
-    optional: true
-
   '@img/[email protected]':
     optional: true
 
-  '@img/[email protected]':
-    optional: true
-
   '@img/[email protected]':
     optional: true
 
   '@img/[email protected]':
     optional: true
 
-  '@img/[email protected]':
-    optional: true
-
   '@img/[email protected]':
     optional: true
 
   '@img/[email protected]':
     optional: true
 
-  '@img/[email protected]':
-    optional: true
-
   '@img/[email protected]':
     optional: true
 
   '@img/[email protected]':
     optional: true
 
-  '@img/[email protected]':
-    optional: true
-
   '@img/[email protected]':
     optional: true
 
   '@img/[email protected]':
     optional: true
 
-  '@img/[email protected]':
-    optional: true
-
   '@img/[email protected]':
     optional: true
 
@@ -22087,11 +21973,6 @@ snapshots:
       '@img/sharp-libvips-linux-arm64': 1.0.4
     optional: true
 
-  '@img/[email protected]':
-    optionalDependencies:
-      '@img/sharp-libvips-linux-arm64': 1.1.0
-    optional: true
-
   '@img/[email protected]':
     optionalDependencies:
       '@img/sharp-libvips-linux-arm64': 1.2.0
@@ -22102,11 +21983,6 @@ snapshots:
       '@img/sharp-libvips-linux-arm': 1.0.5
     optional: true
 
-  '@img/[email protected]':
-    optionalDependencies:
-      '@img/sharp-libvips-linux-arm': 1.1.0
-    optional: true
-
   '@img/[email protected]':
     optionalDependencies:
       '@img/sharp-libvips-linux-arm': 1.2.0
@@ -22122,11 +21998,6 @@ snapshots:
       '@img/sharp-libvips-linux-s390x': 1.0.4
     optional: true
 
-  '@img/[email protected]':
-    optionalDependencies:
-      '@img/sharp-libvips-linux-s390x': 1.1.0
-    optional: true
-
   '@img/[email protected]':
     optionalDependencies:
       '@img/sharp-libvips-linux-s390x': 1.2.0
@@ -22137,11 +22008,6 @@ snapshots:
       '@img/sharp-libvips-linux-x64': 1.0.4
     optional: true
 
-  '@img/[email protected]':
-    optionalDependencies:
-      '@img/sharp-libvips-linux-x64': 1.1.0
-    optional: true
-
   '@img/[email protected]':
     optionalDependencies:
       '@img/sharp-libvips-linux-x64': 1.2.0
@@ -22152,11 +22018,6 @@ snapshots:
       '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
     optional: true
 
-  '@img/[email protected]':
-    optionalDependencies:
-      '@img/sharp-libvips-linuxmusl-arm64': 1.1.0
-    optional: true
-
   '@img/[email protected]':
     optionalDependencies:
       '@img/sharp-libvips-linuxmusl-arm64': 1.2.0
@@ -22167,11 +22028,6 @@ snapshots:
       '@img/sharp-libvips-linuxmusl-x64': 1.0.4
     optional: true
 
-  '@img/[email protected]':
-    optionalDependencies:
-      '@img/sharp-libvips-linuxmusl-x64': 1.1.0
-    optional: true
-
   '@img/[email protected]':
     optionalDependencies:
       '@img/sharp-libvips-linuxmusl-x64': 1.2.0
@@ -22182,11 +22038,6 @@ snapshots:
       '@emnapi/runtime': 1.4.5
     optional: true
 
-  '@img/[email protected]':
-    dependencies:
-      '@emnapi/runtime': 1.4.5
-    optional: true
-
   '@img/[email protected]':
     dependencies:
       '@emnapi/runtime': 1.4.5
@@ -22198,18 +22049,12 @@ snapshots:
   '@img/[email protected]':
     optional: true
 
-  '@img/[email protected]':
-    optional: true
-
   '@img/[email protected]':
     optional: true
 
   '@img/[email protected]':
     optional: true
 
-  '@img/[email protected]':
-    optional: true
-
   '@img/[email protected]':
     optional: true
 
@@ -22518,7 +22363,7 @@ snapshots:
   '@mapbox/[email protected]([email protected])':
     dependencies:
       consola: 3.4.2
-      detect-libc: 2.0.3
+      detect-libc: 2.0.4
       https-proxy-agent: 7.0.6
       node-fetch: 2.7.0([email protected])
       nopt: 8.0.0
@@ -29407,8 +29252,7 @@ snapshots:
 
   [email protected]: {}
 
-  [email protected]:
-    optional: true
+  [email protected]: {}
 
   [email protected]: {}
 
@@ -30080,7 +29924,7 @@ snapshots:
       eslint: 9.25.1([email protected])
       eslint-import-resolver-node: 0.3.9
       eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected]))
-      eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected])))([email protected]([email protected]))
+      eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected]))
       eslint-plugin-jsx-a11y: 6.10.2([email protected]([email protected]))
       eslint-plugin-react: 7.37.0([email protected]([email protected]))
       eslint-plugin-react-hooks: 5.1.0([email protected]([email protected]))
@@ -30104,7 +29948,7 @@ snapshots:
       enhanced-resolve: 5.18.0
       eslint: 9.25.1([email protected])
       eslint-module-utils: 2.12.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected])))([email protected]([email protected]))
-      eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected])))([email protected]([email protected]))
+      eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected]))
       fast-glob: 3.3.3
       get-tsconfig: 4.10.1
       is-core-module: 2.16.1
@@ -30146,7 +29990,7 @@ snapshots:
       - supports-color
       - typescript
 
-  [email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected])))([email protected]([email protected])))([email protected]([email protected])):
+  [email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected])):
     dependencies:
       '@rtsao/scc': 1.1.0
       array-includes: 3.1.8
@@ -34361,7 +34205,7 @@ snapshots:
   [email protected]([email protected](@playwright/[email protected])([email protected]([email protected]))([email protected]))([email protected]):
     dependencies:
       '@auth/core': 0.37.2
-      next: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+      next: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react: 19.1.0
 
   [email protected]([email protected](@playwright/[email protected])([email protected]([email protected]))([email protected])):
@@ -34370,7 +34214,7 @@ snapshots:
       '@next/env': 13.5.6
       fast-glob: 3.3.3
       minimist: 1.2.8
-      next: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+      next: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
 
   [email protected]([email protected]([email protected]))([email protected]):
     dependencies:
@@ -34379,11 +34223,11 @@ snapshots:
 
   [email protected]([email protected](@playwright/[email protected])([email protected]([email protected]))([email protected]))([email protected]([email protected]))([email protected]):
     dependencies:
-      next: 15.3.1(@playwright/[email protected])([email protected]([email protected]))([email protected])
+      next: 15.3.1(@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])
       react: 19.1.0
       react-dom: 19.1.0([email protected])
 
-  [email protected](@playwright/[email protected])([email protected]([email protected]))([email protected]):
+  [email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected]):
     dependencies:
       '@next/env': 15.3.1
       '@swc/counter': 0.1.3
@@ -34393,7 +34237,7 @@ snapshots:
       postcss: 8.4.31
       react: 19.1.0
       react-dom: 19.1.0([email protected])
-      styled-jsx: 5.1.6([email protected])
+      styled-jsx: 5.1.6(@babel/[email protected])([email protected])
     optionalDependencies:
       '@next/swc-darwin-arm64': 15.3.1
       '@next/swc-darwin-x64': 15.3.1
@@ -34404,7 +34248,7 @@ snapshots:
       '@next/swc-win32-arm64-msvc': 15.3.1
       '@next/swc-win32-x64-msvc': 15.3.1
       '@playwright/test': 1.52.0
-      sharp: 0.34.1
+      sharp: 0.34.3
     transitivePeerDependencies:
       - '@babel/core'
       - babel-plugin-macros
@@ -34417,7 +34261,7 @@ snapshots:
       postcss: 8.4.31
       react: 19.1.0
       react-dom: 19.1.0([email protected])
-      styled-jsx: 5.1.6([email protected])
+      styled-jsx: 5.1.6(@babel/[email protected])([email protected])
     optionalDependencies:
       '@next/swc-darwin-arm64': 15.4.2-canary.51
       '@next/swc-darwin-x64': 15.4.2-canary.51
@@ -37542,34 +37386,6 @@ snapshots:
       '@img/sharp-win32-ia32': 0.33.5
       '@img/sharp-win32-x64': 0.33.5
 
-  [email protected]:
-    dependencies:
-      color: 4.2.3
-      detect-libc: 2.0.3
-      semver: 7.7.2
-    optionalDependencies:
-      '@img/sharp-darwin-arm64': 0.34.1
-      '@img/sharp-darwin-x64': 0.34.1
-      '@img/sharp-libvips-darwin-arm64': 1.1.0
-      '@img/sharp-libvips-darwin-x64': 1.1.0
-      '@img/sharp-libvips-linux-arm': 1.1.0
-      '@img/sharp-libvips-linux-arm64': 1.1.0
-      '@img/sharp-libvips-linux-ppc64': 1.1.0
-      '@img/sharp-libvips-linux-s390x': 1.1.0
-      '@img/sharp-libvips-linux-x64': 1.1.0
-      '@img/sharp-libvips-linuxmusl-arm64': 1.1.0
-      '@img/sharp-libvips-linuxmusl-x64': 1.1.0
-      '@img/sharp-linux-arm': 0.34.1
-      '@img/sharp-linux-arm64': 0.34.1
-      '@img/sharp-linux-s390x': 0.34.1
-      '@img/sharp-linux-x64': 0.34.1
-      '@img/sharp-linuxmusl-arm64': 0.34.1
-      '@img/sharp-linuxmusl-x64': 0.34.1
-      '@img/sharp-wasm32': 0.34.1
-      '@img/sharp-win32-ia32': 0.34.1
-      '@img/sharp-win32-x64': 0.34.1
-    optional: true
-
   [email protected]:
     dependencies:
       color: 4.2.3
@@ -38015,10 +37831,12 @@ snapshots:
     dependencies:
       inline-style-parser: 0.2.4
 
-  [email protected]([email protected]):
+  [email protected](@babel/[email protected])([email protected]):
     dependencies:
       client-only: 0.0.1
       react: 19.1.0
+    optionalDependencies:
+      '@babel/core': 7.27.4
 
   [email protected]([email protected]):
     dependencies:

Analysis

In the createRouteHandler function, the request body is being read using await req.text() at line 53. This approach has a fundamental flaw for file uploads: it converts binary data to a UTF-8 string, which corrupts binary file content.

When the binary data is later reconstructed in convex.ts at line 58 using new Blob([args.request.body]), the corrupted string data cannot be properly converted back to the original binary format. This will cause file uploads to fail because the uploaded files will be corrupted.

For file upload functionality to work correctly, the request body needs to be preserved as binary data throughout the serialization process. The code should use req.arrayBuffer() or req.blob() instead, and then encode the binary data appropriately (such as base64) for serialization to Convex, with corresponding decoding in the internal action handler.

This issue will manifest as corrupted uploaded files or complete upload failures for any non-text file types (images, videos, documents, etc.), which are the primary use case for UploadThing.

Copy link
Contributor

@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

♻️ Duplicate comments (5)
examples/minimal-convex/.env.example (1)

7-9: Call out CLIENT_ORIGIN alongside UPLOADTHING_TOKEN (Convex env, not .env).

The adapter’s CORS relies on CLIENT_ORIGIN. Document and show how to set it in Convex env next to UPLOADTHING_TOKEN to avoid confusing 4xx/5xx at runtime.

Apply:

 # Required by the UploadThing Convex adapter — set these in Convex env (not this .env):
-#   pnpx convex env set UPLOADTHING_TOKEN='<your-uploadthing-token>'
-# The adapter will 500/throw early if these are missing.
+#   pnpx convex env set UPLOADTHING_TOKEN '<your-uploadthing-token>'
+#   pnpx convex env set CLIENT_ORIGIN 'http://localhost:3000'  # your frontend origin
+# Requests will be rejected early if these are missing.
packages/uploadthing/src/convex-helpers.ts (4)

51-57: OPTIONS preflight must include CORS headers and echo requested headers.

Without these, browsers will block requests.

   http.route({
     method: "OPTIONS",
     path,
-    handler: httpActionGeneric(async () =>
-      Promise.resolve(new Response(null, { status: 204 })),
-    ),
+    handler: httpActionGeneric(async (_ctx, req) => {
+      return new Response(null, {
+        status: 204,
+        headers: addCorsHeaders(undefined, req.headers),
+      });
+    }),
   });

1-4: Add robust CORS helper (echo requested headers, set Vary).

Preflight currently returns no CORS headers; add a helper and reuse on all responses.

 import type { CorsHttpRouter } from "convex-helpers/server/cors";
 import type { FunctionReference, HttpRouter } from "convex/server";
 import { httpActionGeneric } from "convex/server";
 
+const addCorsHeaders = (
+  headers?: Record<string, string>,
+  reqHeaders?: Headers,
+) => {
+  const origin = process.env.CLIENT_ORIGIN;
+  if (!origin) {
+    // Surface misconfiguration early
+    throw new Error("Convex env CLIENT_ORIGIN is not set");
+  }
+  const acrh =
+    reqHeaders?.get("access-control-request-headers") ??
+    "content-type, authorization, x-uploadthing-*";
+  return new Headers({
+    ...headers,
+    "Access-Control-Allow-Origin": origin,
+    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
+    "Access-Control-Allow-Headers": acrh,
+    "Access-Control-Max-Age": "86400",
+    Vary: "Origin, Access-Control-Request-Headers, Access-Control-Request-Method",
+  });
+};

31-41: Don’t use req.text() for request bodies; preserve bytes and avoid empty-body on GET/HEAD.

Using text corrupts non-UTF8 payloads and sends spurious empty strings for methods that should have no body. Read as ArrayBuffer, base64-encode only when present, and skip bodies for GET/HEAD.

   const handler = httpActionGeneric(async (ctx, req) => {
     const headers: Record<string, string> = {};
     req.headers.forEach((value, key) => {
       headers[key] = value;
     });
-    const request = {
-      url: req.url,
-      method: req.method,
-      headers,
-      body: await req.text(),
-    };
+    const method = req.method;
+    let encodedBody: string | undefined;
+    if (method !== "GET" && method !== "HEAD") {
+      const ab = await req.arrayBuffer();
+      if (ab.byteLength > 0) {
+        // Base64 preserves bytes through Convex serialization
+        // Buffer is available in Convex runtime (Node)
+        // eslint-disable-next-line no-undef
+        encodedBody = Buffer.from(ab).toString("base64");
+      }
+    }
+    const request = {
+      url: req.url,
+      method,
+      headers,
+      ...(encodedBody ? { body: encodedBody } : {}),
+    };
     const response = await ctx.runAction(internalAction, { request });

-    return new Response(response.body, {
+    return new Response(response.body, {
       status: response.status,
       statusText: response.statusText,
-      headers: response.headers,
+      headers: addCorsHeaders(response.headers, req.headers),
     });
   });

Note: Pair this with the corresponding base64 decode in createInternalAction (convex.ts): new Blob([Buffer.from(args.request.body, "base64")]).


31-49: Ensure base64 decoding of args.request.body in createInternalAction
In packages/uploadthing/src/convex.ts, inside the handler for internalActionGeneric, replace:

body: args.request.body ? new Blob([args.request.body]) : null,

with something that decodes the base64 string back into raw bytes, for example:

const raw = args.request.body ? Buffer.from(args.request.body, "base64") : undefined;
body: raw ? new Blob([raw]) : null,

This ensures uploaded data isn’t sent to the server still encoded.

🧹 Nitpick comments (7)
examples/minimal-convex/.env.example (1)

4-5: Satisfy dotenv-linter key ordering.

Place NEXT_PUBLIC_CONVEX_SITE_URL before NEXT_PUBLIC_CONVEX_URL to silence UnorderedKey.

-NEXT_PUBLIC_CONVEX_URL=
-NEXT_PUBLIC_CONVEX_SITE_URL=
+NEXT_PUBLIC_CONVEX_SITE_URL=
+NEXT_PUBLIC_CONVEX_URL=
examples/minimal-convex/README.md (2)

3-5: Add alt text to the image.

Improves accessibility and satisfies markdownlint MD045.

-<a href="https://stackblitz.com/github/pingdotgg/uploadthing/tree/main/examples/minimal-convex">
-  <img height="64" src="https://github.com/pingdotgg/uploadthing/assets/51714798/45907a4e-aa64-401a-afb3-b6c6df6eb71f" />
-</a>
+<a href="https://stackblitz.com/github/pingdotgg/uploadthing/tree/main/examples/minimal-convex">
+  <img alt="Open minimal-convex example in StackBlitz" height="64" src="https://github.com/pingdotgg/uploadthing/assets/51714798/45907a4e-aa64-401a-afb3-b6c6df6eb71f" />
+</a>

11-17: Reorder QuickStart so required Convex env vars are set before dev servers start; avoid bare URL.

Prevents boot-time failures and fixes MD034.

 1. `pnpm i`
 2. `pnpm dev:setup`
 3. Add the `NEXT_PUBLIC_CONVEX_SITE_URL` to the .env file
-4. Grab an API key from the UploadThing dashboard:
-   https://uploadthing.com/dashboard
-5. `pnpm dev:convex`
-6. `pnpx convex env set UPLOADTHING_TOKEN=<your-token>`
-7. `pnpm dev`
-8. Upload files!
+4. Grab an API key from the [UploadThing dashboard](https://uploadthing.com/dashboard)
+5. `pnpx convex env set UPLOADTHING_TOKEN=<your-token>`
+6. `pnpx convex env set CLIENT_ORIGIN=http://localhost:3000`
+7. `pnpm dev:convex`
+8. `pnpm dev`
+9. Upload files!
docs/src/app/(docs)/backend-adapters/convex/page.mdx (2)

41-46: Minor copy: “etc.” punctuation and clarity.

Tiny clarity/readability improvement; keep the rest as-is.

-- Permitted types ["image", "video", etc]
+- Permitted types ["image", "video", etc.]

132-135: Clarify that the helper adds CORS from CLIENT_ORIGIN.

Reduces guesswork for readers.

-<Note>
-  Please note that you might need to setup some CORS rules on your server to
-  allow the client to make requests to the server.
-</Note>
+<Note>
+  `createRouteHandler` attaches CORS headers using the `CLIENT_ORIGIN`
+  environment variable you set in Convex. Ensure it matches your frontend origin.
+</Note>
packages/uploadthing/src/convex-helpers.ts (2)

59-61: Consider adding HEAD route (optional).

Some clients issue HEAD for liveness/CORS probes; returning CORS headers helps.

   http.route({ method: "GET", path, handler });
 
   http.route({ method: "POST", path, handler });
+
+  http.route({
+    method: "HEAD",
+    path,
+    handler: httpActionGeneric(async (_ctx, req) => {
+      return new Response(null, {
+        status: 204,
+        headers: addCorsHeaders(undefined, req.headers),
+      });
+    }),
+  });

5-29: Type contract: body remains optional string, but is now base64. Document this in the JSDoc.

Small DX improvement to set expectations for consumers and for the paired decoder in convex.ts.

 export const createRouteHandler = ({
@@
-        body?: string;
+        /**
+         * Base64-encoded request body (if any). Omitted for GET/HEAD or empty bodies.
+         */
+        body?: string;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 121f21a and 681e127.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • docs/src/app/(docs)/backend-adapters/convex/page.mdx (1 hunks)
  • docs/src/components/Libraries.tsx (3 hunks)
  • docs/src/site-config.ts (1 hunks)
  • examples/minimal-convex/.env.example (1 hunks)
  • examples/minimal-convex/README.md (1 hunks)
  • examples/minimal-convex/convex/http.ts (1 hunks)
  • examples/minimal-convex/package.json (1 hunks)
  • packages/uploadthing/package.json (4 hunks)
  • packages/uploadthing/src/convex-helpers.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/uploadthing/package.json
  • docs/src/components/Libraries.tsx
  • docs/src/site-config.ts
  • examples/minimal-convex/package.json
  • examples/minimal-convex/convex/http.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-09T22:39:50.896Z
Learnt from: riordanpawley
PR: pingdotgg/uploadthing#1005
File: packages/uploadthing/src/internal/upload.server.ts:35-35
Timestamp: 2024-10-09T22:39:50.896Z
Learning: In `packages/uploadthing/src/internal/upload.server.ts`, using `Effect.andThen(unsafeCoerce<unknown, UploadPutResult>)` when processing the JSON response is appropriate and should not be replaced with a type assertion.

Applied to files:

  • packages/uploadthing/src/convex-helpers.ts
📚 Learning: 2024-10-03T20:04:00.113Z
Learnt from: markflorkowski
PR: pingdotgg/uploadthing#978
File: examples/minimal-appdir/src/server/uploadthing.ts:41-43
Timestamp: 2024-10-03T20:04:00.113Z
Learning: In the `examples/minimal-appdir/src/server/uploadthing.ts` and other example files, the commented-out header validation code is intentional and serves as an example for users.

Applied to files:

  • packages/uploadthing/src/convex-helpers.ts
🪛 LanguageTool
docs/src/app/(docs)/backend-adapters/convex/page.mdx

[grammar] ~3-~3: There might be a mistake here.
Context: ... export const metadata = docsMetadata({ title: "Convex", description: "Adapter...

(QB_NEW_EN)


[grammar] ~6-~6: There might be a mistake here.
Context: ...cation", category: "Backend Adapters", }); # Getting started with Convex > Ad...

(QB_NEW_EN)


[grammar] ~37-~37: There might be a mistake here.
Context: ...sociated with a FileRoute. The following is a very minimalistic example, with a s...

(QB_NEW_EN)


[grammar] ~38-~38: There might be a mistake here.
Context: ... single FileRoute "imageUploader". Think of a FileRoute similar to an endpoint, i...

(QB_NEW_EN)


[style] ~41-~41: In American English, abbreviations like “etc.” require a period.
Context: ...: - Permitted types ["image", "video", etc] - Max file size - How many files are a...

(ETC_PERIOD)


[grammar] ~41-~41: There might be a mistake here.
Context: ... Permitted types ["image", "video", etc] - Max file size - How many files are allow...

(QB_NEW_EN)


[grammar] ~42-~42: There might be a mistake here.
Context: ... ["image", "video", etc] - Max file size - How many files are allowed to be uploade...

(QB_NEW_EN)


[grammar] ~43-~43: There might be a mistake here.
Context: ...ow many files are allowed to be uploaded - (Optional) input validation to validat...

(QB_NEW_EN)


[grammar] ~44-~44: There might be a mistake here.
Context: ...idate client-side data sent to the route - (Optional) middleware to authenticate ...

(QB_NEW_EN)


[grammar] ~48-~48: There might be a mistake here.
Context: ... do with the FileRoutes, please refer to the File Router API. ``...

(QB_NEW_EN)


[grammar] ~114-~114: There might be a mistake here.
Context: ...ttp; ``` > See configuration options in > [server API reference](/api-reference/...

(QB_NEW_EN)


[grammar] ~119-~119: There might be a mistake here.
Context: ...htly from the fullstack framework setups when using a separate backend server. Yo...

(QB_NEW_EN)


[grammar] ~120-~120: There might be a mistake here.
Context: ...ou'll need to set the URL of your server when you generate the components and hel...

(QB_NEW_EN)


[grammar] ~140-~140: There might be a mistake here.
Context: ... fullstack framework guides: - Next.js - [Solid.js](/getting-started/solid#creatin...

(QB_NEW_EN)


[grammar] ~141-~141: There might be a mistake here.
Context: ...the-upload-thing-components) - Solid.js - [Vue](https://github.com/pingdotgg/upload...

(QB_NEW_EN)


[grammar] ~142-~142: There might be a mistake here.
Context: ...ting-the-upload-thing-components) - Vue - [Svelte](/getting-started/svelte#creating...

(QB_NEW_EN)

examples/minimal-convex/README.md

[grammar] ~12-~12: There might be a mistake here.
Context: ... API key from the UploadThing dashboard: https://uploadthing.com/dashboard 5. `pn...

(QB_NEW_EN)

🪛 dotenv-linter (3.3.0)
examples/minimal-convex/.env.example

[warning] 5-5: [UnorderedKey] The NEXT_PUBLIC_CONVEX_SITE_URL key should go before the NEXT_PUBLIC_CONVEX_URL key

(UnorderedKey)

🪛 markdownlint-cli2 (0.17.2)
examples/minimal-convex/README.md

4-4: Images should have alternate text (alt text)

(MD045, no-alt-text)


13-13: Bare URL used

(MD034, no-bare-urls)


21-21: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vade Review

url: req.url,
method: req.method,
headers,
body: await req.text(),
Copy link

Choose a reason for hiding this comment

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

Request body is converted to text using req.text(), which will corrupt binary file upload data and cause uploads to fail.

View Details
📝 Patch Details
diff --git a/packages/uploadthing/src/convex-helpers.ts b/packages/uploadthing/src/convex-helpers.ts
index caccb1c5..1892097f 100644
--- a/packages/uploadthing/src/convex-helpers.ts
+++ b/packages/uploadthing/src/convex-helpers.ts
@@ -33,11 +33,12 @@ export const createRouteHandler = ({
     req.headers.forEach((value, key) => {
       headers[key] = value;
     });
+    const bodyBuffer = await req.arrayBuffer();
     const request = {
       url: req.url,
       method: req.method,
       headers,
-      body: await req.text(),
+      body: bodyBuffer.byteLength > 0 ? btoa(String.fromCharCode(...new Uint8Array(bodyBuffer))) : undefined,
     };
     const response = await ctx.runAction(internalAction, { request });
 
diff --git a/packages/uploadthing/src/convex.ts b/packages/uploadthing/src/convex.ts
index 75f60db5..96bc0f98 100644
--- a/packages/uploadthing/src/convex.ts
+++ b/packages/uploadthing/src/convex.ts
@@ -55,7 +55,7 @@ export const createInternalAction = <TRouter extends FileRouter>(
       const request = new Request(args.request.url, {
         method: args.request.method,
         headers: new Headers(args.request.headers),
-        body: args.request.body ? new Blob([args.request.body]) : null,
+        body: args.request.body ? new Blob([Uint8Array.from(atob(args.request.body), c => c.charCodeAt(0))]) : null,
       });
 
       const response = await handler(ctx, request);

Analysis

The code uses await req.text() to read the request body before serializing it for the internal Convex action. This is problematic because file uploads typically contain binary data (multipart/form-data or raw binary), and converting binary data to text will corrupt the data.

When req.text() is called on a request containing binary file data, it attempts to decode the bytes as UTF-8 text, which will fail for binary content or produce corrupted data. This corrupted text string is then passed through Convex's serialization system and later reconstructed as a Blob in convex.ts line 58, but the underlying data will be corrupted.

The correct approach would be to use req.arrayBuffer() to preserve the binary data integrity, then convert it to a base64 string for serialization, and decode it back to binary data when reconstructing the Request object.

This bug will cause all file uploads through the Convex adapter to fail with corrupted or invalid file data.

@IslamIhab
Copy link
Author

@markflorkowski Sorry for spamming but could I get a review? If the PR is a bit too bit I can break it down to several smaller PRs to make each part take less time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants