import path from "path"; import { uploadFile } from "@agent-native/core/file-upload"; import { getSession } from "@agent-native/core/server"; import { runWithRequestContext } from "@agent-native/core/server"; import { and, desc, eq } from "drizzle-orm"; import { defineEventHandler, getRouterParam, setResponseStatus, readMultipartFormData, } from "h3"; import { nanoid } from "nanoid"; import { getDb, schema } from "../db/index.js"; export const MAX_ASSET_FILE_SIZE = 10 * 1024 * 1024; // 10 MB export interface UploadedAsset { url: string; filename: string; type: string; size: number; provider?: string; } export interface ListedUploadedAsset { id: string; url: string; filename: string; size: number; createdAt: string; } async function requireSession(event: Parameters[0]) { const session = await getSession(event).catch(() => null); if (!session?.email) { setResponseStatus(event, 401); return null; } return session; } function isRasterAssetExtension(ext: string): boolean { return new Set([ ".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif", ".ico", ]).has(ext); } function ascii(data: Uint8Array, start: number, end: number): string { return Buffer.from(data.subarray(start, end)).toString("ascii"); } function hasExpectedImageSignature(ext: string, data: Uint8Array): boolean { if (ext === ".png") { return ( data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47 ); } if (ext === ".jpg" || ext === ".jpeg") { return data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff; } if (ext === ".gif") { const header = ascii(data, 0, 6); return header === "GIF87a" || header === "GIF89a"; } if (ext === ".webp") { return ascii(data, 0, 4) === "RIFF" && ascii(data, 8, 12) === "WEBP"; } if (ext === ".ico") { return ( data[0] === 0x00 && data[1] === 0x00 && data[2] === 0x01 && data[3] === 0x00 ); } if (ext === ".avif") { return ascii(data, 4, 12).includes("ftyp"); } return false; } export function canSaveAsUploadedAsset(args: { originalName: string; data: Uint8Array; }): boolean { return ( args.data.length <= MAX_ASSET_FILE_SIZE && isRasterAssetExtension(path.extname(args.originalName).toLowerCase()) ); } /** * Upload an image asset through the framework's `uploadFile()` provider chain. * * All uploads go to the configured remote provider — Builder.io by default, * or any provider registered via `registerFileUploadProvider()` (S3, R2, etc.). * There is intentionally NO local-disk fallback: writing into the source tree * (`public/uploads/`) pollutes git, doesn't persist on serverless deploys, * and isn't reachable across nodes. If no provider is configured, the request * fails with a clear 503 instructing the caller to configure one — connect * Builder.io or register a custom provider. */ export async function uploadImageAsset(args: { email: string; originalName: string; data: Uint8Array; type?: string; }): Promise { if (args.data.length > MAX_ASSET_FILE_SIZE) { throw new Error("File too large (max 10 MB)"); } const ext = path.extname(args.originalName).toLowerCase(); // SVG is excluded — it can embed