uploaderkit 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/dist/index.cjs +349 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +245 -0
- package/dist/index.d.ts +245 -0
- package/dist/index.js +326 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/constants.ts
|
|
4
|
+
var KB = 1024;
|
|
5
|
+
var MB = 1024 * KB;
|
|
6
|
+
var GB = 1024 * MB;
|
|
7
|
+
var MAGIC_NUMBERS = {
|
|
8
|
+
png: [137, 80, 78, 71, 13, 10, 26, 10],
|
|
9
|
+
jpg: [255, 216, 255],
|
|
10
|
+
jpeg: [255, 216, 255],
|
|
11
|
+
gif: [71, 73, 70, 56],
|
|
12
|
+
webp: [82, 73, 70, 70],
|
|
13
|
+
// RIFF; bytes 8-11 spell WEBP
|
|
14
|
+
ico: [0, 0, 1, 0],
|
|
15
|
+
pdf: [37, 80, 68, 70],
|
|
16
|
+
// %PDF
|
|
17
|
+
// OOXML files are ZIP containers, so docx/xlsx share the PK signature.
|
|
18
|
+
docx: [80, 75, 3, 4],
|
|
19
|
+
xlsx: [80, 75, 3, 4],
|
|
20
|
+
doc: [208, 207, 17, 224],
|
|
21
|
+
xls: [208, 207, 17, 224],
|
|
22
|
+
// DER/ASN.1 sequence header.
|
|
23
|
+
cer: [48, 130],
|
|
24
|
+
crt: [48, 130],
|
|
25
|
+
key: [48, 130],
|
|
26
|
+
pem: [45, 45, 45, 45, 45],
|
|
27
|
+
// -----
|
|
28
|
+
webm: [26, 69, 223, 163],
|
|
29
|
+
avi: [82, 73, 70, 70],
|
|
30
|
+
mp3: [73, 68, 51],
|
|
31
|
+
// ID3
|
|
32
|
+
wav: [82, 73, 70, 70],
|
|
33
|
+
ogg: [79, 103, 103, 83],
|
|
34
|
+
aac: [255, 241]
|
|
35
|
+
};
|
|
36
|
+
var MIME_TYPES = {
|
|
37
|
+
png: "image/png",
|
|
38
|
+
jpg: "image/jpeg",
|
|
39
|
+
jpeg: "image/jpeg",
|
|
40
|
+
svg: "image/svg+xml",
|
|
41
|
+
webp: "image/webp",
|
|
42
|
+
gif: "image/gif",
|
|
43
|
+
ico: "image/x-icon",
|
|
44
|
+
heic: "image/heic",
|
|
45
|
+
cer: "application/x-x509-ca-cert",
|
|
46
|
+
crt: "application/x-x509-ca-cert",
|
|
47
|
+
pem: "application/x-pem-file",
|
|
48
|
+
key: "application/x-pem-file",
|
|
49
|
+
pdf: "application/pdf",
|
|
50
|
+
doc: "application/msword",
|
|
51
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
52
|
+
xls: "application/vnd.ms-excel",
|
|
53
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
54
|
+
csv: "text/csv",
|
|
55
|
+
txt: "text/plain",
|
|
56
|
+
xml: "application/xml",
|
|
57
|
+
json: "application/json",
|
|
58
|
+
mp4: "video/mp4",
|
|
59
|
+
mov: "video/quicktime",
|
|
60
|
+
avi: "video/x-msvideo",
|
|
61
|
+
webm: "video/webm",
|
|
62
|
+
mp3: "audio/mpeg",
|
|
63
|
+
wav: "audio/wav",
|
|
64
|
+
ogg: "audio/ogg",
|
|
65
|
+
aac: "audio/aac"
|
|
66
|
+
};
|
|
67
|
+
var FILE_CATEGORY_CONFIG = {
|
|
68
|
+
image: {
|
|
69
|
+
extensions: ["png", "jpg", "jpeg", "svg", "webp", "gif"],
|
|
70
|
+
maxBytes: 5 * MB,
|
|
71
|
+
validateMagicNumbers: true,
|
|
72
|
+
label: "imagen",
|
|
73
|
+
accept: "image/*"
|
|
74
|
+
},
|
|
75
|
+
certificate: {
|
|
76
|
+
extensions: ["cer", "crt", "pem"],
|
|
77
|
+
maxBytes: 50 * KB,
|
|
78
|
+
validateMagicNumbers: true,
|
|
79
|
+
label: "certificado",
|
|
80
|
+
accept: ".cer,.crt,.pem"
|
|
81
|
+
},
|
|
82
|
+
key: {
|
|
83
|
+
extensions: ["key"],
|
|
84
|
+
maxBytes: 50 * KB,
|
|
85
|
+
validateMagicNumbers: true,
|
|
86
|
+
label: "llave privada",
|
|
87
|
+
accept: ".key"
|
|
88
|
+
},
|
|
89
|
+
pdf: {
|
|
90
|
+
extensions: ["pdf"],
|
|
91
|
+
maxBytes: 20 * MB,
|
|
92
|
+
validateMagicNumbers: true,
|
|
93
|
+
label: "PDF",
|
|
94
|
+
accept: ".pdf"
|
|
95
|
+
},
|
|
96
|
+
document: {
|
|
97
|
+
extensions: ["pdf", "doc", "docx", "xls", "xlsx", "csv", "txt"],
|
|
98
|
+
maxBytes: 20 * MB,
|
|
99
|
+
validateMagicNumbers: false,
|
|
100
|
+
label: "documento",
|
|
101
|
+
accept: ".pdf,.doc,.docx,.xls,.xlsx,.csv,.txt"
|
|
102
|
+
},
|
|
103
|
+
data: {
|
|
104
|
+
extensions: ["xml", "json"],
|
|
105
|
+
maxBytes: 5 * MB,
|
|
106
|
+
validateMagicNumbers: false,
|
|
107
|
+
label: "archivo de datos",
|
|
108
|
+
accept: ".xml,.json"
|
|
109
|
+
},
|
|
110
|
+
video: {
|
|
111
|
+
extensions: ["mp4", "mov", "avi", "webm"],
|
|
112
|
+
maxBytes: 100 * MB,
|
|
113
|
+
validateMagicNumbers: false,
|
|
114
|
+
label: "video",
|
|
115
|
+
accept: "video/*"
|
|
116
|
+
},
|
|
117
|
+
audio: {
|
|
118
|
+
extensions: ["mp3", "wav", "ogg", "aac"],
|
|
119
|
+
maxBytes: 20 * MB,
|
|
120
|
+
validateMagicNumbers: false,
|
|
121
|
+
label: "audio",
|
|
122
|
+
accept: "audio/*"
|
|
123
|
+
},
|
|
124
|
+
any: {
|
|
125
|
+
extensions: [],
|
|
126
|
+
maxBytes: 20 * MB,
|
|
127
|
+
validateMagicNumbers: false,
|
|
128
|
+
label: "archivo",
|
|
129
|
+
accept: "*/*"
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/file.ts
|
|
134
|
+
var getFileExtension = (fileName) => {
|
|
135
|
+
const dot = fileName.lastIndexOf(".");
|
|
136
|
+
if (dot < 1 || dot === fileName.length - 1) return "";
|
|
137
|
+
return fileName.slice(dot + 1).toLowerCase();
|
|
138
|
+
};
|
|
139
|
+
var getMimeType = (fileName) => {
|
|
140
|
+
const ext = getFileExtension(fileName);
|
|
141
|
+
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
142
|
+
};
|
|
143
|
+
var isKnownExtension = (value) => value in MIME_TYPES;
|
|
144
|
+
var formatFileSize = (bytes) => {
|
|
145
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
146
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
147
|
+
let size = bytes / 1024;
|
|
148
|
+
let unit = 0;
|
|
149
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
150
|
+
size /= 1024;
|
|
151
|
+
unit++;
|
|
152
|
+
}
|
|
153
|
+
return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;
|
|
154
|
+
};
|
|
155
|
+
var toAcceptAttribute = (extensions) => extensions.length === 0 ? "*/*" : extensions.map((extension) => `.${extension}`).join(",");
|
|
156
|
+
var fromMulterFile = (file) => ({
|
|
157
|
+
name: file.originalname,
|
|
158
|
+
size: file.size,
|
|
159
|
+
type: file.mimetype,
|
|
160
|
+
arrayBuffer: async () => file.buffer.buffer.slice(
|
|
161
|
+
file.buffer.byteOffset,
|
|
162
|
+
file.buffer.byteOffset + file.buffer.byteLength
|
|
163
|
+
)
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// src/validation.ts
|
|
167
|
+
var ok = { valid: true };
|
|
168
|
+
var fail = (code, message) => ({ valid: false, code, message });
|
|
169
|
+
var validateExtension = (fileName, allowed) => {
|
|
170
|
+
if (allowed.length === 0) return ok;
|
|
171
|
+
const extension = getFileExtension(fileName);
|
|
172
|
+
if (!extension) {
|
|
173
|
+
return fail("extension-not-allowed", "El archivo no tiene extensi\xF3n");
|
|
174
|
+
}
|
|
175
|
+
if (!allowed.includes(extension)) {
|
|
176
|
+
return fail(
|
|
177
|
+
"extension-not-allowed",
|
|
178
|
+
`Formato no permitido. Se aceptan: ${allowed.join(", ")}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return ok;
|
|
182
|
+
};
|
|
183
|
+
var validateSize = (size, maxBytes) => {
|
|
184
|
+
if (size === 0) return fail("empty-file", "El archivo est\xE1 vac\xEDo");
|
|
185
|
+
if (size > maxBytes) {
|
|
186
|
+
return fail(
|
|
187
|
+
"too-large",
|
|
188
|
+
`El archivo pesa ${formatFileSize(size)} y el m\xE1ximo es ${formatFileSize(maxBytes)}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return ok;
|
|
192
|
+
};
|
|
193
|
+
var validateMagicNumbers = async (file) => {
|
|
194
|
+
const extension = getFileExtension(file.name);
|
|
195
|
+
if (!isKnownExtension(extension)) return ok;
|
|
196
|
+
const expected = MAGIC_NUMBERS[extension];
|
|
197
|
+
if (!expected) return ok;
|
|
198
|
+
const head = new Uint8Array(await file.arrayBuffer()).slice(
|
|
199
|
+
0,
|
|
200
|
+
expected.length
|
|
201
|
+
);
|
|
202
|
+
if (head.length < expected.length) {
|
|
203
|
+
return fail("magic-number-mismatch", "El archivo est\xE1 incompleto");
|
|
204
|
+
}
|
|
205
|
+
const matches = expected.every((byte, index) => head[index] === byte);
|
|
206
|
+
return matches ? ok : fail(
|
|
207
|
+
"magic-number-mismatch",
|
|
208
|
+
"El contenido del archivo no corresponde a su extensi\xF3n"
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
var validateFile = async (file, options = {}) => {
|
|
212
|
+
const {
|
|
213
|
+
maxBytes = Infinity,
|
|
214
|
+
allowedExtensions = [],
|
|
215
|
+
validateMagicNumbers: checkMagic = false,
|
|
216
|
+
customValidation
|
|
217
|
+
} = options;
|
|
218
|
+
const size = validateSize(file.size, maxBytes);
|
|
219
|
+
if (!size.valid) return size;
|
|
220
|
+
const extension = validateExtension(file.name, allowedExtensions);
|
|
221
|
+
if (!extension.valid) return extension;
|
|
222
|
+
if (checkMagic) {
|
|
223
|
+
const magic = await validateMagicNumbers(file);
|
|
224
|
+
if (!magic.valid) return magic;
|
|
225
|
+
}
|
|
226
|
+
if (customValidation) return customValidation(file);
|
|
227
|
+
return ok;
|
|
228
|
+
};
|
|
229
|
+
var validateFiles = async (files, options = {}) => Promise.all(
|
|
230
|
+
files.map(async (file) => ({
|
|
231
|
+
file,
|
|
232
|
+
result: await validateFile(file, options)
|
|
233
|
+
}))
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// src/scopes.ts
|
|
237
|
+
var ScopeError = class extends Error {
|
|
238
|
+
name = "ScopeError";
|
|
239
|
+
};
|
|
240
|
+
var assertDefinition = (name, scope) => {
|
|
241
|
+
if (typeof scope.path !== "function") {
|
|
242
|
+
throw new ScopeError(`Scope "${name}": "path" must be a function`);
|
|
243
|
+
}
|
|
244
|
+
if (!Number.isFinite(scope.maxBytes) || scope.maxBytes <= 0) {
|
|
245
|
+
throw new ScopeError(
|
|
246
|
+
`Scope "${name}": "maxBytes" must be a positive number`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (scope.accept.length === 0) {
|
|
250
|
+
throw new ScopeError(
|
|
251
|
+
`Scope "${name}": "accept" cannot be empty \u2014 list the extensions this scope takes`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (scope.category && scope.category !== "any") {
|
|
255
|
+
const allowed = FILE_CATEGORY_CONFIG[scope.category].extensions;
|
|
256
|
+
const outside = scope.accept.filter(
|
|
257
|
+
(extension) => !allowed.includes(extension)
|
|
258
|
+
);
|
|
259
|
+
if (outside.length > 0) {
|
|
260
|
+
throw new ScopeError(
|
|
261
|
+
`Scope "${name}": ${outside.join(", ")} not allowed by category "${scope.category}"`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (scope.compress) {
|
|
266
|
+
const imageOnly = FILE_CATEGORY_CONFIG.image.extensions;
|
|
267
|
+
const notImages = scope.accept.filter(
|
|
268
|
+
(extension) => !imageOnly.includes(extension)
|
|
269
|
+
);
|
|
270
|
+
if (notImages.length > 0) {
|
|
271
|
+
throw new ScopeError(
|
|
272
|
+
`Scope "${name}": "compress" only applies to images, but it accepts ${notImages.join(", ")}`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
var defineScopes = (scopes) => {
|
|
278
|
+
for (const [name, scope] of Object.entries(scopes)) {
|
|
279
|
+
assertDefinition(name, scope);
|
|
280
|
+
}
|
|
281
|
+
const names = Object.keys(scopes);
|
|
282
|
+
const get = (name) => {
|
|
283
|
+
const scope = scopes[name];
|
|
284
|
+
if (!scope) {
|
|
285
|
+
throw new ScopeError(
|
|
286
|
+
`Unknown scope "${name}". Declared: ${names.join(", ")}`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return scope;
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
scopes,
|
|
293
|
+
names,
|
|
294
|
+
get,
|
|
295
|
+
has: (name) => name in scopes,
|
|
296
|
+
accept: (name) => toAcceptAttribute(get(name).accept)
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
var validateForScope = async (registry, name, file) => {
|
|
300
|
+
const scope = registry.get(name);
|
|
301
|
+
const category = scope.category ? FILE_CATEGORY_CONFIG[scope.category] : void 0;
|
|
302
|
+
return validateFile(file, {
|
|
303
|
+
maxBytes: scope.maxBytes,
|
|
304
|
+
allowedExtensions: scope.accept,
|
|
305
|
+
validateMagicNumbers: category?.validateMagicNumbers ?? true
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
var assertProviderSupports = (registry, provider) => {
|
|
309
|
+
const needsSigning = registry.names.filter(
|
|
310
|
+
(name) => registry.get(name).visibility === "private"
|
|
311
|
+
);
|
|
312
|
+
if (needsSigning.length > 0 && !provider.capabilities.signedUrl) {
|
|
313
|
+
throw new ScopeError(
|
|
314
|
+
`Provider "${provider.name}" cannot sign URLs, but these scopes are private: ${needsSigning.join(", ")}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var resolveKey = (registry, name, entityId, file) => {
|
|
319
|
+
const key = registry.get(name).path(entityId, file);
|
|
320
|
+
if (key.startsWith("/") || key.includes("..")) {
|
|
321
|
+
throw new ScopeError(`Scope "${name}" produced an unsafe key: ${key}`);
|
|
322
|
+
}
|
|
323
|
+
return key;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
exports.FILE_CATEGORY_CONFIG = FILE_CATEGORY_CONFIG;
|
|
327
|
+
exports.GB = GB;
|
|
328
|
+
exports.KB = KB;
|
|
329
|
+
exports.MAGIC_NUMBERS = MAGIC_NUMBERS;
|
|
330
|
+
exports.MB = MB;
|
|
331
|
+
exports.MIME_TYPES = MIME_TYPES;
|
|
332
|
+
exports.ScopeError = ScopeError;
|
|
333
|
+
exports.assertProviderSupports = assertProviderSupports;
|
|
334
|
+
exports.defineScopes = defineScopes;
|
|
335
|
+
exports.formatFileSize = formatFileSize;
|
|
336
|
+
exports.fromMulterFile = fromMulterFile;
|
|
337
|
+
exports.getFileExtension = getFileExtension;
|
|
338
|
+
exports.getMimeType = getMimeType;
|
|
339
|
+
exports.isKnownExtension = isKnownExtension;
|
|
340
|
+
exports.resolveKey = resolveKey;
|
|
341
|
+
exports.toAcceptAttribute = toAcceptAttribute;
|
|
342
|
+
exports.validateExtension = validateExtension;
|
|
343
|
+
exports.validateFile = validateFile;
|
|
344
|
+
exports.validateFiles = validateFiles;
|
|
345
|
+
exports.validateForScope = validateForScope;
|
|
346
|
+
exports.validateMagicNumbers = validateMagicNumbers;
|
|
347
|
+
exports.validateSize = validateSize;
|
|
348
|
+
//# sourceMappingURL=index.cjs.map
|
|
349
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/file.ts","../src/validation.ts","../src/scopes.ts"],"names":[],"mappings":"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","file":"index.cjs","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: 'imagen',\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: 'certificado',\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: 'llave privada',\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: 'documento',\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: 'archivo de datos',\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: 'archivo',\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/** 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","import { MAGIC_NUMBERS } from './constants'\nimport { formatFileSize, getFileExtension, isKnownExtension } from './file'\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): ValidationResult => {\n\tif (allowed.length === 0) return ok\n\n\tconst extension = getFileExtension(fileName)\n\tif (!extension) {\n\t\treturn fail('extension-not-allowed', 'El archivo no tiene extensión')\n\t}\n\tif (!allowed.includes(extension as FileExtension)) {\n\t\treturn fail(\n\t\t\t'extension-not-allowed',\n\t\t\t`Formato no permitido. Se aceptan: ${allowed.join(', ')}`\n\t\t)\n\t}\n\treturn ok\n}\n\nexport const validateSize = (\n\tsize: number,\n\tmaxBytes: number\n): ValidationResult => {\n\tif (size === 0) return fail('empty-file', 'El archivo está vacío')\n\tif (size > maxBytes) {\n\t\treturn fail(\n\t\t\t'too-large',\n\t\t\t`El archivo pesa ${formatFileSize(size)} y el máximo es ${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): Promise<ValidationResult> => {\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', 'El archivo está incompleto')\n\t}\n\n\tconst matches = expected.every((byte, index) => head[index] === byte)\n\treturn matches\n\t\t? ok\n\t\t: fail(\n\t\t\t\t'magic-number-mismatch',\n\t\t\t\t'El contenido del archivo no corresponde a su extensión'\n\t\t\t)\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\tcustomValidation,\n\t} = options\n\n\tconst size = validateSize(file.size, maxBytes)\n\tif (!size.valid) return size\n\n\tconst extension = validateExtension(file.name, allowedExtensions)\n\tif (!extension.valid) return extension\n\n\tif (checkMagic) {\n\t\tconst magic = await validateMagicNumbers(file)\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 {\n\tFileLike,\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\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\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.\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\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 */\nexport const validateForScope = async <T extends Record<string, ScopeConfig>>(\n\tregistry: ScopeRegistry<T>,\n\tname: string,\n\tfile: FileLike\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})\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/** Resolves the storage key for an upload. */\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 (key.startsWith('/') || key.includes('..')) {\n\t\tthrow new ScopeError(`Scope \"${name}\" produced an unsafe key: ${key}`)\n\t}\n\treturn key\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every type in this file must be usable from a browser and from a server.
|
|
3
|
+
* That is what lets one scope definition drive the client's guards and the
|
|
4
|
+
* server's guards without the two drifting apart.
|
|
5
|
+
*/
|
|
6
|
+
type ImageExtension = 'png' | 'jpg' | 'jpeg' | 'svg' | 'webp' | 'gif' | 'ico' | 'heic';
|
|
7
|
+
type CertificateExtension = 'cer' | 'crt' | 'pem';
|
|
8
|
+
type KeyExtension = 'key';
|
|
9
|
+
type DocumentExtension = 'pdf' | 'doc' | 'docx' | 'xls' | 'xlsx' | 'csv' | 'txt';
|
|
10
|
+
type DataExtension = 'xml' | 'json';
|
|
11
|
+
type VideoExtension = 'mp4' | 'mov' | 'avi' | 'webm';
|
|
12
|
+
type AudioExtension = 'mp3' | 'wav' | 'ogg' | 'aac';
|
|
13
|
+
type FileExtension = ImageExtension | CertificateExtension | KeyExtension | DocumentExtension | DataExtension | VideoExtension | AudioExtension;
|
|
14
|
+
/** Validation preset. Picks extensions, size ceiling and header checking. */
|
|
15
|
+
type FileCategory = 'image' | 'certificate' | 'key' | 'pdf' | 'document' | 'data' | 'video' | 'audio' | 'any';
|
|
16
|
+
type FileCategoryConfig = {
|
|
17
|
+
extensions: FileExtension[];
|
|
18
|
+
maxBytes: number;
|
|
19
|
+
/** Read the leading bytes to catch an .exe renamed to .pdf. */
|
|
20
|
+
validateMagicNumbers: boolean;
|
|
21
|
+
/** Lower-case singular noun used in error messages. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Value for an `<input accept="…">`. */
|
|
24
|
+
accept: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The subset of the DOM `File` this package needs. A browser `File`, a Node 18+
|
|
28
|
+
* `File` and a multer memory file (via `fromMulterFile`) all satisfy it, so
|
|
29
|
+
* `validateFile` is the same call on both sides of the wire.
|
|
30
|
+
*/
|
|
31
|
+
type FileLike = {
|
|
32
|
+
name: string;
|
|
33
|
+
size: number;
|
|
34
|
+
/** MIME type as reported by the source. Never trusted on its own. */
|
|
35
|
+
type: string;
|
|
36
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
37
|
+
};
|
|
38
|
+
type UploadStatus = 'idle' | 'validating' | 'uploading' | 'success' | 'error';
|
|
39
|
+
/** Machine-readable reason a file was rejected. Pair with `message` for UI. */
|
|
40
|
+
type ValidationCode = 'extension-not-allowed' | 'too-large' | 'empty-file' | 'magic-number-mismatch' | 'mime-not-allowed' | 'custom';
|
|
41
|
+
type ValidationResult = {
|
|
42
|
+
valid: true;
|
|
43
|
+
} | {
|
|
44
|
+
valid: false;
|
|
45
|
+
code: ValidationCode;
|
|
46
|
+
message: string;
|
|
47
|
+
};
|
|
48
|
+
type ValidationOptions = {
|
|
49
|
+
maxBytes?: number;
|
|
50
|
+
allowedExtensions?: FileExtension[];
|
|
51
|
+
validateMagicNumbers?: boolean;
|
|
52
|
+
/** Runs last, only when every built-in check passed. */
|
|
53
|
+
customValidation?: (file: FileLike) => Promise<ValidationResult>;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Public files are served straight from their URL; private files are only ever
|
|
57
|
+
* reachable through a signed, expiring URL minted by the server.
|
|
58
|
+
*/
|
|
59
|
+
type Visibility = 'public' | 'private';
|
|
60
|
+
/** Client-side image pipeline applied before upload. */
|
|
61
|
+
type CompressOptions = {
|
|
62
|
+
maxWidth?: number;
|
|
63
|
+
maxHeight?: number;
|
|
64
|
+
/** 0–1. Ignored by formats without lossy encoding. */
|
|
65
|
+
quality?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Drop EXIF metadata. Defaults to true: camera photos carry GPS
|
|
68
|
+
* coordinates, and a public bucket is the wrong place for them.
|
|
69
|
+
*/
|
|
70
|
+
stripExif?: boolean;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* A named destination. The single source of truth for where a file lands, who
|
|
74
|
+
* may read it, and what is accepted there.
|
|
75
|
+
*/
|
|
76
|
+
type ScopeConfig = {
|
|
77
|
+
/**
|
|
78
|
+
* Storage key for an upload. Receives the owning entity and the incoming
|
|
79
|
+
* file so the caller controls collisions and folder shape.
|
|
80
|
+
*/
|
|
81
|
+
path: (entityId: string, file: FileLike) => string;
|
|
82
|
+
visibility: Visibility;
|
|
83
|
+
/** Extensions accepted here. Narrower than the category preset, never wider. */
|
|
84
|
+
accept: FileExtension[];
|
|
85
|
+
maxBytes: number;
|
|
86
|
+
category?: FileCategory;
|
|
87
|
+
/** Hand the bytes to the app's cipher before they leave the server. */
|
|
88
|
+
encrypt?: boolean;
|
|
89
|
+
compress?: CompressOptions;
|
|
90
|
+
/** Replace the object at the same key instead of adding a new one. */
|
|
91
|
+
overwrite?: boolean;
|
|
92
|
+
/** Documentation-only today; drives lifecycle rules once adapters read it. */
|
|
93
|
+
retention?: string;
|
|
94
|
+
/** Free-form tags forwarded to the provider when it supports metadata. */
|
|
95
|
+
metadata?: Record<string, string>;
|
|
96
|
+
};
|
|
97
|
+
type ScopeRegistry<T extends Record<string, ScopeConfig>> = {
|
|
98
|
+
scopes: T;
|
|
99
|
+
names: (keyof T & string)[];
|
|
100
|
+
get(name: string): ScopeConfig;
|
|
101
|
+
has(name: string): boolean;
|
|
102
|
+
/** `<input accept>` string for a scope, derived from its extensions. */
|
|
103
|
+
accept(name: keyof T & string): string;
|
|
104
|
+
};
|
|
105
|
+
/** What the app persists after a successful upload. */
|
|
106
|
+
type StoredFile = {
|
|
107
|
+
/** Provider key. The handle for delete, re-sign and download. */
|
|
108
|
+
key: string;
|
|
109
|
+
/** Public URL, or a signed expiring URL when the scope is private. */
|
|
110
|
+
url: string;
|
|
111
|
+
scope: string;
|
|
112
|
+
entityId: string;
|
|
113
|
+
fileName: string;
|
|
114
|
+
mimeType: string;
|
|
115
|
+
size: number;
|
|
116
|
+
/** Enables dedupe and integrity checks when the provider reports it. */
|
|
117
|
+
checksum?: string;
|
|
118
|
+
uploadedAt: number;
|
|
119
|
+
uploadedBy?: string;
|
|
120
|
+
};
|
|
121
|
+
type PutInput = {
|
|
122
|
+
key: string;
|
|
123
|
+
body: Uint8Array;
|
|
124
|
+
contentType: string;
|
|
125
|
+
visibility: Visibility;
|
|
126
|
+
metadata?: Record<string, string>;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* What a provider can actually do. Declared rather than discovered so a scope
|
|
130
|
+
* that needs signed URLs fails at boot, not the first time a user opens a file.
|
|
131
|
+
*/
|
|
132
|
+
type ProviderCapabilities = {
|
|
133
|
+
signedUrl: boolean;
|
|
134
|
+
resumable: boolean;
|
|
135
|
+
rangeRead: boolean;
|
|
136
|
+
};
|
|
137
|
+
type SignedUrlOptions = {
|
|
138
|
+
/** Seconds until the URL stops working. */
|
|
139
|
+
expiresIn: number;
|
|
140
|
+
/** Force a download instead of inline rendering. */
|
|
141
|
+
download?: boolean;
|
|
142
|
+
};
|
|
143
|
+
type StorageProvider = {
|
|
144
|
+
name: string;
|
|
145
|
+
capabilities: ProviderCapabilities;
|
|
146
|
+
put(input: PutInput): Promise<{
|
|
147
|
+
key: string;
|
|
148
|
+
url: string;
|
|
149
|
+
checksum?: string;
|
|
150
|
+
}>;
|
|
151
|
+
get(key: string): Promise<Uint8Array>;
|
|
152
|
+
delete(key: string): Promise<boolean>;
|
|
153
|
+
signedUrl?(key: string, options: SignedUrlOptions): Promise<string>;
|
|
154
|
+
list(prefix: string): Promise<{
|
|
155
|
+
key: string;
|
|
156
|
+
size: number;
|
|
157
|
+
}[]>;
|
|
158
|
+
};
|
|
159
|
+
/** Injected cipher for `encrypt: true` scopes. The package never ships one. */
|
|
160
|
+
type CryptoHooks = {
|
|
161
|
+
encrypt: (data: Uint8Array) => Uint8Array | Promise<Uint8Array>;
|
|
162
|
+
decrypt: (data: Uint8Array) => Uint8Array | Promise<Uint8Array>;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
declare const KB = 1024;
|
|
166
|
+
declare const MB: number;
|
|
167
|
+
declare const GB: number;
|
|
168
|
+
/**
|
|
169
|
+
* Leading bytes that identify a format regardless of its extension. The check
|
|
170
|
+
* that uses them is the only defence against an executable renamed to `.pdf`,
|
|
171
|
+
* since both the extension and the browser-reported MIME come from the client.
|
|
172
|
+
*/
|
|
173
|
+
declare const MAGIC_NUMBERS: Partial<Record<FileExtension, number[]>>;
|
|
174
|
+
declare const MIME_TYPES: Record<FileExtension, string>;
|
|
175
|
+
/**
|
|
176
|
+
* Defaults per category. A scope always narrows these; it can never widen the
|
|
177
|
+
* extension list, which is what keeps `any` from leaking into a strict scope.
|
|
178
|
+
*/
|
|
179
|
+
declare const FILE_CATEGORY_CONFIG: Record<FileCategory, FileCategoryConfig>;
|
|
180
|
+
|
|
181
|
+
/** Lower-case extension without the dot, or `''` when the name has none. */
|
|
182
|
+
declare const getFileExtension: (fileName: string) => string;
|
|
183
|
+
declare const getMimeType: (fileName: string) => string;
|
|
184
|
+
declare const isKnownExtension: (value: string) => value is FileExtension;
|
|
185
|
+
declare const formatFileSize: (bytes: number) => string;
|
|
186
|
+
/** Value for an `<input accept="…">` built from an extension list. */
|
|
187
|
+
declare const toAcceptAttribute: (extensions: FileExtension[]) => string;
|
|
188
|
+
/**
|
|
189
|
+
* Adapts a multer memory-storage file to `FileLike` so server code validates
|
|
190
|
+
* with the very same function the browser ran.
|
|
191
|
+
*/
|
|
192
|
+
declare const fromMulterFile: (file: {
|
|
193
|
+
originalname: string;
|
|
194
|
+
size: number;
|
|
195
|
+
mimetype: string;
|
|
196
|
+
buffer: Uint8Array;
|
|
197
|
+
}) => FileLike;
|
|
198
|
+
|
|
199
|
+
/** Thrown for a definition or wiring mistake — never for a user's bad file. */
|
|
200
|
+
declare class ScopeError extends Error {
|
|
201
|
+
name: string;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Declares every destination an app can write to. The returned registry is
|
|
205
|
+
* imported by the client (to configure inputs and pre-validate) and by the
|
|
206
|
+
* server (to authorize and re-validate), which is what keeps the two in sync.
|
|
207
|
+
*
|
|
208
|
+
* Definitions are checked eagerly: a malformed scope throws at import time,
|
|
209
|
+
* not on the first upload.
|
|
210
|
+
*/
|
|
211
|
+
declare const defineScopes: <T extends Record<string, ScopeConfig>>(scopes: T) => ScopeRegistry<T>;
|
|
212
|
+
/**
|
|
213
|
+
* The one validation call. Client runs it for feedback, server runs it for
|
|
214
|
+
* safety, both against the same scope.
|
|
215
|
+
*/
|
|
216
|
+
declare const validateForScope: <T extends Record<string, ScopeConfig>>(registry: ScopeRegistry<T>, name: string, file: FileLike) => Promise<ValidationResult>;
|
|
217
|
+
/**
|
|
218
|
+
* Fails fast when a provider cannot honour what the scopes promise — a private
|
|
219
|
+
* scope on a provider that cannot sign URLs is a runtime 403 waiting to happen.
|
|
220
|
+
*/
|
|
221
|
+
declare const assertProviderSupports: <T extends Record<string, ScopeConfig>>(registry: ScopeRegistry<T>, provider: StorageProvider) => void;
|
|
222
|
+
/** Resolves the storage key for an upload. */
|
|
223
|
+
declare const resolveKey: <T extends Record<string, ScopeConfig>>(registry: ScopeRegistry<T>, name: string, entityId: string, file: FileLike) => string;
|
|
224
|
+
|
|
225
|
+
declare const validateExtension: (fileName: string, allowed: FileExtension[]) => ValidationResult;
|
|
226
|
+
declare const validateSize: (size: number, maxBytes: number) => ValidationResult;
|
|
227
|
+
/**
|
|
228
|
+
* Compares the file's leading bytes against the signature its extension
|
|
229
|
+
* claims. Extensions without a known signature pass — absence of a signature
|
|
230
|
+
* is not evidence of tampering.
|
|
231
|
+
*/
|
|
232
|
+
declare const validateMagicNumbers: (file: FileLike) => Promise<ValidationResult>;
|
|
233
|
+
/**
|
|
234
|
+
* Runs the checks cheapest-first so a 2 GB file is rejected on its size before
|
|
235
|
+
* anything reads its bytes.
|
|
236
|
+
*/
|
|
237
|
+
declare const validateFile: (file: FileLike, options?: ValidationOptions) => Promise<ValidationResult>;
|
|
238
|
+
type FileValidation = {
|
|
239
|
+
file: FileLike;
|
|
240
|
+
result: ValidationResult;
|
|
241
|
+
};
|
|
242
|
+
/** Validates every file and reports each result; never short-circuits. */
|
|
243
|
+
declare const validateFiles: (files: FileLike[], options?: ValidationOptions) => Promise<FileValidation[]>;
|
|
244
|
+
|
|
245
|
+
export { type AudioExtension, type CertificateExtension, type CompressOptions, type CryptoHooks, type DataExtension, type DocumentExtension, FILE_CATEGORY_CONFIG, type FileCategory, type FileCategoryConfig, type FileExtension, type FileLike, type FileValidation, GB, type ImageExtension, KB, type KeyExtension, MAGIC_NUMBERS, MB, MIME_TYPES, type ProviderCapabilities, type PutInput, type ScopeConfig, ScopeError, type ScopeRegistry, type SignedUrlOptions, type StorageProvider, type StoredFile, type UploadStatus, type ValidationCode, type ValidationOptions, type ValidationResult, type VideoExtension, type Visibility, assertProviderSupports, defineScopes, formatFileSize, fromMulterFile, getFileExtension, getMimeType, isKnownExtension, resolveKey, toAcceptAttribute, validateExtension, validateFile, validateFiles, validateForScope, validateMagicNumbers, validateSize };
|