@uplift-io/uplift 1.0.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 -0
- package/README.md +85 -0
- package/dist/client.cjs +102 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +9 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +75 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +275 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +78 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +236 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +155 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +15 -0
- package/dist/react.d.ts +15 -0
- package/dist/react.js +130 -0
- package/dist/react.js.map +1 -0
- package/dist/server.cjs +243 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +5 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +216 -0
- package/dist/server.js.map +1 -0
- package/dist/types-BdcszAj8.d.cts +128 -0
- package/dist/types-BdcszAj8.d.ts +128 -0
- package/package.json +74 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var UploadError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "UploadError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
toJSON() {
|
|
10
|
+
return {
|
|
11
|
+
message: this.message,
|
|
12
|
+
code: this.code
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/utils.ts
|
|
18
|
+
var sizeUnits = {
|
|
19
|
+
b: 1,
|
|
20
|
+
kb: 1024,
|
|
21
|
+
mb: 1024 * 1024,
|
|
22
|
+
gb: 1024 * 1024 * 1024
|
|
23
|
+
};
|
|
24
|
+
function extensionFor(name) {
|
|
25
|
+
const index = name.lastIndexOf(".");
|
|
26
|
+
if (index < 0 || index === name.length - 1) return void 0;
|
|
27
|
+
return name.slice(index + 1).toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
function toInputFile(file) {
|
|
30
|
+
const extension = extensionFor(file.name);
|
|
31
|
+
const input = {
|
|
32
|
+
name: file.name,
|
|
33
|
+
type: file.type,
|
|
34
|
+
size: file.size,
|
|
35
|
+
file
|
|
36
|
+
};
|
|
37
|
+
if (extension) input.extension = extension;
|
|
38
|
+
return input;
|
|
39
|
+
}
|
|
40
|
+
function defaultKey(file) {
|
|
41
|
+
const random = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
|
|
42
|
+
return `${random}/${file.name}`;
|
|
43
|
+
}
|
|
44
|
+
async function readJsonFile(file) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(await file.text());
|
|
47
|
+
} catch {
|
|
48
|
+
throw new UploadError("VALIDATION_FAILED", "Invalid JSON file.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/server.ts
|
|
53
|
+
async function handleUploadRequest(app, req) {
|
|
54
|
+
try {
|
|
55
|
+
if (req.method !== "POST") {
|
|
56
|
+
return json({ error: new UploadError("VALIDATION_FAILED", "Only POST upload requests are supported.") }, 405);
|
|
57
|
+
}
|
|
58
|
+
const routeName = routeNameFromRequest(req);
|
|
59
|
+
const route = app.routes[routeName];
|
|
60
|
+
if (!route) throw new UploadError("VALIDATION_FAILED", `Unknown upload route: ${routeName}`);
|
|
61
|
+
const user = await resolveUser(app, route._def, req);
|
|
62
|
+
const files = await filesFromRequest(req);
|
|
63
|
+
if (files.length === 0) throw new UploadError("VALIDATION_FAILED", "No files were uploaded.");
|
|
64
|
+
if (!route._def.multiple && files.length !== 1) {
|
|
65
|
+
throw new UploadError("VALIDATION_FAILED", "This route accepts exactly one file.");
|
|
66
|
+
}
|
|
67
|
+
if (route._def.multipleLimit !== void 0 && files.length > route._def.multipleLimit) {
|
|
68
|
+
throw new UploadError("VALIDATION_FAILED", `This route accepts at most ${route._def.multipleLimit} files.`);
|
|
69
|
+
}
|
|
70
|
+
const stored = [];
|
|
71
|
+
for (const item of files) {
|
|
72
|
+
const meta = await deriveMeta(route._def, req, item.input, user);
|
|
73
|
+
await validateFile(route._def, req, item, user, meta);
|
|
74
|
+
const key = route._def.key ? await route._def.key({ req, file: item.input, user, meta }) : defaultKey(item.input);
|
|
75
|
+
assertSafeStorageKey(key);
|
|
76
|
+
stored.push({
|
|
77
|
+
file: await app.storage.put({ key, file: item.input, body: item.body }),
|
|
78
|
+
meta
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const uploaded = stored.map((item) => item.file);
|
|
82
|
+
const metas = stored.map((item) => item.meta);
|
|
83
|
+
const firstUpload = uploaded[0];
|
|
84
|
+
if (!firstUpload) throw new UploadError("UPLOAD_FAILED", "Upload did not produce a result.");
|
|
85
|
+
const result = route._def.multiple ? uploaded : firstUpload;
|
|
86
|
+
if (route._def.done) {
|
|
87
|
+
if (route._def.multiple) {
|
|
88
|
+
await route._def.done({ req, files: uploaded, user, meta: metas });
|
|
89
|
+
} else {
|
|
90
|
+
await route._def.done({ req, file: firstUpload, user, meta: metas[0] });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (app.onUploadComplete) await app.onUploadComplete({ route: routeName, result, user });
|
|
94
|
+
return json({ result }, 200);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const uploadError = normalizeError(error);
|
|
97
|
+
return json({ error: uploadError }, statusFor(uploadError));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function assertSafeStorageKey(key) {
|
|
101
|
+
if (key.length === 0) {
|
|
102
|
+
throw new UploadError("VALIDATION_FAILED", "Storage key cannot be empty.");
|
|
103
|
+
}
|
|
104
|
+
if (key.includes("\0") || key.includes("\\") || key.includes("://") || key.split("/").includes("..") || /^([a-zA-Z]:)?\//.test(key)) {
|
|
105
|
+
throw new UploadError("VALIDATION_FAILED", "Storage key must be a relative object key.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function routeNameFromRequest(req) {
|
|
109
|
+
const url = new URL(req.url);
|
|
110
|
+
const explicit = url.searchParams.get("route");
|
|
111
|
+
if (explicit) return explicit;
|
|
112
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
113
|
+
return parts[parts.length - 1] ?? "";
|
|
114
|
+
}
|
|
115
|
+
async function resolveUser(app, route, req) {
|
|
116
|
+
if (route.overrideAuth) return void 0;
|
|
117
|
+
const middleware = route.auth ?? app.middleware;
|
|
118
|
+
if (!middleware) return void 0;
|
|
119
|
+
try {
|
|
120
|
+
return await middleware({ req });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : "Authentication failed.";
|
|
123
|
+
throw new UploadError("AUTH_FAILED", message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function filesFromRequest(req) {
|
|
127
|
+
const contentType = req.headers.get("content-type") ?? "";
|
|
128
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
129
|
+
throw new UploadError("VALIDATION_FAILED", "Upload requests must be multipart/form-data.");
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const form = await req.formData();
|
|
133
|
+
const files = [];
|
|
134
|
+
for (const value of form.values()) {
|
|
135
|
+
if (!(value instanceof File)) continue;
|
|
136
|
+
files.push({ input: toInputFile(value), body: value });
|
|
137
|
+
}
|
|
138
|
+
return files;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error instanceof UploadError) throw error;
|
|
141
|
+
const message = error instanceof Error ? error.message : "Upload request body could not be parsed.";
|
|
142
|
+
throw new UploadError("VALIDATION_FAILED", message);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function deriveMeta(route, req, file, user) {
|
|
146
|
+
if (!route.meta) return void 0;
|
|
147
|
+
return route.meta({ req, file, user });
|
|
148
|
+
}
|
|
149
|
+
async function validateFile(route, req, item, user, meta) {
|
|
150
|
+
if (route.maxBytes !== void 0 && item.input.size > route.maxBytes) {
|
|
151
|
+
throw new UploadError("FILE_TOO_LARGE", "File is larger than the route allows.");
|
|
152
|
+
}
|
|
153
|
+
if (route.minBytes !== void 0 && item.input.size < route.minBytes) {
|
|
154
|
+
throw new UploadError("FILE_TOO_SMALL", "File is smaller than the route allows.");
|
|
155
|
+
}
|
|
156
|
+
if (route.kind !== "any" && route.extensions && item.input.extension && !route.extensions.includes(item.input.extension)) {
|
|
157
|
+
throw new UploadError("INVALID_TYPE", "File type is not allowed.");
|
|
158
|
+
}
|
|
159
|
+
if (route.kind !== "any" && route.mimeTypes && route.mimeTypes.length > 0) {
|
|
160
|
+
const matches = route.mimeTypes.some((mime) => mime.endsWith("/") ? item.input.type.startsWith(mime) : item.input.type === mime);
|
|
161
|
+
if (!matches) throw new UploadError("INVALID_TYPE", "File MIME type is not allowed.");
|
|
162
|
+
}
|
|
163
|
+
if (route.schema) {
|
|
164
|
+
try {
|
|
165
|
+
route.schema.parse(await readJsonFile(item.body));
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof UploadError) throw error;
|
|
168
|
+
const message = error instanceof Error ? error.message : "Schema validation failed.";
|
|
169
|
+
throw new UploadError("VALIDATION_FAILED", message);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await validateConfiguredInspection(route, item.body);
|
|
173
|
+
if (route.validate) {
|
|
174
|
+
const result = await route.validate({ req, file: item.input, user, meta });
|
|
175
|
+
if (result !== true) throw new UploadError("VALIDATION_FAILED", result);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function validateConfiguredInspection(route, file) {
|
|
179
|
+
if (route.dimensionRule || route.requireSquare || route.aspectRatio) {
|
|
180
|
+
throw new UploadError("VALIDATION_FAILED", "Image dimension validation requires an image inspector and is not enabled in core.");
|
|
181
|
+
}
|
|
182
|
+
if (route.encoding) {
|
|
183
|
+
const text = await file.text();
|
|
184
|
+
if (route.encoding === "ascii" && /[^\x00-\x7F]/.test(text)) {
|
|
185
|
+
throw new UploadError("VALIDATION_FAILED", "Text file is not ASCII encoded.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (route.headers) {
|
|
189
|
+
const [headerLine = ""] = (await file.text()).split(/\r?\n/, 1);
|
|
190
|
+
const delimiter = route.delimiter ?? ",";
|
|
191
|
+
const actualHeaders = headerLine.split(delimiter).map((header) => header.trim());
|
|
192
|
+
if (route.headers.some((header, index) => actualHeaders[index] !== header)) {
|
|
193
|
+
throw new UploadError("VALIDATION_FAILED", "CSV headers do not match the route definition.");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (route.pageRule || route.encrypted !== void 0 || route.durationRule) {
|
|
197
|
+
throw new UploadError("VALIDATION_FAILED", "Rich inspection is configured but no runtime inspector has been installed for this route.");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function normalizeError(error) {
|
|
201
|
+
if (error instanceof UploadError) return error;
|
|
202
|
+
if (error instanceof Error) return new UploadError("UNKNOWN", error.message);
|
|
203
|
+
return new UploadError("UNKNOWN", "Unknown upload failure.");
|
|
204
|
+
}
|
|
205
|
+
function statusFor(error) {
|
|
206
|
+
if (error.code === "AUTH_FAILED") return 401;
|
|
207
|
+
if (error.code === "UPLOAD_FAILED" || error.code === "UNKNOWN") return 500;
|
|
208
|
+
return 400;
|
|
209
|
+
}
|
|
210
|
+
function json(body, status) {
|
|
211
|
+
return Response.json(body, { status });
|
|
212
|
+
}
|
|
213
|
+
export {
|
|
214
|
+
handleUploadRequest
|
|
215
|
+
};
|
|
216
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +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":[]}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;
|
|
2
|
+
type DurationValue = `${number}s` | `${number}m` | `${number}h`;
|
|
3
|
+
type UploadInputFile = {
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
size: number;
|
|
7
|
+
extension?: string;
|
|
8
|
+
file?: File;
|
|
9
|
+
};
|
|
10
|
+
type UploadedFile = {
|
|
11
|
+
url: string;
|
|
12
|
+
key: string;
|
|
13
|
+
name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
size: number;
|
|
16
|
+
extension?: string | undefined;
|
|
17
|
+
provider: string;
|
|
18
|
+
};
|
|
19
|
+
type UploadErrorCode = "FILE_TOO_LARGE" | "FILE_TOO_SMALL" | "INVALID_TYPE" | "AUTH_FAILED" | "VALIDATION_FAILED" | "UPLOAD_FAILED" | "UNKNOWN";
|
|
20
|
+
declare class UploadError extends Error {
|
|
21
|
+
readonly code: UploadErrorCode;
|
|
22
|
+
constructor(code: UploadErrorCode, message: string);
|
|
23
|
+
toJSON(): {
|
|
24
|
+
message: string;
|
|
25
|
+
code: UploadErrorCode;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
type StandardSchema<T = unknown> = {
|
|
29
|
+
parse(input: unknown): T;
|
|
30
|
+
};
|
|
31
|
+
type KeyContext<TAuth = unknown, TMeta = unknown> = {
|
|
32
|
+
req: Request;
|
|
33
|
+
file: UploadInputFile;
|
|
34
|
+
user: TAuth;
|
|
35
|
+
meta: TMeta;
|
|
36
|
+
};
|
|
37
|
+
type DoneContext<TAuth = unknown, TMeta = unknown, TMultiple extends boolean = false> = TMultiple extends true ? {
|
|
38
|
+
req: Request;
|
|
39
|
+
files: UploadedFile[];
|
|
40
|
+
user: TAuth;
|
|
41
|
+
meta: TMeta[];
|
|
42
|
+
} : {
|
|
43
|
+
req: Request;
|
|
44
|
+
file: UploadedFile;
|
|
45
|
+
user: TAuth;
|
|
46
|
+
meta: TMeta;
|
|
47
|
+
};
|
|
48
|
+
type StoragePutInput = {
|
|
49
|
+
key: string;
|
|
50
|
+
file: UploadInputFile;
|
|
51
|
+
body: File;
|
|
52
|
+
};
|
|
53
|
+
type StorageAdapter = {
|
|
54
|
+
provider: string;
|
|
55
|
+
put(input: StoragePutInput): Promise<UploadedFile>;
|
|
56
|
+
};
|
|
57
|
+
type Middleware<TUser = unknown> = (ctx: {
|
|
58
|
+
req: Request;
|
|
59
|
+
}) => TUser | Promise<TUser>;
|
|
60
|
+
type UploadKind = "any" | "image" | "pdf" | "video" | "audio" | "text" | "json" | "csv" | "custom";
|
|
61
|
+
type UploadRouteDefinition = {
|
|
62
|
+
kind: UploadKind;
|
|
63
|
+
maxBytes?: number;
|
|
64
|
+
minBytes?: number;
|
|
65
|
+
multiple: boolean;
|
|
66
|
+
multipleLimit?: number;
|
|
67
|
+
auth?: Middleware<unknown>;
|
|
68
|
+
overrideAuth: boolean;
|
|
69
|
+
key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;
|
|
70
|
+
meta?: (ctx: {
|
|
71
|
+
req: Request;
|
|
72
|
+
file: UploadInputFile;
|
|
73
|
+
user: unknown;
|
|
74
|
+
}) => unknown | Promise<unknown>;
|
|
75
|
+
validate?: (ctx: {
|
|
76
|
+
req: Request;
|
|
77
|
+
file: UploadInputFile;
|
|
78
|
+
user: unknown;
|
|
79
|
+
meta: unknown;
|
|
80
|
+
}) => true | string | Promise<true | string>;
|
|
81
|
+
done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;
|
|
82
|
+
extensions?: string[];
|
|
83
|
+
mimeTypes?: string[];
|
|
84
|
+
dimensionRule?: {
|
|
85
|
+
minWidth?: number;
|
|
86
|
+
minHeight?: number;
|
|
87
|
+
maxWidth?: number;
|
|
88
|
+
maxHeight?: number;
|
|
89
|
+
};
|
|
90
|
+
requireSquare?: boolean;
|
|
91
|
+
aspectRatio?: `${number}:${number}`;
|
|
92
|
+
encoding?: "utf-8" | "utf-16" | "ascii";
|
|
93
|
+
schema?: StandardSchema;
|
|
94
|
+
headers?: string[];
|
|
95
|
+
delimiter?: "," | ";" | "\t" | "|";
|
|
96
|
+
pageRule?: {
|
|
97
|
+
min?: number;
|
|
98
|
+
max?: number;
|
|
99
|
+
};
|
|
100
|
+
encrypted?: boolean;
|
|
101
|
+
durationRule?: {
|
|
102
|
+
min?: DurationValue;
|
|
103
|
+
max?: DurationValue;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
type UploadRoutes = Record<string, {
|
|
107
|
+
_def: UploadRouteDefinition;
|
|
108
|
+
}>;
|
|
109
|
+
type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {
|
|
110
|
+
storage: StorageAdapter;
|
|
111
|
+
routes: TRoutes;
|
|
112
|
+
middleware?: Middleware<unknown> | undefined;
|
|
113
|
+
onUploadComplete?: ((ctx: {
|
|
114
|
+
route: keyof TRoutes & string;
|
|
115
|
+
result: UploadedFile | UploadedFile[];
|
|
116
|
+
user: unknown;
|
|
117
|
+
}) => void | Promise<void>) | undefined;
|
|
118
|
+
};
|
|
119
|
+
type IsMultiple<TRoute> = TRoute extends {
|
|
120
|
+
__multiple?: infer TMultiple;
|
|
121
|
+
} ? TMultiple extends true ? true : false : false;
|
|
122
|
+
type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;
|
|
123
|
+
type ClientOutput<TRoute> = IsMultiple<TRoute> extends true ? UploadedFile[] : UploadedFile;
|
|
124
|
+
type UploadClient<TApp extends UpliftApp> = {
|
|
125
|
+
[TRouteName in keyof TApp["routes"] & string]: (input: ClientInput<TApp["routes"][TRouteName]>) => Promise<ClientOutput<TApp["routes"][TRouteName]>>;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export { type ClientInput as C, type DurationValue as D, type KeyContext as K, type Middleware as M, type SizeValue as S, type UploadKind as U, type UploadRouteDefinition as a, type UploadInputFile as b, type DoneContext as c, type StandardSchema as d, type UploadRoutes as e, type StorageAdapter as f, type UpliftApp as g, type ClientOutput as h, type StoragePutInput as i, type UploadClient as j, UploadError as k, type UploadErrorCode as l, type UploadedFile as m };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;
|
|
2
|
+
type DurationValue = `${number}s` | `${number}m` | `${number}h`;
|
|
3
|
+
type UploadInputFile = {
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
size: number;
|
|
7
|
+
extension?: string;
|
|
8
|
+
file?: File;
|
|
9
|
+
};
|
|
10
|
+
type UploadedFile = {
|
|
11
|
+
url: string;
|
|
12
|
+
key: string;
|
|
13
|
+
name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
size: number;
|
|
16
|
+
extension?: string | undefined;
|
|
17
|
+
provider: string;
|
|
18
|
+
};
|
|
19
|
+
type UploadErrorCode = "FILE_TOO_LARGE" | "FILE_TOO_SMALL" | "INVALID_TYPE" | "AUTH_FAILED" | "VALIDATION_FAILED" | "UPLOAD_FAILED" | "UNKNOWN";
|
|
20
|
+
declare class UploadError extends Error {
|
|
21
|
+
readonly code: UploadErrorCode;
|
|
22
|
+
constructor(code: UploadErrorCode, message: string);
|
|
23
|
+
toJSON(): {
|
|
24
|
+
message: string;
|
|
25
|
+
code: UploadErrorCode;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
type StandardSchema<T = unknown> = {
|
|
29
|
+
parse(input: unknown): T;
|
|
30
|
+
};
|
|
31
|
+
type KeyContext<TAuth = unknown, TMeta = unknown> = {
|
|
32
|
+
req: Request;
|
|
33
|
+
file: UploadInputFile;
|
|
34
|
+
user: TAuth;
|
|
35
|
+
meta: TMeta;
|
|
36
|
+
};
|
|
37
|
+
type DoneContext<TAuth = unknown, TMeta = unknown, TMultiple extends boolean = false> = TMultiple extends true ? {
|
|
38
|
+
req: Request;
|
|
39
|
+
files: UploadedFile[];
|
|
40
|
+
user: TAuth;
|
|
41
|
+
meta: TMeta[];
|
|
42
|
+
} : {
|
|
43
|
+
req: Request;
|
|
44
|
+
file: UploadedFile;
|
|
45
|
+
user: TAuth;
|
|
46
|
+
meta: TMeta;
|
|
47
|
+
};
|
|
48
|
+
type StoragePutInput = {
|
|
49
|
+
key: string;
|
|
50
|
+
file: UploadInputFile;
|
|
51
|
+
body: File;
|
|
52
|
+
};
|
|
53
|
+
type StorageAdapter = {
|
|
54
|
+
provider: string;
|
|
55
|
+
put(input: StoragePutInput): Promise<UploadedFile>;
|
|
56
|
+
};
|
|
57
|
+
type Middleware<TUser = unknown> = (ctx: {
|
|
58
|
+
req: Request;
|
|
59
|
+
}) => TUser | Promise<TUser>;
|
|
60
|
+
type UploadKind = "any" | "image" | "pdf" | "video" | "audio" | "text" | "json" | "csv" | "custom";
|
|
61
|
+
type UploadRouteDefinition = {
|
|
62
|
+
kind: UploadKind;
|
|
63
|
+
maxBytes?: number;
|
|
64
|
+
minBytes?: number;
|
|
65
|
+
multiple: boolean;
|
|
66
|
+
multipleLimit?: number;
|
|
67
|
+
auth?: Middleware<unknown>;
|
|
68
|
+
overrideAuth: boolean;
|
|
69
|
+
key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;
|
|
70
|
+
meta?: (ctx: {
|
|
71
|
+
req: Request;
|
|
72
|
+
file: UploadInputFile;
|
|
73
|
+
user: unknown;
|
|
74
|
+
}) => unknown | Promise<unknown>;
|
|
75
|
+
validate?: (ctx: {
|
|
76
|
+
req: Request;
|
|
77
|
+
file: UploadInputFile;
|
|
78
|
+
user: unknown;
|
|
79
|
+
meta: unknown;
|
|
80
|
+
}) => true | string | Promise<true | string>;
|
|
81
|
+
done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;
|
|
82
|
+
extensions?: string[];
|
|
83
|
+
mimeTypes?: string[];
|
|
84
|
+
dimensionRule?: {
|
|
85
|
+
minWidth?: number;
|
|
86
|
+
minHeight?: number;
|
|
87
|
+
maxWidth?: number;
|
|
88
|
+
maxHeight?: number;
|
|
89
|
+
};
|
|
90
|
+
requireSquare?: boolean;
|
|
91
|
+
aspectRatio?: `${number}:${number}`;
|
|
92
|
+
encoding?: "utf-8" | "utf-16" | "ascii";
|
|
93
|
+
schema?: StandardSchema;
|
|
94
|
+
headers?: string[];
|
|
95
|
+
delimiter?: "," | ";" | "\t" | "|";
|
|
96
|
+
pageRule?: {
|
|
97
|
+
min?: number;
|
|
98
|
+
max?: number;
|
|
99
|
+
};
|
|
100
|
+
encrypted?: boolean;
|
|
101
|
+
durationRule?: {
|
|
102
|
+
min?: DurationValue;
|
|
103
|
+
max?: DurationValue;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
type UploadRoutes = Record<string, {
|
|
107
|
+
_def: UploadRouteDefinition;
|
|
108
|
+
}>;
|
|
109
|
+
type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {
|
|
110
|
+
storage: StorageAdapter;
|
|
111
|
+
routes: TRoutes;
|
|
112
|
+
middleware?: Middleware<unknown> | undefined;
|
|
113
|
+
onUploadComplete?: ((ctx: {
|
|
114
|
+
route: keyof TRoutes & string;
|
|
115
|
+
result: UploadedFile | UploadedFile[];
|
|
116
|
+
user: unknown;
|
|
117
|
+
}) => void | Promise<void>) | undefined;
|
|
118
|
+
};
|
|
119
|
+
type IsMultiple<TRoute> = TRoute extends {
|
|
120
|
+
__multiple?: infer TMultiple;
|
|
121
|
+
} ? TMultiple extends true ? true : false : false;
|
|
122
|
+
type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;
|
|
123
|
+
type ClientOutput<TRoute> = IsMultiple<TRoute> extends true ? UploadedFile[] : UploadedFile;
|
|
124
|
+
type UploadClient<TApp extends UpliftApp> = {
|
|
125
|
+
[TRouteName in keyof TApp["routes"] & string]: (input: ClientInput<TApp["routes"][TRouteName]>) => Promise<ClientOutput<TApp["routes"][TRouteName]>>;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export { type ClientInput as C, type DurationValue as D, type KeyContext as K, type Middleware as M, type SizeValue as S, type UploadKind as U, type UploadRouteDefinition as a, type UploadInputFile as b, type DoneContext as c, type StandardSchema as d, type UploadRoutes as e, type StorageAdapter as f, type UpliftApp as g, type ClientOutput as h, type StoragePutInput as i, type UploadClient as j, UploadError as k, type UploadErrorCode as l, type UploadedFile as m };
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uplift-io/uplift",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Dead-simple, type-safe file handling for TypeScript applications.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://itzfeminisce.github.io/uplift/",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Itzfeminisce/uplift.git",
|
|
10
|
+
"directory": "packages/uplift"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Itzfeminisce/uplift/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"upload",
|
|
17
|
+
"file-upload",
|
|
18
|
+
"typescript",
|
|
19
|
+
"multipart",
|
|
20
|
+
"s3",
|
|
21
|
+
"r2",
|
|
22
|
+
"nextjs",
|
|
23
|
+
"hono",
|
|
24
|
+
"express",
|
|
25
|
+
"react"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.cjs",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js",
|
|
35
|
+
"require": "./dist/index.cjs"
|
|
36
|
+
},
|
|
37
|
+
"./client": {
|
|
38
|
+
"types": "./dist/client.d.ts",
|
|
39
|
+
"import": "./dist/client.js",
|
|
40
|
+
"require": "./dist/client.cjs"
|
|
41
|
+
},
|
|
42
|
+
"./react": {
|
|
43
|
+
"types": "./dist/react.d.ts",
|
|
44
|
+
"import": "./dist/react.js",
|
|
45
|
+
"require": "./dist/react.cjs"
|
|
46
|
+
},
|
|
47
|
+
"./server": {
|
|
48
|
+
"types": "./dist/server.d.ts",
|
|
49
|
+
"import": "./dist/server.js",
|
|
50
|
+
"require": "./dist/server.cjs"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist",
|
|
55
|
+
"README.md",
|
|
56
|
+
"LICENSE"
|
|
57
|
+
],
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"react": ">=18.0.0"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"react": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"react": "^19.0.0"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"build": "tsup",
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"typecheck": "tsc --noEmit"
|
|
73
|
+
}
|
|
74
|
+
}
|