uploaderkit 1.0.0 → 2.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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +16 -0
  3. package/README.es.md +1140 -0
  4. package/README.md +1135 -0
  5. package/dist/FileViewer-ChTf7-uY.d.cts +49 -0
  6. package/dist/FileViewer-nuD-GEdJ.d.ts +49 -0
  7. package/dist/adapters/gcs.cjs +83 -0
  8. package/dist/adapters/gcs.cjs.map +1 -0
  9. package/dist/adapters/gcs.d.cts +31 -0
  10. package/dist/adapters/gcs.d.ts +31 -0
  11. package/dist/adapters/gcs.js +81 -0
  12. package/dist/adapters/gcs.js.map +1 -0
  13. package/dist/adapters/memory.cjs +27 -0
  14. package/dist/adapters/memory.cjs.map +1 -0
  15. package/dist/adapters/memory.d.cts +18 -0
  16. package/dist/adapters/memory.d.ts +18 -0
  17. package/dist/adapters/memory.js +25 -0
  18. package/dist/adapters/memory.js.map +1 -0
  19. package/dist/adapters/s3.cjs +75 -0
  20. package/dist/adapters/s3.cjs.map +1 -0
  21. package/dist/adapters/s3.d.cts +27 -0
  22. package/dist/adapters/s3.d.ts +27 -0
  23. package/dist/adapters/s3.js +73 -0
  24. package/dist/adapters/s3.js.map +1 -0
  25. package/dist/chunk-3FI44IOW.js +150 -0
  26. package/dist/chunk-3FI44IOW.js.map +1 -0
  27. package/dist/chunk-H7BRW5IY.js +308 -0
  28. package/dist/chunk-H7BRW5IY.js.map +1 -0
  29. package/dist/chunk-PDKAF4GX.js +707 -0
  30. package/dist/chunk-PDKAF4GX.js.map +1 -0
  31. package/dist/chunk-PTEX7F4R.js +309 -0
  32. package/dist/chunk-PTEX7F4R.js.map +1 -0
  33. package/dist/chunk-T5YZG6JW.js +511 -0
  34. package/dist/chunk-T5YZG6JW.js.map +1 -0
  35. package/dist/index.cjs +220 -27
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +57 -170
  38. package/dist/index.d.ts +57 -170
  39. package/dist/index.js +1 -324
  40. package/dist/index.js.map +1 -1
  41. package/dist/presets.cjs +1665 -0
  42. package/dist/presets.cjs.map +1 -0
  43. package/dist/presets.d.cts +100 -0
  44. package/dist/presets.d.ts +100 -0
  45. package/dist/presets.js +405 -0
  46. package/dist/presets.js.map +1 -0
  47. package/dist/react.cjs +981 -0
  48. package/dist/react.cjs.map +1 -0
  49. package/dist/react.d.cts +106 -0
  50. package/dist/react.d.ts +106 -0
  51. package/dist/react.js +5 -0
  52. package/dist/react.js.map +1 -0
  53. package/dist/server/express.cjs +179 -0
  54. package/dist/server/express.cjs.map +1 -0
  55. package/dist/server/express.d.cts +64 -0
  56. package/dist/server/express.d.ts +64 -0
  57. package/dist/server/express.js +111 -0
  58. package/dist/server/express.js.map +1 -0
  59. package/dist/server/next.cjs +165 -0
  60. package/dist/server/next.cjs.map +1 -0
  61. package/dist/server/next.d.cts +38 -0
  62. package/dist/server/next.d.ts +38 -0
  63. package/dist/server/next.js +107 -0
  64. package/dist/server/next.js.map +1 -0
  65. package/dist/server.cjs +550 -0
  66. package/dist/server.cjs.map +1 -0
  67. package/dist/server.d.cts +19 -0
  68. package/dist/server.d.ts +19 -0
  69. package/dist/server.js +80 -0
  70. package/dist/server.js.map +1 -0
  71. package/dist/storage-CYkSHWZX.d.cts +133 -0
  72. package/dist/storage-Qc9epG0G.d.ts +133 -0
  73. package/dist/types-BSlJJwti.d.cts +341 -0
  74. package/dist/types-BSlJJwti.d.ts +341 -0
  75. package/dist/ui.cjs +2325 -0
  76. package/dist/ui.cjs.map +1 -0
  77. package/dist/ui.d.cts +331 -0
  78. package/dist/ui.d.ts +331 -0
  79. package/dist/ui.js +749 -0
  80. package/dist/ui.js.map +1 -0
  81. package/dist/useSlottedUploader-BzT5jV8c.d.cts +139 -0
  82. package/dist/useSlottedUploader-IkG5RhEO.d.ts +139 -0
  83. package/dist/useUploader-BiBdS-7y.d.cts +117 -0
  84. package/dist/useUploader-CQHpj_oI.d.ts +117 -0
  85. package/package.json +189 -3
  86. package/tailwind.css +80 -0
@@ -0,0 +1,511 @@
1
+ // src/constants.ts
2
+ var KB = 1024;
3
+ var MB = 1024 * KB;
4
+ var GB = 1024 * MB;
5
+ var MAGIC_NUMBERS = {
6
+ png: [137, 80, 78, 71, 13, 10, 26, 10],
7
+ jpg: [255, 216, 255],
8
+ jpeg: [255, 216, 255],
9
+ gif: [71, 73, 70, 56],
10
+ webp: [82, 73, 70, 70],
11
+ // RIFF; bytes 8-11 spell WEBP
12
+ ico: [0, 0, 1, 0],
13
+ pdf: [37, 80, 68, 70],
14
+ // %PDF
15
+ // OOXML files are ZIP containers, so docx/xlsx share the PK signature.
16
+ docx: [80, 75, 3, 4],
17
+ xlsx: [80, 75, 3, 4],
18
+ doc: [208, 207, 17, 224],
19
+ xls: [208, 207, 17, 224],
20
+ // DER/ASN.1 sequence header.
21
+ cer: [48, 130],
22
+ crt: [48, 130],
23
+ key: [48, 130],
24
+ pem: [45, 45, 45, 45, 45],
25
+ // -----
26
+ webm: [26, 69, 223, 163],
27
+ avi: [82, 73, 70, 70],
28
+ mp3: [73, 68, 51],
29
+ // ID3
30
+ wav: [82, 73, 70, 70],
31
+ ogg: [79, 103, 103, 83],
32
+ aac: [255, 241]
33
+ };
34
+ var MIME_TYPES = {
35
+ png: "image/png",
36
+ jpg: "image/jpeg",
37
+ jpeg: "image/jpeg",
38
+ svg: "image/svg+xml",
39
+ webp: "image/webp",
40
+ gif: "image/gif",
41
+ ico: "image/x-icon",
42
+ heic: "image/heic",
43
+ cer: "application/x-x509-ca-cert",
44
+ crt: "application/x-x509-ca-cert",
45
+ pem: "application/x-pem-file",
46
+ key: "application/x-pem-file",
47
+ pdf: "application/pdf",
48
+ doc: "application/msword",
49
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
50
+ xls: "application/vnd.ms-excel",
51
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
52
+ csv: "text/csv",
53
+ txt: "text/plain",
54
+ xml: "application/xml",
55
+ json: "application/json",
56
+ mp4: "video/mp4",
57
+ mov: "video/quicktime",
58
+ avi: "video/x-msvideo",
59
+ webm: "video/webm",
60
+ mp3: "audio/mpeg",
61
+ wav: "audio/wav",
62
+ ogg: "audio/ogg",
63
+ aac: "audio/aac"
64
+ };
65
+ var FILE_CATEGORY_CONFIG = {
66
+ image: {
67
+ extensions: ["png", "jpg", "jpeg", "svg", "webp", "gif"],
68
+ maxBytes: 5 * MB,
69
+ validateMagicNumbers: true,
70
+ label: "image",
71
+ accept: "image/*"
72
+ },
73
+ certificate: {
74
+ extensions: ["cer", "crt", "pem"],
75
+ maxBytes: 50 * KB,
76
+ validateMagicNumbers: true,
77
+ label: "certificate",
78
+ accept: ".cer,.crt,.pem"
79
+ },
80
+ key: {
81
+ extensions: ["key"],
82
+ maxBytes: 50 * KB,
83
+ validateMagicNumbers: true,
84
+ label: "private key",
85
+ accept: ".key"
86
+ },
87
+ pdf: {
88
+ extensions: ["pdf"],
89
+ maxBytes: 20 * MB,
90
+ validateMagicNumbers: true,
91
+ label: "PDF",
92
+ accept: ".pdf"
93
+ },
94
+ document: {
95
+ extensions: ["pdf", "doc", "docx", "xls", "xlsx", "csv", "txt"],
96
+ maxBytes: 20 * MB,
97
+ validateMagicNumbers: false,
98
+ label: "document",
99
+ accept: ".pdf,.doc,.docx,.xls,.xlsx,.csv,.txt"
100
+ },
101
+ data: {
102
+ extensions: ["xml", "json"],
103
+ maxBytes: 5 * MB,
104
+ validateMagicNumbers: false,
105
+ label: "data file",
106
+ accept: ".xml,.json"
107
+ },
108
+ video: {
109
+ extensions: ["mp4", "mov", "avi", "webm"],
110
+ maxBytes: 100 * MB,
111
+ validateMagicNumbers: false,
112
+ label: "video",
113
+ accept: "video/*"
114
+ },
115
+ audio: {
116
+ extensions: ["mp3", "wav", "ogg", "aac"],
117
+ maxBytes: 20 * MB,
118
+ validateMagicNumbers: false,
119
+ label: "audio",
120
+ accept: "audio/*"
121
+ },
122
+ any: {
123
+ extensions: [],
124
+ maxBytes: 20 * MB,
125
+ validateMagicNumbers: false,
126
+ label: "file",
127
+ accept: "*/*"
128
+ }
129
+ };
130
+
131
+ // src/file.ts
132
+ var getFileExtension = (fileName) => {
133
+ const dot = fileName.lastIndexOf(".");
134
+ if (dot < 1 || dot === fileName.length - 1) return "";
135
+ return fileName.slice(dot + 1).toLowerCase();
136
+ };
137
+ var getMimeType = (fileName) => {
138
+ const ext = getFileExtension(fileName);
139
+ return MIME_TYPES[ext] ?? "application/octet-stream";
140
+ };
141
+ var isKnownExtension = (value) => value in MIME_TYPES;
142
+ var formatFileSize = (bytes) => {
143
+ if (bytes < 1024) return `${bytes} B`;
144
+ const units = ["KB", "MB", "GB", "TB"];
145
+ let size = bytes / 1024;
146
+ let unit = 0;
147
+ while (size >= 1024 && unit < units.length - 1) {
148
+ size /= 1024;
149
+ unit++;
150
+ }
151
+ return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;
152
+ };
153
+ var MAX_STEM_LENGTH = 80;
154
+ var FALLBACK_STEM = "file";
155
+ var slugSegment = (value) => value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
156
+ var sanitizeFileName = (fileName) => {
157
+ const base = fileName.split(/[/\\]/).pop() ?? "";
158
+ const dot = base.lastIndexOf(".");
159
+ const hasExtension = dot > 0;
160
+ const stem = slugSegment(hasExtension ? base.slice(0, dot) : base).slice(
161
+ 0,
162
+ MAX_STEM_LENGTH
163
+ );
164
+ const extension = hasExtension ? slugSegment(base.slice(dot + 1)) : "";
165
+ const safeStem = stem.replace(/-+$/, "") || FALLBACK_STEM;
166
+ return extension ? `${safeStem}.${extension}` : safeStem;
167
+ };
168
+ var toAcceptAttribute = (extensions) => extensions.length === 0 ? "*/*" : extensions.map((extension) => `.${extension}`).join(",");
169
+ var fromMulterFile = (file) => ({
170
+ name: file.originalname,
171
+ size: file.size,
172
+ type: file.mimetype,
173
+ arrayBuffer: async () => file.buffer.buffer.slice(
174
+ file.buffer.byteOffset,
175
+ file.buffer.byteOffset + file.buffer.byteLength
176
+ )
177
+ });
178
+
179
+ // src/labels.ts
180
+ var ES_LABELS = {
181
+ dropPrompt: "Selecciona un archivo o arr\xE1stralo aqu\xED",
182
+ tapPrompt: "Toca para elegir un archivo",
183
+ bulkDropPrompt: "Arrastra varios archivos \u2014 cada uno cae en su documento",
184
+ bulkTapPrompt: "Toca para agregar varios archivos",
185
+ uploadAll: "Subir archivos",
186
+ view: "Ver",
187
+ remove: "Quitar",
188
+ cancel: "Cancelar",
189
+ ready: "Listo",
190
+ staged: "Listo para subir",
191
+ upload: "Subir",
192
+ replace: "Reemplazar",
193
+ dropToReplace: "Suelta para reemplazar",
194
+ previous: "Anterior",
195
+ next: "Siguiente",
196
+ uploadFailed: "No se pudo subir el archivo",
197
+ uploadNetworkFailed: "No se pudo subir el archivo. Revisa tu conexi\xF3n",
198
+ removeFailed: "No se pudo borrar el archivo del almacenamiento",
199
+ readFailed: "No se pudo cargar el archivo",
200
+ maxFilesReached: (max) => `M\xE1ximo ${max} archivo(s)`,
201
+ slotFormatNotAllowed: (slotLabel, extensions) => `${slotLabel}: formato no permitido. Se aceptan: ${extensions.join(", ")}`,
202
+ fileWithoutSlot: (fileName) => `"${fileName}" no corresponde a ning\xFAn documento`,
203
+ viewerLoading: "Cargando\u2026",
204
+ viewerError: "No se pudo cargar la vista previa",
205
+ viewerErrorHint: "El archivo puede haberse movido o el enlace expir\xF3. Puedes reintentar o descargarlo.",
206
+ viewerRetry: "Reintentar",
207
+ openInTab: "Abrir en pesta\xF1a",
208
+ downloadShort: "Descargar",
209
+ close: "Cerrar",
210
+ noPreview: "Este formato no tiene vista previa",
211
+ download: (fileName) => `Descargar ${fileName}`,
212
+ filePreview: "Archivo",
213
+ confirm: "Confirmar",
214
+ confirmRemoveTitle: "Quitar archivo",
215
+ confirmRemoveMessage: (fileName) => `\xBFQuitar "${fileName}"? Podr\xE1s subir otro despu\xE9s.`,
216
+ confirmReplaceTitle: "Reemplazar archivo",
217
+ confirmReplaceMessage: (current, incoming) => `\xBFReemplazar "${current}" por "${incoming}"?`,
218
+ dropAnywhereTitle: "Suelta los archivos aqu\xED",
219
+ dropAnywhereHint: "Cualquier lugar de la pantalla cuenta",
220
+ avatarChange: "Cambiar foto",
221
+ avatarDropHere: "Suelta aqu\xED",
222
+ avatarRemove: "Quitar foto",
223
+ galleryAdd: "Agregar",
224
+ fileHasNoExtension: "El archivo no tiene extensi\xF3n",
225
+ formatNotAllowed: (extensions) => `Formato no permitido. Se aceptan: ${extensions.join(", ")}`,
226
+ fileIsEmpty: "El archivo est\xE1 vac\xEDo",
227
+ fileTooLarge: (size, max) => `El archivo pesa ${size} y el m\xE1ximo es ${max}`,
228
+ fileIncomplete: "El archivo est\xE1 incompleto",
229
+ contentDoesNotMatchExtension: "El contenido del archivo no corresponde a su extensi\xF3n",
230
+ requestIncomplete: "Solicitud incompleta",
231
+ unauthorized: "No autorizado",
232
+ fileRequired: "Archivo requerido",
233
+ unknownScope: "Destino de archivo no v\xE1lido",
234
+ processingFailed: "No se pudo procesar el archivo",
235
+ unnamedFile: "archivo"
236
+ };
237
+ var DEFAULT_LABELS = {
238
+ dropPrompt: "Choose a file or drag it here",
239
+ tapPrompt: "Tap to choose a file",
240
+ bulkDropPrompt: "Drop several files \u2014 each lands on its document",
241
+ bulkTapPrompt: "Tap to add several files",
242
+ uploadAll: "Upload files",
243
+ view: "View",
244
+ remove: "Remove",
245
+ cancel: "Cancel",
246
+ ready: "Done",
247
+ staged: "Ready to upload",
248
+ upload: "Upload",
249
+ replace: "Replace",
250
+ dropToReplace: "Drop to replace",
251
+ previous: "Previous",
252
+ next: "Next",
253
+ uploadFailed: "The file could not be uploaded",
254
+ uploadNetworkFailed: "The file could not be uploaded. Check your connection",
255
+ removeFailed: "The file could not be removed from storage",
256
+ readFailed: "The file could not be loaded",
257
+ maxFilesReached: (max) => `At most ${max} file(s)`,
258
+ slotFormatNotAllowed: (slotLabel, extensions) => `${slotLabel}: format not allowed. Accepted: ${extensions.join(", ")}`,
259
+ fileWithoutSlot: (fileName) => `"${fileName}" does not match any document`,
260
+ viewerLoading: "Loading\u2026",
261
+ viewerError: "The preview could not be loaded",
262
+ viewerErrorHint: "The file may have moved or the link expired. You can retry or download it.",
263
+ viewerRetry: "Retry",
264
+ openInTab: "Open in tab",
265
+ downloadShort: "Download",
266
+ close: "Close",
267
+ noPreview: "This format has no preview",
268
+ download: (fileName) => `Download ${fileName}`,
269
+ filePreview: "File",
270
+ confirm: "Confirm",
271
+ confirmRemoveTitle: "Remove file",
272
+ confirmRemoveMessage: (fileName) => `Remove "${fileName}"? You can upload another one later.`,
273
+ confirmReplaceTitle: "Replace file",
274
+ confirmReplaceMessage: (current, incoming) => `Replace "${current}" with "${incoming}"?`,
275
+ dropAnywhereTitle: "Drop the files here",
276
+ dropAnywhereHint: "Anywhere on the screen counts",
277
+ avatarChange: "Change photo",
278
+ avatarDropHere: "Drop here",
279
+ avatarRemove: "Remove photo",
280
+ galleryAdd: "Add",
281
+ fileHasNoExtension: "The file has no extension",
282
+ formatNotAllowed: (extensions) => `Format not allowed. Accepted: ${extensions.join(", ")}`,
283
+ fileIsEmpty: "The file is empty",
284
+ fileTooLarge: (size, max) => `The file weighs ${size} and the maximum is ${max}`,
285
+ fileIncomplete: "The file is incomplete",
286
+ contentDoesNotMatchExtension: "The file contents do not match its extension",
287
+ requestIncomplete: "Incomplete request",
288
+ unauthorized: "Unauthorized",
289
+ fileRequired: "A file is required",
290
+ unknownScope: "Unknown file destination",
291
+ processingFailed: "The file could not be processed",
292
+ unnamedFile: "file"
293
+ };
294
+ var resolveLabels = (labels, base = DEFAULT_LABELS) => labels ? { ...base, ...labels } : base;
295
+ var EN_LABELS = { ...DEFAULT_LABELS };
296
+
297
+ // src/validation.ts
298
+ var ok = { valid: true };
299
+ var fail = (code, message) => ({ valid: false, code, message });
300
+ var validateExtension = (fileName, allowed, labels) => {
301
+ if (allowed.length === 0) return ok;
302
+ const copy = resolveLabels(labels);
303
+ const extension = getFileExtension(fileName);
304
+ if (!extension) {
305
+ return fail("extension-not-allowed", copy.fileHasNoExtension);
306
+ }
307
+ if (!allowed.includes(extension)) {
308
+ return fail("extension-not-allowed", copy.formatNotAllowed(allowed));
309
+ }
310
+ return ok;
311
+ };
312
+ var validateSize = (size, maxBytes, labels) => {
313
+ const copy = resolveLabels(labels);
314
+ if (size === 0) return fail("empty-file", copy.fileIsEmpty);
315
+ if (size > maxBytes) {
316
+ return fail(
317
+ "too-large",
318
+ copy.fileTooLarge(formatFileSize(size), formatFileSize(maxBytes))
319
+ );
320
+ }
321
+ return ok;
322
+ };
323
+ var validateMagicNumbers = async (file, labels) => {
324
+ const copy = resolveLabels(labels);
325
+ const extension = getFileExtension(file.name);
326
+ if (!isKnownExtension(extension)) return ok;
327
+ const expected = MAGIC_NUMBERS[extension];
328
+ if (!expected) return ok;
329
+ const head = new Uint8Array(await file.arrayBuffer()).slice(
330
+ 0,
331
+ expected.length
332
+ );
333
+ if (head.length < expected.length) {
334
+ return fail("magic-number-mismatch", copy.fileIncomplete);
335
+ }
336
+ const matches = expected.every((byte, index) => head[index] === byte);
337
+ return matches ? ok : fail("magic-number-mismatch", copy.contentDoesNotMatchExtension);
338
+ };
339
+ var validateFile = async (file, options = {}) => {
340
+ const {
341
+ maxBytes = Infinity,
342
+ allowedExtensions = [],
343
+ validateMagicNumbers: checkMagic = false,
344
+ labels,
345
+ customValidation
346
+ } = options;
347
+ const size = validateSize(file.size, maxBytes, labels);
348
+ if (!size.valid) return size;
349
+ const extension = validateExtension(file.name, allowedExtensions, labels);
350
+ if (!extension.valid) return extension;
351
+ if (checkMagic) {
352
+ const magic = await validateMagicNumbers(file, labels);
353
+ if (!magic.valid) return magic;
354
+ }
355
+ if (customValidation) return customValidation(file);
356
+ return ok;
357
+ };
358
+ var validateFiles = async (files, options = {}) => Promise.all(
359
+ files.map(async (file) => ({
360
+ file,
361
+ result: await validateFile(file, options)
362
+ }))
363
+ );
364
+
365
+ // src/scopes.ts
366
+ var ScopeError = class extends Error {
367
+ name = "ScopeError";
368
+ };
369
+ var PROBE_ENTITY = "__entity__";
370
+ var probeFile = (name) => ({
371
+ name,
372
+ size: 0,
373
+ type: "",
374
+ arrayBuffer: async () => new ArrayBuffer(0)
375
+ });
376
+ var hasStableKey = (scope) => scope.path(PROBE_ENTITY, probeFile("a.png")) === scope.path(PROBE_ENTITY, probeFile("b.png"));
377
+ var resolveReplaceMode = (scope) => {
378
+ if (scope.replace !== void 0) return scope.replace;
379
+ if ((scope.maxFiles ?? 1) > 1) return "key";
380
+ return hasStableKey(scope) ? "key" : "entity";
381
+ };
382
+ var dirname = (key) => {
383
+ const cut = key.lastIndexOf("/");
384
+ return cut === -1 ? "" : key.slice(0, cut);
385
+ };
386
+ var resolveScopePrefix = (scope, entityId) => scope.prefix?.(entityId) ?? dirname(scope.path(entityId, probeFile("probe.bin")));
387
+ var containsPath = (a, b) => b === a || b.startsWith(`${a}/`);
388
+ var assertDefinition = (name, scope) => {
389
+ if (typeof scope.path !== "function") {
390
+ throw new ScopeError(`Scope "${name}": "path" must be a function`);
391
+ }
392
+ if (!Number.isFinite(scope.maxBytes) || scope.maxBytes <= 0) {
393
+ throw new ScopeError(
394
+ `Scope "${name}": "maxBytes" must be a positive number`
395
+ );
396
+ }
397
+ if (scope.accept.length === 0) {
398
+ throw new ScopeError(
399
+ `Scope "${name}": "accept" cannot be empty \u2014 list the extensions this scope takes`
400
+ );
401
+ }
402
+ if (scope.category && scope.category !== "any") {
403
+ const allowed = FILE_CATEGORY_CONFIG[scope.category].extensions;
404
+ const outside = scope.accept.filter(
405
+ (extension) => !allowed.includes(extension)
406
+ );
407
+ if (outside.length > 0) {
408
+ throw new ScopeError(
409
+ `Scope "${name}": ${outside.join(", ")} not allowed by category "${scope.category}"`
410
+ );
411
+ }
412
+ }
413
+ if (scope.compress) {
414
+ const imageOnly = FILE_CATEGORY_CONFIG.image.extensions;
415
+ const notImages = scope.accept.filter(
416
+ (extension) => !imageOnly.includes(extension)
417
+ );
418
+ if (notImages.length > 0) {
419
+ throw new ScopeError(
420
+ `Scope "${name}": "compress" only applies to images, but it accepts ${notImages.join(", ")}`
421
+ );
422
+ }
423
+ }
424
+ if (scope.maxFiles !== void 0) {
425
+ if (!Number.isInteger(scope.maxFiles) || scope.maxFiles < 1) {
426
+ throw new ScopeError(
427
+ `Scope "${name}": "maxFiles" must be a positive integer, received ${String(scope.maxFiles)}`
428
+ );
429
+ }
430
+ }
431
+ if (scope.replace === "entity" && (scope.maxFiles ?? 1) > 1) {
432
+ throw new ScopeError(
433
+ `Scope "${name}": replace "entity" contradicts maxFiles ${scope.maxFiles} \u2014 an entity-wide replace would delete the other files on the next upload. Use 'key' to overwrite one slot, or false to keep every version.`
434
+ );
435
+ }
436
+ if (resolveReplaceMode(scope) === "entity" && resolveScopePrefix(scope, PROBE_ENTITY) === "") {
437
+ throw new ScopeError(
438
+ `Scope "${name}": replace "entity" needs a folder to sweep, but "path" resolves to a bucket-root key. Nest the key under a folder, or declare "prefix".`
439
+ );
440
+ }
441
+ };
442
+ var assertNoSweepCollision = (scopes) => {
443
+ const entries = Object.entries(scopes);
444
+ for (const [name, scope] of entries) {
445
+ if (resolveReplaceMode(scope) !== "entity") continue;
446
+ const prefix = resolveScopePrefix(scope, PROBE_ENTITY);
447
+ for (const [otherName, other] of entries) {
448
+ if (otherName === name) continue;
449
+ const otherKey = other.path(PROBE_ENTITY, probeFile("probe.bin"));
450
+ if (!containsPath(prefix, otherKey)) continue;
451
+ throw new ScopeError(
452
+ `Scope "${name}" replaces per entity and would sweep "${prefix}", which also holds scope "${otherName}" \u2014 an upload to one would delete the other's files. Give each its own folder, or declare a narrower "prefix".`
453
+ );
454
+ }
455
+ }
456
+ };
457
+ var defineScopes = (scopes) => {
458
+ for (const [name, scope] of Object.entries(scopes)) {
459
+ assertDefinition(name, scope);
460
+ }
461
+ assertNoSweepCollision(scopes);
462
+ const names = Object.keys(scopes);
463
+ const get = (name) => {
464
+ const scope = scopes[name];
465
+ if (!scope) {
466
+ throw new ScopeError(
467
+ `Unknown scope "${name}". Declared: ${names.join(", ")}`
468
+ );
469
+ }
470
+ return scope;
471
+ };
472
+ return {
473
+ scopes,
474
+ names,
475
+ get,
476
+ has: (name) => name in scopes,
477
+ accept: (name) => toAcceptAttribute(get(name).accept)
478
+ };
479
+ };
480
+ var validateForScope = async (registry, name, file, labels) => {
481
+ const scope = registry.get(name);
482
+ const category = scope.category ? FILE_CATEGORY_CONFIG[scope.category] : void 0;
483
+ return validateFile(file, {
484
+ maxBytes: scope.maxBytes,
485
+ allowedExtensions: scope.accept,
486
+ validateMagicNumbers: category?.validateMagicNumbers ?? true,
487
+ labels
488
+ });
489
+ };
490
+ var assertProviderSupports = (registry, provider) => {
491
+ const needsSigning = registry.names.filter(
492
+ (name) => registry.get(name).visibility === "private"
493
+ );
494
+ if (needsSigning.length > 0 && !provider.capabilities.signedUrl) {
495
+ throw new ScopeError(
496
+ `Provider "${provider.name}" cannot sign URLs, but these scopes are private: ${needsSigning.join(", ")}`
497
+ );
498
+ }
499
+ };
500
+ var escapesPrefix = (key) => key.startsWith("/") || key.split("/").some((segment) => segment === ".." || segment === ".");
501
+ var resolveKey = (registry, name, entityId, file) => {
502
+ const key = registry.get(name).path(entityId, file);
503
+ if (escapesPrefix(key)) {
504
+ throw new ScopeError(`Scope "${name}" produced an unsafe key: ${key}`);
505
+ }
506
+ return key;
507
+ };
508
+
509
+ export { DEFAULT_LABELS, EN_LABELS, ES_LABELS, FILE_CATEGORY_CONFIG, GB, KB, MAGIC_NUMBERS, MB, MIME_TYPES, ScopeError, assertProviderSupports, defineScopes, formatFileSize, fromMulterFile, getFileExtension, getMimeType, hasStableKey, isKnownExtension, resolveKey, resolveLabels, resolveReplaceMode, resolveScopePrefix, sanitizeFileName, toAcceptAttribute, validateExtension, validateFile, validateFiles, validateForScope, validateMagicNumbers, validateSize };
510
+ //# sourceMappingURL=chunk-T5YZG6JW.js.map
511
+ //# sourceMappingURL=chunk-T5YZG6JW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/file.ts","../src/labels.ts","../src/validation.ts","../src/scopes.ts"],"names":[],"mappings":";AAEO,IAAM,EAAA,GAAK;AACX,IAAM,KAAK,IAAA,GAAO;AAClB,IAAM,KAAK,IAAA,GAAO;AAOlB,IAAM,aAAA,GAA0D;AAAA,EACtE,GAAA,EAAK,CAAC,GAAA,EAAM,EAAA,EAAM,IAAM,EAAA,EAAM,EAAA,EAAM,EAAA,EAAM,EAAA,EAAM,EAAI,CAAA;AAAA,EACpD,GAAA,EAAK,CAAC,GAAA,EAAM,GAAA,EAAM,GAAI,CAAA;AAAA,EACtB,IAAA,EAAM,CAAC,GAAA,EAAM,GAAA,EAAM,GAAI,CAAA;AAAA,EACvB,GAAA,EAAK,CAAC,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA,EAC5B,IAAA,EAAM,CAAC,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA;AAAA,EAC7B,GAAA,EAAK,CAAC,CAAA,EAAM,CAAA,EAAM,GAAM,CAAI,CAAA;AAAA,EAE5B,GAAA,EAAK,CAAC,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA;AAAA;AAAA,EAG5B,IAAA,EAAM,CAAC,EAAA,EAAM,EAAA,EAAM,GAAM,CAAI,CAAA;AAAA,EAC7B,IAAA,EAAM,CAAC,EAAA,EAAM,EAAA,EAAM,GAAM,CAAI,CAAA;AAAA,EAC7B,GAAA,EAAK,CAAC,GAAA,EAAM,GAAA,EAAM,IAAM,GAAI,CAAA;AAAA,EAC5B,GAAA,EAAK,CAAC,GAAA,EAAM,GAAA,EAAM,IAAM,GAAI,CAAA;AAAA;AAAA,EAG5B,GAAA,EAAK,CAAC,EAAA,EAAM,GAAI,CAAA;AAAA,EAChB,GAAA,EAAK,CAAC,EAAA,EAAM,GAAI,CAAA;AAAA,EAChB,GAAA,EAAK,CAAC,EAAA,EAAM,GAAI,CAAA;AAAA,EAChB,KAAK,CAAC,EAAA,EAAM,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA;AAAA,EAElC,IAAA,EAAM,CAAC,EAAA,EAAM,EAAA,EAAM,KAAM,GAAI,CAAA;AAAA,EAC7B,GAAA,EAAK,CAAC,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA,EAC5B,GAAA,EAAK,CAAC,EAAA,EAAM,EAAA,EAAM,EAAI,CAAA;AAAA;AAAA,EACtB,GAAA,EAAK,CAAC,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA,EAC5B,GAAA,EAAK,CAAC,EAAA,EAAM,GAAA,EAAM,KAAM,EAAI,CAAA;AAAA,EAC5B,GAAA,EAAK,CAAC,GAAA,EAAM,GAAI;AACjB;AAEO,IAAM,UAAA,GAA4C;AAAA,EACxD,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,YAAA;AAAA,EACL,IAAA,EAAM,YAAA;AAAA,EACN,GAAA,EAAK,eAAA;AAAA,EACL,IAAA,EAAM,YAAA;AAAA,EACN,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,cAAA;AAAA,EACL,IAAA,EAAM,YAAA;AAAA,EAEN,GAAA,EAAK,4BAAA;AAAA,EACL,GAAA,EAAK,4BAAA;AAAA,EACL,GAAA,EAAK,wBAAA;AAAA,EACL,GAAA,EAAK,wBAAA;AAAA,EAEL,GAAA,EAAK,iBAAA;AAAA,EACL,GAAA,EAAK,oBAAA;AAAA,EACL,IAAA,EAAM,yEAAA;AAAA,EACN,GAAA,EAAK,0BAAA;AAAA,EACL,IAAA,EAAM,mEAAA;AAAA,EACN,GAAA,EAAK,UAAA;AAAA,EACL,GAAA,EAAK,YAAA;AAAA,EAEL,GAAA,EAAK,iBAAA;AAAA,EACL,IAAA,EAAM,kBAAA;AAAA,EAEN,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,iBAAA;AAAA,EACL,GAAA,EAAK,iBAAA;AAAA,EACL,IAAA,EAAM,YAAA;AAAA,EAEN,GAAA,EAAK,YAAA;AAAA,EACL,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK;AACN;AAMO,IAAM,oBAAA,GAAiE;AAAA,EAC7E,KAAA,EAAO;AAAA,IACN,YAAY,CAAC,KAAA,EAAO,OAAO,MAAA,EAAQ,KAAA,EAAO,QAAQ,KAAK,CAAA;AAAA,IACvD,UAAU,CAAA,GAAI,EAAA;AAAA,IACd,oBAAA,EAAsB,IAAA;AAAA,IACtB,KAAA,EAAO,OAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,WAAA,EAAa;AAAA,IACZ,UAAA,EAAY,CAAC,KAAA,EAAO,KAAA,EAAO,KAAK,CAAA;AAAA,IAChC,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,IAAA;AAAA,IACtB,KAAA,EAAO,aAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,GAAA,EAAK;AAAA,IACJ,UAAA,EAAY,CAAC,KAAK,CAAA;AAAA,IAClB,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,IAAA;AAAA,IACtB,KAAA,EAAO,aAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,GAAA,EAAK;AAAA,IACJ,UAAA,EAAY,CAAC,KAAK,CAAA;AAAA,IAClB,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,IAAA;AAAA,IACtB,KAAA,EAAO,KAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,QAAA,EAAU;AAAA,IACT,UAAA,EAAY,CAAC,KAAA,EAAO,KAAA,EAAO,QAAQ,KAAA,EAAO,MAAA,EAAQ,OAAO,KAAK,CAAA;AAAA,IAC9D,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,KAAA;AAAA,IACtB,KAAA,EAAO,UAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,IAAA,EAAM;AAAA,IACL,UAAA,EAAY,CAAC,KAAA,EAAO,MAAM,CAAA;AAAA,IAC1B,UAAU,CAAA,GAAI,EAAA;AAAA,IACd,oBAAA,EAAsB,KAAA;AAAA,IACtB,KAAA,EAAO,WAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,KAAA,EAAO;AAAA,IACN,UAAA,EAAY,CAAC,KAAA,EAAO,KAAA,EAAO,OAAO,MAAM,CAAA;AAAA,IACxC,UAAU,GAAA,GAAM,EAAA;AAAA,IAChB,oBAAA,EAAsB,KAAA;AAAA,IACtB,KAAA,EAAO,OAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,KAAA,EAAO;AAAA,IACN,UAAA,EAAY,CAAC,KAAA,EAAO,KAAA,EAAO,OAAO,KAAK,CAAA;AAAA,IACvC,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,KAAA;AAAA,IACtB,KAAA,EAAO,OAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,GAAA,EAAK;AAAA,IACJ,YAAY,EAAC;AAAA,IACb,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,KAAA;AAAA,IACtB,KAAA,EAAO,MAAA;AAAA,IACP,MAAA,EAAQ;AAAA;AAEV;;;AC9IO,IAAM,gBAAA,GAAmB,CAAC,QAAA,KAA6B;AAC7D,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AACpC,EAAA,IAAI,MAAM,CAAA,IAAK,GAAA,KAAQ,QAAA,CAAS,MAAA,GAAS,GAAG,OAAO,EAAA;AACnD,EAAA,OAAO,QAAA,CAAS,KAAA,CAAM,GAAA,GAAM,CAAC,EAAE,WAAA,EAAY;AAC5C;AAEO,IAAM,WAAA,GAAc,CAAC,QAAA,KAA6B;AACxD,EAAA,MAAM,GAAA,GAAM,iBAAiB,QAAQ,CAAA;AACrC,EAAA,OAAO,UAAA,CAAW,GAAG,CAAA,IAAK,0BAAA;AAC3B;AAEO,IAAM,gBAAA,GAAmB,CAAC,KAAA,KAChC,KAAA,IAAS;AAEH,IAAM,cAAA,GAAiB,CAAC,KAAA,KAA0B;AACxD,EAAA,IAAI,KAAA,GAAQ,IAAA,EAAM,OAAO,CAAA,EAAG,KAAK,CAAA,EAAA,CAAA;AACjC,EAAA,MAAM,KAAA,GAAQ,CAAC,IAAA,EAAM,IAAA,EAAM,MAAM,IAAI,CAAA;AACrC,EAAA,IAAI,OAAO,KAAA,GAAQ,IAAA;AACnB,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,OAAO,IAAA,IAAQ,IAAA,IAAQ,IAAA,GAAO,KAAA,CAAM,SAAS,CAAA,EAAG;AAC/C,IAAA,IAAA,IAAQ,IAAA;AACR,IAAA,IAAA,EAAA;AAAA,EACD;AACA,EAAA,OAAO,CAAA,EAAG,IAAA,CAAK,OAAA,CAAQ,IAAA,IAAQ,EAAA,GAAK,CAAA,GAAI,CAAC,CAAC,CAAA,CAAA,EAAI,KAAA,CAAM,IAAI,CAAC,CAAA,CAAA;AAC1D;AAGA,IAAM,eAAA,GAAkB,EAAA;AAGxB,IAAM,aAAA,GAAgB,MAAA;AAEtB,IAAM,cAAc,CAAC,KAAA,KACpB,MACE,SAAA,CAAU,KAAK,EACf,OAAA,CAAQ,kBAAA,EAAoB,EAAE,CAAA,CAC9B,WAAA,GACA,OAAA,CAAQ,aAAA,EAAe,GAAG,CAAA,CAC1B,OAAA,CAAQ,YAAY,EAAE,CAAA;AAyBlB,IAAM,gBAAA,GAAmB,CAAC,QAAA,KAA6B;AAE7D,EAAA,MAAM,OAAO,QAAA,CAAS,KAAA,CAAM,OAAO,CAAA,CAAE,KAAI,IAAK,EAAA;AAE9C,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,WAAA,CAAY,GAAG,CAAA;AAEhC,EAAA,MAAM,eAAe,GAAA,GAAM,CAAA;AAC3B,EAAA,MAAM,IAAA,GAAO,YAAY,YAAA,GAAe,IAAA,CAAK,MAAM,CAAA,EAAG,GAAG,CAAA,GAAI,IAAI,CAAA,CAAE,KAAA;AAAA,IAClE,CAAA;AAAA,IACA;AAAA,GACD;AACA,EAAA,MAAM,SAAA,GAAY,eAAe,WAAA,CAAY,IAAA,CAAK,MAAM,GAAA,GAAM,CAAC,CAAC,CAAA,GAAI,EAAA;AAGpE,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,IAAK,aAAA;AAE5C,EAAA,OAAO,SAAA,GAAY,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,SAAS,CAAA,CAAA,GAAK,QAAA;AACjD;AAGO,IAAM,iBAAA,GAAoB,CAAC,UAAA,KACjC,UAAA,CAAW,WAAW,CAAA,GACnB,KAAA,GACA,UAAA,CAAW,GAAA,CAAI,eAAa,CAAA,CAAA,EAAI,SAAS,CAAA,CAAE,CAAA,CAAE,KAAK,GAAG;AAMlD,IAAM,cAAA,GAAiB,CAAC,IAAA,MAKd;AAAA,EAChB,MAAM,IAAA,CAAK,YAAA;AAAA,EACX,MAAM,IAAA,CAAK,IAAA;AAAA,EACX,MAAM,IAAA,CAAK,QAAA;AAAA,EACX,WAAA,EAAa,YACZ,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,KAAA;AAAA,IAClB,KAAK,MAAA,CAAO,UAAA;AAAA,IACZ,IAAA,CAAK,MAAA,CAAO,UAAA,GAAa,IAAA,CAAK,MAAA,CAAO;AAAA;AAExC,CAAA;;;ACuBO,IAAM,SAAA,GAA4B;AAAA,EACxC,UAAA,EAAY,+CAAA;AAAA,EACZ,SAAA,EAAW,6BAAA;AAAA,EACX,cAAA,EAAgB,8DAAA;AAAA,EAChB,aAAA,EAAe,mCAAA;AAAA,EACf,SAAA,EAAW,gBAAA;AAAA,EACX,IAAA,EAAM,KAAA;AAAA,EACN,MAAA,EAAQ,QAAA;AAAA,EACR,MAAA,EAAQ,UAAA;AAAA,EACR,KAAA,EAAO,OAAA;AAAA,EACP,MAAA,EAAQ,kBAAA;AAAA,EACR,MAAA,EAAQ,OAAA;AAAA,EACR,OAAA,EAAS,YAAA;AAAA,EACT,aAAA,EAAe,wBAAA;AAAA,EACf,QAAA,EAAU,UAAA;AAAA,EACV,IAAA,EAAM,WAAA;AAAA,EACN,YAAA,EAAc,6BAAA;AAAA,EACd,mBAAA,EAAqB,oDAAA;AAAA,EACrB,YAAA,EAAc,iDAAA;AAAA,EACd,UAAA,EAAY,8BAAA;AAAA,EACZ,eAAA,EAAiB,CAAA,GAAA,KAAO,CAAA,UAAA,EAAU,GAAG,CAAA,WAAA,CAAA;AAAA,EACrC,oBAAA,EAAsB,CAAC,SAAA,EAAW,UAAA,KACjC,CAAA,EAAG,SAAS,CAAA,oCAAA,EAAuC,UAAA,CAAW,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAAA,EACzE,eAAA,EAAiB,CAAA,QAAA,KAChB,CAAA,CAAA,EAAI,QAAQ,CAAA,sCAAA,CAAA;AAAA,EACb,aAAA,EAAe,gBAAA;AAAA,EACf,WAAA,EAAa,mCAAA;AAAA,EACb,eAAA,EACC,yFAAA;AAAA,EACD,WAAA,EAAa,YAAA;AAAA,EACb,SAAA,EAAW,qBAAA;AAAA,EACX,aAAA,EAAe,WAAA;AAAA,EACf,KAAA,EAAO,QAAA;AAAA,EACP,SAAA,EAAW,oCAAA;AAAA,EACX,QAAA,EAAU,CAAA,QAAA,KAAY,CAAA,UAAA,EAAa,QAAQ,CAAA,CAAA;AAAA,EAC3C,WAAA,EAAa,SAAA;AAAA,EACb,OAAA,EAAS,WAAA;AAAA,EACT,kBAAA,EAAoB,gBAAA;AAAA,EACpB,oBAAA,EAAsB,CAAA,QAAA,KACrB,CAAA,YAAA,EAAY,QAAQ,CAAA,mCAAA,CAAA;AAAA,EACrB,mBAAA,EAAqB,oBAAA;AAAA,EACrB,uBAAuB,CAAC,OAAA,EAAS,aAChC,CAAA,gBAAA,EAAgB,OAAO,UAAU,QAAQ,CAAA,EAAA,CAAA;AAAA,EAC1C,iBAAA,EAAmB,6BAAA;AAAA,EACnB,gBAAA,EAAkB,uCAAA;AAAA,EAClB,YAAA,EAAc,cAAA;AAAA,EACd,cAAA,EAAgB,gBAAA;AAAA,EAChB,YAAA,EAAc,aAAA;AAAA,EACd,UAAA,EAAY,SAAA;AAAA,EAEZ,kBAAA,EAAoB,kCAAA;AAAA,EACpB,kBAAkB,CAAA,UAAA,KACjB,CAAA,kCAAA,EAAqC,UAAA,CAAW,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAAA,EAC3D,WAAA,EAAa,6BAAA;AAAA,EACb,cAAc,CAAC,IAAA,EAAM,QAAQ,CAAA,gBAAA,EAAmB,IAAI,sBAAmB,GAAG,CAAA,CAAA;AAAA,EAC1E,cAAA,EAAgB,+BAAA;AAAA,EAChB,4BAAA,EACC,2DAAA;AAAA,EAED,iBAAA,EAAmB,sBAAA;AAAA,EACnB,YAAA,EAAc,eAAA;AAAA,EACd,YAAA,EAAc,mBAAA;AAAA,EACd,YAAA,EAAc,iCAAA;AAAA,EACd,gBAAA,EAAkB,gCAAA;AAAA,EAClB,WAAA,EAAa;AACd;AAGO,IAAM,cAAA,GAAiC;AAAA,EAC7C,UAAA,EAAY,+BAAA;AAAA,EACZ,SAAA,EAAW,sBAAA;AAAA,EACX,cAAA,EAAgB,sDAAA;AAAA,EAChB,aAAA,EAAe,0BAAA;AAAA,EACf,SAAA,EAAW,cAAA;AAAA,EACX,IAAA,EAAM,MAAA;AAAA,EACN,MAAA,EAAQ,QAAA;AAAA,EACR,MAAA,EAAQ,QAAA;AAAA,EACR,KAAA,EAAO,MAAA;AAAA,EACP,MAAA,EAAQ,iBAAA;AAAA,EACR,MAAA,EAAQ,QAAA;AAAA,EACR,OAAA,EAAS,SAAA;AAAA,EACT,aAAA,EAAe,iBAAA;AAAA,EACf,QAAA,EAAU,UAAA;AAAA,EACV,IAAA,EAAM,MAAA;AAAA,EACN,YAAA,EAAc,gCAAA;AAAA,EACd,mBAAA,EAAqB,uDAAA;AAAA,EACrB,YAAA,EAAc,4CAAA;AAAA,EACd,UAAA,EAAY,8BAAA;AAAA,EACZ,eAAA,EAAiB,CAAA,GAAA,KAAO,CAAA,QAAA,EAAW,GAAG,CAAA,QAAA,CAAA;AAAA,EACtC,oBAAA,EAAsB,CAAC,SAAA,EAAW,UAAA,KACjC,CAAA,EAAG,SAAS,CAAA,gCAAA,EAAmC,UAAA,CAAW,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAAA,EACrE,eAAA,EAAiB,CAAA,QAAA,KAAY,CAAA,CAAA,EAAI,QAAQ,CAAA,6BAAA,CAAA;AAAA,EACzC,aAAA,EAAe,eAAA;AAAA,EACf,WAAA,EAAa,iCAAA;AAAA,EACb,eAAA,EACC,4EAAA;AAAA,EACD,WAAA,EAAa,OAAA;AAAA,EACb,SAAA,EAAW,aAAA;AAAA,EACX,aAAA,EAAe,UAAA;AAAA,EACf,KAAA,EAAO,OAAA;AAAA,EACP,SAAA,EAAW,4BAAA;AAAA,EACX,QAAA,EAAU,CAAA,QAAA,KAAY,CAAA,SAAA,EAAY,QAAQ,CAAA,CAAA;AAAA,EAC1C,WAAA,EAAa,MAAA;AAAA,EACb,OAAA,EAAS,SAAA;AAAA,EACT,kBAAA,EAAoB,aAAA;AAAA,EACpB,oBAAA,EAAsB,CAAA,QAAA,KACrB,CAAA,QAAA,EAAW,QAAQ,CAAA,oCAAA,CAAA;AAAA,EACpB,mBAAA,EAAqB,cAAA;AAAA,EACrB,uBAAuB,CAAC,OAAA,EAAS,aAChC,CAAA,SAAA,EAAY,OAAO,WAAW,QAAQ,CAAA,EAAA,CAAA;AAAA,EACvC,iBAAA,EAAmB,qBAAA;AAAA,EACnB,gBAAA,EAAkB,+BAAA;AAAA,EAClB,YAAA,EAAc,cAAA;AAAA,EACd,cAAA,EAAgB,WAAA;AAAA,EAChB,YAAA,EAAc,cAAA;AAAA,EACd,UAAA,EAAY,KAAA;AAAA,EAEZ,kBAAA,EAAoB,2BAAA;AAAA,EACpB,kBAAkB,CAAA,UAAA,KACjB,CAAA,8BAAA,EAAiC,UAAA,CAAW,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAAA,EACvD,WAAA,EAAa,mBAAA;AAAA,EACb,cAAc,CAAC,IAAA,EAAM,QACpB,CAAA,gBAAA,EAAmB,IAAI,uBAAuB,GAAG,CAAA,CAAA;AAAA,EAClD,cAAA,EAAgB,wBAAA;AAAA,EAChB,4BAAA,EAA8B,8CAAA;AAAA,EAE9B,iBAAA,EAAmB,oBAAA;AAAA,EACnB,YAAA,EAAc,cAAA;AAAA,EACd,YAAA,EAAc,oBAAA;AAAA,EACd,YAAA,EAAc,0BAAA;AAAA,EACd,gBAAA,EAAkB,iCAAA;AAAA,EAClB,WAAA,EAAa;AACd;AAGO,IAAM,aAAA,GAAgB,CAC5B,MAAA,EACA,IAAA,GAAuB,cAAA,KACF,MAAA,GAAS,EAAE,GAAG,IAAA,EAAM,GAAG,MAAA,EAAO,GAAI;AAMjD,IAAM,SAAA,GAA4B,EAAE,GAAG,cAAA;;;AC3Q9C,IAAM,EAAA,GAAuB,EAAE,KAAA,EAAO,IAAA,EAAK;AAE3C,IAAM,IAAA,GAAO,CACZ,IAAA,EACA,OAAA,MACuB,EAAE,KAAA,EAAO,KAAA,EAAO,MAAM,OAAA,EAAQ,CAAA;AAE/C,IAAM,iBAAA,GAAoB,CAChC,QAAA,EACA,OAAA,EACA,MAAA,KACsB;AACtB,EAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,EAAA;AAEjC,EAAA,MAAM,IAAA,GAAO,cAAc,MAAM,CAAA;AACjC,EAAA,MAAM,SAAA,GAAY,iBAAiB,QAAQ,CAAA;AAC3C,EAAA,IAAI,CAAC,SAAA,EAAW;AACf,IAAA,OAAO,IAAA,CAAK,uBAAA,EAAyB,IAAA,CAAK,kBAAkB,CAAA;AAAA,EAC7D;AACA,EAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,SAA0B,CAAA,EAAG;AAClD,IAAA,OAAO,IAAA,CAAK,uBAAA,EAAyB,IAAA,CAAK,gBAAA,CAAiB,OAAO,CAAC,CAAA;AAAA,EACpE;AACA,EAAA,OAAO,EAAA;AACR;AAEO,IAAM,YAAA,GAAe,CAC3B,IAAA,EACA,QAAA,EACA,MAAA,KACsB;AACtB,EAAA,MAAM,IAAA,GAAO,cAAc,MAAM,CAAA;AACjC,EAAA,IAAI,SAAS,CAAA,EAAG,OAAO,IAAA,CAAK,YAAA,EAAc,KAAK,WAAW,CAAA;AAC1D,EAAA,IAAI,OAAO,QAAA,EAAU;AACpB,IAAA,OAAO,IAAA;AAAA,MACN,WAAA;AAAA,MACA,KAAK,YAAA,CAAa,cAAA,CAAe,IAAI,CAAA,EAAG,cAAA,CAAe,QAAQ,CAAC;AAAA,KACjE;AAAA,EACD;AACA,EAAA,OAAO,EAAA;AACR;AAOO,IAAM,oBAAA,GAAuB,OACnC,IAAA,EACA,MAAA,KAC+B;AAC/B,EAAA,MAAM,IAAA,GAAO,cAAc,MAAM,CAAA;AACjC,EAAA,MAAM,SAAA,GAAY,gBAAA,CAAiB,IAAA,CAAK,IAAI,CAAA;AAC5C,EAAA,IAAI,CAAC,gBAAA,CAAiB,SAAS,CAAA,EAAG,OAAO,EAAA;AAEzC,EAAA,MAAM,QAAA,GAAW,cAAc,SAAS,CAAA;AACxC,EAAA,IAAI,CAAC,UAAU,OAAO,EAAA;AAEtB,EAAA,MAAM,OAAO,IAAI,UAAA,CAAW,MAAM,IAAA,CAAK,WAAA,EAAa,CAAA,CAAE,KAAA;AAAA,IACrD,CAAA;AAAA,IACA,QAAA,CAAS;AAAA,GACV;AACA,EAAA,IAAI,IAAA,CAAK,MAAA,GAAS,QAAA,CAAS,MAAA,EAAQ;AAClC,IAAA,OAAO,IAAA,CAAK,uBAAA,EAAyB,IAAA,CAAK,cAAc,CAAA;AAAA,EACzD;AAEA,EAAA,MAAM,OAAA,GAAU,SAAS,KAAA,CAAM,CAAC,MAAM,KAAA,KAAU,IAAA,CAAK,KAAK,CAAA,KAAM,IAAI,CAAA;AACpE,EAAA,OAAO,OAAA,GACJ,EAAA,GACA,IAAA,CAAK,uBAAA,EAAyB,KAAK,4BAA4B,CAAA;AACnE;AAMO,IAAM,YAAA,GAAe,OAC3B,IAAA,EACA,OAAA,GAA6B,EAAC,KACC;AAC/B,EAAA,MAAM;AAAA,IACL,QAAA,GAAW,QAAA;AAAA,IACX,oBAAoB,EAAC;AAAA,IACrB,sBAAsB,UAAA,GAAa,KAAA;AAAA,IACnC,MAAA;AAAA,IACA;AAAA,GACD,GAAI,OAAA;AAEJ,EAAA,MAAM,IAAA,GAAO,YAAA,CAAa,IAAA,CAAK,IAAA,EAAM,UAAU,MAAM,CAAA;AACrD,EAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,OAAO,IAAA;AAExB,EAAA,MAAM,SAAA,GAAY,iBAAA,CAAkB,IAAA,CAAK,IAAA,EAAM,mBAAmB,MAAM,CAAA;AACxE,EAAA,IAAI,CAAC,SAAA,CAAU,KAAA,EAAO,OAAO,SAAA;AAE7B,EAAA,IAAI,UAAA,EAAY;AACf,IAAA,MAAM,KAAA,GAAQ,MAAM,oBAAA,CAAqB,IAAA,EAAM,MAAM,CAAA;AACrD,IAAA,IAAI,CAAC,KAAA,CAAM,KAAA,EAAO,OAAO,KAAA;AAAA,EAC1B;AAEA,EAAA,IAAI,gBAAA,EAAkB,OAAO,gBAAA,CAAiB,IAAI,CAAA;AAElD,EAAA,OAAO,EAAA;AACR;AAQO,IAAM,gBAAgB,OAC5B,KAAA,EACA,OAAA,GAA6B,OAE7B,OAAA,CAAQ,GAAA;AAAA,EACP,KAAA,CAAM,GAAA,CAAI,OAAM,IAAA,MAAS;AAAA,IACxB,IAAA;AAAA,IACA,MAAA,EAAQ,MAAM,YAAA,CAAa,IAAA,EAAM,OAAO;AAAA,GACzC,CAAE;AACH;;;AClHM,IAAM,UAAA,GAAN,cAAyB,KAAA,CAAM;AAAA,EAC5B,IAAA,GAAO,YAAA;AACjB;AAGA,IAAM,YAAA,GAAe,YAAA;AAErB,IAAM,SAAA,GAAY,CAAC,IAAA,MAA4B;AAAA,EAC9C,IAAA;AAAA,EACA,IAAA,EAAM,CAAA;AAAA,EACN,IAAA,EAAM,EAAA;AAAA,EACN,WAAA,EAAa,YAAY,IAAI,WAAA,CAAY,CAAC;AAC3C,CAAA,CAAA;AAGO,IAAM,YAAA,GAAe,CAAC,KAAA,KAC5B,KAAA,CAAM,KAAK,YAAA,EAAc,SAAA,CAAU,OAAO,CAAC,MAC3C,KAAA,CAAM,IAAA,CAAK,YAAA,EAAc,SAAA,CAAU,OAAO,CAAC;AAOrC,IAAM,kBAAA,GAAqB,CAAC,KAAA,KAAoC;AACtE,EAAA,IAAI,KAAA,CAAM,OAAA,KAAY,MAAA,EAAW,OAAO,KAAA,CAAM,OAAA;AAC9C,EAAA,IAAA,CAAK,KAAA,CAAM,QAAA,IAAY,CAAA,IAAK,CAAA,EAAG,OAAO,KAAA;AACtC,EAAA,OAAO,YAAA,CAAa,KAAK,CAAA,GAAI,KAAA,GAAQ,QAAA;AACtC;AAEA,IAAM,OAAA,GAAU,CAAC,GAAA,KAAgB;AAChC,EAAA,MAAM,GAAA,GAAM,GAAA,CAAI,WAAA,CAAY,GAAG,CAAA;AAC/B,EAAA,OAAO,QAAQ,EAAA,GAAK,EAAA,GAAK,GAAA,CAAI,KAAA,CAAM,GAAG,GAAG,CAAA;AAC1C,CAAA;AAGO,IAAM,kBAAA,GAAqB,CACjC,KAAA,EACA,QAAA,KAEA,MAAM,MAAA,GAAS,QAAQ,CAAA,IACvB,OAAA,CAAQ,MAAM,IAAA,CAAK,QAAA,EAAU,SAAA,CAAU,WAAW,CAAC,CAAC;AAGrD,IAAM,YAAA,GAAe,CAAC,CAAA,EAAW,CAAA,KAAc,CAAA,KAAM,KAAK,CAAA,CAAE,UAAA,CAAW,CAAA,EAAG,CAAC,CAAA,CAAA,CAAG,CAAA;AAE9E,IAAM,gBAAA,GAAmB,CAAC,IAAA,EAAc,KAAA,KAA6B;AACpE,EAAA,IAAI,OAAO,KAAA,CAAM,IAAA,KAAS,UAAA,EAAY;AACrC,IAAA,MAAM,IAAI,UAAA,CAAW,CAAA,OAAA,EAAU,IAAI,CAAA,4BAAA,CAA8B,CAAA;AAAA,EAClE;AACA,EAAA,IAAI,CAAC,OAAO,QAAA,CAAS,KAAA,CAAM,QAAQ,CAAA,IAAK,KAAA,CAAM,YAAY,CAAA,EAAG;AAC5D,IAAA,MAAM,IAAI,UAAA;AAAA,MACT,UAAU,IAAI,CAAA,uCAAA;AAAA,KACf;AAAA,EACD;AACA,EAAA,IAAI,KAAA,CAAM,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG;AAC9B,IAAA,MAAM,IAAI,UAAA;AAAA,MACT,UAAU,IAAI,CAAA,uEAAA;AAAA,KACf;AAAA,EACD;AAIA,EAAA,IAAI,KAAA,CAAM,QAAA,IAAY,KAAA,CAAM,QAAA,KAAa,KAAA,EAAO;AAC/C,IAAA,MAAM,OAAA,GAAU,oBAAA,CAAqB,KAAA,CAAM,QAAQ,CAAA,CAAE,UAAA;AACrD,IAAA,MAAM,OAAA,GAAU,MAAM,MAAA,CAAO,MAAA;AAAA,MAC5B,CAAA,SAAA,KAAa,CAAC,OAAA,CAAQ,QAAA,CAAS,SAAS;AAAA,KACzC;AACA,IAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACvB,MAAA,MAAM,IAAI,UAAA;AAAA,QACT,CAAA,OAAA,EAAU,IAAI,CAAA,GAAA,EAAM,OAAA,CAAQ,KAAK,IAAI,CAAC,CAAA,0BAAA,EAA6B,KAAA,CAAM,QAAQ,CAAA,CAAA;AAAA,OAClF;AAAA,IACD;AAAA,EACD;AAEA,EAAA,IAAI,MAAM,QAAA,EAAU;AACnB,IAAA,MAAM,SAAA,GAAY,qBAAqB,KAAA,CAAM,UAAA;AAC7C,IAAA,MAAM,SAAA,GAAY,MAAM,MAAA,CAAO,MAAA;AAAA,MAC9B,CAAA,SAAA,KAAa,CAAC,SAAA,CAAU,QAAA,CAAS,SAAS;AAAA,KAC3C;AACA,IAAA,IAAI,SAAA,CAAU,SAAS,CAAA,EAAG;AACzB,MAAA,MAAM,IAAI,UAAA;AAAA,QACT,UAAU,IAAI,CAAA,qDAAA,EAAwD,SAAA,CAAU,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,OAC3F;AAAA,IACD;AAAA,EACD;AAEA,EAAA,IAAI,KAAA,CAAM,aAAa,MAAA,EAAW;AACjC,IAAA,IAAI,CAAC,OAAO,SAAA,CAAU,KAAA,CAAM,QAAQ,CAAA,IAAK,KAAA,CAAM,WAAW,CAAA,EAAG;AAC5D,MAAA,MAAM,IAAI,UAAA;AAAA,QACT,UAAU,IAAI,CAAA,mDAAA,EAAsD,MAAA,CAAO,KAAA,CAAM,QAAQ,CAAC,CAAA;AAAA,OAC3F;AAAA,IACD;AAAA,EACD;AAIA,EAAA,IAAI,MAAM,OAAA,KAAY,QAAA,IAAA,CAAa,KAAA,CAAM,QAAA,IAAY,KAAK,CAAA,EAAG;AAC5D,IAAA,MAAM,IAAI,UAAA;AAAA,MACT,CAAA,OAAA,EAAU,IAAI,CAAA,yCAAA,EAA4C,KAAA,CAAM,QAAQ,CAAA,gJAAA;AAAA,KAGzE;AAAA,EACD;AAEA,EAAA,IACC,kBAAA,CAAmB,KAAK,CAAA,KAAM,QAAA,IAC9B,mBAAmB,KAAA,EAAO,YAAY,MAAM,EAAA,EAC3C;AACD,IAAA,MAAM,IAAI,UAAA;AAAA,MACT,UAAU,IAAI,CAAA,wIAAA;AAAA,KAEf;AAAA,EACD;AACD,CAAA;AAUA,IAAM,sBAAA,GAAyB,CAAC,MAAA,KAA8C;AAC7E,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA;AAErC,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,KAAK,CAAA,IAAK,OAAA,EAAS;AACpC,IAAA,IAAI,kBAAA,CAAmB,KAAK,CAAA,KAAM,QAAA,EAAU;AAC5C,IAAA,MAAM,MAAA,GAAS,kBAAA,CAAmB,KAAA,EAAO,YAAY,CAAA;AAErD,IAAA,KAAA,MAAW,CAAC,SAAA,EAAW,KAAK,CAAA,IAAK,OAAA,EAAS;AACzC,MAAA,IAAI,cAAc,IAAA,EAAM;AACxB,MAAA,MAAM,WAAW,KAAA,CAAM,IAAA,CAAK,YAAA,EAAc,SAAA,CAAU,WAAW,CAAC,CAAA;AAChE,MAAA,IAAI,CAAC,YAAA,CAAa,MAAA,EAAQ,QAAQ,CAAA,EAAG;AAErC,MAAA,MAAM,IAAI,UAAA;AAAA,QACT,CAAA,OAAA,EAAU,IAAI,CAAA,uCAAA,EAA0C,MAAM,8BACnD,SAAS,CAAA,mHAAA;AAAA,OAErB;AAAA,IACD;AAAA,EACD;AACD,CAAA;AAaO,IAAM,YAAA,GAAe,CAC3B,MAAA,KACsB;AACtB,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,EAAG;AACnD,IAAA,gBAAA,CAAiB,MAAM,KAAK,CAAA;AAAA,EAC7B;AACA,EAAA,sBAAA,CAAuB,MAAM,CAAA;AAE7B,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA;AAEhC,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KAA8B;AAC1C,IAAA,MAAM,KAAA,GAAQ,OAAO,IAAe,CAAA;AACpC,IAAA,IAAI,CAAC,KAAA,EAAO;AACX,MAAA,MAAM,IAAI,UAAA;AAAA,QACT,kBAAkB,IAAI,CAAA,aAAA,EAAgB,KAAA,CAAM,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,OACvD;AAAA,IACD;AACA,IAAA,OAAO,KAAA;AAAA,EACR,CAAA;AAEA,EAAA,OAAO;AAAA,IACN,MAAA;AAAA,IACA,KAAA;AAAA,IACA,GAAA;AAAA,IACA,GAAA,EAAK,UAAQ,IAAA,IAAQ,MAAA;AAAA,IACrB,QAAQ,CAAA,IAAA,KAAQ,iBAAA,CAAkB,GAAA,CAAI,IAAI,EAAE,MAAM;AAAA,GACnD;AACD;AAUO,IAAM,gBAAA,GAAmB,OAC/B,QAAA,EACA,IAAA,EACA,MACA,MAAA,KAC+B;AAC/B,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,GAAA,CAAI,IAAI,CAAA;AAC/B,EAAA,MAAM,WAAW,KAAA,CAAM,QAAA,GACpB,oBAAA,CAAqB,KAAA,CAAM,QAAQ,CAAA,GACnC,MAAA;AAEH,EAAA,OAAO,aAAa,IAAA,EAAM;AAAA,IACzB,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,mBAAmB,KAAA,CAAM,MAAA;AAAA,IACzB,oBAAA,EAAsB,UAAU,oBAAA,IAAwB,IAAA;AAAA,IACxD;AAAA,GACA,CAAA;AACF;AAMO,IAAM,sBAAA,GAAyB,CACrC,QAAA,EACA,QAAA,KACU;AACV,EAAA,MAAM,YAAA,GAAe,SAAS,KAAA,CAAM,MAAA;AAAA,IACnC,CAAA,IAAA,KAAQ,QAAA,CAAS,GAAA,CAAI,IAAI,EAAE,UAAA,KAAe;AAAA,GAC3C;AAEA,EAAA,IAAI,aAAa,MAAA,GAAS,CAAA,IAAK,CAAC,QAAA,CAAS,aAAa,SAAA,EAAW;AAChE,IAAA,MAAM,IAAI,UAAA;AAAA,MACT,aAAa,QAAA,CAAS,IAAI,qDAAqD,YAAA,CAAa,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,KACvG;AAAA,EACD;AACD;AAWA,IAAM,gBAAgB,CAAC,GAAA,KACtB,GAAA,CAAI,UAAA,CAAW,GAAG,CAAA,IAClB,GAAA,CAAI,KAAA,CAAM,GAAG,EAAE,IAAA,CAAK,CAAA,OAAA,KAAW,OAAA,KAAY,IAAA,IAAQ,YAAY,GAAG,CAAA;AAU5D,IAAM,UAAA,GAAa,CACzB,QAAA,EACA,IAAA,EACA,UACA,IAAA,KACY;AACZ,EAAA,MAAM,MAAM,QAAA,CAAS,GAAA,CAAI,IAAI,CAAA,CAAE,IAAA,CAAK,UAAU,IAAI,CAAA;AAClD,EAAA,IAAI,aAAA,CAAc,GAAG,CAAA,EAAG;AACvB,IAAA,MAAM,IAAI,UAAA,CAAW,CAAA,OAAA,EAAU,IAAI,CAAA,0BAAA,EAA6B,GAAG,CAAA,CAAE,CAAA;AAAA,EACtE;AACA,EAAA,OAAO,GAAA;AACR","file":"chunk-T5YZG6JW.js","sourcesContent":["import type { FileCategory, FileCategoryConfig, FileExtension } from './types'\n\nexport const KB = 1024\nexport const MB = 1024 * KB\nexport const GB = 1024 * MB\n\n/**\n * Leading bytes that identify a format regardless of its extension. The check\n * that uses them is the only defence against an executable renamed to `.pdf`,\n * since both the extension and the browser-reported MIME come from the client.\n */\nexport const MAGIC_NUMBERS: Partial<Record<FileExtension, number[]>> = {\n\tpng: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],\n\tjpg: [0xff, 0xd8, 0xff],\n\tjpeg: [0xff, 0xd8, 0xff],\n\tgif: [0x47, 0x49, 0x46, 0x38],\n\twebp: [0x52, 0x49, 0x46, 0x46], // RIFF; bytes 8-11 spell WEBP\n\tico: [0x00, 0x00, 0x01, 0x00],\n\n\tpdf: [0x25, 0x50, 0x44, 0x46], // %PDF\n\n\t// OOXML files are ZIP containers, so docx/xlsx share the PK signature.\n\tdocx: [0x50, 0x4b, 0x03, 0x04],\n\txlsx: [0x50, 0x4b, 0x03, 0x04],\n\tdoc: [0xd0, 0xcf, 0x11, 0xe0],\n\txls: [0xd0, 0xcf, 0x11, 0xe0],\n\n\t// DER/ASN.1 sequence header.\n\tcer: [0x30, 0x82],\n\tcrt: [0x30, 0x82],\n\tkey: [0x30, 0x82],\n\tpem: [0x2d, 0x2d, 0x2d, 0x2d, 0x2d], // -----\n\n\twebm: [0x1a, 0x45, 0xdf, 0xa3],\n\tavi: [0x52, 0x49, 0x46, 0x46],\n\tmp3: [0x49, 0x44, 0x33], // ID3\n\twav: [0x52, 0x49, 0x46, 0x46],\n\togg: [0x4f, 0x67, 0x67, 0x53],\n\taac: [0xff, 0xf1],\n}\n\nexport const MIME_TYPES: Record<FileExtension, string> = {\n\tpng: 'image/png',\n\tjpg: 'image/jpeg',\n\tjpeg: 'image/jpeg',\n\tsvg: 'image/svg+xml',\n\twebp: 'image/webp',\n\tgif: 'image/gif',\n\tico: 'image/x-icon',\n\theic: 'image/heic',\n\n\tcer: 'application/x-x509-ca-cert',\n\tcrt: 'application/x-x509-ca-cert',\n\tpem: 'application/x-pem-file',\n\tkey: 'application/x-pem-file',\n\n\tpdf: 'application/pdf',\n\tdoc: 'application/msword',\n\tdocx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n\txls: 'application/vnd.ms-excel',\n\txlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n\tcsv: 'text/csv',\n\ttxt: 'text/plain',\n\n\txml: 'application/xml',\n\tjson: 'application/json',\n\n\tmp4: 'video/mp4',\n\tmov: 'video/quicktime',\n\tavi: 'video/x-msvideo',\n\twebm: 'video/webm',\n\n\tmp3: 'audio/mpeg',\n\twav: 'audio/wav',\n\togg: 'audio/ogg',\n\taac: 'audio/aac',\n}\n\n/**\n * Defaults per category. A scope always narrows these; it can never widen the\n * extension list, which is what keeps `any` from leaking into a strict scope.\n */\nexport const FILE_CATEGORY_CONFIG: Record<FileCategory, FileCategoryConfig> = {\n\timage: {\n\t\textensions: ['png', 'jpg', 'jpeg', 'svg', 'webp', 'gif'],\n\t\tmaxBytes: 5 * MB,\n\t\tvalidateMagicNumbers: true,\n\t\tlabel: 'image',\n\t\taccept: 'image/*',\n\t},\n\tcertificate: {\n\t\textensions: ['cer', 'crt', 'pem'],\n\t\tmaxBytes: 50 * KB,\n\t\tvalidateMagicNumbers: true,\n\t\tlabel: 'certificate',\n\t\taccept: '.cer,.crt,.pem',\n\t},\n\tkey: {\n\t\textensions: ['key'],\n\t\tmaxBytes: 50 * KB,\n\t\tvalidateMagicNumbers: true,\n\t\tlabel: 'private key',\n\t\taccept: '.key',\n\t},\n\tpdf: {\n\t\textensions: ['pdf'],\n\t\tmaxBytes: 20 * MB,\n\t\tvalidateMagicNumbers: true,\n\t\tlabel: 'PDF',\n\t\taccept: '.pdf',\n\t},\n\tdocument: {\n\t\textensions: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'txt'],\n\t\tmaxBytes: 20 * MB,\n\t\tvalidateMagicNumbers: false,\n\t\tlabel: 'document',\n\t\taccept: '.pdf,.doc,.docx,.xls,.xlsx,.csv,.txt',\n\t},\n\tdata: {\n\t\textensions: ['xml', 'json'],\n\t\tmaxBytes: 5 * MB,\n\t\tvalidateMagicNumbers: false,\n\t\tlabel: 'data file',\n\t\taccept: '.xml,.json',\n\t},\n\tvideo: {\n\t\textensions: ['mp4', 'mov', 'avi', 'webm'],\n\t\tmaxBytes: 100 * MB,\n\t\tvalidateMagicNumbers: false,\n\t\tlabel: 'video',\n\t\taccept: 'video/*',\n\t},\n\taudio: {\n\t\textensions: ['mp3', 'wav', 'ogg', 'aac'],\n\t\tmaxBytes: 20 * MB,\n\t\tvalidateMagicNumbers: false,\n\t\tlabel: 'audio',\n\t\taccept: 'audio/*',\n\t},\n\tany: {\n\t\textensions: [],\n\t\tmaxBytes: 20 * MB,\n\t\tvalidateMagicNumbers: false,\n\t\tlabel: 'file',\n\t\taccept: '*/*',\n\t},\n}\n","import { MIME_TYPES } from './constants'\nimport type { FileExtension, FileLike } from './types'\n\n/** Lower-case extension without the dot, or `''` when the name has none. */\nexport const getFileExtension = (fileName: string): string => {\n\tconst dot = fileName.lastIndexOf('.')\n\tif (dot < 1 || dot === fileName.length - 1) return ''\n\treturn fileName.slice(dot + 1).toLowerCase()\n}\n\nexport const getMimeType = (fileName: string): string => {\n\tconst ext = getFileExtension(fileName) as FileExtension\n\treturn MIME_TYPES[ext] ?? 'application/octet-stream'\n}\n\nexport const isKnownExtension = (value: string): value is FileExtension =>\n\tvalue in MIME_TYPES\n\nexport const formatFileSize = (bytes: number): string => {\n\tif (bytes < 1024) return `${bytes} B`\n\tconst units = ['KB', 'MB', 'GB', 'TB']\n\tlet size = bytes / 1024\n\tlet unit = 0\n\twhile (size >= 1024 && unit < units.length - 1) {\n\t\tsize /= 1024\n\t\tunit++\n\t}\n\treturn `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`\n}\n\n/** Longest stem kept. Object stores cap the whole key and the prefix spends part of it. */\nconst MAX_STEM_LENGTH = 80\n\n/** What an all-punctuation name degrades to, so a key never ends in a bare dot. */\nconst FALLBACK_STEM = 'file'\n\nconst slugSegment = (value: string): string =>\n\tvalue\n\t\t.normalize('NFD')\n\t\t.replace(/[\\u0300-\\u036f]/g, '')\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t.replace(/^-+|-+$/g, '')\n\n/**\n * Turns a user's file name into a safe key segment: ASCII, lower case, one\n * extension, no path syntax.\n *\n * Call it INSIDE your scope's `path()`, not around it:\n *\n * ```ts\n * path: (id, file) => `Docs/${id}/${sanitizeFileName(file.name)}`\n * ```\n *\n * `resolveKey` deliberately does not apply it for you. It would rewrite the key\n * your `path()` returned, and a client computing the same key to decide replace\n * mode would then disagree with the server. Inside `path()` both sides run the\n * one function and cannot drift.\n *\n * This exists because a macOS screenshot took an API down. `Screenshot\n * 2026-08-31 at 4.18.54 p.m..png` ends in `p.m.` + `.png`, so the raw name\n * carries `..`; the traversal guard rejected the key and the consumer dropped\n * the rejection, killing the process.\n *\n * Only the KEY is slugged — keep the original in `StoredFile.fileName`, which\n * is what a UI should render.\n */\nexport const sanitizeFileName = (fileName: string): string => {\n\t// A name arriving as a path is already an attempt, deliberate or not.\n\tconst base = fileName.split(/[/\\\\]/).pop() ?? ''\n\n\tconst dot = base.lastIndexOf('.')\n\t// A leading dot is a dotfile, not an extension: `.env` has no stem.\n\tconst hasExtension = dot > 0\n\tconst stem = slugSegment(hasExtension ? base.slice(0, dot) : base).slice(\n\t\t0,\n\t\tMAX_STEM_LENGTH\n\t)\n\tconst extension = hasExtension ? slugSegment(base.slice(dot + 1)) : ''\n\n\t// A trailing `-` can reappear when the length cut lands mid-separator.\n\tconst safeStem = stem.replace(/-+$/, '') || FALLBACK_STEM\n\n\treturn extension ? `${safeStem}.${extension}` : safeStem\n}\n\n/** Value for an `<input accept=\"…\">` built from an extension list. */\nexport const toAcceptAttribute = (extensions: FileExtension[]): string =>\n\textensions.length === 0\n\t\t? '*/*'\n\t\t: extensions.map(extension => `.${extension}`).join(',')\n\n/**\n * Adapts a multer memory-storage file to `FileLike` so server code validates\n * with the very same function the browser ran.\n */\nexport const fromMulterFile = (file: {\n\toriginalname: string\n\tsize: number\n\tmimetype: string\n\tbuffer: Uint8Array\n}): FileLike => ({\n\tname: file.originalname,\n\tsize: file.size,\n\ttype: file.mimetype,\n\tarrayBuffer: async () =>\n\t\tfile.buffer.buffer.slice(\n\t\t\tfile.buffer.byteOffset,\n\t\t\tfile.buffer.byteOffset + file.buffer.byteLength\n\t\t) as ArrayBuffer,\n})\n","/**\n * Every user-facing string the package renders, so no copy is hardcoded\n * inside a component and apps translate or reword without forking.\n *\n * English is the default so the kit ships globalized; `ES_LABELS` ships for\n * Spanish apps, selected once through `UploaderProvider language='es'`. Hooks\n * take `labels` in their options, components as a prop — always a `Partial`,\n * merged over the provider's base.\n */\nexport type UploaderLabels = {\n\t/** Prompt inside the single dropzone of `Uploader` (fine pointers). */\n\tdropPrompt: string\n\t/** Same zone on a touch device, where dragging barely exists. */\n\ttapPrompt: string\n\t/** Prompt inside the bulk dropzone of `SlottedUploader` (fine pointers). */\n\tbulkDropPrompt: string\n\t/** Same bulk zone on a touch device. */\n\tbulkTapPrompt: string\n\t/** Button that fires the batch when `uploadOn` is `'manual'`. */\n\tuploadAll: string\n\t/** Row action: preview a file. */\n\tview: string\n\t/** Row action: forget a file. */\n\tremove: string\n\t/** Row action: abort an in-flight upload. */\n\tcancel: string\n\t/** Suffix on a row the server confirmed. */\n\tready: string\n\t/**\n\t * Suffix on a row holding a picked file that has NOT been sent yet.\n\t * Distinct from {@link UploaderLabels.ready}, which means the server\n\t * confirmed it — two states, two strings.\n\t */\n\tstaged: string\n\t/** Slot action when the position is empty. */\n\tupload: string\n\t/** Slot action when the position is filled. */\n\treplace: string\n\t/** Pill shown while a file hovers over a filled slot row. */\n\tdropToReplace: string\n\t/** Viewer: aria-label of the previous-image arrow. */\n\tprevious: string\n\t/** Viewer: aria-label of the next-image arrow. */\n\tnext: string\n\t/** Fallback when a strategy rejects without a message. */\n\tuploadFailed: string\n\t/** The request never reached the server — offline, DNS, CORS preflight. */\n\tuploadNetworkFailed: string\n\t/** Message when the DELETE transport refused or failed. */\n\tremoveFailed: string\n\t/** A stored file could not be fetched for preview or for bytes. */\n\treadFailed: string\n\t/** A batch past `maxFiles`. Receives the cap. */\n\tmaxFilesReached: (max: number) => string\n\t/** A file picked for a slot whose extension the slot rejects. */\n\tslotFormatNotAllowed: (slotLabel: string, extensions: string[]) => string\n\t/** A bulk-dropped file no slot matched. */\n\tfileWithoutSlot: (fileName: string) => string\n\t/** Viewer: while the URL resolves. */\n\tviewerLoading: string\n\t/** Viewer: the URL could not be resolved (expired signature, network). */\n\tviewerError: string\n\t/** Second line of the error panel — what the reader can do about it. */\n\tviewerErrorHint: string\n\t/** Viewer: button that retries the resolution. */\n\tviewerRetry: string\n\t/** Viewer: open the file in a browser tab. */\n\topenInTab: string\n\t/** Header action; the long `download` names a file, this labels a button. */\n\tdownloadShort: string\n\t/** Viewer: close button. */\n\tclose: string\n\t/** Viewer: formats without an inline preview. */\n\tnoPreview: string\n\t/** Viewer: download button. Receives the file name. */\n\tdownload: (fileName: string) => string\n\t/** Viewer: fallback title when the file carries no name. */\n\tfilePreview: string\n\t/** Confirm dialog: affirmative button. */\n\tconfirm: string\n\t/** Confirm dialog heading before forgetting a file. */\n\tconfirmRemoveTitle: string\n\t/** Confirm dialog body before forgetting. Receives the file name. */\n\tconfirmRemoveMessage: (fileName: string) => string\n\t/** Confirm dialog heading before replacing a slot's file. */\n\tconfirmReplaceTitle: string\n\t/** Confirm dialog body before replacing. Receives current and incoming names. */\n\tconfirmReplaceMessage: (current: string, incoming: string) => string\n\t/** `DropAnywhereOverlay` heading while a drag is over the window. */\n\tdropAnywhereTitle: string\n\t/** `DropAnywhereOverlay` second line. */\n\tdropAnywhereHint: string\n\t/** `AvatarUploader`: hover label and aria-label of the picture. */\n\tavatarChange: string\n\t/** `AvatarUploader`: overlay while a file hovers the picture. */\n\tavatarDropHere: string\n\t/** `AvatarUploader`: the remove link under the picture. */\n\tavatarRemove: string\n\t/** `GalleryUploader`: caption of the add tile. */\n\tgalleryAdd: string\n\n\t// Validation — `validateFile` and everything that calls it. These run on\n\t// BOTH sides: the browser shows them, the server answers them as JSON.\n\t/** A name the extension check cannot read at all. */\n\tfileHasNoExtension: string\n\t/** The extension is not on the scope's list. Receives that list. */\n\tformatNotAllowed: (extensions: string[]) => string\n\t/** Zero bytes — a failed drag, a cancelled save. */\n\tfileIsEmpty: string\n\t/** Over the scope ceiling. Receives both sizes already formatted. */\n\tfileTooLarge: (size: string, max: string) => string\n\t/** Fewer bytes than the signature needs: a truncated transfer. */\n\tfileIncomplete: string\n\t/** The leading bytes contradict the extension — renamed, or corrupt. */\n\tcontentDoesNotMatchExtension: string\n\n\t// Server — the HTTP surface. `createStorage` and both framework adapters\n\t// resolve the same object, so one `labels` wires the whole round trip.\n\t/** 400: a route parameter or body field the handler needs is missing. */\n\trequestIncomplete: string\n\t/** 401: `getUser` answered null. */\n\tunauthorized: string\n\t/** 400: an upload arrived with no file part. */\n\tfileRequired: string\n\t/** 404: the URL named a scope the registry does not declare. */\n\tunknownScope: string\n\t/** 500: the catch-all when a handler throws something unrecognized. */\n\tprocessingFailed: string\n\t/** Download name for a key that ends in nothing usable. */\n\tunnamedFile: string\n}\n\n/** Spanish copy — select it once: `<UploaderProvider language='es'>`. */\nexport const ES_LABELS: UploaderLabels = {\n\tdropPrompt: 'Selecciona un archivo o arrástralo aquí',\n\ttapPrompt: 'Toca para elegir un archivo',\n\tbulkDropPrompt: 'Arrastra varios archivos — cada uno cae en su documento',\n\tbulkTapPrompt: 'Toca para agregar varios archivos',\n\tuploadAll: 'Subir archivos',\n\tview: 'Ver',\n\tremove: 'Quitar',\n\tcancel: 'Cancelar',\n\tready: 'Listo',\n\tstaged: 'Listo para subir',\n\tupload: 'Subir',\n\treplace: 'Reemplazar',\n\tdropToReplace: 'Suelta para reemplazar',\n\tprevious: 'Anterior',\n\tnext: 'Siguiente',\n\tuploadFailed: 'No se pudo subir el archivo',\n\tuploadNetworkFailed: 'No se pudo subir el archivo. Revisa tu conexión',\n\tremoveFailed: 'No se pudo borrar el archivo del almacenamiento',\n\treadFailed: 'No se pudo cargar el archivo',\n\tmaxFilesReached: max => `Máximo ${max} archivo(s)`,\n\tslotFormatNotAllowed: (slotLabel, extensions) =>\n\t\t`${slotLabel}: formato no permitido. Se aceptan: ${extensions.join(', ')}`,\n\tfileWithoutSlot: fileName =>\n\t\t`\"${fileName}\" no corresponde a ningún documento`,\n\tviewerLoading: 'Cargando…',\n\tviewerError: 'No se pudo cargar la vista previa',\n\tviewerErrorHint:\n\t\t'El archivo puede haberse movido o el enlace expiró. Puedes reintentar o descargarlo.',\n\tviewerRetry: 'Reintentar',\n\topenInTab: 'Abrir en pestaña',\n\tdownloadShort: 'Descargar',\n\tclose: 'Cerrar',\n\tnoPreview: 'Este formato no tiene vista previa',\n\tdownload: fileName => `Descargar ${fileName}`,\n\tfilePreview: 'Archivo',\n\tconfirm: 'Confirmar',\n\tconfirmRemoveTitle: 'Quitar archivo',\n\tconfirmRemoveMessage: fileName =>\n\t\t`¿Quitar \"${fileName}\"? Podrás subir otro después.`,\n\tconfirmReplaceTitle: 'Reemplazar archivo',\n\tconfirmReplaceMessage: (current, incoming) =>\n\t\t`¿Reemplazar \"${current}\" por \"${incoming}\"?`,\n\tdropAnywhereTitle: 'Suelta los archivos aquí',\n\tdropAnywhereHint: 'Cualquier lugar de la pantalla cuenta',\n\tavatarChange: 'Cambiar foto',\n\tavatarDropHere: 'Suelta aquí',\n\tavatarRemove: 'Quitar foto',\n\tgalleryAdd: 'Agregar',\n\n\tfileHasNoExtension: 'El archivo no tiene extensión',\n\tformatNotAllowed: extensions =>\n\t\t`Formato no permitido. Se aceptan: ${extensions.join(', ')}`,\n\tfileIsEmpty: 'El archivo está vacío',\n\tfileTooLarge: (size, max) => `El archivo pesa ${size} y el máximo es ${max}`,\n\tfileIncomplete: 'El archivo está incompleto',\n\tcontentDoesNotMatchExtension:\n\t\t'El contenido del archivo no corresponde a su extensión',\n\n\trequestIncomplete: 'Solicitud incompleta',\n\tunauthorized: 'No autorizado',\n\tfileRequired: 'Archivo requerido',\n\tunknownScope: 'Destino de archivo no válido',\n\tprocessingFailed: 'No se pudo procesar el archivo',\n\tunnamedFile: 'archivo',\n}\n\n/** English copy — the package default, so the kit ships globalized. */\nexport const DEFAULT_LABELS: UploaderLabels = {\n\tdropPrompt: 'Choose a file or drag it here',\n\ttapPrompt: 'Tap to choose a file',\n\tbulkDropPrompt: 'Drop several files — each lands on its document',\n\tbulkTapPrompt: 'Tap to add several files',\n\tuploadAll: 'Upload files',\n\tview: 'View',\n\tremove: 'Remove',\n\tcancel: 'Cancel',\n\tready: 'Done',\n\tstaged: 'Ready to upload',\n\tupload: 'Upload',\n\treplace: 'Replace',\n\tdropToReplace: 'Drop to replace',\n\tprevious: 'Previous',\n\tnext: 'Next',\n\tuploadFailed: 'The file could not be uploaded',\n\tuploadNetworkFailed: 'The file could not be uploaded. Check your connection',\n\tremoveFailed: 'The file could not be removed from storage',\n\treadFailed: 'The file could not be loaded',\n\tmaxFilesReached: max => `At most ${max} file(s)`,\n\tslotFormatNotAllowed: (slotLabel, extensions) =>\n\t\t`${slotLabel}: format not allowed. Accepted: ${extensions.join(', ')}`,\n\tfileWithoutSlot: fileName => `\"${fileName}\" does not match any document`,\n\tviewerLoading: 'Loading…',\n\tviewerError: 'The preview could not be loaded',\n\tviewerErrorHint:\n\t\t'The file may have moved or the link expired. You can retry or download it.',\n\tviewerRetry: 'Retry',\n\topenInTab: 'Open in tab',\n\tdownloadShort: 'Download',\n\tclose: 'Close',\n\tnoPreview: 'This format has no preview',\n\tdownload: fileName => `Download ${fileName}`,\n\tfilePreview: 'File',\n\tconfirm: 'Confirm',\n\tconfirmRemoveTitle: 'Remove file',\n\tconfirmRemoveMessage: fileName =>\n\t\t`Remove \"${fileName}\"? You can upload another one later.`,\n\tconfirmReplaceTitle: 'Replace file',\n\tconfirmReplaceMessage: (current, incoming) =>\n\t\t`Replace \"${current}\" with \"${incoming}\"?`,\n\tdropAnywhereTitle: 'Drop the files here',\n\tdropAnywhereHint: 'Anywhere on the screen counts',\n\tavatarChange: 'Change photo',\n\tavatarDropHere: 'Drop here',\n\tavatarRemove: 'Remove photo',\n\tgalleryAdd: 'Add',\n\n\tfileHasNoExtension: 'The file has no extension',\n\tformatNotAllowed: extensions =>\n\t\t`Format not allowed. Accepted: ${extensions.join(', ')}`,\n\tfileIsEmpty: 'The file is empty',\n\tfileTooLarge: (size, max) =>\n\t\t`The file weighs ${size} and the maximum is ${max}`,\n\tfileIncomplete: 'The file is incomplete',\n\tcontentDoesNotMatchExtension: 'The file contents do not match its extension',\n\n\trequestIncomplete: 'Incomplete request',\n\tunauthorized: 'Unauthorized',\n\tfileRequired: 'A file is required',\n\tunknownScope: 'Unknown file destination',\n\tprocessingFailed: 'The file could not be processed',\n\tunnamedFile: 'file',\n}\n\n/** The merge every entry point runs: partial overrides over the default. */\nexport const resolveLabels = (\n\tlabels?: Partial<UploaderLabels>,\n\tbase: UploaderLabels = DEFAULT_LABELS\n): UploaderLabels => (labels ? { ...base, ...labels } : base)\n\n/**\n * @deprecated English IS {@link DEFAULT_LABELS} now. A shallow copy, kept so\n * 1.x consumers passing it keep working.\n */\nexport const EN_LABELS: UploaderLabels = { ...DEFAULT_LABELS }\n","import { MAGIC_NUMBERS } from './constants'\nimport { formatFileSize, getFileExtension, isKnownExtension } from './file'\nimport { resolveLabels, type UploaderLabels } from './labels'\nimport type {\n\tFileExtension,\n\tFileLike,\n\tValidationOptions,\n\tValidationResult,\n} from './types'\n\nconst ok: ValidationResult = { valid: true }\n\nconst fail = (\n\tcode: Exclude<ValidationResult, { valid: true }>['code'],\n\tmessage: string\n): ValidationResult => ({ valid: false, code, message })\n\nexport const validateExtension = (\n\tfileName: string,\n\tallowed: FileExtension[],\n\tlabels?: Partial<UploaderLabels>\n): ValidationResult => {\n\tif (allowed.length === 0) return ok\n\n\tconst copy = resolveLabels(labels)\n\tconst extension = getFileExtension(fileName)\n\tif (!extension) {\n\t\treturn fail('extension-not-allowed', copy.fileHasNoExtension)\n\t}\n\tif (!allowed.includes(extension as FileExtension)) {\n\t\treturn fail('extension-not-allowed', copy.formatNotAllowed(allowed))\n\t}\n\treturn ok\n}\n\nexport const validateSize = (\n\tsize: number,\n\tmaxBytes: number,\n\tlabels?: Partial<UploaderLabels>\n): ValidationResult => {\n\tconst copy = resolveLabels(labels)\n\tif (size === 0) return fail('empty-file', copy.fileIsEmpty)\n\tif (size > maxBytes) {\n\t\treturn fail(\n\t\t\t'too-large',\n\t\t\tcopy.fileTooLarge(formatFileSize(size), formatFileSize(maxBytes))\n\t\t)\n\t}\n\treturn ok\n}\n\n/**\n * Compares the file's leading bytes against the signature its extension\n * claims. Extensions without a known signature pass — absence of a signature\n * is not evidence of tampering.\n */\nexport const validateMagicNumbers = async (\n\tfile: FileLike,\n\tlabels?: Partial<UploaderLabels>\n): Promise<ValidationResult> => {\n\tconst copy = resolveLabels(labels)\n\tconst extension = getFileExtension(file.name)\n\tif (!isKnownExtension(extension)) return ok\n\n\tconst expected = MAGIC_NUMBERS[extension]\n\tif (!expected) return ok\n\n\tconst head = new Uint8Array(await file.arrayBuffer()).slice(\n\t\t0,\n\t\texpected.length\n\t)\n\tif (head.length < expected.length) {\n\t\treturn fail('magic-number-mismatch', copy.fileIncomplete)\n\t}\n\n\tconst matches = expected.every((byte, index) => head[index] === byte)\n\treturn matches\n\t\t? ok\n\t\t: fail('magic-number-mismatch', copy.contentDoesNotMatchExtension)\n}\n\n/**\n * Runs the checks cheapest-first so a 2 GB file is rejected on its size before\n * anything reads its bytes.\n */\nexport const validateFile = async (\n\tfile: FileLike,\n\toptions: ValidationOptions = {}\n): Promise<ValidationResult> => {\n\tconst {\n\t\tmaxBytes = Infinity,\n\t\tallowedExtensions = [],\n\t\tvalidateMagicNumbers: checkMagic = false,\n\t\tlabels,\n\t\tcustomValidation,\n\t} = options\n\n\tconst size = validateSize(file.size, maxBytes, labels)\n\tif (!size.valid) return size\n\n\tconst extension = validateExtension(file.name, allowedExtensions, labels)\n\tif (!extension.valid) return extension\n\n\tif (checkMagic) {\n\t\tconst magic = await validateMagicNumbers(file, labels)\n\t\tif (!magic.valid) return magic\n\t}\n\n\tif (customValidation) return customValidation(file)\n\n\treturn ok\n}\n\nexport type FileValidation = {\n\tfile: FileLike\n\tresult: ValidationResult\n}\n\n/** Validates every file and reports each result; never short-circuits. */\nexport const validateFiles = async (\n\tfiles: FileLike[],\n\toptions: ValidationOptions = {}\n): Promise<FileValidation[]> =>\n\tPromise.all(\n\t\tfiles.map(async file => ({\n\t\t\tfile,\n\t\t\tresult: await validateFile(file, options),\n\t\t}))\n\t)\n","import { FILE_CATEGORY_CONFIG } from './constants'\nimport { toAcceptAttribute } from './file'\nimport type { UploaderLabels } from './labels'\nimport type {\n\tFileLike,\n\tReplaceMode,\n\tScopeConfig,\n\tScopeRegistry,\n\tStorageProvider,\n\tValidationResult,\n} from './types'\nimport { validateFile } from './validation'\n\n/** Thrown for a definition or wiring mistake — never for a user's bad file. */\nexport class ScopeError extends Error {\n\toverride name = 'ScopeError'\n}\n\n/** Stand-in entity used to compare the SHAPE of two `path` callbacks. */\nconst PROBE_ENTITY = '__entity__'\n\nconst probeFile = (name: string): FileLike => ({\n\tname,\n\tsize: 0,\n\ttype: '',\n\tarrayBuffer: async () => new ArrayBuffer(0),\n})\n\n/** True when `path` returns the same key whatever the file is called. */\nexport const hasStableKey = (scope: ScopeConfig): boolean =>\n\tscope.path(PROBE_ENTITY, probeFile('a.png')) ===\n\tscope.path(PROBE_ENTITY, probeFile('b.png'))\n\n/**\n * The mode a scope runs in when it does not name one.\n *\n * @see AGENTS.md §3 — why a stable key needs no sweep and a name-carrying one does\n */\nexport const resolveReplaceMode = (scope: ScopeConfig): ReplaceMode => {\n\tif (scope.replace !== undefined) return scope.replace\n\tif ((scope.maxFiles ?? 1) > 1) return 'key'\n\treturn hasStableKey(scope) ? 'key' : 'entity'\n}\n\nconst dirname = (key: string) => {\n\tconst cut = key.lastIndexOf('/')\n\treturn cut === -1 ? '' : key.slice(0, cut)\n}\n\n/** Folder an `'entity'` replace is allowed to sweep. */\nexport const resolveScopePrefix = (\n\tscope: ScopeConfig,\n\tentityId: string\n): string =>\n\tscope.prefix?.(entityId) ??\n\tdirname(scope.path(entityId, probeFile('probe.bin')))\n\n/** `a` contains `b`, counting only whole path segments. */\nconst containsPath = (a: string, b: string) => b === a || b.startsWith(`${a}/`)\n\nconst assertDefinition = (name: string, scope: ScopeConfig): void => {\n\tif (typeof scope.path !== 'function') {\n\t\tthrow new ScopeError(`Scope \"${name}\": \"path\" must be a function`)\n\t}\n\tif (!Number.isFinite(scope.maxBytes) || scope.maxBytes <= 0) {\n\t\tthrow new ScopeError(\n\t\t\t`Scope \"${name}\": \"maxBytes\" must be a positive number`\n\t\t)\n\t}\n\tif (scope.accept.length === 0) {\n\t\tthrow new ScopeError(\n\t\t\t`Scope \"${name}\": \"accept\" cannot be empty — list the extensions this scope takes`\n\t\t)\n\t}\n\n\t// A scope narrows its category, never widens it. Otherwise a 'pdf' scope\n\t// could quietly start taking executables.\n\tif (scope.category && scope.category !== 'any') {\n\t\tconst allowed = FILE_CATEGORY_CONFIG[scope.category].extensions\n\t\tconst outside = scope.accept.filter(\n\t\t\textension => !allowed.includes(extension)\n\t\t)\n\t\tif (outside.length > 0) {\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Scope \"${name}\": ${outside.join(', ')} not allowed by category \"${scope.category}\"`\n\t\t\t)\n\t\t}\n\t}\n\n\tif (scope.compress) {\n\t\tconst imageOnly = FILE_CATEGORY_CONFIG.image.extensions\n\t\tconst notImages = scope.accept.filter(\n\t\t\textension => !imageOnly.includes(extension)\n\t\t)\n\t\tif (notImages.length > 0) {\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Scope \"${name}\": \"compress\" only applies to images, but it accepts ${notImages.join(', ')}`\n\t\t\t)\n\t\t}\n\t}\n\n\tif (scope.maxFiles !== undefined) {\n\t\tif (!Number.isInteger(scope.maxFiles) || scope.maxFiles < 1) {\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Scope \"${name}\": \"maxFiles\" must be a positive integer, received ${String(scope.maxFiles)}`\n\t\t\t)\n\t\t}\n\t}\n\n\t// The contradiction the derived default exists to prevent: a scope that\n\t// holds a collection cannot also erase the collection on every upload.\n\tif (scope.replace === 'entity' && (scope.maxFiles ?? 1) > 1) {\n\t\tthrow new ScopeError(\n\t\t\t`Scope \"${name}\": replace \"entity\" contradicts maxFiles ${scope.maxFiles} — ` +\n\t\t\t\t'an entity-wide replace would delete the other files on the next upload. ' +\n\t\t\t\t\"Use 'key' to overwrite one slot, or false to keep every version.\"\n\t\t)\n\t}\n\n\tif (\n\t\tresolveReplaceMode(scope) === 'entity' &&\n\t\tresolveScopePrefix(scope, PROBE_ENTITY) === ''\n\t) {\n\t\tthrow new ScopeError(\n\t\t\t`Scope \"${name}\": replace \"entity\" needs a folder to sweep, but \"path\" resolves to a ` +\n\t\t\t\t'bucket-root key. Nest the key under a folder, or declare \"prefix\".'\n\t\t)\n\t}\n}\n\n/**\n * Rejects a registry where sweeping one scope would reach another's objects.\n *\n * `path` shapes are compared through a placeholder entity, so this catches the\n * real hazard — two scopes sharing a folder for the SAME entity — while two\n * scopes rooted at different entity folders stay independent. A false positive\n * is answered with an explicit `prefix`, and the message says so.\n */\nconst assertNoSweepCollision = (scopes: Record<string, ScopeConfig>): void => {\n\tconst entries = Object.entries(scopes)\n\n\tfor (const [name, scope] of entries) {\n\t\tif (resolveReplaceMode(scope) !== 'entity') continue\n\t\tconst prefix = resolveScopePrefix(scope, PROBE_ENTITY)\n\n\t\tfor (const [otherName, other] of entries) {\n\t\t\tif (otherName === name) continue\n\t\t\tconst otherKey = other.path(PROBE_ENTITY, probeFile('probe.bin'))\n\t\t\tif (!containsPath(prefix, otherKey)) continue\n\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Scope \"${name}\" replaces per entity and would sweep \"${prefix}\", which also holds ` +\n\t\t\t\t\t`scope \"${otherName}\" — an upload to one would delete the other's files. ` +\n\t\t\t\t\t'Give each its own folder, or declare a narrower \"prefix\".'\n\t\t\t)\n\t\t}\n\t}\n}\n\n/**\n * Declares every destination an app can write to. The returned registry is\n * imported by the client (to configure inputs and pre-validate) and by the\n * server (to authorize and re-validate), which is what keeps the two in sync.\n *\n * Definitions are checked eagerly: a malformed scope throws at import time,\n * not on the first upload. That includes the two replace mistakes no runtime\n * check could recover from — an entity-wide replace on a collection, and two\n * scopes whose folders overlap, where an avatar upload would delete the same\n * user's documents.\n */\nexport const defineScopes = <T extends Record<string, ScopeConfig>>(\n\tscopes: T\n): ScopeRegistry<T> => {\n\tfor (const [name, scope] of Object.entries(scopes)) {\n\t\tassertDefinition(name, scope)\n\t}\n\tassertNoSweepCollision(scopes)\n\n\tconst names = Object.keys(scopes) as (keyof T & string)[]\n\n\tconst get = (name: string): ScopeConfig => {\n\t\tconst scope = scopes[name as keyof T]\n\t\tif (!scope) {\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Unknown scope \"${name}\". Declared: ${names.join(', ')}`\n\t\t\t)\n\t\t}\n\t\treturn scope\n\t}\n\n\treturn {\n\t\tscopes,\n\t\tnames,\n\t\tget,\n\t\thas: name => name in scopes,\n\t\taccept: name => toAcceptAttribute(get(name).accept),\n\t}\n}\n\n/**\n * The one validation call. Client runs it for feedback, server runs it for\n * safety, both against the same scope.\n *\n * `labels` is what keeps the two answers identical in wording as well as in\n * verdict: the hook passes the copy it renders with, `createStorage` passes\n * the copy it was configured with.\n */\nexport const validateForScope = async <T extends Record<string, ScopeConfig>>(\n\tregistry: ScopeRegistry<T>,\n\tname: string,\n\tfile: FileLike,\n\tlabels?: Partial<UploaderLabels>\n): Promise<ValidationResult> => {\n\tconst scope = registry.get(name)\n\tconst category = scope.category\n\t\t? FILE_CATEGORY_CONFIG[scope.category]\n\t\t: undefined\n\n\treturn validateFile(file, {\n\t\tmaxBytes: scope.maxBytes,\n\t\tallowedExtensions: scope.accept,\n\t\tvalidateMagicNumbers: category?.validateMagicNumbers ?? true,\n\t\tlabels,\n\t})\n}\n\n/**\n * Fails fast when a provider cannot honour what the scopes promise — a private\n * scope on a provider that cannot sign URLs is a runtime 403 waiting to happen.\n */\nexport const assertProviderSupports = <T extends Record<string, ScopeConfig>>(\n\tregistry: ScopeRegistry<T>,\n\tprovider: StorageProvider\n): void => {\n\tconst needsSigning = registry.names.filter(\n\t\tname => registry.get(name).visibility === 'private'\n\t)\n\n\tif (needsSigning.length > 0 && !provider.capabilities.signedUrl) {\n\t\tthrow new ScopeError(\n\t\t\t`Provider \"${provider.name}\" cannot sign URLs, but these scopes are private: ${needsSigning.join(', ')}`\n\t\t)\n\t}\n}\n\n/**\n * Whether a key would escape its own prefix.\n *\n * Judged per SEGMENT. A `key.includes('..')` test reads as the same check and\n * is not: `report..pdf` and `Screenshot at 4.18.54 p.m..png` carry two dots\n * without ever being traversal, and a macOS screenshot is the common case, not\n * a corner one. Traversal needs a segment that IS `..`, so that is what this\n * asks.\n */\nconst escapesPrefix = (key: string): boolean =>\n\tkey.startsWith('/') ||\n\tkey.split('/').some(segment => segment === '..' || segment === '.')\n\n/**\n * Resolves the storage key for an upload.\n *\n * The guard is a backstop, not a sanitizer: it refuses a key rather than\n * rewriting one, because a client computing the same key to decide replace\n * mode would then disagree with the server. Run {@link sanitizeFileName}\n * inside the scope's `path()` and both sides stay identical.\n */\nexport const resolveKey = <T extends Record<string, ScopeConfig>>(\n\tregistry: ScopeRegistry<T>,\n\tname: string,\n\tentityId: string,\n\tfile: FileLike\n): string => {\n\tconst key = registry.get(name).path(entityId, file)\n\tif (escapesPrefix(key)) {\n\t\tthrow new ScopeError(`Scope \"${name}\" produced an unsafe key: ${key}`)\n\t}\n\treturn key\n}\n"]}