ocel 0.0.0 → 0.0.1-alpha.1

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.
Files changed (74) hide show
  1. package/bin/run.js +39 -0
  2. package/dist/blob/bucket.d.ts +55 -0
  3. package/dist/blob/bucket.js +65 -0
  4. package/dist/blob/index.d.ts +8 -0
  5. package/dist/blob/index.js +13 -0
  6. package/dist/blob/integrations/client.d.ts +25 -0
  7. package/dist/blob/integrations/client.js +71 -0
  8. package/dist/blob/integrations/hono.d.ts +31 -0
  9. package/dist/blob/integrations/hono.js +11 -0
  10. package/dist/blob/mime-types.d.ts +1 -0
  11. package/dist/blob/mime-types.js +1 -0
  12. package/dist/blob/parse.d.ts +16 -0
  13. package/dist/blob/parse.js +69 -0
  14. package/dist/blob/s3-listener.d.ts +7 -0
  15. package/dist/blob/s3-listener.js +35 -0
  16. package/dist/blob/types.d.ts +128 -0
  17. package/dist/blob/types.js +1 -0
  18. package/dist/blob/upload-error.d.ts +35 -0
  19. package/dist/blob/upload-error.js +54 -0
  20. package/dist/blob/uploader.d.ts +67 -0
  21. package/dist/blob/uploader.js +323 -0
  22. package/dist/internal/db/index.d.ts +68 -0
  23. package/dist/internal/db/index.js +73 -0
  24. package/dist/lambda/index.d.ts +2 -0
  25. package/dist/lambda/index.js +4 -0
  26. package/dist/lambda/integrations/express.d.ts +3 -0
  27. package/dist/lambda/integrations/express.js +13 -0
  28. package/dist/lambda/integrations/hono.d.ts +3 -0
  29. package/dist/lambda/integrations/hono.js +13 -0
  30. package/dist/lambda/lambda.d.ts +43 -0
  31. package/dist/lambda/lambda.js +30 -0
  32. package/dist/postgres/index.d.ts +2 -0
  33. package/dist/postgres/index.js +4 -0
  34. package/dist/postgres/pg.d.ts +11 -0
  35. package/dist/postgres/pg.js +24 -0
  36. package/dist/utils/component.d.ts +3 -0
  37. package/dist/utils/component.js +1 -0
  38. package/dist/utils/constants.d.ts +2 -0
  39. package/dist/utils/constants.js +2 -0
  40. package/dist/utils/nanoid.d.ts +1 -0
  41. package/dist/utils/nanoid.js +6 -0
  42. package/dist/utils/rpc.d.ts +11 -0
  43. package/dist/utils/rpc.js +17 -0
  44. package/dist/utils/stack.d.ts +4 -0
  45. package/dist/utils/stack.js +23 -0
  46. package/dist/workflow/index.d.ts +3 -0
  47. package/dist/workflow/index.js +4 -0
  48. package/dist/workflow/workflow.d.ts +34 -0
  49. package/dist/workflow/workflow.js +146 -0
  50. package/package.json +93 -8
  51. package/src/blob/bucket.ts +92 -0
  52. package/src/blob/index.ts +34 -0
  53. package/src/blob/integrations/client.ts +127 -0
  54. package/src/blob/integrations/hono.ts +16 -0
  55. package/src/blob/mime-types.ts +68 -0
  56. package/src/blob/parse.ts +93 -0
  57. package/src/blob/s3-listener.ts +48 -0
  58. package/src/blob/types.ts +142 -0
  59. package/src/blob/upload-error.ts +67 -0
  60. package/src/blob/uploader.ts +426 -0
  61. package/src/internal/db/index.ts +78 -0
  62. package/src/lambda/index.ts +8 -0
  63. package/src/lambda/integrations/express.ts +25 -0
  64. package/src/lambda/integrations/hono.ts +25 -0
  65. package/src/lambda/lambda.ts +94 -0
  66. package/src/postgres/index.ts +5 -0
  67. package/src/postgres/pg.ts +33 -0
  68. package/src/utils/component.ts +3 -0
  69. package/src/utils/constants.ts +2 -0
  70. package/src/utils/nanoid.ts +9 -0
  71. package/src/utils/rpc.ts +28 -0
  72. package/src/utils/stack.ts +26 -0
  73. package/src/workflow/index.ts +9 -0
  74. package/src/workflow/workflow.ts +235 -0
package/bin/run.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { join } from "path";
4
+ import { createRequire } from "node:module";
5
+
6
+ const { platform, arch } = process;
7
+ const require = createRequire(import.meta.url);
8
+
9
+ let packageName = "";
10
+
11
+ switch (platform) {
12
+ case "win32":
13
+ packageName = `win32-${arch}`;
14
+ break;
15
+ case "darwin":
16
+ packageName = `darwin-${arch}`;
17
+ break;
18
+ case "linux":
19
+ packageName = `linux-${arch}`;
20
+ break;
21
+ default:
22
+ throw new Error(`Unsupported platform: ${platform}`);
23
+ }
24
+
25
+ const binaryPkg = `@ocel/${packageName}`;
26
+
27
+ try {
28
+ const binary = process.platform === "win32" ? "ocel.exe" : "ocel";
29
+ const binaryPath = require.resolve(join(binaryPkg, "bin", binary));
30
+
31
+ const { spawnSync } = require("child_process");
32
+ const result = spawnSync(binaryPath, process.argv.slice(2), {
33
+ stdio: "inherit",
34
+ });
35
+ process.exit(result.status);
36
+ } catch (e) {
37
+ console.error(`Failed to locate binary for ${binaryPkg}.`);
38
+ process.exit(1);
39
+ }
@@ -0,0 +1,55 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { HandleUploadBody, TBucketConfig, TUploaderConfig } from "./types";
3
+ import type { Component } from "../utils/component";
4
+ export declare class Bucket<TUploaderShape extends Record<string, TUploaderConfig<any, any>>> implements Component {
5
+ readonly name: string;
6
+ readonly _shape: TUploaderShape;
7
+ private uploaders;
8
+ constructor(name: string, args: TBucketConfig<TUploaderShape>);
9
+ __id(): string;
10
+ __name(): string;
11
+ upload: () => Promise<void>;
12
+ uploadExternal: () => Promise<void>;
13
+ handleUpload: ({ body, request, }: {
14
+ request: Request | IncomingMessage;
15
+ body: HandleUploadBody;
16
+ }) => Promise<{
17
+ files: {
18
+ url: string;
19
+ key: string;
20
+ name: string;
21
+ }[];
22
+ sessionId: string;
23
+ } | {
24
+ files: {
25
+ fileKey: string;
26
+ status: "PENDING" | "SUCCESS" | "FAILED";
27
+ file: {
28
+ path: string;
29
+ contentDisposition: string;
30
+ contentType: string;
31
+ } | undefined;
32
+ metadata: any;
33
+ }[];
34
+ completed: boolean;
35
+ type?: undefined;
36
+ response?: undefined;
37
+ error?: undefined;
38
+ } | {
39
+ type: "UPLOAD_COMPLETED";
40
+ response: "ok";
41
+ files?: undefined;
42
+ completed?: undefined;
43
+ error?: undefined;
44
+ } | {
45
+ error: {
46
+ message: string;
47
+ code: string;
48
+ status: number;
49
+ };
50
+ files?: undefined;
51
+ completed?: undefined;
52
+ type?: undefined;
53
+ response?: undefined;
54
+ }>;
55
+ }
@@ -0,0 +1,65 @@
1
+ import z from "zod";
2
+ import { getCallerFile } from "../utils/stack";
3
+ import { rpc } from "../utils/rpc";
4
+ import { Uploader } from "./uploader";
5
+ import { parseReq } from "./parse";
6
+ export class Bucket {
7
+ name;
8
+ _shape = {};
9
+ uploaders = {};
10
+ constructor(name, args) {
11
+ this.name = name;
12
+ if (process.env.OCEL_PHASE === "discovery") {
13
+ rpc.register({
14
+ id: name,
15
+ type: "bucket",
16
+ source: getCallerFile() || "unknown",
17
+ // TODO:
18
+ config: {},
19
+ });
20
+ }
21
+ this._shape = args.uploaders;
22
+ Object.entries(args.uploaders).forEach(([key, config]) => {
23
+ this.uploaders[key] = new Uploader(this, key, config);
24
+ });
25
+ }
26
+ __id() {
27
+ return this.name;
28
+ }
29
+ __name() {
30
+ const id = process.env[`RESOURCE_${this.name}_BUCKET_NAME`];
31
+ if (!id) {
32
+ throw new Error(`Bucket ID for bucket "${this.name}" is not defined. Make sure the bucket is properly configured in your Ocel project.`);
33
+ }
34
+ return id;
35
+ }
36
+ upload = async () => { };
37
+ uploadExternal = async () => { };
38
+ handleUpload = async ({ body, request, }) => {
39
+ const parsedReq = parseReq(request);
40
+ const hasUploaders = Object.keys(this._shape).length > 0;
41
+ if (!hasUploaders) {
42
+ throw new Error("No uploaders configured for this bucket");
43
+ }
44
+ const querySchema = z.object({
45
+ action: z.enum(["presign", "callback", "poll"]),
46
+ uploader: z.enum(Object.keys(this._shape)),
47
+ sessionId: z.string().optional(),
48
+ });
49
+ const queryRes = querySchema.safeParse(Object.fromEntries(parsedReq.query));
50
+ if (!queryRes.success) {
51
+ throw new Error(`Invalid query parameters: ${queryRes.error.message}`);
52
+ }
53
+ const query = queryRes.data;
54
+ const uploader = this.uploaders[query.uploader];
55
+ if (!uploader) {
56
+ throw new Error(`Uploader not found: ${query.uploader}`);
57
+ }
58
+ return uploader.handleUpload({
59
+ body,
60
+ request,
61
+ action: query.action,
62
+ sessionId: query.sessionId,
63
+ });
64
+ };
65
+ }
@@ -0,0 +1,8 @@
1
+ import { type ZodType } from "zod";
2
+ import type { TBucketConfig, TUploaderConfig } from "./types";
3
+ import { Bucket } from "./bucket";
4
+ export declare function bucket<TUploaderShape extends Record<string, TUploaderConfig<any, any>>>(name: string, args: TBucketConfig<TUploaderShape>): Bucket<TUploaderShape>;
5
+ export declare function uploader<TInput extends ZodType = never, TMeta = never, TReturn = void>({ middleware, input, }: {
6
+ input?: TInput;
7
+ middleware: TUploaderConfig<TInput, TMeta, TReturn>["onBeforeUpload"];
8
+ }, config: Omit<TUploaderConfig<TInput, TMeta, TReturn>, "onBeforeUpload" | "_def" | "input">): TUploaderConfig<TInput, TMeta, TReturn>;
@@ -0,0 +1,13 @@
1
+ import {} from "zod";
2
+ import { Bucket } from "./bucket";
3
+ export function bucket(name, args) {
4
+ return new Bucket(name, args);
5
+ }
6
+ export function uploader({ middleware, input, }, config) {
7
+ return {
8
+ ...config,
9
+ input,
10
+ onBeforeUpload: middleware,
11
+ _def: {},
12
+ };
13
+ }
@@ -0,0 +1,25 @@
1
+ import type z from "zod";
2
+ import type { Bucket } from "../bucket";
3
+ type InferReturn<TRouter, K extends keyof TRouter> = TRouter[K] extends {
4
+ _def: {
5
+ output: infer R;
6
+ };
7
+ } ? R : never;
8
+ type InferInput<TRouter, K extends keyof TRouter> = TRouter[K] extends {
9
+ _def: {
10
+ input: infer R;
11
+ };
12
+ } ? R : never;
13
+ export declare function createUploadClient<TBucket extends Bucket<any>>(opts: {
14
+ url: string;
15
+ }): {
16
+ upload: <K extends keyof TBucket["_shape"]>(uploader: K, args: InferInput<TBucket["_shape"], K> extends never ? {
17
+ files: File[];
18
+ onClientUploadComplete?: (res: InferReturn<TBucket["_shape"], K>) => void;
19
+ } : {
20
+ files: File[];
21
+ input: z.infer<InferInput<TBucket["_shape"], K>>;
22
+ onClientUploadComplete?: (res: InferReturn<TBucket["_shape"], K>) => void;
23
+ }) => Promise<void>;
24
+ };
25
+ export {};
@@ -0,0 +1,71 @@
1
+ export function createUploadClient(opts) {
2
+ return {
3
+ upload: async (uploader, args) => {
4
+ const url = opts.url;
5
+ const searchParams = new URLSearchParams();
6
+ searchParams.append("action", "presign");
7
+ searchParams.append("uploader", String(uploader));
8
+ // get presign urls
9
+ const result = (await fetch(`${url}?${searchParams.toString()}`, {
10
+ method: "POST",
11
+ headers: {
12
+ "Content-Type": "application/json",
13
+ },
14
+ body: JSON.stringify({
15
+ files: args.files.map((f) => {
16
+ return {
17
+ name: f.name,
18
+ size: f.size,
19
+ type: f.type,
20
+ lastModified: f.lastModified,
21
+ mimeType: f.type,
22
+ };
23
+ }),
24
+ ...("input" in args ? { input: args.input } : {}),
25
+ }),
26
+ }).then((r) => r.json()));
27
+ // upload files to presign urls (sessionId required for S3 listener to update DynamoDB)
28
+ await Promise.all(args.files.map((file, index) => {
29
+ if (!result.files[index]?.url) {
30
+ throw new Error(`No presign URL returned for file index ${index}`);
31
+ }
32
+ return fetch(result.files[index].url, {
33
+ method: "PUT",
34
+ headers: {
35
+ "Content-Type": file.type,
36
+ "Content-Disposition": "inline",
37
+ },
38
+ body: file,
39
+ });
40
+ }));
41
+ // poll for completion (S3 listener Lambda updates DynamoDB when upload lands)
42
+ const pollParams = new URLSearchParams();
43
+ pollParams.append("action", "poll");
44
+ pollParams.append("uploader", String(uploader));
45
+ pollParams.append("sessionId", result.sessionId);
46
+ const pollUntilComplete = async () => {
47
+ const res = await fetch(`${url}?${pollParams.toString()}`, {
48
+ method: "POST",
49
+ headers: { "Content-Type": "application/json" },
50
+ body: JSON.stringify({}),
51
+ });
52
+ const data = (await res.json());
53
+ if (data.completed)
54
+ return data;
55
+ await new Promise((r) => setTimeout(r, 1000));
56
+ return pollUntilComplete();
57
+ };
58
+ const pollResult = await pollUntilComplete();
59
+ if (args.onClientUploadComplete && pollResult.files) {
60
+ const completedFiles = pollResult.files.filter((f) => f.status === "SUCCESS" && f.file);
61
+ const results = completedFiles.map((f) => ({
62
+ file: f.file,
63
+ metadata: f.metadata ?? {},
64
+ }));
65
+ if (results.length > 0) {
66
+ args.onClientUploadComplete(results.length === 1 ? results[0] : results);
67
+ }
68
+ }
69
+ },
70
+ };
71
+ }
@@ -0,0 +1,31 @@
1
+ import type { Context } from "hono";
2
+ import type { Bucket } from "../bucket";
3
+ export declare function createRouteHandler(bucket: Bucket<any>): (c: Context) => Promise<Response & import("hono").TypedResponse<{
4
+ files: {
5
+ url: string;
6
+ key: string;
7
+ name: string;
8
+ }[];
9
+ sessionId: string;
10
+ } | {
11
+ files: {
12
+ fileKey: string;
13
+ status: "PENDING" | "SUCCESS" | "FAILED";
14
+ file: {
15
+ path: string;
16
+ contentDisposition: string;
17
+ contentType: string;
18
+ } | undefined;
19
+ metadata: any;
20
+ }[];
21
+ completed: boolean;
22
+ } | {
23
+ type: "UPLOAD_COMPLETED";
24
+ response: "ok";
25
+ } | {
26
+ error: {
27
+ message: string;
28
+ code: string;
29
+ status: number;
30
+ };
31
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">>;
@@ -0,0 +1,11 @@
1
+ export function createRouteHandler(bucket) {
2
+ return async (c) => {
3
+ const req = c.req.raw;
4
+ const body = (await req.json());
5
+ const result = await bucket.handleUpload({
6
+ body,
7
+ request: req,
8
+ });
9
+ return c.json(result);
10
+ };
11
+ }
@@ -0,0 +1 @@
1
+ export type KnownMimeType = "application/json" | "application/xml" | "application/octet-stream" | "application/zip" | "application/gzip" | "application/x-tar" | "application/wasm" | "application/x-7z-compressed" | "application/x-sh" | "application/*" | "text/plain" | "text/csv" | "text/html" | "text/css" | "text/javascript" | "application/javascript" | "application/typescript" | "application/pdf" | "application/rtf" | "text/*" | "image/jpeg" | "image/png" | "image/gif" | "image/webp" | "image/svg+xml" | "image/bmp" | "image/tiff" | "image/avif" | "image/vnd.microsoft.icon" | "image/*" | "audio/mpeg" | "audio/wav" | "audio/ogg" | "audio/aac" | "audio/flac" | "audio/*" | "video/mp4" | "video/webm" | "video/ogg" | "video/quicktime" | "video/x-msvideo" | "video/*" | "application/msword" | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" | "application/vnd.ms-excel" | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" | "application/vnd.ms-powerpoint" | "application/vnd.openxmlformats-officedocument.presentationml.presentation" | "application/vnd.oasis.opendocument.text" | "application/vnd.oasis.opendocument.spreadsheet" | "font/otf" | "font/ttf" | "font/woff" | "font/woff2";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { FileSize } from "./types";
3
+ export declare function parseReq(req: Request | IncomingMessage): {
4
+ headers: Record<string, string>;
5
+ method: string;
6
+ url: string;
7
+ path: string;
8
+ query: URLSearchParams;
9
+ };
10
+ /**
11
+ * Converts a human-readable file size string (e.g., "10MB", "2.5GB") into bytes.
12
+ * Throws an error for invalid input formats.
13
+ * * @param size The file size represented as a number (bytes) or a string with units (KB, MB, GB, B).
14
+ * @returns The size in bytes (number).
15
+ */
16
+ export declare function parseFileSize(size: FileSize): number;
@@ -0,0 +1,69 @@
1
+ import { UploadError } from "./upload-error";
2
+ export function parseReq(req) {
3
+ if (req instanceof Request) {
4
+ const headers = {};
5
+ req.headers.forEach((value, key) => {
6
+ headers[key] = value;
7
+ });
8
+ return {
9
+ headers,
10
+ method: req.method,
11
+ url: req.url,
12
+ path: new URL(req.url).pathname,
13
+ query: new URL(req.url).searchParams,
14
+ };
15
+ }
16
+ const headers = {};
17
+ for (const [key, value] of Object.entries(req.headers)) {
18
+ if (typeof value === "string") {
19
+ headers[key] = value;
20
+ }
21
+ else if (Array.isArray(value)) {
22
+ headers[key] = value.join(", ");
23
+ }
24
+ }
25
+ const url = new URL(req.url || "", "http://localhost");
26
+ return {
27
+ headers,
28
+ method: req.method || "GET",
29
+ url: req.url || "",
30
+ path: url.pathname,
31
+ query: url.searchParams,
32
+ };
33
+ }
34
+ /**
35
+ * Converts a human-readable file size string (e.g., "10MB", "2.5GB") into bytes.
36
+ * Throws an error for invalid input formats.
37
+ * * @param size The file size represented as a number (bytes) or a string with units (KB, MB, GB, B).
38
+ * @returns The size in bytes (number).
39
+ */
40
+ export function parseFileSize(size) {
41
+ if (typeof size === "number") {
42
+ if (size < 0) {
43
+ throw UploadError.badRequest("File size cannot be negative.");
44
+ }
45
+ return size;
46
+ }
47
+ // Convert the input to uppercase to handle 'mb', 'MB', 'Mb' uniformly
48
+ const cleanSize = size.toUpperCase().trim();
49
+ // Regular expression to extract the numeric value and the unit
50
+ // Captures: (1) The number part, (2) The unit part (B, KB, MB, GB)
51
+ const match = cleanSize.match(/^(\d+(\.\d+)?)([A-Z]+)$/);
52
+ if (!match) {
53
+ throw UploadError.badRequest(`Invalid file size format: "${size}". Expected formats: "10MB", "2GB", "512KB", or raw bytes.`);
54
+ }
55
+ const numericValue = Number.parseFloat(match[1] ?? "0");
56
+ const unit = match[3] ?? "";
57
+ const byteConversionMap = {
58
+ B: 1,
59
+ KB: 1024,
60
+ MB: 1024 ** 2, // 1,048,576
61
+ GB: 1024 ** 3, // 1,073,741,824
62
+ };
63
+ const multiplier = byteConversionMap[unit];
64
+ if (!multiplier) {
65
+ throw new Error(`Invalid file size unit: "${unit}". Must be B, KB, MB, or GB.`);
66
+ }
67
+ // Return the size rounded to the nearest integer byte
68
+ return Math.round(numericValue * multiplier);
69
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * S3 event listener Lambda for ocel/blob.
3
+ * Receives ObjectCreated events, reads x-ocel-session-id from object metadata,
4
+ * and updates the UploadSession in DynamoDB to SUCCESS.
5
+ */
6
+ import type { S3Handler } from "aws-lambda";
7
+ export declare const handler: S3Handler;
@@ -0,0 +1,35 @@
1
+ import { HeadObjectCommand, S3Client } from "@aws-sdk/client-s3";
2
+ import { UpdateItemCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb";
3
+ const s3 = new S3Client({});
4
+ const dynamo = new DynamoDBClient({});
5
+ const TABLE_NAME = process.env.OCEL_TABLE_NAME || "OcelTable";
6
+ export const handler = async (event) => {
7
+ for (const record of event.Records) {
8
+ const bucket = record.s3.bucket.name;
9
+ const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));
10
+ try {
11
+ const head = await s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
12
+ const sessionId = head.Metadata?.["x-ocel-session-id"];
13
+ if (!sessionId)
14
+ continue;
15
+ const pk = `SESSION#${sessionId}`.toLowerCase();
16
+ const sk = `FILE#${bucket}#${key}`.toLowerCase();
17
+ await dynamo.send(new UpdateItemCommand({
18
+ TableName: TABLE_NAME,
19
+ Key: {
20
+ pk: { S: pk },
21
+ sk: { S: sk },
22
+ },
23
+ UpdateExpression: "SET #status = :status, updatedAt = :now",
24
+ ExpressionAttributeNames: { "#status": "status" },
25
+ ExpressionAttributeValues: {
26
+ ":status": { S: "SUCCESS" },
27
+ ":now": { S: new Date().toISOString() },
28
+ },
29
+ }));
30
+ }
31
+ catch (err) {
32
+ console.error(`Failed to update session for ${bucket}/${key}:`, err);
33
+ }
34
+ }
35
+ };
@@ -0,0 +1,128 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import z, { type ZodType } from "zod";
3
+ import type { KnownMimeType } from "./mime-types";
4
+ import type { UploadError } from "./upload-error";
5
+ type SuggestedSize = "1KB" | "100KB" | "1MB" | "10MB" | "100MB" | "1GB";
6
+ export type FileSizeUnit = "B" | "KB" | "MB" | "GB";
7
+ export type UnitAutocomplete<TUnit extends string> = `${number}${TUnit}`;
8
+ export type FileSize = SuggestedSize | UnitAutocomplete<FileSizeUnit> | number | (string & {});
9
+ export type StringWithAutocomplete<T extends string> = T | (string & {});
10
+ export type HandleUploadBody = PresignUploadBody | CallbackBody<any>;
11
+ export type HandleUploadResult = PresignUploadResponse;
12
+ export type PresignUploadBody = {
13
+ files: {
14
+ name: string;
15
+ size: number;
16
+ mimeType: string;
17
+ lastModified: number;
18
+ }[];
19
+ input?: Record<string, any>;
20
+ };
21
+ export type PresignUploadResponse = {
22
+ files: {
23
+ url: string;
24
+ key: string;
25
+ name: string;
26
+ }[];
27
+ sessionId: string;
28
+ };
29
+ export type CallbackBody<TMeta> = {
30
+ metadata: TMeta;
31
+ file: {
32
+ path: string;
33
+ contentDisposition: string;
34
+ contentType: string;
35
+ };
36
+ };
37
+ export type UploadFileBody = {
38
+ key: string;
39
+ name: string;
40
+ size: number;
41
+ mimeType: string;
42
+ disposition: string;
43
+ lastModified: number;
44
+ metadata?: Record<string, string>;
45
+ };
46
+ export type PathConfig = {
47
+ prefix?: string;
48
+ suffix?: string;
49
+ randomSuffix?: boolean;
50
+ };
51
+ export type PathContext<TMeta> = [TMeta] extends [never] ? {
52
+ /**
53
+ * File extension including the dot (e.g., .jpg, .png)
54
+ */
55
+ ext: string;
56
+ /**
57
+ * File name without extension
58
+ */
59
+ fileNameWithoutExt: string;
60
+ /**
61
+ * Original file name as uploaded by user
62
+ */
63
+ fileNameWithExt: string;
64
+ } : {
65
+ /**
66
+ * File extension including the dot (e.g., .jpg, .png)
67
+ */
68
+ ext: string;
69
+ /**
70
+ * File name without extension
71
+ */
72
+ fileNameWithoutExt: string;
73
+ /**
74
+ * Original file name as uploaded by user
75
+ */
76
+ fileNameWithExt: string;
77
+ /**
78
+ * Any metadata returned by middleware function
79
+ */
80
+ metadata: TMeta;
81
+ };
82
+ export type PathGenerator<TMeta> = (ctx: PathContext<TMeta>) => string;
83
+ export type PathInput<TMeta> = PathConfig | PathGenerator<TMeta>;
84
+ export type TUploaderConfig<TInput extends ZodType = never, TMeta = never, TReturn = unknown> = {
85
+ path?: PathInput<TMeta>;
86
+ input?: TInput;
87
+ accept?: StringWithAutocomplete<KnownMimeType>[];
88
+ limits?: {
89
+ maxFileSize?: FileSize | ((ctx: {
90
+ metadata?: TMeta;
91
+ }) => FileSize);
92
+ maxFileCount?: number | ((ctx: {
93
+ metadata?: TMeta;
94
+ }) => number);
95
+ minFileCount?: number | ((ctx: {
96
+ metadata?: TMeta;
97
+ }) => number);
98
+ };
99
+ contentDisposition?: "inline" | "attachment";
100
+ onBeforeUpload: (opts: [TInput] extends [never] ? {
101
+ files: PresignUploadBody["files"];
102
+ req: Request | IncomingMessage;
103
+ } : {
104
+ files: PresignUploadBody["files"];
105
+ input: z.infer<TInput>;
106
+ req: Request | IncomingMessage;
107
+ }) => TMeta | Promise<TMeta>;
108
+ onUploadError?: (args: {
109
+ error: UploadError;
110
+ }) => void | Promise<void>;
111
+ onUploadComplete?: (args: {
112
+ metadata: TMeta;
113
+ file: {
114
+ path: string;
115
+ contentDisposition: string;
116
+ contentType: string;
117
+ };
118
+ }) => TReturn | Promise<TReturn>;
119
+ _def: {
120
+ meta: TMeta;
121
+ input: TInput;
122
+ output: TReturn;
123
+ };
124
+ };
125
+ export type TBucketConfig<TUploaderShape> = {
126
+ uploaders: TUploaderShape;
127
+ };
128
+ export {};
@@ -0,0 +1 @@
1
+ import z, {} from "zod";
@@ -0,0 +1,35 @@
1
+ export declare class UploadError extends Error {
2
+ readonly statusCode: number;
3
+ readonly code: string;
4
+ constructor(message: string, statusCode?: number, code?: string);
5
+ /**
6
+ * 400 Bad Request
7
+ * Use when the input is invalid (e.g. file too big, wrong extension)
8
+ */
9
+ static badRequest(message?: string): UploadError;
10
+ /**
11
+ * 401 Unauthorized
12
+ * Use when the user is not logged in or missing headers
13
+ */
14
+ static unauthorized(message?: string): UploadError;
15
+ /**
16
+ * 403 Forbidden
17
+ * Use when the user is logged in but doesn't have permission (e.g. wrong plan)
18
+ */
19
+ static forbidden(message?: string): UploadError;
20
+ /**
21
+ * 404 Not Found
22
+ * Use when a resource required for the upload is missing
23
+ */
24
+ static notFound(message?: string): UploadError;
25
+ /**
26
+ * 429 Too Many Requests
27
+ * Use for rate limiting logic
28
+ */
29
+ static tooManyRequests(message?: string): UploadError;
30
+ /**
31
+ * 500 Internal Server Error
32
+ * Use for unexpected crashes or database failures
33
+ */
34
+ static internal(message?: string): UploadError;
35
+ }