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,550 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var stream = require('stream');
5
+
6
+ // src/server/crypto.ts
7
+
8
+ // src/constants.ts
9
+ var KB = 1024;
10
+ var MB = 1024 * KB;
11
+ var MAGIC_NUMBERS = {
12
+ png: [137, 80, 78, 71, 13, 10, 26, 10],
13
+ jpg: [255, 216, 255],
14
+ jpeg: [255, 216, 255],
15
+ gif: [71, 73, 70, 56],
16
+ webp: [82, 73, 70, 70],
17
+ // RIFF; bytes 8-11 spell WEBP
18
+ ico: [0, 0, 1, 0],
19
+ pdf: [37, 80, 68, 70],
20
+ // %PDF
21
+ // OOXML files are ZIP containers, so docx/xlsx share the PK signature.
22
+ docx: [80, 75, 3, 4],
23
+ xlsx: [80, 75, 3, 4],
24
+ doc: [208, 207, 17, 224],
25
+ xls: [208, 207, 17, 224],
26
+ // DER/ASN.1 sequence header.
27
+ cer: [48, 130],
28
+ crt: [48, 130],
29
+ key: [48, 130],
30
+ pem: [45, 45, 45, 45, 45],
31
+ // -----
32
+ webm: [26, 69, 223, 163],
33
+ avi: [82, 73, 70, 70],
34
+ mp3: [73, 68, 51],
35
+ // ID3
36
+ wav: [82, 73, 70, 70],
37
+ ogg: [79, 103, 103, 83],
38
+ aac: [255, 241]
39
+ };
40
+ var MIME_TYPES = {
41
+ png: "image/png",
42
+ jpg: "image/jpeg",
43
+ jpeg: "image/jpeg",
44
+ svg: "image/svg+xml",
45
+ webp: "image/webp",
46
+ gif: "image/gif",
47
+ ico: "image/x-icon",
48
+ heic: "image/heic",
49
+ cer: "application/x-x509-ca-cert",
50
+ crt: "application/x-x509-ca-cert",
51
+ pem: "application/x-pem-file",
52
+ key: "application/x-pem-file",
53
+ pdf: "application/pdf",
54
+ doc: "application/msword",
55
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
56
+ xls: "application/vnd.ms-excel",
57
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
58
+ csv: "text/csv",
59
+ txt: "text/plain",
60
+ xml: "application/xml",
61
+ json: "application/json",
62
+ mp4: "video/mp4",
63
+ mov: "video/quicktime",
64
+ avi: "video/x-msvideo",
65
+ webm: "video/webm",
66
+ mp3: "audio/mpeg",
67
+ wav: "audio/wav",
68
+ ogg: "audio/ogg",
69
+ aac: "audio/aac"
70
+ };
71
+ var FILE_CATEGORY_CONFIG = {
72
+ image: {
73
+ extensions: ["png", "jpg", "jpeg", "svg", "webp", "gif"],
74
+ maxBytes: 5 * MB,
75
+ validateMagicNumbers: true,
76
+ label: "image",
77
+ accept: "image/*"
78
+ },
79
+ certificate: {
80
+ extensions: ["cer", "crt", "pem"],
81
+ maxBytes: 50 * KB,
82
+ validateMagicNumbers: true,
83
+ label: "certificate",
84
+ accept: ".cer,.crt,.pem"
85
+ },
86
+ key: {
87
+ extensions: ["key"],
88
+ maxBytes: 50 * KB,
89
+ validateMagicNumbers: true,
90
+ label: "private key",
91
+ accept: ".key"
92
+ },
93
+ pdf: {
94
+ extensions: ["pdf"],
95
+ maxBytes: 20 * MB,
96
+ validateMagicNumbers: true,
97
+ label: "PDF",
98
+ accept: ".pdf"
99
+ },
100
+ document: {
101
+ extensions: ["pdf", "doc", "docx", "xls", "xlsx", "csv", "txt"],
102
+ maxBytes: 20 * MB,
103
+ validateMagicNumbers: false,
104
+ label: "document",
105
+ accept: ".pdf,.doc,.docx,.xls,.xlsx,.csv,.txt"
106
+ },
107
+ data: {
108
+ extensions: ["xml", "json"],
109
+ maxBytes: 5 * MB,
110
+ validateMagicNumbers: false,
111
+ label: "data file",
112
+ accept: ".xml,.json"
113
+ },
114
+ video: {
115
+ extensions: ["mp4", "mov", "avi", "webm"],
116
+ maxBytes: 100 * MB,
117
+ validateMagicNumbers: false,
118
+ label: "video",
119
+ accept: "video/*"
120
+ },
121
+ audio: {
122
+ extensions: ["mp3", "wav", "ogg", "aac"],
123
+ maxBytes: 20 * MB,
124
+ validateMagicNumbers: false,
125
+ label: "audio",
126
+ accept: "audio/*"
127
+ },
128
+ any: {
129
+ extensions: [],
130
+ maxBytes: 20 * MB,
131
+ validateMagicNumbers: false,
132
+ label: "file",
133
+ accept: "*/*"
134
+ }
135
+ };
136
+
137
+ // src/file.ts
138
+ var getFileExtension = (fileName) => {
139
+ const dot = fileName.lastIndexOf(".");
140
+ if (dot < 1 || dot === fileName.length - 1) return "";
141
+ return fileName.slice(dot + 1).toLowerCase();
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
+
156
+ // src/labels.ts
157
+ var DEFAULT_LABELS = {
158
+ dropPrompt: "Choose a file or drag it here",
159
+ tapPrompt: "Tap to choose a file",
160
+ bulkDropPrompt: "Drop several files \u2014 each lands on its document",
161
+ bulkTapPrompt: "Tap to add several files",
162
+ uploadAll: "Upload files",
163
+ view: "View",
164
+ remove: "Remove",
165
+ cancel: "Cancel",
166
+ ready: "Done",
167
+ staged: "Ready to upload",
168
+ upload: "Upload",
169
+ replace: "Replace",
170
+ dropToReplace: "Drop to replace",
171
+ previous: "Previous",
172
+ next: "Next",
173
+ uploadFailed: "The file could not be uploaded",
174
+ uploadNetworkFailed: "The file could not be uploaded. Check your connection",
175
+ removeFailed: "The file could not be removed from storage",
176
+ readFailed: "The file could not be loaded",
177
+ maxFilesReached: (max) => `At most ${max} file(s)`,
178
+ slotFormatNotAllowed: (slotLabel, extensions) => `${slotLabel}: format not allowed. Accepted: ${extensions.join(", ")}`,
179
+ fileWithoutSlot: (fileName) => `"${fileName}" does not match any document`,
180
+ viewerLoading: "Loading\u2026",
181
+ viewerError: "The preview could not be loaded",
182
+ viewerErrorHint: "The file may have moved or the link expired. You can retry or download it.",
183
+ viewerRetry: "Retry",
184
+ openInTab: "Open in tab",
185
+ downloadShort: "Download",
186
+ close: "Close",
187
+ noPreview: "This format has no preview",
188
+ download: (fileName) => `Download ${fileName}`,
189
+ filePreview: "File",
190
+ confirm: "Confirm",
191
+ confirmRemoveTitle: "Remove file",
192
+ confirmRemoveMessage: (fileName) => `Remove "${fileName}"? You can upload another one later.`,
193
+ confirmReplaceTitle: "Replace file",
194
+ confirmReplaceMessage: (current, incoming) => `Replace "${current}" with "${incoming}"?`,
195
+ dropAnywhereTitle: "Drop the files here",
196
+ dropAnywhereHint: "Anywhere on the screen counts",
197
+ avatarChange: "Change photo",
198
+ avatarDropHere: "Drop here",
199
+ avatarRemove: "Remove photo",
200
+ galleryAdd: "Add",
201
+ fileHasNoExtension: "The file has no extension",
202
+ formatNotAllowed: (extensions) => `Format not allowed. Accepted: ${extensions.join(", ")}`,
203
+ fileIsEmpty: "The file is empty",
204
+ fileTooLarge: (size, max) => `The file weighs ${size} and the maximum is ${max}`,
205
+ fileIncomplete: "The file is incomplete",
206
+ contentDoesNotMatchExtension: "The file contents do not match its extension",
207
+ requestIncomplete: "Incomplete request",
208
+ unauthorized: "Unauthorized",
209
+ fileRequired: "A file is required",
210
+ unknownScope: "Unknown file destination",
211
+ processingFailed: "The file could not be processed",
212
+ unnamedFile: "file"
213
+ };
214
+ var resolveLabels = (labels, base = DEFAULT_LABELS) => labels ? { ...base, ...labels } : base;
215
+ ({ ...DEFAULT_LABELS });
216
+
217
+ // src/validation.ts
218
+ var ok = { valid: true };
219
+ var fail = (code, message) => ({ valid: false, code, message });
220
+ var validateExtension = (fileName, allowed, labels) => {
221
+ if (allowed.length === 0) return ok;
222
+ const copy = resolveLabels(labels);
223
+ const extension = getFileExtension(fileName);
224
+ if (!extension) {
225
+ return fail("extension-not-allowed", copy.fileHasNoExtension);
226
+ }
227
+ if (!allowed.includes(extension)) {
228
+ return fail("extension-not-allowed", copy.formatNotAllowed(allowed));
229
+ }
230
+ return ok;
231
+ };
232
+ var validateSize = (size, maxBytes, labels) => {
233
+ const copy = resolveLabels(labels);
234
+ if (size === 0) return fail("empty-file", copy.fileIsEmpty);
235
+ if (size > maxBytes) {
236
+ return fail(
237
+ "too-large",
238
+ copy.fileTooLarge(formatFileSize(size), formatFileSize(maxBytes))
239
+ );
240
+ }
241
+ return ok;
242
+ };
243
+ var validateMagicNumbers = async (file, labels) => {
244
+ const copy = resolveLabels(labels);
245
+ const extension = getFileExtension(file.name);
246
+ if (!isKnownExtension(extension)) return ok;
247
+ const expected = MAGIC_NUMBERS[extension];
248
+ if (!expected) return ok;
249
+ const head = new Uint8Array(await file.arrayBuffer()).slice(
250
+ 0,
251
+ expected.length
252
+ );
253
+ if (head.length < expected.length) {
254
+ return fail("magic-number-mismatch", copy.fileIncomplete);
255
+ }
256
+ const matches = expected.every((byte, index) => head[index] === byte);
257
+ return matches ? ok : fail("magic-number-mismatch", copy.contentDoesNotMatchExtension);
258
+ };
259
+ var validateFile = async (file, options = {}) => {
260
+ const {
261
+ maxBytes = Infinity,
262
+ allowedExtensions = [],
263
+ validateMagicNumbers: checkMagic = false,
264
+ labels,
265
+ customValidation
266
+ } = options;
267
+ const size = validateSize(file.size, maxBytes, labels);
268
+ if (!size.valid) return size;
269
+ const extension = validateExtension(file.name, allowedExtensions, labels);
270
+ if (!extension.valid) return extension;
271
+ if (checkMagic) {
272
+ const magic = await validateMagicNumbers(file, labels);
273
+ if (!magic.valid) return magic;
274
+ }
275
+ if (customValidation) return customValidation(file);
276
+ return ok;
277
+ };
278
+
279
+ // src/scopes.ts
280
+ var ScopeError = class extends Error {
281
+ name = "ScopeError";
282
+ };
283
+ var PROBE_ENTITY = "__entity__";
284
+ var probeFile = (name) => ({
285
+ name,
286
+ size: 0,
287
+ type: "",
288
+ arrayBuffer: async () => new ArrayBuffer(0)
289
+ });
290
+ var hasStableKey = (scope) => scope.path(PROBE_ENTITY, probeFile("a.png")) === scope.path(PROBE_ENTITY, probeFile("b.png"));
291
+ var resolveReplaceMode = (scope) => {
292
+ if (scope.replace !== void 0) return scope.replace;
293
+ if ((scope.maxFiles ?? 1) > 1) return "key";
294
+ return hasStableKey(scope) ? "key" : "entity";
295
+ };
296
+ var dirname = (key) => {
297
+ const cut = key.lastIndexOf("/");
298
+ return cut === -1 ? "" : key.slice(0, cut);
299
+ };
300
+ var resolveScopePrefix = (scope, entityId) => scope.prefix?.(entityId) ?? dirname(scope.path(entityId, probeFile("probe.bin")));
301
+ var validateForScope = async (registry, name, file, labels) => {
302
+ const scope = registry.get(name);
303
+ const category = scope.category ? FILE_CATEGORY_CONFIG[scope.category] : void 0;
304
+ return validateFile(file, {
305
+ maxBytes: scope.maxBytes,
306
+ allowedExtensions: scope.accept,
307
+ validateMagicNumbers: category?.validateMagicNumbers ?? true,
308
+ labels
309
+ });
310
+ };
311
+ var assertProviderSupports = (registry, provider) => {
312
+ const needsSigning = registry.names.filter(
313
+ (name) => registry.get(name).visibility === "private"
314
+ );
315
+ if (needsSigning.length > 0 && !provider.capabilities.signedUrl) {
316
+ throw new ScopeError(
317
+ `Provider "${provider.name}" cannot sign URLs, but these scopes are private: ${needsSigning.join(", ")}`
318
+ );
319
+ }
320
+ };
321
+ var escapesPrefix = (key) => key.startsWith("/") || key.split("/").some((segment) => segment === ".." || segment === ".");
322
+ var resolveKey = (registry, name, entityId, file) => {
323
+ const key = registry.get(name).path(entityId, file);
324
+ if (escapesPrefix(key)) {
325
+ throw new ScopeError(`Scope "${name}" produced an unsafe key: ${key}`);
326
+ }
327
+ return key;
328
+ };
329
+
330
+ // src/server/crypto.ts
331
+ var IV_LENGTH = 12;
332
+ var TAG_LENGTH = 16;
333
+ var HEADER_LENGTH = IV_LENGTH + TAG_LENGTH;
334
+ var KEY_PATTERN = /^[0-9a-fA-F]{64}$/;
335
+ var GcmDecryptStream = class extends stream.Transform {
336
+ constructor(key) {
337
+ super();
338
+ this.key = key;
339
+ }
340
+ key;
341
+ #header = Buffer.alloc(0);
342
+ #decipher = null;
343
+ _transform(chunk, _encoding, done) {
344
+ let body = chunk;
345
+ if (!this.#decipher) {
346
+ this.#header = Buffer.concat([this.#header, chunk]);
347
+ if (this.#header.length < HEADER_LENGTH) return done();
348
+ this.#decipher = crypto.createDecipheriv(
349
+ "aes-256-gcm",
350
+ this.key,
351
+ this.#header.subarray(0, IV_LENGTH)
352
+ );
353
+ this.#decipher.setAuthTag(this.#header.subarray(IV_LENGTH, HEADER_LENGTH));
354
+ body = this.#header.subarray(HEADER_LENGTH);
355
+ if (body.length === 0) return done();
356
+ }
357
+ try {
358
+ this.push(this.#decipher.update(body));
359
+ done();
360
+ } catch (error) {
361
+ done(error);
362
+ }
363
+ }
364
+ _flush(done) {
365
+ if (!this.#decipher) {
366
+ return done(new Error("Encrypted object ended before its header"));
367
+ }
368
+ try {
369
+ this.push(this.#decipher.final());
370
+ done();
371
+ } catch (error) {
372
+ done(error);
373
+ }
374
+ }
375
+ };
376
+ var createAesGcmCrypto = (hexKey) => {
377
+ if (!KEY_PATTERN.test(hexKey)) {
378
+ throw new ScopeError(
379
+ "createAesGcmCrypto: the key must be exactly 64 hex characters (32 bytes). Generate one with `openssl rand -hex 32`."
380
+ );
381
+ }
382
+ const key = Buffer.from(hexKey, "hex");
383
+ return {
384
+ encrypt: (data) => {
385
+ const iv = crypto.randomBytes(IV_LENGTH);
386
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
387
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
388
+ return Buffer.concat([iv, cipher.getAuthTag(), encrypted]);
389
+ },
390
+ decrypt: (data) => {
391
+ const buffer = Buffer.from(data);
392
+ const iv = buffer.subarray(0, IV_LENGTH);
393
+ const tag = buffer.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
394
+ const ciphertext = buffer.subarray(IV_LENGTH + TAG_LENGTH);
395
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
396
+ decipher.setAuthTag(tag);
397
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
398
+ },
399
+ decryptStream: (source) => source.pipe(new GcmDecryptStream(key))
400
+ };
401
+ };
402
+ var StorageRequestError = class extends Error {
403
+ constructor(message, status) {
404
+ super(message);
405
+ this.status = status;
406
+ }
407
+ status;
408
+ name = "StorageRequestError";
409
+ };
410
+ var toHex = (buffer) => [...new Uint8Array(buffer)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
411
+ var createStorage = ({
412
+ scopes,
413
+ provider,
414
+ crypto,
415
+ signedUrlTtl = 300,
416
+ encryptedUrl,
417
+ labels
418
+ }) => {
419
+ const copy = resolveLabels(labels);
420
+ assertProviderSupports(scopes, provider);
421
+ const encrypted = scopes.names.filter((name) => scopes.get(name).encrypt);
422
+ if (encrypted.length > 0 && !crypto) {
423
+ throw new ScopeError(
424
+ `Scopes ${encrypted.join(", ")} declare "encrypt" but no CryptoHooks were provided to createStorage`
425
+ );
426
+ }
427
+ if (encrypted.length > 0 && !encryptedUrl) {
428
+ throw new ScopeError(
429
+ `Scopes ${encrypted.join(", ")} declare "encrypt" but no "encryptedUrl" was provided \u2014 a signed URL would serve ciphertext. Point it at the app's authenticated view endpoint.`
430
+ );
431
+ }
432
+ const getScope = (name) => {
433
+ if (!scopes.has(name)) {
434
+ throw new StorageRequestError(copy.unknownScope, 404);
435
+ }
436
+ return scopes.get(name);
437
+ };
438
+ const upload = async ({
439
+ scope: name,
440
+ entityId,
441
+ file,
442
+ uploadedBy
443
+ }) => {
444
+ const scope = getScope(name);
445
+ const result = await validateForScope(scopes, name, file, copy);
446
+ if (!result.valid) throw new StorageRequestError(result.message, 422);
447
+ const key = resolveKey(scopes, name, entityId, file);
448
+ const raw = new Uint8Array(await file.arrayBuffer());
449
+ const checksum = toHex(
450
+ await globalThis.crypto.subtle.digest("SHA-256", raw)
451
+ );
452
+ const body = scope.encrypt ? await crypto.encrypt(raw) : raw;
453
+ const put = await provider.put({
454
+ key,
455
+ body,
456
+ // Encrypted bytes are opaque on purpose: serving them with the real
457
+ // MIME would let a browser try to render ciphertext.
458
+ contentType: scope.encrypt ? "application/octet-stream" : file.type,
459
+ visibility: scope.visibility,
460
+ metadata: scope.metadata
461
+ });
462
+ const replaced = [];
463
+ if (resolveReplaceMode(scope) === "entity") {
464
+ const prefix = resolveScopePrefix(scope, entityId);
465
+ const stale = (await provider.list(prefix)).filter(
466
+ (object) => object.key !== put.key
467
+ );
468
+ await Promise.all(
469
+ stale.map(
470
+ (object) => provider.delete(object.key).then((deleted) => {
471
+ if (deleted) replaced.push(object.key);
472
+ }).catch(() => void 0)
473
+ )
474
+ );
475
+ }
476
+ const contentChecksum = put.checksum ?? checksum;
477
+ const publicUrl = put.url ? `${put.url}${put.url.includes("?") ? "&" : "?"}v=${contentChecksum.slice(0, 16)}` : put.url;
478
+ return {
479
+ key: put.key,
480
+ replaced,
481
+ url: scope.encrypt ? encryptedUrl({ scope: name, entityId, key: put.key }) : scope.visibility === "private" ? await provider.signedUrl(put.key, { expiresIn: signedUrlTtl }) : publicUrl,
482
+ scope: name,
483
+ entityId,
484
+ fileName: file.name,
485
+ mimeType: file.type,
486
+ size: file.size,
487
+ checksum: contentChecksum,
488
+ uploadedAt: Date.now(),
489
+ ...uploadedBy ? { uploadedBy } : {}
490
+ };
491
+ };
492
+ const read = async ({ scope: name, key }) => {
493
+ const scope = getScope(name);
494
+ const data = await provider.get(key);
495
+ return scope.encrypt ? crypto.decrypt(data) : data;
496
+ };
497
+ const readStream = async ({
498
+ scope: name,
499
+ key
500
+ }) => {
501
+ const scope = getScope(name);
502
+ const canStream = provider.getStream && (!scope.encrypt || crypto?.decryptStream);
503
+ if (!canStream) {
504
+ return stream.Readable.from([Buffer.from(await read({ scope: name, key }))]);
505
+ }
506
+ const source = await provider.getStream(key);
507
+ return scope.encrypt ? crypto.decryptStream(source) : source;
508
+ };
509
+ const remove = ({ scope: name, key }) => {
510
+ const scope = getScope(name);
511
+ if (scope.keepOnRemove) {
512
+ return Promise.resolve(false);
513
+ }
514
+ return provider.delete(key);
515
+ };
516
+ const signedUrl = ({
517
+ scope: name,
518
+ key,
519
+ download,
520
+ expiresIn
521
+ }) => {
522
+ const scope = getScope(name);
523
+ if (scope.visibility !== "private") {
524
+ throw new ScopeError(
525
+ `Scope "${name}" is public; its objects are reachable by their stored URL and never signed`
526
+ );
527
+ }
528
+ const options = {
529
+ expiresIn: expiresIn ?? signedUrlTtl,
530
+ ...download !== void 0 ? { download } : {}
531
+ };
532
+ return provider.signedUrl(key, options);
533
+ };
534
+ return {
535
+ upload,
536
+ read,
537
+ readStream,
538
+ remove,
539
+ signedUrl,
540
+ list: (prefix) => provider.list(prefix),
541
+ scopes,
542
+ labels: copy
543
+ };
544
+ };
545
+
546
+ exports.StorageRequestError = StorageRequestError;
547
+ exports.createAesGcmCrypto = createAesGcmCrypto;
548
+ exports.createStorage = createStorage;
549
+ //# sourceMappingURL=server.cjs.map
550
+ //# sourceMappingURL=server.cjs.map