@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/LICENSE +21 -21
- package/README.md +102 -85
- package/dist/client.cjs +23 -2
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +23 -2
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +32 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -7
- package/dist/index.d.ts +10 -7
- package/dist/index.js +32 -2
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +23 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +23 -2
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +75 -3
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +75 -3
- package/dist/server.js.map +1 -1
- package/dist/{types-BdcszAj8.d.cts → types-Cw4fuNs_.d.cts} +36 -2
- package/dist/{types-BdcszAj8.d.ts → types-Cw4fuNs_.d.ts} +36 -2
- package/package.json +1 -1
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
|
|
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
|
|
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.");
|
package/dist/server.cjs.map
CHANGED
|
@@ -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":"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","names":[]}
|
package/dist/server.d.cts
CHANGED
package/dist/server.d.ts
CHANGED
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
|
|
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
|
|
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.");
|
package/dist/server.js.map
CHANGED
|
@@ -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 ?
|
|
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
|
|
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 };
|