type Request = (path: string, options?: RequestInit) => Promise; export type CanvasAsset = { id: string; url: string; width: number; height: number; bytes: number; mime_type: string }; export const ASSET_LIMIT = 20 * 1024 * 1024; export async function uploadCanvasImage(request: Request, workspace: string, file: Blob, name: string, signal?: AbortSignal, key = crypto.randomUUID()): Promise { if (!file.size || file.size > ASSET_LIMIT) throw new Error("asset_too_large"); const form = new FormData(); form.append("image", file, name); const send = () => request(`workspaces/${encodeURIComponent(workspace)}/assets`, { method: "POST", headers: { "Idempotency-Key": key }, body: form, signal }); let asset: CanvasAsset; try { asset = await send(); } catch (error) { if (signal?.aborted || !(error instanceof TypeError || typeof error === "object" && error && "status" in error && Number(error.status) >= 500)) throw error; asset = await send(); // The same bytes/key retry a possibly committed upload. } if (!asset || typeof asset.url !== "string") throw new Error("invalid_asset_response"); let url: URL; try { url = new URL(asset.url); } catch { throw new Error("invalid_asset_response"); } if (!/^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$/i.test(asset.id) || url.origin !== "https://api.foundry.entroia.com" || url.pathname !== `/projects/${process.env.NEXT_PUBLIC_FOUNDRY_PROJECT || "endoleap"}/workspaces/${workspace}/assets/${asset.id}` || url.search || !Number.isFinite(asset.width) || asset.width < 1 || !Number.isFinite(asset.height) || asset.height < 1) throw new Error("invalid_asset_response"); return asset; }