Restore the community photo after reload and reject empty, oversized, SVG, and spoofed uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-10 15:50:32 -06:00
co-authored by Cursor
parent 6ccc1e8c8e
commit 234f3998ad
18 changed files with 1072 additions and 112 deletions
+62 -4
View File
@@ -1,4 +1,10 @@
import type { CreateFlowUploadPurpose } from "./createFlowUploadPurpose";
import {
CreateFlowUploadValidationError,
messageKeyForCreateFlowUploadReason,
validateCreateFlowUploadFile,
type CreateFlowUploadValidationReason,
} from "./createFlowUploadValidation";
export type UploadToServerResult = {
url: string;
@@ -7,6 +13,20 @@ export type UploadToServerResult = {
byteLength: number;
};
const VALIDATION_REASONS = new Set<CreateFlowUploadValidationReason>([
"empty",
"tooLarge",
"svg",
"invalidType",
"undecodable",
]);
function reasonFromUnknown(value: unknown): CreateFlowUploadValidationReason | null {
return typeof value === "string" && VALIDATION_REASONS.has(value as CreateFlowUploadValidationReason)
? (value as CreateFlowUploadValidationReason)
: null;
}
/**
* Authenticated multipart upload to `POST /api/uploads`.
* Caller must have a session cookie (same-origin fetch).
@@ -15,6 +35,11 @@ export async function uploadCreateFlowFile(
file: File,
purpose: CreateFlowUploadPurpose,
): Promise<UploadToServerResult> {
const validated = await validateCreateFlowUploadFile(file, purpose);
if (validated.ok === false) {
throw new CreateFlowUploadValidationError(validated.reason);
}
const formData = new FormData();
formData.append("purpose", purpose);
formData.append("file", file);
@@ -36,14 +61,24 @@ export async function uploadCreateFlowFile(
if (body && typeof body === "object" && "error" in body) {
const e = (body as {
error?: { message?: string; code?: string };
details?: { reason?: unknown };
}).error;
if (!e) return { message: null as string | null, code: null as string | null };
const details = (body as { details?: { reason?: unknown } }).details;
const reason = reasonFromUnknown(details?.reason);
if (!e) {
return {
message: null as string | null,
code: null as string | null,
reason,
};
}
return {
message: typeof e.message === "string" ? e.message : null,
code: typeof e.code === "string" ? e.code : null,
reason,
};
}
return { message: null, code: null };
return { message: null, code: null, reason: null };
})();
if (!res.ok) {
@@ -54,7 +89,7 @@ export async function uploadCreateFlowFile(
? "UNAUTHORIZED"
: "UPLOAD_FAILED";
const code = errParts.code ?? errParts.message ?? fallback;
throw new UploadToServerError(res.status, code);
throw new UploadToServerError(res.status, code, errParts.reason);
}
const data = body as {
@@ -83,11 +118,34 @@ export async function uploadCreateFlowFile(
export class UploadToServerError extends Error {
readonly status: number;
readonly code: string;
readonly reason: CreateFlowUploadValidationReason | null;
constructor(status: number, code: string) {
constructor(
status: number,
code: string,
reason: CreateFlowUploadValidationReason | null = null,
) {
super(code);
this.name = "UploadToServerError";
this.status = status;
this.code = code;
this.reason = reason;
}
}
export function createFlowUploadFailureMessageKey(err: unknown): string {
if (err instanceof CreateFlowUploadValidationError) {
return messageKeyForCreateFlowUploadReason(err.reason);
}
if (err instanceof UploadToServerError) {
if (err.reason) return messageKeyForCreateFlowUploadReason(err.reason);
if (err.status === 413) return "errors.tooLarge";
if (err.status === 401) return "errors.unauthorized";
if (err.code === "server_misconfigured") {
return "errors.misconfigured";
}
}
return "errors.generic";
}
export { CreateFlowUploadValidationError };