Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
33 changes: 33 additions & 0 deletions app/api/upload/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { uploadFileHandler } from "@/lib/arweave/uploadFileHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns A NextResponse with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}

/**
* POST /api/upload
*
* Uploads a file (multipart/form-data, field name `file`) to Arweave and
* returns the gateway URL.
*
* @param request - The incoming request carrying the file.
* @returns A NextResponse with `{ success, fileName, fileType, fileSize, url }` on 200
* or `{ success: false, error }` on 500.
*/
export async function POST(request: NextRequest) {
return uploadFileHandler(request);
}
Comment on lines +27 to +29
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add auth validation before delegating to the upload handler.

POST currently accepts uploads without calling validateAuthContext(). That leaves the endpoint open for unauthenticated writes.

Suggested fix
 import { NextRequest, NextResponse } from "next/server";
 import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
 import { uploadFileHandler } from "@/lib/arweave/uploadFileHandler";
+import { validateAuthContext } from "@/lib/auth/validateAuthContext";

 ...
 export async function POST(request: NextRequest) {
+  const auth = await validateAuthContext(request);
+  if (!auth.valid) {
+    return NextResponse.json(
+      { success: false, error: "Unauthorized" },
+      { status: 401, headers: getCorsHeaders() },
+    );
+  }
   return uploadFileHandler(request);
 }

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

📝 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
export async function POST(request: NextRequest) {
return uploadFileHandler(request);
}
export async function POST(request: NextRequest) {
const auth = await validateAuthContext(request);
if (!auth.valid) {
return NextResponse.json(
{ success: false, error: "Unauthorized" },
{ status: 401, headers: getCorsHeaders() },
);
}
return uploadFileHandler(request);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/upload/route.ts` around lines 27 - 29, The POST handler currently
delegates straight to uploadFileHandler without authentication; call await
validateAuthContext(request) at the start of the exported POST function
(importing it if necessary), handle failures by returning an appropriate 401/403
response, and only then call uploadFileHandler (optionally passing the returned
auth context) so uploads require valid x-api-key or Authorization: Bearer
credentials; keep the function signature (POST(request: NextRequest)) and
preserve uploadFileHandler usage.


export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
96 changes: 96 additions & 0 deletions lib/arweave/__tests__/uploadFileHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";
import { uploadFileHandler } from "@/lib/arweave/uploadFileHandler";
import { uploadToArweave } from "@/lib/arweave/uploadToArweave";
import { getFetchableUrl } from "@/lib/arweave/getFetchableUrl";

vi.mock("@/lib/networking/getCorsHeaders", () => ({
getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })),
}));

vi.mock("@/lib/arweave/uploadToArweave", () => ({
uploadToArweave: vi.fn(),
}));

vi.mock("@/lib/arweave/getFetchableUrl", () => ({
getFetchableUrl: vi.fn(),
}));

const buildRequest = (formData: FormData) =>
new NextRequest("https://example.com/api/upload", {
method: "POST",
body: formData,
});

describe("uploadFileHandler", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("uploads the file and returns the gateway URL", async () => {
vi.mocked(uploadToArweave).mockResolvedValue({ id: "tx_abc" } as never);
vi.mocked(getFetchableUrl).mockReturnValue("https://arweave.net/tx_abc");

const file = new File([new Uint8Array([1, 2, 3, 4])], "hello.png", {
type: "image/png",
});
const formData = new FormData();
formData.append("file", file);

const response = await uploadFileHandler(buildRequest(formData));
const body = await response.json();

expect(response.status).toBe(200);
expect(uploadToArweave).toHaveBeenCalledWith(expect.any(Buffer), "image/png");
const [bufferArg] = vi.mocked(uploadToArweave).mock.calls[0];
expect((bufferArg as Buffer).length).toBe(4);
expect(getFetchableUrl).toHaveBeenCalledWith("ar://tx_abc");
expect(body).toEqual({
success: true,
fileName: "hello.png",
fileType: "image/png",
fileSize: 4,
url: "https://arweave.net/tx_abc",
});
});

it("falls back to application/octet-stream when file.type is empty", async () => {
vi.mocked(uploadToArweave).mockResolvedValue({ id: "tx_xyz" } as never);
vi.mocked(getFetchableUrl).mockReturnValue("https://arweave.net/tx_xyz");

const file = new File([new Uint8Array([9])], "blob.bin", { type: "" });
const formData = new FormData();
formData.append("file", file);

const response = await uploadFileHandler(buildRequest(formData));
const body = await response.json();

expect(response.status).toBe(200);
expect(uploadToArweave).toHaveBeenCalledWith(expect.any(Buffer), "application/octet-stream");
expect(body.fileType).toBe("application/octet-stream");
});

it("returns 500 with success:false when no file is provided", async () => {
const formData = new FormData();
const response = await uploadFileHandler(buildRequest(formData));
const body = await response.json();

expect(response.status).toBe(500);
expect(body).toEqual({ success: false, error: "No file provided" });
expect(uploadToArweave).not.toHaveBeenCalled();
});

it("returns 500 with success:false when uploadToArweave throws", async () => {
vi.mocked(uploadToArweave).mockRejectedValue(new Error("network down"));

const file = new File([new Uint8Array([1])], "x.png", { type: "image/png" });
const formData = new FormData();
formData.append("file", file);

const response = await uploadFileHandler(buildRequest(formData));
const body = await response.json();

expect(response.status).toBe(500);
expect(body).toEqual({ success: false, error: "network down" });
});
});
58 changes: 58 additions & 0 deletions lib/arweave/uploadFileHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { uploadToArweave } from "@/lib/arweave/uploadToArweave";
import { getFetchableUrl } from "@/lib/arweave/getFetchableUrl";

/**
* Handles POST /api/upload — uploads a file to Arweave and returns a gateway URL.
*
* Mirrors the chat-side response shape exactly so callers can migrate
* with a single base-URL swap.
*
* @param request - The incoming request carrying multipart/form-data with a `file` field.
* @returns A NextResponse with `{ success, fileName, fileType, fileSize, url }` on 200
* or `{ success: false, error }` on 500.
*/
export async function uploadFileHandler(request: NextRequest): Promise<NextResponse> {
try {
const formData = await request.formData();
const file = formData.get("file");

if (!file || typeof file === "string") {
throw new Error("No file provided");
}

const fileBuffer = Buffer.from(await file.arrayBuffer());
const fileSize = fileBuffer.length;
const fileType = file.type || "application/octet-stream";
const fileName = file.name;

const transaction = await uploadToArweave(fileBuffer, fileType);
Comment on lines +25 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce a maximum file size before buffering bytes.

arrayBuffer() + Buffer.from(...) loads the full payload into memory with no upper bound. Add a strict file.size cap and reject oversized uploads early to avoid memory exhaustion.

Suggested fix
+const MAX_UPLOAD_BYTES = 25 * 1024 * 1024; // 25MB
...
-const fileBuffer = Buffer.from(await file.arrayBuffer());
+if (file.size > MAX_UPLOAD_BYTES) {
+  throw new Error("File too large");
+}
+const fileBuffer = Buffer.from(await file.arrayBuffer());
📝 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 fileBuffer = Buffer.from(await file.arrayBuffer());
const fileSize = fileBuffer.length;
const fileType = file.type || "application/octet-stream";
const fileName = file.name;
const transaction = await uploadToArweave(fileBuffer, fileType);
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024; // 25MB
// ... (other code)
if (file.size > MAX_UPLOAD_BYTES) {
throw new Error("File too large");
}
const fileBuffer = Buffer.from(await file.arrayBuffer());
const fileSize = fileBuffer.length;
const fileType = file.type || "application/octet-stream";
const fileName = file.name;
const transaction = await uploadToArweave(fileBuffer, fileType);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/arweave/uploadFileHandler.ts` around lines 25 - 30, Before calling
file.arrayBuffer() and Buffer.from(...) in uploadFileHandler (which currently
creates fileBuffer/fileSize and then calls uploadToArweave), check the incoming
file.size against a configured MAX_FILE_SIZE constant and reject/throw an error
(or return an HTTP 413) immediately if file.size exceeds the limit; only if
file.size is within bounds proceed to await file.arrayBuffer(), create
fileBuffer, set fileType/fileName, and call uploadToArweave. Ensure the
MAX_FILE_SIZE constant is documented and used where the size check happens to
prevent buffering oversized uploads.


return NextResponse.json(
{
success: true,
fileName,
fileType,
fileSize,
url: getFetchableUrl(`ar://${transaction.id}`),
},
{
status: 200,
headers: getCorsHeaders(),
},
);
} catch (error) {
console.error("/api/upload error", error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "Unknown error",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Do not expose raw exception messages in 500 responses; return a generic error string instead.

(Based on your team's feedback about preventing internal error detail leaks in API 500 responses.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/arweave/uploadFileHandler.ts, line 50:

<comment>Do not expose raw exception messages in 500 responses; return a generic error string instead.

(Based on your team's feedback about preventing internal error detail leaks in API 500 responses.) </comment>

<file context>
@@ -0,0 +1,58 @@
+    return NextResponse.json(
+      {
+        success: false,
+        error: error instanceof Error ? error.message : "Unknown error",
+      },
+      {
</file context>

},
{
status: 500,
headers: getCorsHeaders(),
},
);
}
}
Loading