Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/swift-beds-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"uploadthing": minor
---

Add Convex Adapter
26 changes: 26 additions & 0 deletions packages/uploadthing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,26 @@
"default": "./remix/index.cjs"
}
},
"./convex": {
"import": {
"types": "./convex/index.d.ts",
"default": "./convex/index.js"
},
"require": {
"types": "./convex/index.d.cts",
"default": "./convex/index.cjs"
}
},
"./convex-helpers": {
"import": {
"types": "./convex-helpers/index.d.ts",
"default": "./convex-helpers/index.js"
},
"require": {
"types": "./convex-helpers/index.d.cts",
"default": "./convex-helpers/index.cjs"
}
},
"./types": {
"types": "./types/index.d.ts",
"default": "./types/index.js"
Expand All @@ -134,6 +154,8 @@
"files": [
"client",
"client-future",
"convex",
"convex-helpers",
"dist",
"effect-platform",
"express",
Expand Down Expand Up @@ -194,6 +216,7 @@
"zod": "^3.24.1"
},
"peerDependencies": {
"convex": "*",
"express": "*",
"h3": "*",
"tailwindcss": "^3.0.0 || ^4.0.0-beta.0"
Expand All @@ -211,6 +234,9 @@
"h3": {
"optional": true
},
"convex": {
"optional": true
},
"tailwindcss": {
"optional": true
}
Expand Down
72 changes: 72 additions & 0 deletions packages/uploadthing/src/convex-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { FunctionReference, HttpRouter } from "convex/server";
import { httpActionGeneric } from "convex/server";

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.


export const createRouteHandler = (
http: HttpRouter,
internalAction: FunctionReference<
"action",
"internal",
{
request: {
url: string;
method: string;
headers: Record<string, string>;
body?: string;
};
},
{
status: number;
statusText: string;
headers: Record<string, string>;
body: string;
}
>,
) => {
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(),
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

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

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

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.

};
const response = await ctx.runAction(internalAction, { request });

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

http.route({
method: "OPTIONS",
path: "/api/uploadthing",
handler: httpActionGeneric(async () =>
Promise.resolve(
new Response(null, { status: 204, headers: addCorsHeaders() }),
),
),
});

http.route({ method: "GET", path: "/api/uploadthing", handler });

http.route({ method: "POST", path: "/api/uploadthing", handler });
};
76 changes: 76 additions & 0 deletions packages/uploadthing/src/convex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { internalActionGeneric } from "convex/server";
import type { GenericActionCtx, GenericDataModel } from "convex/server";
import { v } from "convex/values";
import * as Effect from "effect/Effect";

import type { Json } from "@uploadthing/shared";

import { makeAdapterHandler } from "./_internal/handler";
import { createBuilder } from "./_internal/upload-builder";
import type { CreateBuilderOptions } from "./_internal/upload-builder";
import type { FileRouter, RouteHandlerOptions } from "./types";

export {
UTFiles,
/**
* This is an experimental feature.
* You need to be feature flagged on our backend to use this
*/
UTRegion as experimental_UTRegion,
} from "./_internal/types";
export type { FileRouter };

type AdapterArgs = {
ctx: GenericActionCtx<GenericDataModel>;
req: Request;
};

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

export const createInternalAction = <TRouter extends FileRouter>(
opts: RouteHandlerOptions<TRouter>,
) => {
const handler = makeAdapterHandler<
[GenericActionCtx<GenericDataModel>, Request],
AdapterArgs
>(
(ctx, req) => Effect.succeed({ ctx, req }),
(_, req) => Effect.succeed(req),
opts,
"convex",
);

return internalActionGeneric({
args: {
request: v.object({
url: v.string(),
method: v.string(),
headers: v.record(v.string(), v.string()),
body: v.optional(v.string()),
}),
},
handler: async (ctx: GenericActionCtx<GenericDataModel>, args) => {
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 response = await handler(ctx, request);

const headers: Record<string, string> = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});

return {
status: response.status,
statusText: response.statusText,
headers,
body: await response.text(),
};
},
});
};
2 changes: 2 additions & 0 deletions packages/uploadthing/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export default defineConfig({
"../h3/index": "src/h3.ts",
"../remix/index": "src/remix.ts",
"../types/index": "src/types.ts",
"../convex/index": "src/convex.ts",
"../convex-helpers/index": "src/convex-helpers.ts",
},
format: ["esm", "cjs"],
dts: {
Expand Down
2 changes: 2 additions & 0 deletions packages/uploadthing/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"outputs": [
"client/**",
"client-future/**",
"convex/**",
"convex-helpers/**",
"dist/**",
"effect-platform/**",
"express/**",
Expand Down
Loading
Loading