@uplift-io/uplift 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react.ts","../src/types.ts","../src/client.ts"],"sourcesContent":["import { useMemo, useState } from \"react\";\r\nimport { createUploadClient } from \"./client\";\r\nimport type { ClientInput, ClientOutput, UpliftApp, UploadError } from \"./types\";\r\n\r\ntype RouteState<TData> = {\r\n progress: number;\r\n isUploading: boolean;\r\n error: UploadError | null;\r\n data: TData | null;\r\n};\r\n\r\ntype ReactUploadMethod<TApp extends UpliftApp, TRouteName extends keyof TApp[\"routes\"] & string> =\r\n ((input: ClientInput<TApp[\"routes\"][TRouteName]>) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>) &\r\n RouteState<ClientOutput<TApp[\"routes\"][TRouteName]>>;\r\n\r\nexport type ReactUploadClient<TApp extends UpliftApp> = {\r\n [TRouteName in keyof TApp[\"routes\"] & string]: ReactUploadMethod<TApp, TRouteName>;\r\n};\r\n\r\nexport function useUploads<TApp extends UpliftApp>(baseUrl: string): ReactUploadClient<TApp> {\r\n const [states, setStates] = useState<Record<string, RouteState<unknown>>>({});\r\n\r\n return useMemo(() => {\r\n const client = createUploadClient<TApp>(baseUrl, {\r\n onProgress(route, progress) {\r\n setStates((current) => ({\r\n ...current,\r\n [route]: { ...(current[route] ?? emptyState()), progress }\r\n }));\r\n }\r\n });\r\n\r\n return new Proxy({}, {\r\n get(_target, property) {\r\n if (typeof property !== \"string\") return undefined;\r\n const state = states[property] ?? emptyState();\r\n const method = async (input: never) => {\r\n setStates((current) => ({\r\n ...current,\r\n [property]: { ...(current[property] ?? emptyState()), isUploading: true, error: null }\r\n }));\r\n try {\r\n const upload = (client as Record<string, (value: never) => Promise<unknown>>)[property];\r\n if (!upload) throw new Error(`Unknown upload route: ${property}`);\r\n const data = await upload(input);\r\n setStates((current) => ({\r\n ...current,\r\n [property]: { progress: 100, isUploading: false, error: null, data }\r\n }));\r\n return data;\r\n } catch (error) {\r\n setStates((current) => ({\r\n ...current,\r\n [property]: { ...(current[property] ?? emptyState()), isUploading: false, error: error as UploadError }\r\n }));\r\n throw error;\r\n }\r\n };\r\n\r\n return Object.assign(method, state);\r\n }\r\n }) as ReactUploadClient<TApp>;\r\n }, [baseUrl, states]);\r\n}\r\n\r\nfunction emptyState(): RouteState<unknown> {\r\n return {\r\n progress: 0,\r\n isUploading: false,\r\n error: null,\r\n data: null\r\n };\r\n}\r\n","export type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;\r\nexport type DurationValue = `${number}s` | `${number}m` | `${number}h`;\r\n\r\nexport type UploadInputFile = {\r\n name: string;\r\n type: string;\r\n size: number;\r\n extension?: string;\r\n file?: File;\r\n};\r\n\r\nexport type UploadedFile = {\r\n url: string;\r\n key: string;\r\n name: string;\r\n type: string;\r\n size: number;\r\n extension?: string | undefined;\r\n provider: string;\r\n};\r\n\r\nexport type UploadErrorCode =\r\n | \"FILE_TOO_LARGE\"\r\n | \"FILE_TOO_SMALL\"\r\n | \"INVALID_TYPE\"\r\n | \"AUTH_FAILED\"\r\n | \"VALIDATION_FAILED\"\r\n | \"UPLOAD_FAILED\"\r\n | \"UNKNOWN\";\r\n\r\nexport class UploadError extends Error {\r\n readonly code: UploadErrorCode;\r\n\r\n constructor(code: UploadErrorCode, message: string) {\r\n super(message);\r\n this.name = \"UploadError\";\r\n this.code = code;\r\n }\r\n\r\n toJSON() {\r\n return {\r\n message: this.message,\r\n code: this.code\r\n };\r\n }\r\n}\r\n\r\nexport type StandardSchema<T = unknown> = {\r\n parse(input: unknown): T;\r\n};\r\n\r\nexport type KeyContext<TAuth = unknown, TMeta = unknown> = {\r\n req: Request;\r\n file: UploadInputFile;\r\n user: TAuth;\r\n meta: TMeta;\r\n};\r\n\r\nexport type DoneContext<\r\n TAuth = unknown,\r\n TMeta = unknown,\r\n TMultiple extends boolean = false\r\n> = TMultiple extends true\r\n ? { req: Request; files: UploadedFile[]; user: TAuth; meta: TMeta[] }\r\n : { req: Request; file: UploadedFile; user: TAuth; meta: TMeta };\r\n\r\nexport type StoragePutInput = {\r\n key: string;\r\n file: UploadInputFile;\r\n body: File;\r\n};\r\n\r\nexport type StorageAdapter = {\r\n provider: string;\r\n put(input: StoragePutInput): Promise<UploadedFile>;\r\n};\r\n\r\nexport type Middleware<TUser = unknown> = (ctx: { req: Request }) => TUser | Promise<TUser>;\r\n\r\nexport type UploadKind =\r\n | \"any\"\r\n | \"image\"\r\n | \"pdf\"\r\n | \"video\"\r\n | \"audio\"\r\n | \"text\"\r\n | \"json\"\r\n | \"csv\"\r\n | \"custom\";\r\n\r\nexport type UploadRouteDefinition = {\r\n kind: UploadKind;\r\n maxBytes?: number;\r\n minBytes?: number;\r\n multiple: boolean;\r\n multipleLimit?: number;\r\n auth?: Middleware<unknown>;\r\n overrideAuth: boolean;\r\n key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;\r\n meta?: (ctx: { req: Request; file: UploadInputFile; user: unknown }) => unknown | Promise<unknown>;\r\n validate?: (ctx: {\r\n req: Request;\r\n file: UploadInputFile;\r\n user: unknown;\r\n meta: unknown;\r\n }) => true | string | Promise<true | string>;\r\n done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;\r\n extensions?: string[];\r\n mimeTypes?: string[];\r\n dimensionRule?: { minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number };\r\n requireSquare?: boolean;\r\n aspectRatio?: `${number}:${number}`;\r\n encoding?: \"utf-8\" | \"utf-16\" | \"ascii\";\r\n schema?: StandardSchema;\r\n headers?: string[];\r\n delimiter?: \",\" | \";\" | \"\\t\" | \"|\";\r\n pageRule?: { min?: number; max?: number };\r\n encrypted?: boolean;\r\n durationRule?: { min?: DurationValue; max?: DurationValue };\r\n};\r\n\r\nexport type UploadRoutes = Record<string, { _def: UploadRouteDefinition }>;\r\n\r\nexport type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {\r\n storage: StorageAdapter;\r\n routes: TRoutes;\r\n middleware?: Middleware<unknown> | undefined;\r\n onUploadComplete?: ((ctx: {\r\n route: keyof TRoutes & string;\r\n result: UploadedFile | UploadedFile[];\r\n user: unknown;\r\n }) => void | Promise<void>) | undefined;\r\n};\r\n\r\nexport type IsMultiple<TRoute> = TRoute extends { __multiple?: infer TMultiple }\r\n ? TMultiple extends true\r\n ? true\r\n : false\r\n : false;\r\n\r\nexport type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;\r\nexport type ClientOutput<TRoute> = IsMultiple<TRoute> extends true ? UploadedFile[] : UploadedFile;\r\n\r\nexport type UploadClient<TApp extends UpliftApp> = {\r\n [TRouteName in keyof TApp[\"routes\"] & string]: (\r\n input: ClientInput<TApp[\"routes\"][TRouteName]>\r\n ) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>;\r\n};\r\n","import { UploadError, type UpliftApp, type UploadClient } from \"./types\";\r\n\r\nexport type UploadProgressHandler = (progress: number) => void;\r\n\r\nexport function createUploadClient<TApp extends UpliftApp>(\r\n baseUrl: string,\r\n options: { fetch?: typeof fetch; onProgress?: (route: string, progress: number) => void } = {}\r\n): UploadClient<TApp> {\r\n const fetcher = options.fetch ?? fetch;\r\n\r\n return new Proxy({}, {\r\n get(_target, property) {\r\n if (typeof property !== \"string\") return undefined;\r\n return async (input: File | File[] | FileList) => {\r\n const files = input instanceof File ? [input] : Array.from(input);\r\n const form = new FormData();\r\n const field = files.length === 1 ? \"file\" : \"files\";\r\n for (const file of files) form.append(field, file);\r\n\r\n const url = routeUrl(baseUrl, property);\r\n if (!options.fetch && typeof XMLHttpRequest !== \"undefined\") {\r\n return uploadWithXhr(url, form, property, options.onProgress);\r\n }\r\n\r\n options.onProgress?.(property, 0);\r\n const response = await fetcher(url, { method: \"POST\", body: form });\r\n const body = await response.json() as { result?: unknown; error?: { code: string; message: string } };\r\n if (!response.ok) {\r\n throw new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\");\r\n }\r\n options.onProgress?.(property, 100);\r\n return body.result;\r\n };\r\n }\r\n }) as UploadClient<TApp>;\r\n}\r\n\r\nfunction routeUrl(baseUrl: string, route: string): string {\r\n const url = new URL(baseUrl, globalThis.location?.href ?? \"http://localhost\");\r\n url.searchParams.set(\"route\", route);\r\n const value = url.toString();\r\n return baseUrl.startsWith(\"/\") ? `${url.pathname}${url.search}` : value;\r\n}\r\n\r\nfunction uploadWithXhr(\r\n url: string,\r\n form: FormData,\r\n route: string,\r\n onProgress?: (route: string, progress: number) => void\r\n): Promise<unknown> {\r\n return new Promise((resolve, reject) => {\r\n const xhr = new XMLHttpRequest();\r\n xhr.open(\"POST\", url);\r\n xhr.upload.onprogress = (event) => {\r\n if (!event.lengthComputable) return;\r\n onProgress?.(route, Math.round((event.loaded / event.total) * 100));\r\n };\r\n xhr.onload = () => {\r\n const body = JSON.parse(xhr.responseText || \"{}\") as {\r\n result?: unknown;\r\n error?: { code: string; message: string };\r\n };\r\n if (xhr.status < 200 || xhr.status >= 300) {\r\n reject(new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\"));\r\n return;\r\n }\r\n onProgress?.(route, 100);\r\n resolve(body.result);\r\n };\r\n xhr.onerror = () => reject(new UploadError(\"UPLOAD_FAILED\", \"Upload request failed.\"));\r\n onProgress?.(route, 0);\r\n xhr.send(form);\r\n });\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/react.ts","../src/types.ts","../src/client.ts"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport { createUploadClient } from \"./client\";\nimport type { ClientInput, ClientOutput, UpliftApp, UploadError } from \"./types\";\n\ntype RouteState<TData> = {\n progress: number;\n isUploading: boolean;\n error: UploadError | null;\n data: TData | null;\n};\n\ntype ReactUploadMethod<TApp extends UpliftApp, TRouteName extends keyof TApp[\"routes\"] & string> =\n ((input: ClientInput<TApp[\"routes\"][TRouteName]>) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>) &\n RouteState<ClientOutput<TApp[\"routes\"][TRouteName]>>;\n\nexport type ReactUploadClient<TApp extends UpliftApp> = {\n [TRouteName in keyof TApp[\"routes\"] & string]: ReactUploadMethod<TApp, TRouteName>;\n};\n\nexport function useUploads<TApp extends UpliftApp>(baseUrl: string): ReactUploadClient<TApp> {\n const [states, setStates] = useState<Record<string, RouteState<unknown>>>({});\n\n return useMemo(() => {\n const client = createUploadClient<TApp>(baseUrl, {\n onProgress(route, progress) {\n setStates((current) => ({\n ...current,\n [route]: { ...(current[route] ?? emptyState()), progress }\n }));\n }\n });\n\n return new Proxy({}, {\n get(_target, property) {\n if (typeof property !== \"string\") return undefined;\n const state = states[property] ?? emptyState();\n const method = async (input: never) => {\n setStates((current) => ({\n ...current,\n [property]: { ...(current[property] ?? emptyState()), isUploading: true, error: null }\n }));\n try {\n const upload = (client as Record<string, (value: never) => Promise<unknown>>)[property];\n if (!upload) throw new Error(`Unknown upload route: ${property}`);\n const data = await upload(input);\n setStates((current) => ({\n ...current,\n [property]: { progress: 100, isUploading: false, error: null, data }\n }));\n return data;\n } catch (error) {\n setStates((current) => ({\n ...current,\n [property]: { ...(current[property] ?? emptyState()), isUploading: false, error: error as UploadError }\n }));\n throw error;\n }\n };\n\n return Object.assign(method, state);\n }\n }) as ReactUploadClient<TApp>;\n }, [baseUrl, states]);\n}\n\nfunction emptyState(): RouteState<unknown> {\n return {\n progress: 0,\n isUploading: false,\n error: null,\n data: null\n };\n}\n","export type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;\nexport type DurationValue = `${number}s` | `${number}m` | `${number}h`;\n\nexport type UploadInputFile = {\n name: string;\n type: string;\n size: number;\n extension?: string;\n file?: File;\n};\n\nexport type UploadedFile = {\n url: string;\n key: string;\n name: string;\n type: string;\n size: number;\n extension?: string | undefined;\n provider: string;\n outputs?: Record<string, UploadedFile> | undefined;\n};\n\nexport type ClientUploadedFile<TOutputNames extends string = never> = UploadedFile & (\n [TOutputNames] extends [never]\n ? object\n : {\n output<TName extends TOutputNames>(name: TName): UploadedFile;\n }\n);\n\nexport type UploadErrorCode =\n | \"FILE_TOO_LARGE\"\n | \"FILE_TOO_SMALL\"\n | \"INVALID_TYPE\"\n | \"AUTH_FAILED\"\n | \"VALIDATION_FAILED\"\n | \"UPLOAD_FAILED\"\n | \"UNKNOWN\";\n\nexport class UploadError extends Error {\n readonly code: UploadErrorCode;\n\n constructor(code: UploadErrorCode, message: string) {\n super(message);\n this.name = \"UploadError\";\n this.code = code;\n }\n\n toJSON() {\n return {\n message: this.message,\n code: this.code\n };\n }\n}\n\nexport type StandardSchema<T = unknown> = {\n parse(input: unknown): T;\n};\n\nexport type KeyContext<TAuth = unknown, TMeta = unknown> = {\n req: Request;\n file: UploadInputFile;\n user: TAuth;\n meta: TMeta;\n};\n\nexport type DoneContext<\n TAuth = unknown,\n TMeta = unknown,\n TMultiple extends boolean = false\n> = TMultiple extends true\n ? { req: Request; files: UploadedFile[]; user: TAuth; meta: TMeta[] }\n : { req: Request; file: UploadedFile; user: TAuth; meta: TMeta };\n\nexport type StoragePutInput = {\n key: string;\n file: UploadInputFile;\n body: File;\n};\n\nexport type StorageAdapter = {\n provider: string;\n put(input: StoragePutInput): Promise<UploadedFile>;\n delete?(key: string): Promise<void>;\n};\n\nexport type Middleware<TUser = unknown> = (ctx: { req: Request }) => TUser | Promise<TUser>;\n\nexport type UploadKind =\n | \"any\"\n | \"image\"\n | \"pdf\"\n | \"video\"\n | \"audio\"\n | \"text\"\n | \"json\"\n | \"csv\"\n | \"custom\";\n\nexport type PreparedUploadFile = {\n file: UploadInputFile;\n body: File;\n};\n\nexport type TransformContext = PreparedUploadFile;\n\nexport type UploadTransform<TKind extends UploadKind = UploadKind> = {\n readonly __kind?: TKind | undefined;\n transform(ctx: TransformContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;\n};\n\nexport type UploadTransformFunction<TKind extends UploadKind = UploadKind> = ((\n ctx: TransformContext\n) => File | PreparedUploadFile | Promise<File | PreparedUploadFile>) & {\n readonly __kind?: TKind | undefined;\n};\n\nexport type CompatibleTransform<TKind extends UploadKind> = TKind extends \"any\" | \"custom\"\n ? UploadTransform<UploadKind> | UploadTransformFunction<UploadKind>\n : UploadTransform<TKind> | UploadTransform<\"any\"> | UploadTransformFunction<TKind> | UploadTransformFunction<\"any\">;\n\nexport type OutputContext = PreparedUploadFile & {\n primary: UploadedFile;\n};\n\nexport type UploadOutput<TKind extends UploadKind = UploadKind, TName extends string = string> = {\n readonly __kind?: TKind | undefined;\n name: TName;\n produce(ctx: OutputContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;\n};\n\nexport type CompatibleOutput<TKind extends UploadKind, TName extends string = string> = TKind extends \"any\" | \"custom\"\n ? UploadOutput<UploadKind, TName>\n : UploadOutput<TKind, TName> | UploadOutput<\"any\", TName>;\n\nexport type UploadRouteDefinition = {\n kind: UploadKind;\n maxBytes?: number;\n minBytes?: number;\n multiple: boolean;\n multipleLimit?: number;\n auth?: Middleware<unknown>;\n overrideAuth: boolean;\n key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;\n meta?: (ctx: { req: Request; file: UploadInputFile; user: unknown }) => unknown | Promise<unknown>;\n validate?: (ctx: {\n req: Request;\n file: UploadInputFile;\n user: unknown;\n meta: unknown;\n }) => true | string | Promise<true | string>;\n done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;\n extensions?: string[];\n mimeTypes?: string[];\n dimensionRule?: { minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number };\n requireSquare?: boolean;\n aspectRatio?: `${number}:${number}`;\n encoding?: \"utf-8\" | \"utf-16\" | \"ascii\";\n schema?: StandardSchema;\n headers?: string[];\n delimiter?: \",\" | \";\" | \"\\t\" | \"|\";\n pageRule?: { min?: number; max?: number };\n encrypted?: boolean;\n durationRule?: { min?: DurationValue; max?: DurationValue };\n transforms?: Array<UploadTransform | UploadTransformFunction>;\n outputs?: Array<UploadOutput>;\n};\n\nexport type UploadRoutes = Record<string, { _def: UploadRouteDefinition }>;\n\nexport type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {\n storage: StorageAdapter;\n routes: TRoutes;\n middleware?: Middleware<unknown> | undefined;\n onUploadComplete?: ((ctx: {\n route: keyof TRoutes & string;\n result: UploadedFile | UploadedFile[];\n user: unknown;\n }) => void | Promise<void>) | undefined;\n};\n\nexport type IsMultiple<TRoute> = TRoute extends { __multiple?: infer TMultiple }\n ? TMultiple extends true\n ? true\n : false\n : false;\n\nexport type OutputNames<TRoute> = TRoute extends { __outputs?: infer TOutputNames }\n ? TOutputNames extends string\n ? TOutputNames\n : never\n : never;\n\nexport type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;\nexport type ClientOutput<TRoute> = IsMultiple<TRoute> extends true\n ? Array<ClientUploadedFile<OutputNames<TRoute>>>\n : ClientUploadedFile<OutputNames<TRoute>>;\n\nexport type UploadClient<TApp extends UpliftApp> = {\n [TRouteName in keyof TApp[\"routes\"] & string]: (\n input: ClientInput<TApp[\"routes\"][TRouteName]>\n ) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>;\n};\n","import { UploadError, type UpliftApp, type UploadClient } from \"./types\";\n\nexport type UploadProgressHandler = (progress: number) => void;\n\nexport function createUploadClient<TApp extends UpliftApp>(\n baseUrl: string,\n options: { fetch?: typeof fetch; onProgress?: (route: string, progress: number) => void } = {}\n): UploadClient<TApp> {\n const fetcher = options.fetch ?? fetch;\n\n return new Proxy({}, {\n get(_target, property) {\n if (typeof property !== \"string\") return undefined;\n return async (input: File | File[] | FileList) => {\n const files = input instanceof File ? [input] : Array.from(input);\n const form = new FormData();\n const field = files.length === 1 ? \"file\" : \"files\";\n for (const file of files) form.append(field, file);\n\n const url = routeUrl(baseUrl, property);\n if (!options.fetch && typeof XMLHttpRequest !== \"undefined\") {\n return uploadWithXhr(url, form, property, options.onProgress);\n }\n\n options.onProgress?.(property, 0);\n const response = await fetcher(url, { method: \"POST\", body: form });\n const body = await response.json() as { result?: unknown; error?: { code: string; message: string } };\n if (!response.ok) {\n throw new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\");\n }\n options.onProgress?.(property, 100);\n return attachOutputGetters(body.result);\n };\n }\n }) as UploadClient<TApp>;\n}\n\nfunction routeUrl(baseUrl: string, route: string): string {\n const url = new URL(baseUrl, globalThis.location?.href ?? \"http://localhost\");\n url.searchParams.set(\"route\", route);\n const value = url.toString();\n return baseUrl.startsWith(\"/\") ? `${url.pathname}${url.search}` : value;\n}\n\nfunction uploadWithXhr(\n url: string,\n form: FormData,\n route: string,\n onProgress?: (route: string, progress: number) => void\n): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(\"POST\", url);\n xhr.upload.onprogress = (event) => {\n if (!event.lengthComputable) return;\n onProgress?.(route, Math.round((event.loaded / event.total) * 100));\n };\n xhr.onload = () => {\n const body = JSON.parse(xhr.responseText || \"{}\") as {\n result?: unknown;\n error?: { code: string; message: string };\n };\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\"));\n return;\n }\n onProgress?.(route, 100);\n resolve(attachOutputGetters(body.result));\n };\n xhr.onerror = () => reject(new UploadError(\"UPLOAD_FAILED\", \"Upload request failed.\"));\n onProgress?.(route, 0);\n xhr.send(form);\n });\n}\n\nfunction attachOutputGetters(result: unknown): unknown {\n if (Array.isArray(result)) return result.map((item) => attachOutputGetters(item));\n if (!isUploadedFileLike(result)) return result;\n if (!Object.prototype.hasOwnProperty.call(result, \"output\")) {\n Object.defineProperty(result, \"output\", {\n enumerable: false,\n value(name: string) {\n const output = result.outputs?.[name];\n if (!output) throw new UploadError(\"VALIDATION_FAILED\", `Unknown output: ${name}`);\n return attachOutputGetters(output);\n }\n });\n }\n if (result.outputs) {\n for (const output of Object.values(result.outputs)) attachOutputGetters(output);\n }\n return result;\n}\n\nfunction isUploadedFileLike(value: unknown): value is {\n outputs?: Record<string, unknown>;\n output?: (name: string) => unknown;\n} {\n return typeof value === \"object\" && value !== null;\n}\n"],"mappings":"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","names":[]}
package/dist/server.cjs CHANGED
@@ -52,8 +52,33 @@ function extensionFor(name) {
52
52
  if (index < 0 || index === name.length - 1) return void 0;
53
53
  return name.slice(index + 1).toLowerCase();
54
54
  }
55
+ function extensionForType(type) {
56
+ const normalized = type.toLowerCase().split(";")[0]?.trim();
57
+ if (!normalized) return void 0;
58
+ const known = {
59
+ "image/avif": "avif",
60
+ "image/gif": "gif",
61
+ "image/jpeg": "jpg",
62
+ "image/png": "png",
63
+ "image/webp": "webp",
64
+ "video/mp4": "mp4",
65
+ "video/quicktime": "mov",
66
+ "video/webm": "webm",
67
+ "audio/aac": "aac",
68
+ "audio/flac": "flac",
69
+ "audio/mpeg": "mp3",
70
+ "audio/mp4": "m4a",
71
+ "audio/ogg": "ogg",
72
+ "audio/wav": "wav",
73
+ "application/json": "json",
74
+ "application/pdf": "pdf",
75
+ "text/csv": "csv",
76
+ "text/plain": "txt"
77
+ };
78
+ return known[normalized];
79
+ }
55
80
  function toInputFile(file) {
56
- const extension = extensionFor(file.name);
81
+ const extension = extensionForType(file.type) ?? extensionFor(file.name);
57
82
  const input = {
58
83
  name: file.name,
59
84
  type: file.type,
@@ -97,10 +122,14 @@ async function handleUploadRequest(app, req) {
97
122
  for (const item of files) {
98
123
  const meta = await deriveMeta(route._def, req, item.input, user);
99
124
  await validateFile(route._def, req, item, user, meta);
100
- const key = route._def.key ? await route._def.key({ req, file: item.input, user, meta }) : defaultKey(item.input);
125
+ const prepared = await applyTransforms(route._def, item);
126
+ const key = route._def.key ? await route._def.key({ req, file: prepared.file, user, meta }) : defaultKey(prepared.file);
101
127
  assertSafeStorageKey(key);
128
+ const primary = await app.storage.put({ key, file: prepared.file, body: prepared.body });
129
+ const outputs = await writeOutputs(app, route._def, key, prepared, primary);
130
+ const file = Object.keys(outputs).length > 0 ? { ...primary, outputs } : primary;
102
131
  stored.push({
103
- file: await app.storage.put({ key, file: item.input, body: item.body }),
132
+ file,
104
133
  meta
105
134
  });
106
135
  }
@@ -123,6 +152,49 @@ async function handleUploadRequest(app, req) {
123
152
  return json({ error: uploadError }, statusFor(uploadError));
124
153
  }
125
154
  }
155
+ async function applyTransforms(route, item) {
156
+ let prepared = { file: item.input, body: item.body };
157
+ for (const transform of route.transforms ?? []) {
158
+ const runner = typeof transform === "function" ? transform : transform.transform.bind(transform);
159
+ prepared = await normalizePrepared(await runner(prepared));
160
+ }
161
+ return prepared;
162
+ }
163
+ async function writeOutputs(app, route, primaryKey, prepared, primary) {
164
+ const written = {};
165
+ try {
166
+ for (const output of route.outputs ?? []) {
167
+ const outputFile = await normalizePrepared(await output.produce({ ...prepared, primary }));
168
+ const key = outputKey(primaryKey, output.name, outputFile.file);
169
+ assertSafeStorageKey(key);
170
+ written[output.name] = await app.storage.put({ key, file: outputFile.file, body: outputFile.body });
171
+ }
172
+ } catch (error) {
173
+ await rollbackWrittenFiles(app, [primary.key, ...Object.values(written).map((file) => file.key)]);
174
+ throw error;
175
+ }
176
+ return written;
177
+ }
178
+ async function rollbackWrittenFiles(app, keys) {
179
+ if (!app.storage.delete) return;
180
+ for (const key of [...keys].reverse()) {
181
+ try {
182
+ await app.storage.delete(key);
183
+ } catch {
184
+ }
185
+ }
186
+ }
187
+ async function normalizePrepared(value) {
188
+ if (value instanceof File) return { file: toInputFile(value), body: value };
189
+ const extension = extensionForType(value.file.type) ?? value.file.extension;
190
+ const file = { ...value.file, size: value.body.size };
191
+ if (extension) file.extension = extension;
192
+ return { file, body: value.body };
193
+ }
194
+ function outputKey(primaryKey, name, file) {
195
+ const extension = extensionForType(file.type) ?? file.extension;
196
+ return `${primaryKey}/outputs/${name}${extension ? `.${extension}` : ""}`;
197
+ }
126
198
  function assertSafeStorageKey(key) {
127
199
  if (key.length === 0) {
128
200
  throw new UploadError("VALIDATION_FAILED", "Storage key cannot be empty.");
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/types.ts","../src/utils.ts"],"sourcesContent":["import { UploadError, type UpliftApp, type UploadedFile, type UploadInputFile } from \"./types\";\r\nimport { defaultKey, readJsonFile, toInputFile } from \"./utils\";\r\n\r\ntype FileWithInput = {\r\n input: UploadInputFile;\r\n body: File;\r\n};\r\n\r\ntype StoredFile = {\r\n file: UploadedFile;\r\n meta: unknown;\r\n};\r\n\r\nexport async function handleUploadRequest(app: UpliftApp, req: Request): Promise<Response> {\r\n try {\r\n if (req.method !== \"POST\") {\r\n return json({ error: new UploadError(\"VALIDATION_FAILED\", \"Only POST upload requests are supported.\") }, 405);\r\n }\r\n\r\n const routeName = routeNameFromRequest(req);\r\n const route = app.routes[routeName];\r\n if (!route) throw new UploadError(\"VALIDATION_FAILED\", `Unknown upload route: ${routeName}`);\r\n\r\n const user = await resolveUser(app, route._def, req);\r\n const files = await filesFromRequest(req);\r\n if (files.length === 0) throw new UploadError(\"VALIDATION_FAILED\", \"No files were uploaded.\");\r\n if (!route._def.multiple && files.length !== 1) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"This route accepts exactly one file.\");\r\n }\r\n if (route._def.multipleLimit !== undefined && files.length > route._def.multipleLimit) {\r\n throw new UploadError(\"VALIDATION_FAILED\", `This route accepts at most ${route._def.multipleLimit} files.`);\r\n }\r\n\r\n const stored: StoredFile[] = [];\r\n for (const item of files) {\r\n const meta = await deriveMeta(route._def, req, item.input, user);\r\n await validateFile(route._def, req, item, user, meta);\r\n const key = route._def.key ? await route._def.key({ req, file: item.input, user, meta }) : defaultKey(item.input);\r\n assertSafeStorageKey(key);\r\n stored.push({\r\n file: await app.storage.put({ key, file: item.input, body: item.body }),\r\n meta\r\n });\r\n }\r\n\r\n const uploaded = stored.map((item) => item.file);\r\n const metas = stored.map((item) => item.meta);\r\n const firstUpload = uploaded[0];\r\n if (!firstUpload) throw new UploadError(\"UPLOAD_FAILED\", \"Upload did not produce a result.\");\r\n const result: UploadedFile | UploadedFile[] = route._def.multiple ? uploaded : firstUpload;\r\n\r\n if (route._def.done) {\r\n if (route._def.multiple) {\r\n await route._def.done({ req, files: uploaded, user, meta: metas });\r\n } else {\r\n await route._def.done({ req, file: firstUpload, user, meta: metas[0] });\r\n }\r\n }\r\n\r\n if (app.onUploadComplete) await app.onUploadComplete({ route: routeName, result, user });\r\n return json({ result }, 200);\r\n } catch (error) {\r\n const uploadError = normalizeError(error);\r\n return json({ error: uploadError }, statusFor(uploadError));\r\n }\r\n}\r\n\r\nfunction assertSafeStorageKey(key: string): void {\r\n if (key.length === 0) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Storage key cannot be empty.\");\r\n }\r\n if (\r\n key.includes(\"\\0\") ||\r\n key.includes(\"\\\\\") ||\r\n key.includes(\"://\") ||\r\n key.split(\"/\").includes(\"..\") ||\r\n /^([a-zA-Z]:)?\\//.test(key)\r\n ) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Storage key must be a relative object key.\");\r\n }\r\n}\r\n\r\nfunction routeNameFromRequest(req: Request): string {\r\n const url = new URL(req.url);\r\n const explicit = url.searchParams.get(\"route\");\r\n if (explicit) return explicit;\r\n const parts = url.pathname.split(\"/\").filter(Boolean);\r\n return parts[parts.length - 1] ?? \"\";\r\n}\r\n\r\nasync function resolveUser(app: UpliftApp, route: UpliftApp[\"routes\"][string][\"_def\"], req: Request): Promise<unknown> {\r\n if (route.overrideAuth) return undefined;\r\n const middleware = route.auth ?? app.middleware;\r\n if (!middleware) return undefined;\r\n try {\r\n return await middleware({ req });\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"Authentication failed.\";\r\n throw new UploadError(\"AUTH_FAILED\", message);\r\n }\r\n}\r\n\r\nasync function filesFromRequest(req: Request): Promise<FileWithInput[]> {\r\n const contentType = req.headers.get(\"content-type\") ?? \"\";\r\n if (!contentType.includes(\"multipart/form-data\")) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Upload requests must be multipart/form-data.\");\r\n }\r\n\r\n try {\r\n const form = await req.formData();\r\n const files: FileWithInput[] = [];\r\n for (const value of form.values()) {\r\n if (!(value instanceof File)) continue;\r\n files.push({ input: toInputFile(value), body: value });\r\n }\r\n return files;\r\n } catch (error) {\r\n if (error instanceof UploadError) throw error;\r\n const message = error instanceof Error ? error.message : \"Upload request body could not be parsed.\";\r\n throw new UploadError(\"VALIDATION_FAILED\", message);\r\n }\r\n}\r\n\r\nasync function deriveMeta(\r\n route: UpliftApp[\"routes\"][string][\"_def\"],\r\n req: Request,\r\n file: UploadInputFile,\r\n user: unknown\r\n): Promise<unknown> {\r\n if (!route.meta) return undefined;\r\n return route.meta({ req, file, user });\r\n}\r\n\r\nasync function validateFile(\r\n route: UpliftApp[\"routes\"][string][\"_def\"],\r\n req: Request,\r\n item: FileWithInput,\r\n user: unknown,\r\n meta: unknown\r\n) {\r\n if (route.maxBytes !== undefined && item.input.size > route.maxBytes) {\r\n throw new UploadError(\"FILE_TOO_LARGE\", \"File is larger than the route allows.\");\r\n }\r\n if (route.minBytes !== undefined && item.input.size < route.minBytes) {\r\n throw new UploadError(\"FILE_TOO_SMALL\", \"File is smaller than the route allows.\");\r\n }\r\n if (route.kind !== \"any\" && route.extensions && item.input.extension && !route.extensions.includes(item.input.extension)) {\r\n throw new UploadError(\"INVALID_TYPE\", \"File type is not allowed.\");\r\n }\r\n if (route.kind !== \"any\" && route.mimeTypes && route.mimeTypes.length > 0) {\r\n const matches = route.mimeTypes.some((mime) => mime.endsWith(\"/\") ? item.input.type.startsWith(mime) : item.input.type === mime);\r\n if (!matches) throw new UploadError(\"INVALID_TYPE\", \"File MIME type is not allowed.\");\r\n }\r\n if (route.schema) {\r\n try {\r\n route.schema.parse(await readJsonFile(item.body));\r\n } catch (error) {\r\n if (error instanceof UploadError) throw error;\r\n const message = error instanceof Error ? error.message : \"Schema validation failed.\";\r\n throw new UploadError(\"VALIDATION_FAILED\", message);\r\n }\r\n }\r\n await validateConfiguredInspection(route, item.body);\r\n if (route.validate) {\r\n const result = await route.validate({ req, file: item.input, user, meta });\r\n if (result !== true) throw new UploadError(\"VALIDATION_FAILED\", result);\r\n }\r\n}\r\n\r\nasync function validateConfiguredInspection(route: UpliftApp[\"routes\"][string][\"_def\"], file: File): Promise<void> {\r\n if (route.dimensionRule || route.requireSquare || route.aspectRatio) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Image dimension validation requires an image inspector and is not enabled in core.\");\r\n }\r\n if (route.encoding) {\r\n const text = await file.text();\r\n if (route.encoding === \"ascii\" && /[^\\x00-\\x7F]/.test(text)) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Text file is not ASCII encoded.\");\r\n }\r\n }\r\n if (route.headers) {\r\n const [headerLine = \"\"] = (await file.text()).split(/\\r?\\n/, 1);\r\n const delimiter = route.delimiter ?? \",\";\r\n const actualHeaders = headerLine.split(delimiter).map((header) => header.trim());\r\n if (route.headers.some((header, index) => actualHeaders[index] !== header)) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"CSV headers do not match the route definition.\");\r\n }\r\n }\r\n if (route.pageRule || route.encrypted !== undefined || route.durationRule) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Rich inspection is configured but no runtime inspector has been installed for this route.\");\r\n }\r\n}\r\n\r\nfunction normalizeError(error: unknown): UploadError {\r\n if (error instanceof UploadError) return error;\r\n if (error instanceof Error) return new UploadError(\"UNKNOWN\", error.message);\r\n return new UploadError(\"UNKNOWN\", \"Unknown upload failure.\");\r\n}\r\n\r\nfunction statusFor(error: UploadError): number {\r\n if (error.code === \"AUTH_FAILED\") return 401;\r\n if (error.code === \"UPLOAD_FAILED\" || error.code === \"UNKNOWN\") return 500;\r\n return 400;\r\n}\r\n\r\nfunction json(body: unknown, status: number): Response {\r\n return Response.json(body, { status });\r\n}\r\n","export type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;\r\nexport type DurationValue = `${number}s` | `${number}m` | `${number}h`;\r\n\r\nexport type UploadInputFile = {\r\n name: string;\r\n type: string;\r\n size: number;\r\n extension?: string;\r\n file?: File;\r\n};\r\n\r\nexport type UploadedFile = {\r\n url: string;\r\n key: string;\r\n name: string;\r\n type: string;\r\n size: number;\r\n extension?: string | undefined;\r\n provider: string;\r\n};\r\n\r\nexport type UploadErrorCode =\r\n | \"FILE_TOO_LARGE\"\r\n | \"FILE_TOO_SMALL\"\r\n | \"INVALID_TYPE\"\r\n | \"AUTH_FAILED\"\r\n | \"VALIDATION_FAILED\"\r\n | \"UPLOAD_FAILED\"\r\n | \"UNKNOWN\";\r\n\r\nexport class UploadError extends Error {\r\n readonly code: UploadErrorCode;\r\n\r\n constructor(code: UploadErrorCode, message: string) {\r\n super(message);\r\n this.name = \"UploadError\";\r\n this.code = code;\r\n }\r\n\r\n toJSON() {\r\n return {\r\n message: this.message,\r\n code: this.code\r\n };\r\n }\r\n}\r\n\r\nexport type StandardSchema<T = unknown> = {\r\n parse(input: unknown): T;\r\n};\r\n\r\nexport type KeyContext<TAuth = unknown, TMeta = unknown> = {\r\n req: Request;\r\n file: UploadInputFile;\r\n user: TAuth;\r\n meta: TMeta;\r\n};\r\n\r\nexport type DoneContext<\r\n TAuth = unknown,\r\n TMeta = unknown,\r\n TMultiple extends boolean = false\r\n> = TMultiple extends true\r\n ? { req: Request; files: UploadedFile[]; user: TAuth; meta: TMeta[] }\r\n : { req: Request; file: UploadedFile; user: TAuth; meta: TMeta };\r\n\r\nexport type StoragePutInput = {\r\n key: string;\r\n file: UploadInputFile;\r\n body: File;\r\n};\r\n\r\nexport type StorageAdapter = {\r\n provider: string;\r\n put(input: StoragePutInput): Promise<UploadedFile>;\r\n};\r\n\r\nexport type Middleware<TUser = unknown> = (ctx: { req: Request }) => TUser | Promise<TUser>;\r\n\r\nexport type UploadKind =\r\n | \"any\"\r\n | \"image\"\r\n | \"pdf\"\r\n | \"video\"\r\n | \"audio\"\r\n | \"text\"\r\n | \"json\"\r\n | \"csv\"\r\n | \"custom\";\r\n\r\nexport type UploadRouteDefinition = {\r\n kind: UploadKind;\r\n maxBytes?: number;\r\n minBytes?: number;\r\n multiple: boolean;\r\n multipleLimit?: number;\r\n auth?: Middleware<unknown>;\r\n overrideAuth: boolean;\r\n key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;\r\n meta?: (ctx: { req: Request; file: UploadInputFile; user: unknown }) => unknown | Promise<unknown>;\r\n validate?: (ctx: {\r\n req: Request;\r\n file: UploadInputFile;\r\n user: unknown;\r\n meta: unknown;\r\n }) => true | string | Promise<true | string>;\r\n done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;\r\n extensions?: string[];\r\n mimeTypes?: string[];\r\n dimensionRule?: { minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number };\r\n requireSquare?: boolean;\r\n aspectRatio?: `${number}:${number}`;\r\n encoding?: \"utf-8\" | \"utf-16\" | \"ascii\";\r\n schema?: StandardSchema;\r\n headers?: string[];\r\n delimiter?: \",\" | \";\" | \"\\t\" | \"|\";\r\n pageRule?: { min?: number; max?: number };\r\n encrypted?: boolean;\r\n durationRule?: { min?: DurationValue; max?: DurationValue };\r\n};\r\n\r\nexport type UploadRoutes = Record<string, { _def: UploadRouteDefinition }>;\r\n\r\nexport type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {\r\n storage: StorageAdapter;\r\n routes: TRoutes;\r\n middleware?: Middleware<unknown> | undefined;\r\n onUploadComplete?: ((ctx: {\r\n route: keyof TRoutes & string;\r\n result: UploadedFile | UploadedFile[];\r\n user: unknown;\r\n }) => void | Promise<void>) | undefined;\r\n};\r\n\r\nexport type IsMultiple<TRoute> = TRoute extends { __multiple?: infer TMultiple }\r\n ? TMultiple extends true\r\n ? true\r\n : false\r\n : false;\r\n\r\nexport type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;\r\nexport type ClientOutput<TRoute> = IsMultiple<TRoute> extends true ? UploadedFile[] : UploadedFile;\r\n\r\nexport type UploadClient<TApp extends UpliftApp> = {\r\n [TRouteName in keyof TApp[\"routes\"] & string]: (\r\n input: ClientInput<TApp[\"routes\"][TRouteName]>\r\n ) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>;\r\n};\r\n","import { UploadError, type SizeValue, type UploadInputFile } from \"./types\";\r\n\r\nconst sizeUnits = {\r\n b: 1,\r\n kb: 1024,\r\n mb: 1024 * 1024,\r\n gb: 1024 * 1024 * 1024\r\n} as const;\r\n\r\nexport function parseSize(value: SizeValue): number {\r\n const match = /^(\\d+(?:\\.\\d+)?)(b|kb|mb|gb)$/.exec(value);\r\n if (!match) throw new UploadError(\"VALIDATION_FAILED\", `Invalid size value: ${value}`);\r\n const amount = Number(match[1]);\r\n const unit = match[2] as keyof typeof sizeUnits;\r\n return Math.floor(amount * sizeUnits[unit]);\r\n}\r\n\r\nexport function extensionFor(name: string): string | undefined {\r\n const index = name.lastIndexOf(\".\");\r\n if (index < 0 || index === name.length - 1) return undefined;\r\n return name.slice(index + 1).toLowerCase();\r\n}\r\n\r\nexport function toInputFile(file: File): UploadInputFile {\r\n const extension = extensionFor(file.name);\r\n const input: UploadInputFile = {\r\n name: file.name,\r\n type: file.type,\r\n size: file.size,\r\n file\r\n };\r\n if (extension) input.extension = extension;\r\n return input;\r\n}\r\n\r\nexport function defaultKey(file: UploadInputFile): string {\r\n const random = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;\r\n return `${random}/${file.name}`;\r\n}\r\n\r\nexport async function readJsonFile(file: File): Promise<unknown> {\r\n try {\r\n return JSON.parse(await file.text());\r\n } catch {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Invalid JSON file.\");\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/server.ts","../src/types.ts","../src/utils.ts"],"sourcesContent":["import {\n UploadError,\n type PreparedUploadFile,\n type UpliftApp,\n type UploadedFile,\n type UploadInputFile\n} from \"./types\";\nimport { defaultKey, extensionForType, readJsonFile, toInputFile } from \"./utils\";\n\ntype FileWithInput = {\n input: UploadInputFile;\n body: File;\n};\n\ntype StoredFile = {\n file: UploadedFile;\n meta: unknown;\n};\n\nexport async function handleUploadRequest(app: UpliftApp, req: Request): Promise<Response> {\n try {\n if (req.method !== \"POST\") {\n return json({ error: new UploadError(\"VALIDATION_FAILED\", \"Only POST upload requests are supported.\") }, 405);\n }\n\n const routeName = routeNameFromRequest(req);\n const route = app.routes[routeName];\n if (!route) throw new UploadError(\"VALIDATION_FAILED\", `Unknown upload route: ${routeName}`);\n\n const user = await resolveUser(app, route._def, req);\n const files = await filesFromRequest(req);\n if (files.length === 0) throw new UploadError(\"VALIDATION_FAILED\", \"No files were uploaded.\");\n if (!route._def.multiple && files.length !== 1) {\n throw new UploadError(\"VALIDATION_FAILED\", \"This route accepts exactly one file.\");\n }\n if (route._def.multipleLimit !== undefined && files.length > route._def.multipleLimit) {\n throw new UploadError(\"VALIDATION_FAILED\", `This route accepts at most ${route._def.multipleLimit} files.`);\n }\n\n const stored: StoredFile[] = [];\n for (const item of files) {\n const meta = await deriveMeta(route._def, req, item.input, user);\n await validateFile(route._def, req, item, user, meta);\n const prepared = await applyTransforms(route._def, item);\n const key = route._def.key\n ? await route._def.key({ req, file: prepared.file, user, meta })\n : defaultKey(prepared.file);\n assertSafeStorageKey(key);\n const primary = await app.storage.put({ key, file: prepared.file, body: prepared.body });\n const outputs = await writeOutputs(app, route._def, key, prepared, primary);\n const file = Object.keys(outputs).length > 0 ? { ...primary, outputs } : primary;\n stored.push({\n file,\n meta\n });\n }\n\n const uploaded = stored.map((item) => item.file);\n const metas = stored.map((item) => item.meta);\n const firstUpload = uploaded[0];\n if (!firstUpload) throw new UploadError(\"UPLOAD_FAILED\", \"Upload did not produce a result.\");\n const result: UploadedFile | UploadedFile[] = route._def.multiple ? uploaded : firstUpload;\n\n if (route._def.done) {\n if (route._def.multiple) {\n await route._def.done({ req, files: uploaded, user, meta: metas });\n } else {\n await route._def.done({ req, file: firstUpload, user, meta: metas[0] });\n }\n }\n\n if (app.onUploadComplete) await app.onUploadComplete({ route: routeName, result, user });\n return json({ result }, 200);\n } catch (error) {\n const uploadError = normalizeError(error);\n return json({ error: uploadError }, statusFor(uploadError));\n }\n}\n\nasync function applyTransforms(\n route: UpliftApp[\"routes\"][string][\"_def\"],\n item: FileWithInput\n): Promise<PreparedUploadFile> {\n let prepared: PreparedUploadFile = { file: item.input, body: item.body };\n for (const transform of route.transforms ?? []) {\n const runner = typeof transform === \"function\" ? transform : transform.transform.bind(transform);\n prepared = await normalizePrepared(await runner(prepared));\n }\n return prepared;\n}\n\nasync function writeOutputs(\n app: UpliftApp,\n route: UpliftApp[\"routes\"][string][\"_def\"],\n primaryKey: string,\n prepared: PreparedUploadFile,\n primary: UploadedFile\n): Promise<Record<string, UploadedFile>> {\n const written: Record<string, UploadedFile> = {};\n try {\n for (const output of route.outputs ?? []) {\n const outputFile = await normalizePrepared(await output.produce({ ...prepared, primary }));\n const key = outputKey(primaryKey, output.name, outputFile.file);\n assertSafeStorageKey(key);\n written[output.name] = await app.storage.put({ key, file: outputFile.file, body: outputFile.body });\n }\n } catch (error) {\n await rollbackWrittenFiles(app, [primary.key, ...Object.values(written).map((file) => file.key)]);\n throw error;\n }\n return written;\n}\n\nasync function rollbackWrittenFiles(app: UpliftApp, keys: string[]): Promise<void> {\n if (!app.storage.delete) return;\n for (const key of [...keys].reverse()) {\n try {\n await app.storage.delete(key);\n } catch {\n // Preserve the original upload failure; cleanup is best-effort per adapter.\n }\n }\n}\n\nasync function normalizePrepared(value: File | PreparedUploadFile): Promise<PreparedUploadFile> {\n if (value instanceof File) return { file: toInputFile(value), body: value };\n const extension = extensionForType(value.file.type) ?? value.file.extension;\n const file = { ...value.file, size: value.body.size };\n if (extension) file.extension = extension;\n return { file, body: value.body };\n}\n\nfunction outputKey(primaryKey: string, name: string, file: UploadInputFile): string {\n const extension = extensionForType(file.type) ?? file.extension;\n return `${primaryKey}/outputs/${name}${extension ? `.${extension}` : \"\"}`;\n}\n\nfunction assertSafeStorageKey(key: string): void {\n if (key.length === 0) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Storage key cannot be empty.\");\n }\n if (\n key.includes(\"\\0\") ||\n key.includes(\"\\\\\") ||\n key.includes(\"://\") ||\n key.split(\"/\").includes(\"..\") ||\n /^([a-zA-Z]:)?\\//.test(key)\n ) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Storage key must be a relative object key.\");\n }\n}\n\nfunction routeNameFromRequest(req: Request): string {\n const url = new URL(req.url);\n const explicit = url.searchParams.get(\"route\");\n if (explicit) return explicit;\n const parts = url.pathname.split(\"/\").filter(Boolean);\n return parts[parts.length - 1] ?? \"\";\n}\n\nasync function resolveUser(app: UpliftApp, route: UpliftApp[\"routes\"][string][\"_def\"], req: Request): Promise<unknown> {\n if (route.overrideAuth) return undefined;\n const middleware = route.auth ?? app.middleware;\n if (!middleware) return undefined;\n try {\n return await middleware({ req });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Authentication failed.\";\n throw new UploadError(\"AUTH_FAILED\", message);\n }\n}\n\nasync function filesFromRequest(req: Request): Promise<FileWithInput[]> {\n const contentType = req.headers.get(\"content-type\") ?? \"\";\n if (!contentType.includes(\"multipart/form-data\")) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Upload requests must be multipart/form-data.\");\n }\n\n try {\n const form = await req.formData();\n const files: FileWithInput[] = [];\n for (const value of form.values()) {\n if (!(value instanceof File)) continue;\n files.push({ input: toInputFile(value), body: value });\n }\n return files;\n } catch (error) {\n if (error instanceof UploadError) throw error;\n const message = error instanceof Error ? error.message : \"Upload request body could not be parsed.\";\n throw new UploadError(\"VALIDATION_FAILED\", message);\n }\n}\n\nasync function deriveMeta(\n route: UpliftApp[\"routes\"][string][\"_def\"],\n req: Request,\n file: UploadInputFile,\n user: unknown\n): Promise<unknown> {\n if (!route.meta) return undefined;\n return route.meta({ req, file, user });\n}\n\nasync function validateFile(\n route: UpliftApp[\"routes\"][string][\"_def\"],\n req: Request,\n item: FileWithInput,\n user: unknown,\n meta: unknown\n) {\n if (route.maxBytes !== undefined && item.input.size > route.maxBytes) {\n throw new UploadError(\"FILE_TOO_LARGE\", \"File is larger than the route allows.\");\n }\n if (route.minBytes !== undefined && item.input.size < route.minBytes) {\n throw new UploadError(\"FILE_TOO_SMALL\", \"File is smaller than the route allows.\");\n }\n if (route.kind !== \"any\" && route.extensions && item.input.extension && !route.extensions.includes(item.input.extension)) {\n throw new UploadError(\"INVALID_TYPE\", \"File type is not allowed.\");\n }\n if (route.kind !== \"any\" && route.mimeTypes && route.mimeTypes.length > 0) {\n const matches = route.mimeTypes.some((mime) => mime.endsWith(\"/\") ? item.input.type.startsWith(mime) : item.input.type === mime);\n if (!matches) throw new UploadError(\"INVALID_TYPE\", \"File MIME type is not allowed.\");\n }\n if (route.schema) {\n try {\n route.schema.parse(await readJsonFile(item.body));\n } catch (error) {\n if (error instanceof UploadError) throw error;\n const message = error instanceof Error ? error.message : \"Schema validation failed.\";\n throw new UploadError(\"VALIDATION_FAILED\", message);\n }\n }\n await validateConfiguredInspection(route, item.body);\n if (route.validate) {\n const result = await route.validate({ req, file: item.input, user, meta });\n if (result !== true) throw new UploadError(\"VALIDATION_FAILED\", result);\n }\n}\n\nasync function validateConfiguredInspection(route: UpliftApp[\"routes\"][string][\"_def\"], file: File): Promise<void> {\n if (route.dimensionRule || route.requireSquare || route.aspectRatio) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Image dimension validation requires an image inspector and is not enabled in core.\");\n }\n if (route.encoding) {\n const text = await file.text();\n if (route.encoding === \"ascii\" && /[^\\x00-\\x7F]/.test(text)) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Text file is not ASCII encoded.\");\n }\n }\n if (route.headers) {\n const [headerLine = \"\"] = (await file.text()).split(/\\r?\\n/, 1);\n const delimiter = route.delimiter ?? \",\";\n const actualHeaders = headerLine.split(delimiter).map((header) => header.trim());\n if (route.headers.some((header, index) => actualHeaders[index] !== header)) {\n throw new UploadError(\"VALIDATION_FAILED\", \"CSV headers do not match the route definition.\");\n }\n }\n if (route.pageRule || route.encrypted !== undefined || route.durationRule) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Rich inspection is configured but no runtime inspector has been installed for this route.\");\n }\n}\n\nfunction normalizeError(error: unknown): UploadError {\n if (error instanceof UploadError) return error;\n if (error instanceof Error) return new UploadError(\"UNKNOWN\", error.message);\n return new UploadError(\"UNKNOWN\", \"Unknown upload failure.\");\n}\n\nfunction statusFor(error: UploadError): number {\n if (error.code === \"AUTH_FAILED\") return 401;\n if (error.code === \"UPLOAD_FAILED\" || error.code === \"UNKNOWN\") return 500;\n return 400;\n}\n\nfunction json(body: unknown, status: number): Response {\n return Response.json(body, { status });\n}\n","export type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;\nexport type DurationValue = `${number}s` | `${number}m` | `${number}h`;\n\nexport type UploadInputFile = {\n name: string;\n type: string;\n size: number;\n extension?: string;\n file?: File;\n};\n\nexport type UploadedFile = {\n url: string;\n key: string;\n name: string;\n type: string;\n size: number;\n extension?: string | undefined;\n provider: string;\n outputs?: Record<string, UploadedFile> | undefined;\n};\n\nexport type ClientUploadedFile<TOutputNames extends string = never> = UploadedFile & (\n [TOutputNames] extends [never]\n ? object\n : {\n output<TName extends TOutputNames>(name: TName): UploadedFile;\n }\n);\n\nexport type UploadErrorCode =\n | \"FILE_TOO_LARGE\"\n | \"FILE_TOO_SMALL\"\n | \"INVALID_TYPE\"\n | \"AUTH_FAILED\"\n | \"VALIDATION_FAILED\"\n | \"UPLOAD_FAILED\"\n | \"UNKNOWN\";\n\nexport class UploadError extends Error {\n readonly code: UploadErrorCode;\n\n constructor(code: UploadErrorCode, message: string) {\n super(message);\n this.name = \"UploadError\";\n this.code = code;\n }\n\n toJSON() {\n return {\n message: this.message,\n code: this.code\n };\n }\n}\n\nexport type StandardSchema<T = unknown> = {\n parse(input: unknown): T;\n};\n\nexport type KeyContext<TAuth = unknown, TMeta = unknown> = {\n req: Request;\n file: UploadInputFile;\n user: TAuth;\n meta: TMeta;\n};\n\nexport type DoneContext<\n TAuth = unknown,\n TMeta = unknown,\n TMultiple extends boolean = false\n> = TMultiple extends true\n ? { req: Request; files: UploadedFile[]; user: TAuth; meta: TMeta[] }\n : { req: Request; file: UploadedFile; user: TAuth; meta: TMeta };\n\nexport type StoragePutInput = {\n key: string;\n file: UploadInputFile;\n body: File;\n};\n\nexport type StorageAdapter = {\n provider: string;\n put(input: StoragePutInput): Promise<UploadedFile>;\n delete?(key: string): Promise<void>;\n};\n\nexport type Middleware<TUser = unknown> = (ctx: { req: Request }) => TUser | Promise<TUser>;\n\nexport type UploadKind =\n | \"any\"\n | \"image\"\n | \"pdf\"\n | \"video\"\n | \"audio\"\n | \"text\"\n | \"json\"\n | \"csv\"\n | \"custom\";\n\nexport type PreparedUploadFile = {\n file: UploadInputFile;\n body: File;\n};\n\nexport type TransformContext = PreparedUploadFile;\n\nexport type UploadTransform<TKind extends UploadKind = UploadKind> = {\n readonly __kind?: TKind | undefined;\n transform(ctx: TransformContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;\n};\n\nexport type UploadTransformFunction<TKind extends UploadKind = UploadKind> = ((\n ctx: TransformContext\n) => File | PreparedUploadFile | Promise<File | PreparedUploadFile>) & {\n readonly __kind?: TKind | undefined;\n};\n\nexport type CompatibleTransform<TKind extends UploadKind> = TKind extends \"any\" | \"custom\"\n ? UploadTransform<UploadKind> | UploadTransformFunction<UploadKind>\n : UploadTransform<TKind> | UploadTransform<\"any\"> | UploadTransformFunction<TKind> | UploadTransformFunction<\"any\">;\n\nexport type OutputContext = PreparedUploadFile & {\n primary: UploadedFile;\n};\n\nexport type UploadOutput<TKind extends UploadKind = UploadKind, TName extends string = string> = {\n readonly __kind?: TKind | undefined;\n name: TName;\n produce(ctx: OutputContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;\n};\n\nexport type CompatibleOutput<TKind extends UploadKind, TName extends string = string> = TKind extends \"any\" | \"custom\"\n ? UploadOutput<UploadKind, TName>\n : UploadOutput<TKind, TName> | UploadOutput<\"any\", TName>;\n\nexport type UploadRouteDefinition = {\n kind: UploadKind;\n maxBytes?: number;\n minBytes?: number;\n multiple: boolean;\n multipleLimit?: number;\n auth?: Middleware<unknown>;\n overrideAuth: boolean;\n key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;\n meta?: (ctx: { req: Request; file: UploadInputFile; user: unknown }) => unknown | Promise<unknown>;\n validate?: (ctx: {\n req: Request;\n file: UploadInputFile;\n user: unknown;\n meta: unknown;\n }) => true | string | Promise<true | string>;\n done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;\n extensions?: string[];\n mimeTypes?: string[];\n dimensionRule?: { minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number };\n requireSquare?: boolean;\n aspectRatio?: `${number}:${number}`;\n encoding?: \"utf-8\" | \"utf-16\" | \"ascii\";\n schema?: StandardSchema;\n headers?: string[];\n delimiter?: \",\" | \";\" | \"\\t\" | \"|\";\n pageRule?: { min?: number; max?: number };\n encrypted?: boolean;\n durationRule?: { min?: DurationValue; max?: DurationValue };\n transforms?: Array<UploadTransform | UploadTransformFunction>;\n outputs?: Array<UploadOutput>;\n};\n\nexport type UploadRoutes = Record<string, { _def: UploadRouteDefinition }>;\n\nexport type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {\n storage: StorageAdapter;\n routes: TRoutes;\n middleware?: Middleware<unknown> | undefined;\n onUploadComplete?: ((ctx: {\n route: keyof TRoutes & string;\n result: UploadedFile | UploadedFile[];\n user: unknown;\n }) => void | Promise<void>) | undefined;\n};\n\nexport type IsMultiple<TRoute> = TRoute extends { __multiple?: infer TMultiple }\n ? TMultiple extends true\n ? true\n : false\n : false;\n\nexport type OutputNames<TRoute> = TRoute extends { __outputs?: infer TOutputNames }\n ? TOutputNames extends string\n ? TOutputNames\n : never\n : never;\n\nexport type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;\nexport type ClientOutput<TRoute> = IsMultiple<TRoute> extends true\n ? Array<ClientUploadedFile<OutputNames<TRoute>>>\n : ClientUploadedFile<OutputNames<TRoute>>;\n\nexport type UploadClient<TApp extends UpliftApp> = {\n [TRouteName in keyof TApp[\"routes\"] & string]: (\n input: ClientInput<TApp[\"routes\"][TRouteName]>\n ) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>;\n};\n","import { UploadError, type SizeValue, type UploadInputFile } from \"./types\";\n\nconst sizeUnits = {\n b: 1,\n kb: 1024,\n mb: 1024 * 1024,\n gb: 1024 * 1024 * 1024\n} as const;\n\nexport function parseSize(value: SizeValue): number {\n const match = /^(\\d+(?:\\.\\d+)?)(b|kb|mb|gb)$/.exec(value);\n if (!match) throw new UploadError(\"VALIDATION_FAILED\", `Invalid size value: ${value}`);\n const amount = Number(match[1]);\n const unit = match[2] as keyof typeof sizeUnits;\n return Math.floor(amount * sizeUnits[unit]);\n}\n\nexport function extensionFor(name: string): string | undefined {\n const index = name.lastIndexOf(\".\");\n if (index < 0 || index === name.length - 1) return undefined;\n return name.slice(index + 1).toLowerCase();\n}\n\nexport function extensionForType(type: string): string | undefined {\n const normalized = type.toLowerCase().split(\";\")[0]?.trim();\n if (!normalized) return undefined;\n const known: Record<string, string> = {\n \"image/avif\": \"avif\",\n \"image/gif\": \"gif\",\n \"image/jpeg\": \"jpg\",\n \"image/png\": \"png\",\n \"image/webp\": \"webp\",\n \"video/mp4\": \"mp4\",\n \"video/quicktime\": \"mov\",\n \"video/webm\": \"webm\",\n \"audio/aac\": \"aac\",\n \"audio/flac\": \"flac\",\n \"audio/mpeg\": \"mp3\",\n \"audio/mp4\": \"m4a\",\n \"audio/ogg\": \"ogg\",\n \"audio/wav\": \"wav\",\n \"application/json\": \"json\",\n \"application/pdf\": \"pdf\",\n \"text/csv\": \"csv\",\n \"text/plain\": \"txt\"\n };\n return known[normalized];\n}\n\nexport function toInputFile(file: File): UploadInputFile {\n const extension = extensionForType(file.type) ?? extensionFor(file.name);\n const input: UploadInputFile = {\n name: file.name,\n type: file.type,\n size: file.size,\n file\n };\n if (extension) input.extension = extension;\n return input;\n}\n\nexport function defaultKey(file: UploadInputFile): string {\n const random = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;\n return `${random}/${file.name}`;\n}\n\nexport async function readJsonFile(file: File): Promise<unknown> {\n try {\n return JSON.parse(await file.text());\n } catch {\n throw new UploadError(\"VALIDATION_FAILED\", \"Invalid JSON file.\");\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACuCO,IAAM,cAAN,cAA0B,MAAM;AAAA,EAC5B;AAAA,EAET,YAAY,MAAuB,SAAiB;AAClD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,SAAS;AACP,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,MAAM,KAAK;AAAA,IACb;AAAA,EACF;AACF;;;ACpDA,IAAM,YAAY;AAAA,EAChB,GAAG;AAAA,EACH,IAAI;AAAA,EACJ,IAAI,OAAO;AAAA,EACX,IAAI,OAAO,OAAO;AACpB;AAUO,SAAS,aAAa,MAAkC;AAC7D,QAAM,QAAQ,KAAK,YAAY,GAAG;AAClC,MAAI,QAAQ,KAAK,UAAU,KAAK,SAAS,EAAG,QAAO;AACnD,SAAO,KAAK,MAAM,QAAQ,CAAC,EAAE,YAAY;AAC3C;AAEO,SAAS,iBAAiB,MAAkC;AACjE,QAAM,aAAa,KAAK,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AAC1D,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,QAAgC;AAAA,IACpC,cAAc;AAAA,IACd,aAAa;AAAA,IACb,cAAc;AAAA,IACd,aAAa;AAAA,IACb,cAAc;AAAA,IACd,aAAa;AAAA,IACb,mBAAmB;AAAA,IACnB,cAAc;AAAA,IACd,aAAa;AAAA,IACb,cAAc;AAAA,IACd,cAAc;AAAA,IACd,aAAa;AAAA,IACb,aAAa;AAAA,IACb,aAAa;AAAA,IACb,oBAAoB;AAAA,IACpB,mBAAmB;AAAA,IACnB,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB;AACA,SAAO,MAAM,UAAU;AACzB;AAEO,SAAS,YAAY,MAA6B;AACvD,QAAM,YAAY,iBAAiB,KAAK,IAAI,KAAK,aAAa,KAAK,IAAI;AACvE,QAAM,QAAyB;AAAA,IAC7B,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX;AAAA,EACF;AACA,MAAI,UAAW,OAAM,YAAY;AACjC,SAAO;AACT;AAEO,SAAS,WAAW,MAA+B;AACxD,QAAM,SAAS,WAAW,QAAQ,aAAa,KAAK,GAAG,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC;AAClF,SAAO,GAAG,MAAM,IAAI,KAAK,IAAI;AAC/B;AAEA,eAAsB,aAAa,MAA8B;AAC/D,MAAI;AACF,WAAO,KAAK,MAAM,MAAM,KAAK,KAAK,CAAC;AAAA,EACrC,QAAQ;AACN,UAAM,IAAI,YAAY,qBAAqB,oBAAoB;AAAA,EACjE;AACF;;;AFrDA,eAAsB,oBAAoB,KAAgB,KAAiC;AACzF,MAAI;AACF,QAAI,IAAI,WAAW,QAAQ;AACzB,aAAO,KAAK,EAAE,OAAO,IAAI,YAAY,qBAAqB,0CAA0C,EAAE,GAAG,GAAG;AAAA,IAC9G;AAEA,UAAM,YAAY,qBAAqB,GAAG;AAC1C,UAAM,QAAQ,IAAI,OAAO,SAAS;AAClC,QAAI,CAAC,MAAO,OAAM,IAAI,YAAY,qBAAqB,yBAAyB,SAAS,EAAE;AAE3F,UAAM,OAAO,MAAM,YAAY,KAAK,MAAM,MAAM,GAAG;AACnD,UAAM,QAAQ,MAAM,iBAAiB,GAAG;AACxC,QAAI,MAAM,WAAW,EAAG,OAAM,IAAI,YAAY,qBAAqB,yBAAyB;AAC5F,QAAI,CAAC,MAAM,KAAK,YAAY,MAAM,WAAW,GAAG;AAC9C,YAAM,IAAI,YAAY,qBAAqB,sCAAsC;AAAA,IACnF;AACA,QAAI,MAAM,KAAK,kBAAkB,UAAa,MAAM,SAAS,MAAM,KAAK,eAAe;AACrF,YAAM,IAAI,YAAY,qBAAqB,8BAA8B,MAAM,KAAK,aAAa,SAAS;AAAA,IAC5G;AAEA,UAAM,SAAuB,CAAC;AAC9B,eAAW,QAAQ,OAAO;AACxB,YAAM,OAAO,MAAM,WAAW,MAAM,MAAM,KAAK,KAAK,OAAO,IAAI;AAC/D,YAAM,aAAa,MAAM,MAAM,KAAK,MAAM,MAAM,IAAI;AACpD,YAAM,WAAW,MAAM,gBAAgB,MAAM,MAAM,IAAI;AACvD,YAAM,MAAM,MAAM,KAAK,MACnB,MAAM,MAAM,KAAK,IAAI,EAAE,KAAK,MAAM,SAAS,MAAM,MAAM,KAAK,CAAC,IAC7D,WAAW,SAAS,IAAI;AAC5B,2BAAqB,GAAG;AACxB,YAAM,UAAU,MAAM,IAAI,QAAQ,IAAI,EAAE,KAAK,MAAM,SAAS,MAAM,MAAM,SAAS,KAAK,CAAC;AACvF,YAAM,UAAU,MAAM,aAAa,KAAK,MAAM,MAAM,KAAK,UAAU,OAAO;AAC1E,YAAM,OAAO,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,EAAE,GAAG,SAAS,QAAQ,IAAI;AACzE,aAAO,KAAK;AAAA,QACV;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAEA,UAAM,WAAW,OAAO,IAAI,CAAC,SAAS,KAAK,IAAI;AAC/C,UAAM,QAAQ,OAAO,IAAI,CAAC,SAAS,KAAK,IAAI;AAC5C,UAAM,cAAc,SAAS,CAAC;AAC9B,QAAI,CAAC,YAAa,OAAM,IAAI,YAAY,iBAAiB,kCAAkC;AAC3F,UAAM,SAAwC,MAAM,KAAK,WAAW,WAAW;AAE/E,QAAI,MAAM,KAAK,MAAM;AACnB,UAAI,MAAM,KAAK,UAAU;AACvB,cAAM,MAAM,KAAK,KAAK,EAAE,KAAK,OAAO,UAAU,MAAM,MAAM,MAAM,CAAC;AAAA,MACnE,OAAO;AACL,cAAM,MAAM,KAAK,KAAK,EAAE,KAAK,MAAM,aAAa,MAAM,MAAM,MAAM,CAAC,EAAE,CAAC;AAAA,MACxE;AAAA,IACF;AAEA,QAAI,IAAI,iBAAkB,OAAM,IAAI,iBAAiB,EAAE,OAAO,WAAW,QAAQ,KAAK,CAAC;AACvF,WAAO,KAAK,EAAE,OAAO,GAAG,GAAG;AAAA,EAC7B,SAAS,OAAO;AACd,UAAM,cAAc,eAAe,KAAK;AACxC,WAAO,KAAK,EAAE,OAAO,YAAY,GAAG,UAAU,WAAW,CAAC;AAAA,EAC5D;AACF;AAEA,eAAe,gBACb,OACA,MAC6B;AAC7B,MAAI,WAA+B,EAAE,MAAM,KAAK,OAAO,MAAM,KAAK,KAAK;AACvE,aAAW,aAAa,MAAM,cAAc,CAAC,GAAG;AAC9C,UAAM,SAAS,OAAO,cAAc,aAAa,YAAY,UAAU,UAAU,KAAK,SAAS;AAC/F,eAAW,MAAM,kBAAkB,MAAM,OAAO,QAAQ,CAAC;AAAA,EAC3D;AACA,SAAO;AACT;AAEA,eAAe,aACb,KACA,OACA,YACA,UACA,SACuC;AACvC,QAAM,UAAwC,CAAC;AAC/C,MAAI;AACF,eAAW,UAAU,MAAM,WAAW,CAAC,GAAG;AACxC,YAAM,aAAa,MAAM,kBAAkB,MAAM,OAAO,QAAQ,EAAE,GAAG,UAAU,QAAQ,CAAC,CAAC;AACzF,YAAM,MAAM,UAAU,YAAY,OAAO,MAAM,WAAW,IAAI;AAC9D,2BAAqB,GAAG;AACxB,cAAQ,OAAO,IAAI,IAAI,MAAM,IAAI,QAAQ,IAAI,EAAE,KAAK,MAAM,WAAW,MAAM,MAAM,WAAW,KAAK,CAAC;AAAA,IACpG;AAAA,EACF,SAAS,OAAO;AACd,UAAM,qBAAqB,KAAK,CAAC,QAAQ,KAAK,GAAG,OAAO,OAAO,OAAO,EAAE,IAAI,CAAC,SAAS,KAAK,GAAG,CAAC,CAAC;AAChG,UAAM;AAAA,EACR;AACA,SAAO;AACT;AAEA,eAAe,qBAAqB,KAAgB,MAA+B;AACjF,MAAI,CAAC,IAAI,QAAQ,OAAQ;AACzB,aAAW,OAAO,CAAC,GAAG,IAAI,EAAE,QAAQ,GAAG;AACrC,QAAI;AACF,YAAM,IAAI,QAAQ,OAAO,GAAG;AAAA,IAC9B,QAAQ;AAAA,IAER;AAAA,EACF;AACF;AAEA,eAAe,kBAAkB,OAA+D;AAC9F,MAAI,iBAAiB,KAAM,QAAO,EAAE,MAAM,YAAY,KAAK,GAAG,MAAM,MAAM;AAC1E,QAAM,YAAY,iBAAiB,MAAM,KAAK,IAAI,KAAK,MAAM,KAAK;AAClE,QAAM,OAAO,EAAE,GAAG,MAAM,MAAM,MAAM,MAAM,KAAK,KAAK;AACpD,MAAI,UAAW,MAAK,YAAY;AAChC,SAAO,EAAE,MAAM,MAAM,MAAM,KAAK;AAClC;AAEA,SAAS,UAAU,YAAoB,MAAc,MAA+B;AAClF,QAAM,YAAY,iBAAiB,KAAK,IAAI,KAAK,KAAK;AACtD,SAAO,GAAG,UAAU,YAAY,IAAI,GAAG,YAAY,IAAI,SAAS,KAAK,EAAE;AACzE;AAEA,SAAS,qBAAqB,KAAmB;AAC/C,MAAI,IAAI,WAAW,GAAG;AACpB,UAAM,IAAI,YAAY,qBAAqB,8BAA8B;AAAA,EAC3E;AACA,MACE,IAAI,SAAS,IAAI,KACjB,IAAI,SAAS,IAAI,KACjB,IAAI,SAAS,KAAK,KAClB,IAAI,MAAM,GAAG,EAAE,SAAS,IAAI,KAC5B,kBAAkB,KAAK,GAAG,GAC1B;AACA,UAAM,IAAI,YAAY,qBAAqB,4CAA4C;AAAA,EACzF;AACF;AAEA,SAAS,qBAAqB,KAAsB;AAClD,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,WAAW,IAAI,aAAa,IAAI,OAAO;AAC7C,MAAI,SAAU,QAAO;AACrB,QAAM,QAAQ,IAAI,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AACpD,SAAO,MAAM,MAAM,SAAS,CAAC,KAAK;AACpC;AAEA,eAAe,YAAY,KAAgB,OAA4C,KAAgC;AACrH,MAAI,MAAM,aAAc,QAAO;AAC/B,QAAM,aAAa,MAAM,QAAQ,IAAI;AACrC,MAAI,CAAC,WAAY,QAAO;AACxB,MAAI;AACF,WAAO,MAAM,WAAW,EAAE,IAAI,CAAC;AAAA,EACjC,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,UAAM,IAAI,YAAY,eAAe,OAAO;AAAA,EAC9C;AACF;AAEA,eAAe,iBAAiB,KAAwC;AACtE,QAAM,cAAc,IAAI,QAAQ,IAAI,cAAc,KAAK;AACvD,MAAI,CAAC,YAAY,SAAS,qBAAqB,GAAG;AAChD,UAAM,IAAI,YAAY,qBAAqB,8CAA8C;AAAA,EAC3F;AAEA,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,SAAS;AAChC,UAAM,QAAyB,CAAC;AAChC,eAAW,SAAS,KAAK,OAAO,GAAG;AACjC,UAAI,EAAE,iBAAiB,MAAO;AAC9B,YAAM,KAAK,EAAE,OAAO,YAAY,KAAK,GAAG,MAAM,MAAM,CAAC;AAAA,IACvD;AACA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,QAAI,iBAAiB,YAAa,OAAM;AACxC,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,UAAM,IAAI,YAAY,qBAAqB,OAAO;AAAA,EACpD;AACF;AAEA,eAAe,WACb,OACA,KACA,MACA,MACkB;AAClB,MAAI,CAAC,MAAM,KAAM,QAAO;AACxB,SAAO,MAAM,KAAK,EAAE,KAAK,MAAM,KAAK,CAAC;AACvC;AAEA,eAAe,aACb,OACA,KACA,MACA,MACA,MACA;AACA,MAAI,MAAM,aAAa,UAAa,KAAK,MAAM,OAAO,MAAM,UAAU;AACpE,UAAM,IAAI,YAAY,kBAAkB,uCAAuC;AAAA,EACjF;AACA,MAAI,MAAM,aAAa,UAAa,KAAK,MAAM,OAAO,MAAM,UAAU;AACpE,UAAM,IAAI,YAAY,kBAAkB,wCAAwC;AAAA,EAClF;AACA,MAAI,MAAM,SAAS,SAAS,MAAM,cAAc,KAAK,MAAM,aAAa,CAAC,MAAM,WAAW,SAAS,KAAK,MAAM,SAAS,GAAG;AACxH,UAAM,IAAI,YAAY,gBAAgB,2BAA2B;AAAA,EACnE;AACA,MAAI,MAAM,SAAS,SAAS,MAAM,aAAa,MAAM,UAAU,SAAS,GAAG;AACzE,UAAM,UAAU,MAAM,UAAU,KAAK,CAAC,SAAS,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,KAAK,WAAW,IAAI,IAAI,KAAK,MAAM,SAAS,IAAI;AAC/H,QAAI,CAAC,QAAS,OAAM,IAAI,YAAY,gBAAgB,gCAAgC;AAAA,EACtF;AACA,MAAI,MAAM,QAAQ;AAChB,QAAI;AACF,YAAM,OAAO,MAAM,MAAM,aAAa,KAAK,IAAI,CAAC;AAAA,IAClD,SAAS,OAAO;AACd,UAAI,iBAAiB,YAAa,OAAM;AACxC,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,YAAM,IAAI,YAAY,qBAAqB,OAAO;AAAA,IACpD;AAAA,EACF;AACA,QAAM,6BAA6B,OAAO,KAAK,IAAI;AACnD,MAAI,MAAM,UAAU;AAClB,UAAM,SAAS,MAAM,MAAM,SAAS,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,KAAK,CAAC;AACzE,QAAI,WAAW,KAAM,OAAM,IAAI,YAAY,qBAAqB,MAAM;AAAA,EACxE;AACF;AAEA,eAAe,6BAA6B,OAA4C,MAA2B;AACjH,MAAI,MAAM,iBAAiB,MAAM,iBAAiB,MAAM,aAAa;AACnE,UAAM,IAAI,YAAY,qBAAqB,oFAAoF;AAAA,EACjI;AACA,MAAI,MAAM,UAAU;AAClB,UAAM,OAAO,MAAM,KAAK,KAAK;AAC7B,QAAI,MAAM,aAAa,WAAW,eAAe,KAAK,IAAI,GAAG;AAC3D,YAAM,IAAI,YAAY,qBAAqB,iCAAiC;AAAA,IAC9E;AAAA,EACF;AACA,MAAI,MAAM,SAAS;AACjB,UAAM,CAAC,aAAa,EAAE,KAAK,MAAM,KAAK,KAAK,GAAG,MAAM,SAAS,CAAC;AAC9D,UAAM,YAAY,MAAM,aAAa;AACrC,UAAM,gBAAgB,WAAW,MAAM,SAAS,EAAE,IAAI,CAAC,WAAW,OAAO,KAAK,CAAC;AAC/E,QAAI,MAAM,QAAQ,KAAK,CAAC,QAAQ,UAAU,cAAc,KAAK,MAAM,MAAM,GAAG;AAC1E,YAAM,IAAI,YAAY,qBAAqB,gDAAgD;AAAA,IAC7F;AAAA,EACF;AACA,MAAI,MAAM,YAAY,MAAM,cAAc,UAAa,MAAM,cAAc;AACzE,UAAM,IAAI,YAAY,qBAAqB,2FAA2F;AAAA,EACxI;AACF;AAEA,SAAS,eAAe,OAA6B;AACnD,MAAI,iBAAiB,YAAa,QAAO;AACzC,MAAI,iBAAiB,MAAO,QAAO,IAAI,YAAY,WAAW,MAAM,OAAO;AAC3E,SAAO,IAAI,YAAY,WAAW,yBAAyB;AAC7D;AAEA,SAAS,UAAU,OAA4B;AAC7C,MAAI,MAAM,SAAS,cAAe,QAAO;AACzC,MAAI,MAAM,SAAS,mBAAmB,MAAM,SAAS,UAAW,QAAO;AACvE,SAAO;AACT;AAEA,SAAS,KAAK,MAAe,QAA0B;AACrD,SAAO,SAAS,KAAK,MAAM,EAAE,OAAO,CAAC;AACvC;","names":[]}
package/dist/server.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { g as UpliftApp } from './types-BdcszAj8.cjs';
1
+ import { h as UpliftApp } from './types-Cw4fuNs_.cjs';
2
2
 
3
3
  declare function handleUploadRequest(app: UpliftApp, req: Request): Promise<Response>;
4
4
 
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { g as UpliftApp } from './types-BdcszAj8.js';
1
+ import { h as UpliftApp } from './types-Cw4fuNs_.js';
2
2
 
3
3
  declare function handleUploadRequest(app: UpliftApp, req: Request): Promise<Response>;
4
4
 
package/dist/server.js CHANGED
@@ -26,8 +26,33 @@ function extensionFor(name) {
26
26
  if (index < 0 || index === name.length - 1) return void 0;
27
27
  return name.slice(index + 1).toLowerCase();
28
28
  }
29
+ function extensionForType(type) {
30
+ const normalized = type.toLowerCase().split(";")[0]?.trim();
31
+ if (!normalized) return void 0;
32
+ const known = {
33
+ "image/avif": "avif",
34
+ "image/gif": "gif",
35
+ "image/jpeg": "jpg",
36
+ "image/png": "png",
37
+ "image/webp": "webp",
38
+ "video/mp4": "mp4",
39
+ "video/quicktime": "mov",
40
+ "video/webm": "webm",
41
+ "audio/aac": "aac",
42
+ "audio/flac": "flac",
43
+ "audio/mpeg": "mp3",
44
+ "audio/mp4": "m4a",
45
+ "audio/ogg": "ogg",
46
+ "audio/wav": "wav",
47
+ "application/json": "json",
48
+ "application/pdf": "pdf",
49
+ "text/csv": "csv",
50
+ "text/plain": "txt"
51
+ };
52
+ return known[normalized];
53
+ }
29
54
  function toInputFile(file) {
30
- const extension = extensionFor(file.name);
55
+ const extension = extensionForType(file.type) ?? extensionFor(file.name);
31
56
  const input = {
32
57
  name: file.name,
33
58
  type: file.type,
@@ -71,10 +96,14 @@ async function handleUploadRequest(app, req) {
71
96
  for (const item of files) {
72
97
  const meta = await deriveMeta(route._def, req, item.input, user);
73
98
  await validateFile(route._def, req, item, user, meta);
74
- const key = route._def.key ? await route._def.key({ req, file: item.input, user, meta }) : defaultKey(item.input);
99
+ const prepared = await applyTransforms(route._def, item);
100
+ const key = route._def.key ? await route._def.key({ req, file: prepared.file, user, meta }) : defaultKey(prepared.file);
75
101
  assertSafeStorageKey(key);
102
+ const primary = await app.storage.put({ key, file: prepared.file, body: prepared.body });
103
+ const outputs = await writeOutputs(app, route._def, key, prepared, primary);
104
+ const file = Object.keys(outputs).length > 0 ? { ...primary, outputs } : primary;
76
105
  stored.push({
77
- file: await app.storage.put({ key, file: item.input, body: item.body }),
106
+ file,
78
107
  meta
79
108
  });
80
109
  }
@@ -97,6 +126,49 @@ async function handleUploadRequest(app, req) {
97
126
  return json({ error: uploadError }, statusFor(uploadError));
98
127
  }
99
128
  }
129
+ async function applyTransforms(route, item) {
130
+ let prepared = { file: item.input, body: item.body };
131
+ for (const transform of route.transforms ?? []) {
132
+ const runner = typeof transform === "function" ? transform : transform.transform.bind(transform);
133
+ prepared = await normalizePrepared(await runner(prepared));
134
+ }
135
+ return prepared;
136
+ }
137
+ async function writeOutputs(app, route, primaryKey, prepared, primary) {
138
+ const written = {};
139
+ try {
140
+ for (const output of route.outputs ?? []) {
141
+ const outputFile = await normalizePrepared(await output.produce({ ...prepared, primary }));
142
+ const key = outputKey(primaryKey, output.name, outputFile.file);
143
+ assertSafeStorageKey(key);
144
+ written[output.name] = await app.storage.put({ key, file: outputFile.file, body: outputFile.body });
145
+ }
146
+ } catch (error) {
147
+ await rollbackWrittenFiles(app, [primary.key, ...Object.values(written).map((file) => file.key)]);
148
+ throw error;
149
+ }
150
+ return written;
151
+ }
152
+ async function rollbackWrittenFiles(app, keys) {
153
+ if (!app.storage.delete) return;
154
+ for (const key of [...keys].reverse()) {
155
+ try {
156
+ await app.storage.delete(key);
157
+ } catch {
158
+ }
159
+ }
160
+ }
161
+ async function normalizePrepared(value) {
162
+ if (value instanceof File) return { file: toInputFile(value), body: value };
163
+ const extension = extensionForType(value.file.type) ?? value.file.extension;
164
+ const file = { ...value.file, size: value.body.size };
165
+ if (extension) file.extension = extension;
166
+ return { file, body: value.body };
167
+ }
168
+ function outputKey(primaryKey, name, file) {
169
+ const extension = extensionForType(file.type) ?? file.extension;
170
+ return `${primaryKey}/outputs/${name}${extension ? `.${extension}` : ""}`;
171
+ }
100
172
  function assertSafeStorageKey(key) {
101
173
  if (key.length === 0) {
102
174
  throw new UploadError("VALIDATION_FAILED", "Storage key cannot be empty.");
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/utils.ts","../src/server.ts"],"sourcesContent":["export type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;\r\nexport type DurationValue = `${number}s` | `${number}m` | `${number}h`;\r\n\r\nexport type UploadInputFile = {\r\n name: string;\r\n type: string;\r\n size: number;\r\n extension?: string;\r\n file?: File;\r\n};\r\n\r\nexport type UploadedFile = {\r\n url: string;\r\n key: string;\r\n name: string;\r\n type: string;\r\n size: number;\r\n extension?: string | undefined;\r\n provider: string;\r\n};\r\n\r\nexport type UploadErrorCode =\r\n | \"FILE_TOO_LARGE\"\r\n | \"FILE_TOO_SMALL\"\r\n | \"INVALID_TYPE\"\r\n | \"AUTH_FAILED\"\r\n | \"VALIDATION_FAILED\"\r\n | \"UPLOAD_FAILED\"\r\n | \"UNKNOWN\";\r\n\r\nexport class UploadError extends Error {\r\n readonly code: UploadErrorCode;\r\n\r\n constructor(code: UploadErrorCode, message: string) {\r\n super(message);\r\n this.name = \"UploadError\";\r\n this.code = code;\r\n }\r\n\r\n toJSON() {\r\n return {\r\n message: this.message,\r\n code: this.code\r\n };\r\n }\r\n}\r\n\r\nexport type StandardSchema<T = unknown> = {\r\n parse(input: unknown): T;\r\n};\r\n\r\nexport type KeyContext<TAuth = unknown, TMeta = unknown> = {\r\n req: Request;\r\n file: UploadInputFile;\r\n user: TAuth;\r\n meta: TMeta;\r\n};\r\n\r\nexport type DoneContext<\r\n TAuth = unknown,\r\n TMeta = unknown,\r\n TMultiple extends boolean = false\r\n> = TMultiple extends true\r\n ? { req: Request; files: UploadedFile[]; user: TAuth; meta: TMeta[] }\r\n : { req: Request; file: UploadedFile; user: TAuth; meta: TMeta };\r\n\r\nexport type StoragePutInput = {\r\n key: string;\r\n file: UploadInputFile;\r\n body: File;\r\n};\r\n\r\nexport type StorageAdapter = {\r\n provider: string;\r\n put(input: StoragePutInput): Promise<UploadedFile>;\r\n};\r\n\r\nexport type Middleware<TUser = unknown> = (ctx: { req: Request }) => TUser | Promise<TUser>;\r\n\r\nexport type UploadKind =\r\n | \"any\"\r\n | \"image\"\r\n | \"pdf\"\r\n | \"video\"\r\n | \"audio\"\r\n | \"text\"\r\n | \"json\"\r\n | \"csv\"\r\n | \"custom\";\r\n\r\nexport type UploadRouteDefinition = {\r\n kind: UploadKind;\r\n maxBytes?: number;\r\n minBytes?: number;\r\n multiple: boolean;\r\n multipleLimit?: number;\r\n auth?: Middleware<unknown>;\r\n overrideAuth: boolean;\r\n key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;\r\n meta?: (ctx: { req: Request; file: UploadInputFile; user: unknown }) => unknown | Promise<unknown>;\r\n validate?: (ctx: {\r\n req: Request;\r\n file: UploadInputFile;\r\n user: unknown;\r\n meta: unknown;\r\n }) => true | string | Promise<true | string>;\r\n done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;\r\n extensions?: string[];\r\n mimeTypes?: string[];\r\n dimensionRule?: { minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number };\r\n requireSquare?: boolean;\r\n aspectRatio?: `${number}:${number}`;\r\n encoding?: \"utf-8\" | \"utf-16\" | \"ascii\";\r\n schema?: StandardSchema;\r\n headers?: string[];\r\n delimiter?: \",\" | \";\" | \"\\t\" | \"|\";\r\n pageRule?: { min?: number; max?: number };\r\n encrypted?: boolean;\r\n durationRule?: { min?: DurationValue; max?: DurationValue };\r\n};\r\n\r\nexport type UploadRoutes = Record<string, { _def: UploadRouteDefinition }>;\r\n\r\nexport type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {\r\n storage: StorageAdapter;\r\n routes: TRoutes;\r\n middleware?: Middleware<unknown> | undefined;\r\n onUploadComplete?: ((ctx: {\r\n route: keyof TRoutes & string;\r\n result: UploadedFile | UploadedFile[];\r\n user: unknown;\r\n }) => void | Promise<void>) | undefined;\r\n};\r\n\r\nexport type IsMultiple<TRoute> = TRoute extends { __multiple?: infer TMultiple }\r\n ? TMultiple extends true\r\n ? true\r\n : false\r\n : false;\r\n\r\nexport type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;\r\nexport type ClientOutput<TRoute> = IsMultiple<TRoute> extends true ? UploadedFile[] : UploadedFile;\r\n\r\nexport type UploadClient<TApp extends UpliftApp> = {\r\n [TRouteName in keyof TApp[\"routes\"] & string]: (\r\n input: ClientInput<TApp[\"routes\"][TRouteName]>\r\n ) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>;\r\n};\r\n","import { UploadError, type SizeValue, type UploadInputFile } from \"./types\";\r\n\r\nconst sizeUnits = {\r\n b: 1,\r\n kb: 1024,\r\n mb: 1024 * 1024,\r\n gb: 1024 * 1024 * 1024\r\n} as const;\r\n\r\nexport function parseSize(value: SizeValue): number {\r\n const match = /^(\\d+(?:\\.\\d+)?)(b|kb|mb|gb)$/.exec(value);\r\n if (!match) throw new UploadError(\"VALIDATION_FAILED\", `Invalid size value: ${value}`);\r\n const amount = Number(match[1]);\r\n const unit = match[2] as keyof typeof sizeUnits;\r\n return Math.floor(amount * sizeUnits[unit]);\r\n}\r\n\r\nexport function extensionFor(name: string): string | undefined {\r\n const index = name.lastIndexOf(\".\");\r\n if (index < 0 || index === name.length - 1) return undefined;\r\n return name.slice(index + 1).toLowerCase();\r\n}\r\n\r\nexport function toInputFile(file: File): UploadInputFile {\r\n const extension = extensionFor(file.name);\r\n const input: UploadInputFile = {\r\n name: file.name,\r\n type: file.type,\r\n size: file.size,\r\n file\r\n };\r\n if (extension) input.extension = extension;\r\n return input;\r\n}\r\n\r\nexport function defaultKey(file: UploadInputFile): string {\r\n const random = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;\r\n return `${random}/${file.name}`;\r\n}\r\n\r\nexport async function readJsonFile(file: File): Promise<unknown> {\r\n try {\r\n return JSON.parse(await file.text());\r\n } catch {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Invalid JSON file.\");\r\n }\r\n}\r\n","import { UploadError, type UpliftApp, type UploadedFile, type UploadInputFile } from \"./types\";\r\nimport { defaultKey, readJsonFile, toInputFile } from \"./utils\";\r\n\r\ntype FileWithInput = {\r\n input: UploadInputFile;\r\n body: File;\r\n};\r\n\r\ntype StoredFile = {\r\n file: UploadedFile;\r\n meta: unknown;\r\n};\r\n\r\nexport async function handleUploadRequest(app: UpliftApp, req: Request): Promise<Response> {\r\n try {\r\n if (req.method !== \"POST\") {\r\n return json({ error: new UploadError(\"VALIDATION_FAILED\", \"Only POST upload requests are supported.\") }, 405);\r\n }\r\n\r\n const routeName = routeNameFromRequest(req);\r\n const route = app.routes[routeName];\r\n if (!route) throw new UploadError(\"VALIDATION_FAILED\", `Unknown upload route: ${routeName}`);\r\n\r\n const user = await resolveUser(app, route._def, req);\r\n const files = await filesFromRequest(req);\r\n if (files.length === 0) throw new UploadError(\"VALIDATION_FAILED\", \"No files were uploaded.\");\r\n if (!route._def.multiple && files.length !== 1) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"This route accepts exactly one file.\");\r\n }\r\n if (route._def.multipleLimit !== undefined && files.length > route._def.multipleLimit) {\r\n throw new UploadError(\"VALIDATION_FAILED\", `This route accepts at most ${route._def.multipleLimit} files.`);\r\n }\r\n\r\n const stored: StoredFile[] = [];\r\n for (const item of files) {\r\n const meta = await deriveMeta(route._def, req, item.input, user);\r\n await validateFile(route._def, req, item, user, meta);\r\n const key = route._def.key ? await route._def.key({ req, file: item.input, user, meta }) : defaultKey(item.input);\r\n assertSafeStorageKey(key);\r\n stored.push({\r\n file: await app.storage.put({ key, file: item.input, body: item.body }),\r\n meta\r\n });\r\n }\r\n\r\n const uploaded = stored.map((item) => item.file);\r\n const metas = stored.map((item) => item.meta);\r\n const firstUpload = uploaded[0];\r\n if (!firstUpload) throw new UploadError(\"UPLOAD_FAILED\", \"Upload did not produce a result.\");\r\n const result: UploadedFile | UploadedFile[] = route._def.multiple ? uploaded : firstUpload;\r\n\r\n if (route._def.done) {\r\n if (route._def.multiple) {\r\n await route._def.done({ req, files: uploaded, user, meta: metas });\r\n } else {\r\n await route._def.done({ req, file: firstUpload, user, meta: metas[0] });\r\n }\r\n }\r\n\r\n if (app.onUploadComplete) await app.onUploadComplete({ route: routeName, result, user });\r\n return json({ result }, 200);\r\n } catch (error) {\r\n const uploadError = normalizeError(error);\r\n return json({ error: uploadError }, statusFor(uploadError));\r\n }\r\n}\r\n\r\nfunction assertSafeStorageKey(key: string): void {\r\n if (key.length === 0) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Storage key cannot be empty.\");\r\n }\r\n if (\r\n key.includes(\"\\0\") ||\r\n key.includes(\"\\\\\") ||\r\n key.includes(\"://\") ||\r\n key.split(\"/\").includes(\"..\") ||\r\n /^([a-zA-Z]:)?\\//.test(key)\r\n ) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Storage key must be a relative object key.\");\r\n }\r\n}\r\n\r\nfunction routeNameFromRequest(req: Request): string {\r\n const url = new URL(req.url);\r\n const explicit = url.searchParams.get(\"route\");\r\n if (explicit) return explicit;\r\n const parts = url.pathname.split(\"/\").filter(Boolean);\r\n return parts[parts.length - 1] ?? \"\";\r\n}\r\n\r\nasync function resolveUser(app: UpliftApp, route: UpliftApp[\"routes\"][string][\"_def\"], req: Request): Promise<unknown> {\r\n if (route.overrideAuth) return undefined;\r\n const middleware = route.auth ?? app.middleware;\r\n if (!middleware) return undefined;\r\n try {\r\n return await middleware({ req });\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"Authentication failed.\";\r\n throw new UploadError(\"AUTH_FAILED\", message);\r\n }\r\n}\r\n\r\nasync function filesFromRequest(req: Request): Promise<FileWithInput[]> {\r\n const contentType = req.headers.get(\"content-type\") ?? \"\";\r\n if (!contentType.includes(\"multipart/form-data\")) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Upload requests must be multipart/form-data.\");\r\n }\r\n\r\n try {\r\n const form = await req.formData();\r\n const files: FileWithInput[] = [];\r\n for (const value of form.values()) {\r\n if (!(value instanceof File)) continue;\r\n files.push({ input: toInputFile(value), body: value });\r\n }\r\n return files;\r\n } catch (error) {\r\n if (error instanceof UploadError) throw error;\r\n const message = error instanceof Error ? error.message : \"Upload request body could not be parsed.\";\r\n throw new UploadError(\"VALIDATION_FAILED\", message);\r\n }\r\n}\r\n\r\nasync function deriveMeta(\r\n route: UpliftApp[\"routes\"][string][\"_def\"],\r\n req: Request,\r\n file: UploadInputFile,\r\n user: unknown\r\n): Promise<unknown> {\r\n if (!route.meta) return undefined;\r\n return route.meta({ req, file, user });\r\n}\r\n\r\nasync function validateFile(\r\n route: UpliftApp[\"routes\"][string][\"_def\"],\r\n req: Request,\r\n item: FileWithInput,\r\n user: unknown,\r\n meta: unknown\r\n) {\r\n if (route.maxBytes !== undefined && item.input.size > route.maxBytes) {\r\n throw new UploadError(\"FILE_TOO_LARGE\", \"File is larger than the route allows.\");\r\n }\r\n if (route.minBytes !== undefined && item.input.size < route.minBytes) {\r\n throw new UploadError(\"FILE_TOO_SMALL\", \"File is smaller than the route allows.\");\r\n }\r\n if (route.kind !== \"any\" && route.extensions && item.input.extension && !route.extensions.includes(item.input.extension)) {\r\n throw new UploadError(\"INVALID_TYPE\", \"File type is not allowed.\");\r\n }\r\n if (route.kind !== \"any\" && route.mimeTypes && route.mimeTypes.length > 0) {\r\n const matches = route.mimeTypes.some((mime) => mime.endsWith(\"/\") ? item.input.type.startsWith(mime) : item.input.type === mime);\r\n if (!matches) throw new UploadError(\"INVALID_TYPE\", \"File MIME type is not allowed.\");\r\n }\r\n if (route.schema) {\r\n try {\r\n route.schema.parse(await readJsonFile(item.body));\r\n } catch (error) {\r\n if (error instanceof UploadError) throw error;\r\n const message = error instanceof Error ? error.message : \"Schema validation failed.\";\r\n throw new UploadError(\"VALIDATION_FAILED\", message);\r\n }\r\n }\r\n await validateConfiguredInspection(route, item.body);\r\n if (route.validate) {\r\n const result = await route.validate({ req, file: item.input, user, meta });\r\n if (result !== true) throw new UploadError(\"VALIDATION_FAILED\", result);\r\n }\r\n}\r\n\r\nasync function validateConfiguredInspection(route: UpliftApp[\"routes\"][string][\"_def\"], file: File): Promise<void> {\r\n if (route.dimensionRule || route.requireSquare || route.aspectRatio) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Image dimension validation requires an image inspector and is not enabled in core.\");\r\n }\r\n if (route.encoding) {\r\n const text = await file.text();\r\n if (route.encoding === \"ascii\" && /[^\\x00-\\x7F]/.test(text)) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Text file is not ASCII encoded.\");\r\n }\r\n }\r\n if (route.headers) {\r\n const [headerLine = \"\"] = (await file.text()).split(/\\r?\\n/, 1);\r\n const delimiter = route.delimiter ?? \",\";\r\n const actualHeaders = headerLine.split(delimiter).map((header) => header.trim());\r\n if (route.headers.some((header, index) => actualHeaders[index] !== header)) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"CSV headers do not match the route definition.\");\r\n }\r\n }\r\n if (route.pageRule || route.encrypted !== undefined || route.durationRule) {\r\n throw new UploadError(\"VALIDATION_FAILED\", \"Rich inspection is configured but no runtime inspector has been installed for this route.\");\r\n }\r\n}\r\n\r\nfunction normalizeError(error: unknown): UploadError {\r\n if (error instanceof UploadError) return error;\r\n if (error instanceof Error) return new UploadError(\"UNKNOWN\", error.message);\r\n return new UploadError(\"UNKNOWN\", \"Unknown upload failure.\");\r\n}\r\n\r\nfunction statusFor(error: UploadError): number {\r\n if (error.code === \"AUTH_FAILED\") return 401;\r\n if (error.code === \"UPLOAD_FAILED\" || error.code === \"UNKNOWN\") return 500;\r\n return 400;\r\n}\r\n\r\nfunction json(body: unknown, status: number): Response {\r\n return Response.json(body, { status });\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/types.ts","../src/utils.ts","../src/server.ts"],"sourcesContent":["export type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;\nexport type DurationValue = `${number}s` | `${number}m` | `${number}h`;\n\nexport type UploadInputFile = {\n name: string;\n type: string;\n size: number;\n extension?: string;\n file?: File;\n};\n\nexport type UploadedFile = {\n url: string;\n key: string;\n name: string;\n type: string;\n size: number;\n extension?: string | undefined;\n provider: string;\n outputs?: Record<string, UploadedFile> | undefined;\n};\n\nexport type ClientUploadedFile<TOutputNames extends string = never> = UploadedFile & (\n [TOutputNames] extends [never]\n ? object\n : {\n output<TName extends TOutputNames>(name: TName): UploadedFile;\n }\n);\n\nexport type UploadErrorCode =\n | \"FILE_TOO_LARGE\"\n | \"FILE_TOO_SMALL\"\n | \"INVALID_TYPE\"\n | \"AUTH_FAILED\"\n | \"VALIDATION_FAILED\"\n | \"UPLOAD_FAILED\"\n | \"UNKNOWN\";\n\nexport class UploadError extends Error {\n readonly code: UploadErrorCode;\n\n constructor(code: UploadErrorCode, message: string) {\n super(message);\n this.name = \"UploadError\";\n this.code = code;\n }\n\n toJSON() {\n return {\n message: this.message,\n code: this.code\n };\n }\n}\n\nexport type StandardSchema<T = unknown> = {\n parse(input: unknown): T;\n};\n\nexport type KeyContext<TAuth = unknown, TMeta = unknown> = {\n req: Request;\n file: UploadInputFile;\n user: TAuth;\n meta: TMeta;\n};\n\nexport type DoneContext<\n TAuth = unknown,\n TMeta = unknown,\n TMultiple extends boolean = false\n> = TMultiple extends true\n ? { req: Request; files: UploadedFile[]; user: TAuth; meta: TMeta[] }\n : { req: Request; file: UploadedFile; user: TAuth; meta: TMeta };\n\nexport type StoragePutInput = {\n key: string;\n file: UploadInputFile;\n body: File;\n};\n\nexport type StorageAdapter = {\n provider: string;\n put(input: StoragePutInput): Promise<UploadedFile>;\n delete?(key: string): Promise<void>;\n};\n\nexport type Middleware<TUser = unknown> = (ctx: { req: Request }) => TUser | Promise<TUser>;\n\nexport type UploadKind =\n | \"any\"\n | \"image\"\n | \"pdf\"\n | \"video\"\n | \"audio\"\n | \"text\"\n | \"json\"\n | \"csv\"\n | \"custom\";\n\nexport type PreparedUploadFile = {\n file: UploadInputFile;\n body: File;\n};\n\nexport type TransformContext = PreparedUploadFile;\n\nexport type UploadTransform<TKind extends UploadKind = UploadKind> = {\n readonly __kind?: TKind | undefined;\n transform(ctx: TransformContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;\n};\n\nexport type UploadTransformFunction<TKind extends UploadKind = UploadKind> = ((\n ctx: TransformContext\n) => File | PreparedUploadFile | Promise<File | PreparedUploadFile>) & {\n readonly __kind?: TKind | undefined;\n};\n\nexport type CompatibleTransform<TKind extends UploadKind> = TKind extends \"any\" | \"custom\"\n ? UploadTransform<UploadKind> | UploadTransformFunction<UploadKind>\n : UploadTransform<TKind> | UploadTransform<\"any\"> | UploadTransformFunction<TKind> | UploadTransformFunction<\"any\">;\n\nexport type OutputContext = PreparedUploadFile & {\n primary: UploadedFile;\n};\n\nexport type UploadOutput<TKind extends UploadKind = UploadKind, TName extends string = string> = {\n readonly __kind?: TKind | undefined;\n name: TName;\n produce(ctx: OutputContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;\n};\n\nexport type CompatibleOutput<TKind extends UploadKind, TName extends string = string> = TKind extends \"any\" | \"custom\"\n ? UploadOutput<UploadKind, TName>\n : UploadOutput<TKind, TName> | UploadOutput<\"any\", TName>;\n\nexport type UploadRouteDefinition = {\n kind: UploadKind;\n maxBytes?: number;\n minBytes?: number;\n multiple: boolean;\n multipleLimit?: number;\n auth?: Middleware<unknown>;\n overrideAuth: boolean;\n key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;\n meta?: (ctx: { req: Request; file: UploadInputFile; user: unknown }) => unknown | Promise<unknown>;\n validate?: (ctx: {\n req: Request;\n file: UploadInputFile;\n user: unknown;\n meta: unknown;\n }) => true | string | Promise<true | string>;\n done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;\n extensions?: string[];\n mimeTypes?: string[];\n dimensionRule?: { minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number };\n requireSquare?: boolean;\n aspectRatio?: `${number}:${number}`;\n encoding?: \"utf-8\" | \"utf-16\" | \"ascii\";\n schema?: StandardSchema;\n headers?: string[];\n delimiter?: \",\" | \";\" | \"\\t\" | \"|\";\n pageRule?: { min?: number; max?: number };\n encrypted?: boolean;\n durationRule?: { min?: DurationValue; max?: DurationValue };\n transforms?: Array<UploadTransform | UploadTransformFunction>;\n outputs?: Array<UploadOutput>;\n};\n\nexport type UploadRoutes = Record<string, { _def: UploadRouteDefinition }>;\n\nexport type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {\n storage: StorageAdapter;\n routes: TRoutes;\n middleware?: Middleware<unknown> | undefined;\n onUploadComplete?: ((ctx: {\n route: keyof TRoutes & string;\n result: UploadedFile | UploadedFile[];\n user: unknown;\n }) => void | Promise<void>) | undefined;\n};\n\nexport type IsMultiple<TRoute> = TRoute extends { __multiple?: infer TMultiple }\n ? TMultiple extends true\n ? true\n : false\n : false;\n\nexport type OutputNames<TRoute> = TRoute extends { __outputs?: infer TOutputNames }\n ? TOutputNames extends string\n ? TOutputNames\n : never\n : never;\n\nexport type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;\nexport type ClientOutput<TRoute> = IsMultiple<TRoute> extends true\n ? Array<ClientUploadedFile<OutputNames<TRoute>>>\n : ClientUploadedFile<OutputNames<TRoute>>;\n\nexport type UploadClient<TApp extends UpliftApp> = {\n [TRouteName in keyof TApp[\"routes\"] & string]: (\n input: ClientInput<TApp[\"routes\"][TRouteName]>\n ) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>;\n};\n","import { UploadError, type SizeValue, type UploadInputFile } from \"./types\";\n\nconst sizeUnits = {\n b: 1,\n kb: 1024,\n mb: 1024 * 1024,\n gb: 1024 * 1024 * 1024\n} as const;\n\nexport function parseSize(value: SizeValue): number {\n const match = /^(\\d+(?:\\.\\d+)?)(b|kb|mb|gb)$/.exec(value);\n if (!match) throw new UploadError(\"VALIDATION_FAILED\", `Invalid size value: ${value}`);\n const amount = Number(match[1]);\n const unit = match[2] as keyof typeof sizeUnits;\n return Math.floor(amount * sizeUnits[unit]);\n}\n\nexport function extensionFor(name: string): string | undefined {\n const index = name.lastIndexOf(\".\");\n if (index < 0 || index === name.length - 1) return undefined;\n return name.slice(index + 1).toLowerCase();\n}\n\nexport function extensionForType(type: string): string | undefined {\n const normalized = type.toLowerCase().split(\";\")[0]?.trim();\n if (!normalized) return undefined;\n const known: Record<string, string> = {\n \"image/avif\": \"avif\",\n \"image/gif\": \"gif\",\n \"image/jpeg\": \"jpg\",\n \"image/png\": \"png\",\n \"image/webp\": \"webp\",\n \"video/mp4\": \"mp4\",\n \"video/quicktime\": \"mov\",\n \"video/webm\": \"webm\",\n \"audio/aac\": \"aac\",\n \"audio/flac\": \"flac\",\n \"audio/mpeg\": \"mp3\",\n \"audio/mp4\": \"m4a\",\n \"audio/ogg\": \"ogg\",\n \"audio/wav\": \"wav\",\n \"application/json\": \"json\",\n \"application/pdf\": \"pdf\",\n \"text/csv\": \"csv\",\n \"text/plain\": \"txt\"\n };\n return known[normalized];\n}\n\nexport function toInputFile(file: File): UploadInputFile {\n const extension = extensionForType(file.type) ?? extensionFor(file.name);\n const input: UploadInputFile = {\n name: file.name,\n type: file.type,\n size: file.size,\n file\n };\n if (extension) input.extension = extension;\n return input;\n}\n\nexport function defaultKey(file: UploadInputFile): string {\n const random = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;\n return `${random}/${file.name}`;\n}\n\nexport async function readJsonFile(file: File): Promise<unknown> {\n try {\n return JSON.parse(await file.text());\n } catch {\n throw new UploadError(\"VALIDATION_FAILED\", \"Invalid JSON file.\");\n }\n}\n","import {\n UploadError,\n type PreparedUploadFile,\n type UpliftApp,\n type UploadedFile,\n type UploadInputFile\n} from \"./types\";\nimport { defaultKey, extensionForType, readJsonFile, toInputFile } from \"./utils\";\n\ntype FileWithInput = {\n input: UploadInputFile;\n body: File;\n};\n\ntype StoredFile = {\n file: UploadedFile;\n meta: unknown;\n};\n\nexport async function handleUploadRequest(app: UpliftApp, req: Request): Promise<Response> {\n try {\n if (req.method !== \"POST\") {\n return json({ error: new UploadError(\"VALIDATION_FAILED\", \"Only POST upload requests are supported.\") }, 405);\n }\n\n const routeName = routeNameFromRequest(req);\n const route = app.routes[routeName];\n if (!route) throw new UploadError(\"VALIDATION_FAILED\", `Unknown upload route: ${routeName}`);\n\n const user = await resolveUser(app, route._def, req);\n const files = await filesFromRequest(req);\n if (files.length === 0) throw new UploadError(\"VALIDATION_FAILED\", \"No files were uploaded.\");\n if (!route._def.multiple && files.length !== 1) {\n throw new UploadError(\"VALIDATION_FAILED\", \"This route accepts exactly one file.\");\n }\n if (route._def.multipleLimit !== undefined && files.length > route._def.multipleLimit) {\n throw new UploadError(\"VALIDATION_FAILED\", `This route accepts at most ${route._def.multipleLimit} files.`);\n }\n\n const stored: StoredFile[] = [];\n for (const item of files) {\n const meta = await deriveMeta(route._def, req, item.input, user);\n await validateFile(route._def, req, item, user, meta);\n const prepared = await applyTransforms(route._def, item);\n const key = route._def.key\n ? await route._def.key({ req, file: prepared.file, user, meta })\n : defaultKey(prepared.file);\n assertSafeStorageKey(key);\n const primary = await app.storage.put({ key, file: prepared.file, body: prepared.body });\n const outputs = await writeOutputs(app, route._def, key, prepared, primary);\n const file = Object.keys(outputs).length > 0 ? { ...primary, outputs } : primary;\n stored.push({\n file,\n meta\n });\n }\n\n const uploaded = stored.map((item) => item.file);\n const metas = stored.map((item) => item.meta);\n const firstUpload = uploaded[0];\n if (!firstUpload) throw new UploadError(\"UPLOAD_FAILED\", \"Upload did not produce a result.\");\n const result: UploadedFile | UploadedFile[] = route._def.multiple ? uploaded : firstUpload;\n\n if (route._def.done) {\n if (route._def.multiple) {\n await route._def.done({ req, files: uploaded, user, meta: metas });\n } else {\n await route._def.done({ req, file: firstUpload, user, meta: metas[0] });\n }\n }\n\n if (app.onUploadComplete) await app.onUploadComplete({ route: routeName, result, user });\n return json({ result }, 200);\n } catch (error) {\n const uploadError = normalizeError(error);\n return json({ error: uploadError }, statusFor(uploadError));\n }\n}\n\nasync function applyTransforms(\n route: UpliftApp[\"routes\"][string][\"_def\"],\n item: FileWithInput\n): Promise<PreparedUploadFile> {\n let prepared: PreparedUploadFile = { file: item.input, body: item.body };\n for (const transform of route.transforms ?? []) {\n const runner = typeof transform === \"function\" ? transform : transform.transform.bind(transform);\n prepared = await normalizePrepared(await runner(prepared));\n }\n return prepared;\n}\n\nasync function writeOutputs(\n app: UpliftApp,\n route: UpliftApp[\"routes\"][string][\"_def\"],\n primaryKey: string,\n prepared: PreparedUploadFile,\n primary: UploadedFile\n): Promise<Record<string, UploadedFile>> {\n const written: Record<string, UploadedFile> = {};\n try {\n for (const output of route.outputs ?? []) {\n const outputFile = await normalizePrepared(await output.produce({ ...prepared, primary }));\n const key = outputKey(primaryKey, output.name, outputFile.file);\n assertSafeStorageKey(key);\n written[output.name] = await app.storage.put({ key, file: outputFile.file, body: outputFile.body });\n }\n } catch (error) {\n await rollbackWrittenFiles(app, [primary.key, ...Object.values(written).map((file) => file.key)]);\n throw error;\n }\n return written;\n}\n\nasync function rollbackWrittenFiles(app: UpliftApp, keys: string[]): Promise<void> {\n if (!app.storage.delete) return;\n for (const key of [...keys].reverse()) {\n try {\n await app.storage.delete(key);\n } catch {\n // Preserve the original upload failure; cleanup is best-effort per adapter.\n }\n }\n}\n\nasync function normalizePrepared(value: File | PreparedUploadFile): Promise<PreparedUploadFile> {\n if (value instanceof File) return { file: toInputFile(value), body: value };\n const extension = extensionForType(value.file.type) ?? value.file.extension;\n const file = { ...value.file, size: value.body.size };\n if (extension) file.extension = extension;\n return { file, body: value.body };\n}\n\nfunction outputKey(primaryKey: string, name: string, file: UploadInputFile): string {\n const extension = extensionForType(file.type) ?? file.extension;\n return `${primaryKey}/outputs/${name}${extension ? `.${extension}` : \"\"}`;\n}\n\nfunction assertSafeStorageKey(key: string): void {\n if (key.length === 0) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Storage key cannot be empty.\");\n }\n if (\n key.includes(\"\\0\") ||\n key.includes(\"\\\\\") ||\n key.includes(\"://\") ||\n key.split(\"/\").includes(\"..\") ||\n /^([a-zA-Z]:)?\\//.test(key)\n ) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Storage key must be a relative object key.\");\n }\n}\n\nfunction routeNameFromRequest(req: Request): string {\n const url = new URL(req.url);\n const explicit = url.searchParams.get(\"route\");\n if (explicit) return explicit;\n const parts = url.pathname.split(\"/\").filter(Boolean);\n return parts[parts.length - 1] ?? \"\";\n}\n\nasync function resolveUser(app: UpliftApp, route: UpliftApp[\"routes\"][string][\"_def\"], req: Request): Promise<unknown> {\n if (route.overrideAuth) return undefined;\n const middleware = route.auth ?? app.middleware;\n if (!middleware) return undefined;\n try {\n return await middleware({ req });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Authentication failed.\";\n throw new UploadError(\"AUTH_FAILED\", message);\n }\n}\n\nasync function filesFromRequest(req: Request): Promise<FileWithInput[]> {\n const contentType = req.headers.get(\"content-type\") ?? \"\";\n if (!contentType.includes(\"multipart/form-data\")) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Upload requests must be multipart/form-data.\");\n }\n\n try {\n const form = await req.formData();\n const files: FileWithInput[] = [];\n for (const value of form.values()) {\n if (!(value instanceof File)) continue;\n files.push({ input: toInputFile(value), body: value });\n }\n return files;\n } catch (error) {\n if (error instanceof UploadError) throw error;\n const message = error instanceof Error ? error.message : \"Upload request body could not be parsed.\";\n throw new UploadError(\"VALIDATION_FAILED\", message);\n }\n}\n\nasync function deriveMeta(\n route: UpliftApp[\"routes\"][string][\"_def\"],\n req: Request,\n file: UploadInputFile,\n user: unknown\n): Promise<unknown> {\n if (!route.meta) return undefined;\n return route.meta({ req, file, user });\n}\n\nasync function validateFile(\n route: UpliftApp[\"routes\"][string][\"_def\"],\n req: Request,\n item: FileWithInput,\n user: unknown,\n meta: unknown\n) {\n if (route.maxBytes !== undefined && item.input.size > route.maxBytes) {\n throw new UploadError(\"FILE_TOO_LARGE\", \"File is larger than the route allows.\");\n }\n if (route.minBytes !== undefined && item.input.size < route.minBytes) {\n throw new UploadError(\"FILE_TOO_SMALL\", \"File is smaller than the route allows.\");\n }\n if (route.kind !== \"any\" && route.extensions && item.input.extension && !route.extensions.includes(item.input.extension)) {\n throw new UploadError(\"INVALID_TYPE\", \"File type is not allowed.\");\n }\n if (route.kind !== \"any\" && route.mimeTypes && route.mimeTypes.length > 0) {\n const matches = route.mimeTypes.some((mime) => mime.endsWith(\"/\") ? item.input.type.startsWith(mime) : item.input.type === mime);\n if (!matches) throw new UploadError(\"INVALID_TYPE\", \"File MIME type is not allowed.\");\n }\n if (route.schema) {\n try {\n route.schema.parse(await readJsonFile(item.body));\n } catch (error) {\n if (error instanceof UploadError) throw error;\n const message = error instanceof Error ? error.message : \"Schema validation failed.\";\n throw new UploadError(\"VALIDATION_FAILED\", message);\n }\n }\n await validateConfiguredInspection(route, item.body);\n if (route.validate) {\n const result = await route.validate({ req, file: item.input, user, meta });\n if (result !== true) throw new UploadError(\"VALIDATION_FAILED\", result);\n }\n}\n\nasync function validateConfiguredInspection(route: UpliftApp[\"routes\"][string][\"_def\"], file: File): Promise<void> {\n if (route.dimensionRule || route.requireSquare || route.aspectRatio) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Image dimension validation requires an image inspector and is not enabled in core.\");\n }\n if (route.encoding) {\n const text = await file.text();\n if (route.encoding === \"ascii\" && /[^\\x00-\\x7F]/.test(text)) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Text file is not ASCII encoded.\");\n }\n }\n if (route.headers) {\n const [headerLine = \"\"] = (await file.text()).split(/\\r?\\n/, 1);\n const delimiter = route.delimiter ?? \",\";\n const actualHeaders = headerLine.split(delimiter).map((header) => header.trim());\n if (route.headers.some((header, index) => actualHeaders[index] !== header)) {\n throw new UploadError(\"VALIDATION_FAILED\", \"CSV headers do not match the route definition.\");\n }\n }\n if (route.pageRule || route.encrypted !== undefined || route.durationRule) {\n throw new UploadError(\"VALIDATION_FAILED\", \"Rich inspection is configured but no runtime inspector has been installed for this route.\");\n }\n}\n\nfunction normalizeError(error: unknown): UploadError {\n if (error instanceof UploadError) return error;\n if (error instanceof Error) return new UploadError(\"UNKNOWN\", error.message);\n return new UploadError(\"UNKNOWN\", \"Unknown upload failure.\");\n}\n\nfunction statusFor(error: UploadError): number {\n if (error.code === \"AUTH_FAILED\") return 401;\n if (error.code === \"UPLOAD_FAILED\" || error.code === \"UNKNOWN\") return 500;\n return 400;\n}\n\nfunction json(body: unknown, status: number): Response {\n return Response.json(body, { status });\n}\n"],"mappings":"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","names":[]}
@@ -15,7 +15,13 @@ type UploadedFile = {
15
15
  size: number;
16
16
  extension?: string | undefined;
17
17
  provider: string;
18
+ outputs?: Record<string, UploadedFile> | undefined;
18
19
  };
20
+ type ClientUploadedFile<TOutputNames extends string = never> = UploadedFile & ([
21
+ TOutputNames
22
+ ] extends [never] ? object : {
23
+ output<TName extends TOutputNames>(name: TName): UploadedFile;
24
+ });
19
25
  type UploadErrorCode = "FILE_TOO_LARGE" | "FILE_TOO_SMALL" | "INVALID_TYPE" | "AUTH_FAILED" | "VALIDATION_FAILED" | "UPLOAD_FAILED" | "UNKNOWN";
20
26
  declare class UploadError extends Error {
21
27
  readonly code: UploadErrorCode;
@@ -53,11 +59,34 @@ type StoragePutInput = {
53
59
  type StorageAdapter = {
54
60
  provider: string;
55
61
  put(input: StoragePutInput): Promise<UploadedFile>;
62
+ delete?(key: string): Promise<void>;
56
63
  };
57
64
  type Middleware<TUser = unknown> = (ctx: {
58
65
  req: Request;
59
66
  }) => TUser | Promise<TUser>;
60
67
  type UploadKind = "any" | "image" | "pdf" | "video" | "audio" | "text" | "json" | "csv" | "custom";
68
+ type PreparedUploadFile = {
69
+ file: UploadInputFile;
70
+ body: File;
71
+ };
72
+ type TransformContext = PreparedUploadFile;
73
+ type UploadTransform<TKind extends UploadKind = UploadKind> = {
74
+ readonly __kind?: TKind | undefined;
75
+ transform(ctx: TransformContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;
76
+ };
77
+ type UploadTransformFunction<TKind extends UploadKind = UploadKind> = ((ctx: TransformContext) => File | PreparedUploadFile | Promise<File | PreparedUploadFile>) & {
78
+ readonly __kind?: TKind | undefined;
79
+ };
80
+ type CompatibleTransform<TKind extends UploadKind> = TKind extends "any" | "custom" ? UploadTransform<UploadKind> | UploadTransformFunction<UploadKind> : UploadTransform<TKind> | UploadTransform<"any"> | UploadTransformFunction<TKind> | UploadTransformFunction<"any">;
81
+ type OutputContext = PreparedUploadFile & {
82
+ primary: UploadedFile;
83
+ };
84
+ type UploadOutput<TKind extends UploadKind = UploadKind, TName extends string = string> = {
85
+ readonly __kind?: TKind | undefined;
86
+ name: TName;
87
+ produce(ctx: OutputContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;
88
+ };
89
+ type CompatibleOutput<TKind extends UploadKind, TName extends string = string> = TKind extends "any" | "custom" ? UploadOutput<UploadKind, TName> : UploadOutput<TKind, TName> | UploadOutput<"any", TName>;
61
90
  type UploadRouteDefinition = {
62
91
  kind: UploadKind;
63
92
  maxBytes?: number;
@@ -102,6 +131,8 @@ type UploadRouteDefinition = {
102
131
  min?: DurationValue;
103
132
  max?: DurationValue;
104
133
  };
134
+ transforms?: Array<UploadTransform | UploadTransformFunction>;
135
+ outputs?: Array<UploadOutput>;
105
136
  };
106
137
  type UploadRoutes = Record<string, {
107
138
  _def: UploadRouteDefinition;
@@ -119,10 +150,13 @@ type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {
119
150
  type IsMultiple<TRoute> = TRoute extends {
120
151
  __multiple?: infer TMultiple;
121
152
  } ? TMultiple extends true ? true : false : false;
153
+ type OutputNames<TRoute> = TRoute extends {
154
+ __outputs?: infer TOutputNames;
155
+ } ? TOutputNames extends string ? TOutputNames : never : never;
122
156
  type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;
123
- type ClientOutput<TRoute> = IsMultiple<TRoute> extends true ? UploadedFile[] : UploadedFile;
157
+ type ClientOutput<TRoute> = IsMultiple<TRoute> extends true ? Array<ClientUploadedFile<OutputNames<TRoute>>> : ClientUploadedFile<OutputNames<TRoute>>;
124
158
  type UploadClient<TApp extends UpliftApp> = {
125
159
  [TRouteName in keyof TApp["routes"] & string]: (input: ClientInput<TApp["routes"][TRouteName]>) => Promise<ClientOutput<TApp["routes"][TRouteName]>>;
126
160
  };
127
161
 
128
- export { type ClientInput as C, type DurationValue as D, type KeyContext as K, type Middleware as M, type SizeValue as S, type UploadKind as U, type UploadRouteDefinition as a, type UploadInputFile as b, type DoneContext as c, type StandardSchema as d, type UploadRoutes as e, type StorageAdapter as f, type UpliftApp as g, type ClientOutput as h, type StoragePutInput as i, type UploadClient as j, UploadError as k, type UploadErrorCode as l, type UploadedFile as m };
162
+ export { type CompatibleTransform as C, type DurationValue as D, type KeyContext as K, type Middleware as M, type OutputContext as O, type PreparedUploadFile as P, type SizeValue as S, type TransformContext as T, type UploadKind as U, type UploadRouteDefinition as a, type UploadInputFile as b, type DoneContext as c, type StandardSchema as d, type CompatibleOutput as e, type UploadRoutes as f, type StorageAdapter as g, type UpliftApp as h, type ClientInput as i, type ClientOutput as j, type ClientUploadedFile as k, type OutputNames as l, type StoragePutInput as m, type UploadClient as n, UploadError as o, type UploadErrorCode as p, type UploadOutput as q, type UploadTransform as r, type UploadTransformFunction as s, type UploadedFile as t };