@beignet/core 0.0.31 → 0.0.33
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/CHANGELOG.md +115 -0
- package/README.md +346 -21
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +63 -47
- package/dist/application/index.js.map +1 -1
- package/dist/domain/entity.d.ts +37 -6
- package/dist/domain/entity.d.ts.map +1 -1
- package/dist/domain/entity.js +9 -7
- package/dist/domain/entity.js.map +1 -1
- package/dist/domain/index.d.ts +4 -4
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +4 -4
- package/dist/domain/index.js.map +1 -1
- package/dist/events/index.d.ts +6 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +20 -10
- package/dist/events/index.js.map +1 -1
- package/dist/flags/index.d.ts +5 -4
- package/dist/flags/index.d.ts.map +1 -1
- package/dist/flags/index.js +10 -9
- package/dist/flags/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +11 -1
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +4 -3
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.d.ts +449 -9
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +472 -12
- package/dist/jobs/index.js.map +1 -1
- package/dist/outbox/index.d.ts +170 -3
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +192 -8
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/events.d.ts +2 -2
- package/dist/ports/index.d.ts +6 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +1 -0
- package/dist/ports/index.js.map +1 -1
- package/dist/providers/instrumentation.d.ts +4 -0
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/providers/provider.d.ts +10 -2
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js.map +1 -1
- package/dist/schedules/index.d.ts +8 -7
- package/dist/schedules/index.d.ts.map +1 -1
- package/dist/schedules/index.js +47 -16
- package/dist/schedules/index.js.map +1 -1
- package/dist/search/index.d.ts +2 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -0
- package/dist/search/index.js.map +1 -1
- package/dist/server/hooks/index.d.ts +1 -0
- package/dist/server/hooks/index.d.ts.map +1 -1
- package/dist/server/hooks/index.js +1 -0
- package/dist/server/hooks/index.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +26 -13
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +28 -34
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/hooks/security.d.ts +179 -0
- package/dist/server/hooks/security.d.ts.map +1 -0
- package/dist/server/hooks/security.js +225 -0
- package/dist/server/hooks/security.js.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +22 -6
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
- package/dist/server/providers/loadProviderConfig.js +15 -2
- package/dist/server/providers/loadProviderConfig.js.map +1 -1
- package/dist/server/request-context.d.ts +4 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +3 -0
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/runtime-integrity.d.ts +84 -0
- package/dist/server/runtime-integrity.d.ts.map +1 -0
- package/dist/server/runtime-integrity.js +160 -0
- package/dist/server/runtime-integrity.js.map +1 -0
- package/dist/server/server.d.ts +10 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +55 -1
- package/dist/server/server.js.map +1 -1
- package/dist/server/trusted-proxy.d.ts +77 -0
- package/dist/server/trusted-proxy.d.ts.map +1 -0
- package/dist/server/trusted-proxy.js +129 -0
- package/dist/server/trusted-proxy.js.map +1 -0
- package/dist/tasks/index.d.ts +6 -7
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +22 -5
- package/dist/tasks/index.js.map +1 -1
- package/dist/tenancy/index.d.ts +41 -0
- package/dist/tenancy/index.d.ts.map +1 -0
- package/dist/tenancy/index.js +34 -0
- package/dist/tenancy/index.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +12 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/execution.d.ts +14 -0
- package/dist/tracing/execution.d.ts.map +1 -0
- package/dist/tracing/execution.js +17 -0
- package/dist/tracing/execution.js.map +1 -0
- package/dist/tracing/index.d.ts +49 -0
- package/dist/tracing/index.d.ts.map +1 -1
- package/dist/tracing/index.js +59 -0
- package/dist/tracing/index.js.map +1 -1
- package/dist/uploads/client.d.ts.map +1 -1
- package/dist/uploads/client.js +44 -6
- package/dist/uploads/client.js.map +1 -1
- package/dist/uploads/index.d.ts +182 -2
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +468 -76
- package/dist/uploads/index.js.map +1 -1
- package/dist/webhooks/index.d.ts +56 -4
- package/dist/webhooks/index.d.ts.map +1 -1
- package/dist/webhooks/index.js +107 -2
- package/dist/webhooks/index.js.map +1 -1
- package/package.json +5 -1
- package/skills/app-architecture/SKILL.md +23 -2
- package/src/application/index.ts +88 -56
- package/src/domain/entity.ts +61 -13
- package/src/domain/index.ts +8 -7
- package/src/events/index.ts +27 -14
- package/src/flags/index.ts +23 -19
- package/src/idempotency/index.ts +17 -3
- package/src/jobs/index.ts +1022 -19
- package/src/outbox/index.ts +409 -9
- package/src/ports/events.ts +2 -2
- package/src/ports/index.ts +17 -0
- package/src/providers/instrumentation.ts +4 -0
- package/src/providers/provider.ts +11 -2
- package/src/schedules/index.ts +60 -26
- package/src/search/index.ts +3 -1
- package/src/server/hooks/index.ts +10 -0
- package/src/server/hooks/rate-limit.ts +52 -46
- package/src/server/hooks/security.ts +503 -0
- package/src/server/index.ts +8 -0
- package/src/server/instrumentation.ts +25 -5
- package/src/server/providers/loadProviderConfig.ts +19 -2
- package/src/server/request-context.ts +7 -0
- package/src/server/runtime-integrity.ts +322 -0
- package/src/server/server.ts +86 -1
- package/src/server/trusted-proxy.ts +249 -0
- package/src/tasks/index.ts +29 -12
- package/src/tenancy/index.ts +55 -0
- package/src/testing/index.ts +19 -3
- package/src/tracing/execution.ts +37 -0
- package/src/tracing/index.ts +137 -0
- package/src/uploads/client.ts +65 -6
- package/src/uploads/index.ts +713 -76
- package/src/webhooks/index.ts +240 -9
- package/src/domain/events.ts +0 -59
package/dist/uploads/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createProviderInstrumentation } from "../providers/index.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Base error for expected upload failures.
|
|
4
|
+
*
|
|
5
|
+
* Upload workflows throw one of the specific subclasses, such as
|
|
6
|
+
* {@link UploadNotFoundError} or {@link UnauthorizedUploadError}. Catch this
|
|
7
|
+
* base class to handle any expected upload failure; the upload router maps it
|
|
8
|
+
* to an HTTP response using `code`, `status`, and `details`.
|
|
4
9
|
*/
|
|
5
10
|
export class UploadError extends Error {
|
|
6
11
|
code;
|
|
@@ -14,6 +19,87 @@ export class UploadError extends Error {
|
|
|
14
19
|
this.details = args.details;
|
|
15
20
|
}
|
|
16
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Error thrown when an upload name is not registered on the router.
|
|
24
|
+
*/
|
|
25
|
+
export class UploadNotFoundError extends UploadError {
|
|
26
|
+
constructor(args) {
|
|
27
|
+
super({ code: "UPLOAD_NOT_FOUND", status: 404, ...args });
|
|
28
|
+
this.name = "UploadNotFoundError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Error thrown when an upload request targets an unknown upload action.
|
|
33
|
+
*/
|
|
34
|
+
export class InvalidUploadActionError extends UploadError {
|
|
35
|
+
constructor(args) {
|
|
36
|
+
super({ code: "INVALID_UPLOAD_ACTION", status: 400, ...args });
|
|
37
|
+
this.name = "InvalidUploadActionError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Error thrown when upload metadata fails schema validation.
|
|
42
|
+
*/
|
|
43
|
+
export class InvalidUploadMetadataError extends UploadError {
|
|
44
|
+
constructor(args) {
|
|
45
|
+
super({ code: "INVALID_UPLOAD_METADATA", status: 422, ...args });
|
|
46
|
+
this.name = "InvalidUploadMetadataError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Error thrown when a file fails upload validation, such as file count,
|
|
51
|
+
* content type (415), size limits (413), or prepared-file mismatches.
|
|
52
|
+
*/
|
|
53
|
+
export class InvalidUploadFileError extends UploadError {
|
|
54
|
+
constructor(args) {
|
|
55
|
+
super({
|
|
56
|
+
code: "INVALID_UPLOAD_FILE",
|
|
57
|
+
message: args.message,
|
|
58
|
+
status: args.status ?? 422,
|
|
59
|
+
details: args.details,
|
|
60
|
+
});
|
|
61
|
+
this.name = "InvalidUploadFileError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Error thrown when an upload is denied by authorize(...) or lacks
|
|
66
|
+
* authorization configuration.
|
|
67
|
+
*/
|
|
68
|
+
export class UnauthorizedUploadError extends UploadError {
|
|
69
|
+
constructor(args) {
|
|
70
|
+
super({ code: "UNAUTHORIZED_UPLOAD", status: 403, ...args });
|
|
71
|
+
this.name = "UnauthorizedUploadError";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Error thrown when a completed upload references a stored object that does
|
|
76
|
+
* not exist.
|
|
77
|
+
*/
|
|
78
|
+
export class UploadObjectNotFoundError extends UploadError {
|
|
79
|
+
constructor(args) {
|
|
80
|
+
super({ code: "UPLOAD_OBJECT_NOT_FOUND", status: 404, ...args });
|
|
81
|
+
this.name = "UploadObjectNotFoundError";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Error thrown when an upload request body exceeds the configured size limit.
|
|
86
|
+
*/
|
|
87
|
+
export class UploadBodyTooLargeError extends UploadError {
|
|
88
|
+
constructor(args) {
|
|
89
|
+
super({ code: "UPLOAD_BODY_TOO_LARGE", status: 413, ...args });
|
|
90
|
+
this.name = "UploadBodyTooLargeError";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Error thrown when an upload request body cannot be parsed or has an
|
|
95
|
+
* invalid shape.
|
|
96
|
+
*/
|
|
97
|
+
export class InvalidUploadBodyError extends UploadError {
|
|
98
|
+
constructor(args) {
|
|
99
|
+
super({ code: "INVALID_UPLOAD_BODY", status: 400, ...args });
|
|
100
|
+
this.name = "InvalidUploadBodyError";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
17
103
|
/**
|
|
18
104
|
* Define a typed upload workflow.
|
|
19
105
|
*/
|
|
@@ -36,9 +122,26 @@ export function defineUpload(name, options) {
|
|
|
36
122
|
...(options.storageMetadata
|
|
37
123
|
? { storageMetadata: options.storageMetadata }
|
|
38
124
|
: {}),
|
|
125
|
+
...(options.verifyFile ? { verifyFile: options.verifyFile } : {}),
|
|
39
126
|
...(options.onComplete ? { onComplete: options.onComplete } : {}),
|
|
40
127
|
};
|
|
41
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Create upload helper methods bound to an application context type.
|
|
131
|
+
*
|
|
132
|
+
* Call it once in `lib/uploads.ts`:
|
|
133
|
+
*
|
|
134
|
+
* ```ts
|
|
135
|
+
* export const { defineUpload } = createUploads<AppContext>();
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function createUploads() {
|
|
139
|
+
return {
|
|
140
|
+
defineUpload(name, options) {
|
|
141
|
+
return defineUpload(name, options);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
42
145
|
/**
|
|
43
146
|
* Define a nested upload registry while preserving upload names and metadata
|
|
44
147
|
* types for client code.
|
|
@@ -123,9 +226,7 @@ export function createUploadRouter(options) {
|
|
|
123
226
|
const registered = [...uploads.keys()]
|
|
124
227
|
.map((registeredName) => `"${registeredName}"`)
|
|
125
228
|
.join(", ");
|
|
126
|
-
throw new
|
|
127
|
-
code: "UPLOAD_NOT_FOUND",
|
|
128
|
-
status: 404,
|
|
229
|
+
throw new UploadNotFoundError({
|
|
129
230
|
message: `Upload "${name}" is not registered. Registered uploads: ${registered || "none"}. Upload routes resolve the defineUpload(...) name, not the defineUploads({...}) registry key.`,
|
|
130
231
|
});
|
|
131
232
|
}
|
|
@@ -143,7 +244,9 @@ export function createUploadRouter(options) {
|
|
|
143
244
|
const parsed = parsePrepareInput(uploadName, input);
|
|
144
245
|
const ctx = await resolveCtx();
|
|
145
246
|
const metadata = await parseMetadata(upload, parsed.metadata);
|
|
146
|
-
assertFiles(upload, parsed.files
|
|
247
|
+
assertFiles(upload, parsed.files, {
|
|
248
|
+
requireChecksum: Boolean(options.signer),
|
|
249
|
+
});
|
|
147
250
|
const files = [];
|
|
148
251
|
for (const file of parsed.files) {
|
|
149
252
|
const uploadId = id();
|
|
@@ -208,7 +311,7 @@ export function createUploadRouter(options) {
|
|
|
208
311
|
const parsed = parseCompleteInput(uploadName, input);
|
|
209
312
|
const ctx = await resolveCtx();
|
|
210
313
|
const metadata = await parseMetadata(upload, parsed.metadata);
|
|
211
|
-
assertFiles(upload, parsed.files);
|
|
314
|
+
assertFiles(upload, parsed.files, { requireChecksum: true });
|
|
212
315
|
const files = [];
|
|
213
316
|
for (const file of parsed.files) {
|
|
214
317
|
await assertAuthorized(upload, {
|
|
@@ -224,9 +327,7 @@ export function createUploadRouter(options) {
|
|
|
224
327
|
uploadId: file.uploadId,
|
|
225
328
|
});
|
|
226
329
|
if (file.key !== expectedKey) {
|
|
227
|
-
throw new
|
|
228
|
-
code: "INVALID_UPLOAD_FILE",
|
|
229
|
-
status: 422,
|
|
330
|
+
throw new InvalidUploadFileError({
|
|
230
331
|
message: `Uploaded object key does not match upload "${upload.name}".`,
|
|
231
332
|
details: {
|
|
232
333
|
expectedKey,
|
|
@@ -234,16 +335,29 @@ export function createUploadRouter(options) {
|
|
|
234
335
|
},
|
|
235
336
|
});
|
|
236
337
|
}
|
|
237
|
-
const object =
|
|
338
|
+
const object = needsUploadBodyVerification(upload, file)
|
|
339
|
+
? await options.storage.get(file.key)
|
|
340
|
+
: await options.storage.stat(file.key);
|
|
238
341
|
if (!object) {
|
|
239
|
-
throw new
|
|
240
|
-
code: "UPLOAD_OBJECT_NOT_FOUND",
|
|
241
|
-
status: 404,
|
|
342
|
+
throw new UploadObjectNotFoundError({
|
|
242
343
|
message: `Uploaded object "${file.key}" was not found.`,
|
|
243
344
|
});
|
|
244
345
|
}
|
|
245
346
|
assertStoredObject(upload, file, object);
|
|
246
|
-
|
|
347
|
+
const verified = await verifyStoredUploadFile(upload, file, object);
|
|
348
|
+
const completedObject = storageObjectMetadata(object);
|
|
349
|
+
const completedFile = {
|
|
350
|
+
...file,
|
|
351
|
+
...(verified.checksum ? { checksum: verified.checksum } : {}),
|
|
352
|
+
object: completedObject,
|
|
353
|
+
};
|
|
354
|
+
await assertVerifiedFile(upload, {
|
|
355
|
+
ctx,
|
|
356
|
+
metadata,
|
|
357
|
+
file: completedFile,
|
|
358
|
+
storage: options.storage,
|
|
359
|
+
});
|
|
360
|
+
files.push(completedFile);
|
|
247
361
|
}
|
|
248
362
|
const result = await upload.onComplete?.({ ctx, metadata, files });
|
|
249
363
|
instrumentation.custom({
|
|
@@ -280,45 +394,57 @@ export function createUploadRouter(options) {
|
|
|
280
394
|
const metadata = await parseMetadata(definition, metadataFromFormData(input.formData));
|
|
281
395
|
const webFiles = filesFromFormData(input.formData);
|
|
282
396
|
const intents = webFiles.map(fileIntentFromFile);
|
|
283
|
-
assertFiles(definition, intents);
|
|
397
|
+
assertFiles(definition, intents, { requireChecksum: false });
|
|
284
398
|
const completed = [];
|
|
285
399
|
for (const [index, file] of webFiles.entries()) {
|
|
286
400
|
const intent = intents[index];
|
|
287
401
|
if (!intent)
|
|
288
402
|
continue;
|
|
403
|
+
const verified = await verifyBlobUploadFile(definition, intent, file);
|
|
404
|
+
const verifiedIntent = {
|
|
405
|
+
...intent,
|
|
406
|
+
...(verified.checksum ? { checksum: verified.checksum } : {}),
|
|
407
|
+
};
|
|
289
408
|
const uploadId = id();
|
|
290
409
|
await assertAuthorized(definition, {
|
|
291
410
|
ctx,
|
|
292
411
|
metadata,
|
|
293
|
-
file:
|
|
412
|
+
file: verifiedIntent,
|
|
294
413
|
uploadId,
|
|
295
414
|
});
|
|
296
415
|
const key = await definition.key({
|
|
297
416
|
ctx,
|
|
298
417
|
metadata,
|
|
299
|
-
file:
|
|
418
|
+
file: verifiedIntent,
|
|
300
419
|
uploadId,
|
|
301
420
|
});
|
|
302
421
|
const storageMetadata = (await definition.storageMetadata?.({
|
|
303
422
|
ctx,
|
|
304
423
|
metadata,
|
|
305
|
-
file:
|
|
424
|
+
file: verifiedIntent,
|
|
306
425
|
uploadId,
|
|
307
426
|
})) ?? {};
|
|
308
427
|
const object = await options.storage.put(key, file, {
|
|
309
|
-
contentType:
|
|
428
|
+
contentType: verifiedIntent.contentType,
|
|
310
429
|
...(definition.file.cacheControl !== undefined
|
|
311
430
|
? { cacheControl: definition.file.cacheControl }
|
|
312
431
|
: {}),
|
|
313
432
|
metadata: storageMetadata,
|
|
314
433
|
visibility: definition.file.visibility ?? "private",
|
|
315
434
|
});
|
|
316
|
-
|
|
317
|
-
...
|
|
435
|
+
const completedFile = {
|
|
436
|
+
...verifiedIntent,
|
|
318
437
|
uploadId,
|
|
319
438
|
key,
|
|
320
439
|
object,
|
|
440
|
+
};
|
|
441
|
+
await assertVerifiedFile(definition, {
|
|
442
|
+
ctx,
|
|
443
|
+
metadata,
|
|
444
|
+
file: completedFile,
|
|
445
|
+
storage: options.storage,
|
|
321
446
|
});
|
|
447
|
+
completed.push(completedFile);
|
|
322
448
|
}
|
|
323
449
|
const result = await definition.onComplete?.({
|
|
324
450
|
ctx,
|
|
@@ -419,9 +545,7 @@ function assertUploadContentLengthWithinLimit(headers, context) {
|
|
|
419
545
|
if (!Number.isFinite(actualBytes) || actualBytes < 0)
|
|
420
546
|
return;
|
|
421
547
|
if (actualBytes > context.maxBytes) {
|
|
422
|
-
throw new
|
|
423
|
-
code: "UPLOAD_BODY_TOO_LARGE",
|
|
424
|
-
status: 413,
|
|
548
|
+
throw new UploadBodyTooLargeError({
|
|
425
549
|
message: `Upload "${context.uploadName}" ${context.action} body is too large.`,
|
|
426
550
|
details: {
|
|
427
551
|
maxBytes: context.maxBytes,
|
|
@@ -436,9 +560,7 @@ async function readLimitedRequestText(request, context) {
|
|
|
436
560
|
const text = await request.text();
|
|
437
561
|
const actualBytes = new TextEncoder().encode(text).byteLength;
|
|
438
562
|
if (actualBytes > context.maxBytes) {
|
|
439
|
-
throw new
|
|
440
|
-
code: "UPLOAD_BODY_TOO_LARGE",
|
|
441
|
-
status: 413,
|
|
563
|
+
throw new UploadBodyTooLargeError({
|
|
442
564
|
message: `Upload "${context.uploadName}" ${context.action} body is too large.`,
|
|
443
565
|
details: {
|
|
444
566
|
maxBytes: context.maxBytes,
|
|
@@ -459,9 +581,7 @@ async function readLimitedRequestText(request, context) {
|
|
|
459
581
|
break;
|
|
460
582
|
actualBytes += result.value.byteLength;
|
|
461
583
|
if (actualBytes > context.maxBytes) {
|
|
462
|
-
throw new
|
|
463
|
-
code: "UPLOAD_BODY_TOO_LARGE",
|
|
464
|
-
status: 413,
|
|
584
|
+
throw new UploadBodyTooLargeError({
|
|
465
585
|
message: `Upload "${context.uploadName}" ${context.action} body is too large.`,
|
|
466
586
|
details: {
|
|
467
587
|
maxBytes: context.maxBytes,
|
|
@@ -485,9 +605,7 @@ async function readJsonBody(request, context) {
|
|
|
485
605
|
catch (error) {
|
|
486
606
|
if (error instanceof UploadError)
|
|
487
607
|
throw error;
|
|
488
|
-
throw new
|
|
489
|
-
code: "INVALID_UPLOAD_BODY",
|
|
490
|
-
status: 400,
|
|
608
|
+
throw new InvalidUploadBodyError({
|
|
491
609
|
message: `Upload "${context.uploadName}" ${context.action} body must be valid JSON.`,
|
|
492
610
|
});
|
|
493
611
|
}
|
|
@@ -532,9 +650,7 @@ function parsePrepareInput(uploadName, body) {
|
|
|
532
650
|
requireCompletedFileFields: false,
|
|
533
651
|
});
|
|
534
652
|
if (issues.length > 0) {
|
|
535
|
-
throw new
|
|
536
|
-
code: "INVALID_UPLOAD_BODY",
|
|
537
|
-
status: 400,
|
|
653
|
+
throw new InvalidUploadBodyError({
|
|
538
654
|
message: `Upload "${uploadName}" prepare body is invalid.`,
|
|
539
655
|
details: { issues },
|
|
540
656
|
});
|
|
@@ -546,9 +662,7 @@ function parseCompleteInput(uploadName, body) {
|
|
|
546
662
|
requireCompletedFileFields: true,
|
|
547
663
|
});
|
|
548
664
|
if (issues.length > 0) {
|
|
549
|
-
throw new
|
|
550
|
-
code: "INVALID_UPLOAD_BODY",
|
|
551
|
-
status: 400,
|
|
665
|
+
throw new InvalidUploadBodyError({
|
|
552
666
|
message: `Upload "${uploadName}" complete body is invalid.`,
|
|
553
667
|
details: { issues },
|
|
554
668
|
});
|
|
@@ -558,9 +672,7 @@ function parseCompleteInput(uploadName, body) {
|
|
|
558
672
|
async function parseMetadata(upload, input) {
|
|
559
673
|
const result = await upload.metadata["~standard"].validate(input);
|
|
560
674
|
if (result.issues?.length) {
|
|
561
|
-
throw new
|
|
562
|
-
code: "INVALID_UPLOAD_METADATA",
|
|
563
|
-
status: 422,
|
|
675
|
+
throw new InvalidUploadMetadataError({
|
|
564
676
|
message: `Invalid metadata for upload "${upload.name}".`,
|
|
565
677
|
details: { issues: result.issues },
|
|
566
678
|
});
|
|
@@ -570,29 +682,27 @@ async function parseMetadata(upload, input) {
|
|
|
570
682
|
}
|
|
571
683
|
throw new Error("Invalid Standard Schema result: missing value");
|
|
572
684
|
}
|
|
573
|
-
function assertFiles(upload, files) {
|
|
685
|
+
function assertFiles(upload, files, options = {}) {
|
|
574
686
|
const maxFiles = upload.file.maxFiles ?? 1;
|
|
575
687
|
if (files.length === 0 || files.length > maxFiles) {
|
|
576
|
-
throw new
|
|
577
|
-
code: "INVALID_UPLOAD_FILE",
|
|
578
|
-
status: 422,
|
|
688
|
+
throw new InvalidUploadFileError({
|
|
579
689
|
message: `Upload "${upload.name}" requires between 1 and ${maxFiles} file${maxFiles === 1 ? "" : "s"}.`,
|
|
580
690
|
details: { fileCount: files.length, maxFiles },
|
|
581
691
|
});
|
|
582
692
|
}
|
|
583
693
|
for (const file of files) {
|
|
584
694
|
if (!file.name || !file.contentType || !Number.isFinite(file.size)) {
|
|
585
|
-
throw new
|
|
586
|
-
code: "INVALID_UPLOAD_FILE",
|
|
587
|
-
status: 422,
|
|
695
|
+
throw new InvalidUploadFileError({
|
|
588
696
|
message: `Upload "${upload.name}" received invalid file metadata.`,
|
|
589
697
|
details: { file },
|
|
590
698
|
});
|
|
591
699
|
}
|
|
700
|
+
assertFileChecksum(upload, file, options);
|
|
592
701
|
if (upload.file.contentTypes?.length &&
|
|
593
|
-
!upload.file.contentTypes
|
|
594
|
-
|
|
595
|
-
|
|
702
|
+
!upload.file.contentTypes
|
|
703
|
+
.map(normalizeContentType)
|
|
704
|
+
.includes(normalizeContentType(file.contentType))) {
|
|
705
|
+
throw new InvalidUploadFileError({
|
|
596
706
|
status: 415,
|
|
597
707
|
message: `Upload "${upload.name}" does not accept "${file.contentType}".`,
|
|
598
708
|
details: {
|
|
@@ -603,8 +713,7 @@ function assertFiles(upload, files) {
|
|
|
603
713
|
}
|
|
604
714
|
if (upload.file.maxSizeBytes !== undefined &&
|
|
605
715
|
file.size > upload.file.maxSizeBytes) {
|
|
606
|
-
throw new
|
|
607
|
-
code: "INVALID_UPLOAD_FILE",
|
|
716
|
+
throw new InvalidUploadFileError({
|
|
608
717
|
status: 413,
|
|
609
718
|
message: `Upload "${upload.name}" exceeds the maximum file size.`,
|
|
610
719
|
details: {
|
|
@@ -615,13 +724,305 @@ function assertFiles(upload, files) {
|
|
|
615
724
|
}
|
|
616
725
|
}
|
|
617
726
|
}
|
|
727
|
+
function assertFileChecksum(upload, file, options) {
|
|
728
|
+
const requirement = upload.file.checksum;
|
|
729
|
+
if (!requirement && !file.checksum)
|
|
730
|
+
return;
|
|
731
|
+
const configuredAlgorithm = requirement?.algorithm;
|
|
732
|
+
if (configuredAlgorithm && configuredAlgorithm !== "sha256") {
|
|
733
|
+
throw new InvalidUploadFileError({
|
|
734
|
+
message: `Upload "${upload.name}" uses an unsupported checksum algorithm.`,
|
|
735
|
+
details: {
|
|
736
|
+
algorithm: configuredAlgorithm,
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
const required = requirement
|
|
741
|
+
? requirement.required !== false && options.requireChecksum === true
|
|
742
|
+
: false;
|
|
743
|
+
if (!file.checksum) {
|
|
744
|
+
if (!required)
|
|
745
|
+
return;
|
|
746
|
+
throw new InvalidUploadFileError({
|
|
747
|
+
message: `Upload "${upload.name}" requires a sha256 checksum for "${file.name}".`,
|
|
748
|
+
details: {
|
|
749
|
+
fileName: file.name,
|
|
750
|
+
algorithm: "sha256",
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
if (file.checksum.algorithm !== "sha256" ||
|
|
755
|
+
!isSha256Hex(file.checksum.value)) {
|
|
756
|
+
throw new InvalidUploadFileError({
|
|
757
|
+
message: `Upload "${upload.name}" received an invalid checksum for "${file.name}".`,
|
|
758
|
+
details: {
|
|
759
|
+
fileName: file.name,
|
|
760
|
+
checksum: file.checksum,
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const UPLOAD_SIGNATURE_MAX_BYTES = 512;
|
|
766
|
+
function needsUploadBodyVerification(upload, file) {
|
|
767
|
+
return (needsUploadChecksumVerification(upload, file) ||
|
|
768
|
+
needsUploadContentTypeVerification(upload, file));
|
|
769
|
+
}
|
|
770
|
+
function needsUploadChecksumVerification(upload, file) {
|
|
771
|
+
return Boolean(file.checksum ||
|
|
772
|
+
(upload.file.checksum && upload.file.checksum.required !== false));
|
|
773
|
+
}
|
|
774
|
+
function needsUploadContentTypeVerification(upload, file) {
|
|
775
|
+
if (upload.file.contentTypeVerification === false)
|
|
776
|
+
return false;
|
|
777
|
+
if (hasContentTypeSignature(file.contentType))
|
|
778
|
+
return true;
|
|
779
|
+
return Boolean(upload.file.contentTypes?.some((contentType) => hasContentTypeSignature(contentType)));
|
|
780
|
+
}
|
|
781
|
+
async function verifyBlobUploadFile(upload, file, blob) {
|
|
782
|
+
if (!needsUploadBodyVerification(upload, file))
|
|
783
|
+
return {};
|
|
784
|
+
const needsChecksum = needsUploadChecksumVerification(upload, file);
|
|
785
|
+
const bytes = needsChecksum
|
|
786
|
+
? new Uint8Array(await blob.arrayBuffer())
|
|
787
|
+
: new Uint8Array(await blob.slice(0, UPLOAD_SIGNATURE_MAX_BYTES).arrayBuffer());
|
|
788
|
+
assertUploadContentTypeSignature(upload, file, bytes);
|
|
789
|
+
if (!needsChecksum)
|
|
790
|
+
return {};
|
|
791
|
+
const checksum = await createUploadChecksum(bytes);
|
|
792
|
+
assertUploadChecksum(upload, file, checksum);
|
|
793
|
+
return { checksum };
|
|
794
|
+
}
|
|
795
|
+
async function verifyStoredUploadFile(upload, file, object) {
|
|
796
|
+
if (!needsUploadBodyVerification(upload, file))
|
|
797
|
+
return {};
|
|
798
|
+
if (!("bytes" in object)) {
|
|
799
|
+
throw new InvalidUploadFileError({
|
|
800
|
+
message: `Uploaded object "${object.key}" must be readable for verification.`,
|
|
801
|
+
details: {
|
|
802
|
+
key: object.key,
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
const needsChecksum = needsUploadChecksumVerification(upload, file);
|
|
807
|
+
const bytes = needsChecksum
|
|
808
|
+
? await object.bytes()
|
|
809
|
+
: await readObjectPrefix(object, UPLOAD_SIGNATURE_MAX_BYTES);
|
|
810
|
+
assertUploadContentTypeSignature(upload, file, bytes);
|
|
811
|
+
if (!needsChecksum)
|
|
812
|
+
return {};
|
|
813
|
+
const checksum = await createUploadChecksum(bytes);
|
|
814
|
+
assertUploadChecksum(upload, file, checksum);
|
|
815
|
+
return { checksum };
|
|
816
|
+
}
|
|
817
|
+
function storageObjectMetadata(object) {
|
|
818
|
+
return {
|
|
819
|
+
key: object.key,
|
|
820
|
+
size: object.size,
|
|
821
|
+
...(object.contentType !== undefined
|
|
822
|
+
? { contentType: object.contentType }
|
|
823
|
+
: {}),
|
|
824
|
+
...(object.cacheControl !== undefined
|
|
825
|
+
? { cacheControl: object.cacheControl }
|
|
826
|
+
: {}),
|
|
827
|
+
metadata: object.metadata,
|
|
828
|
+
visibility: object.visibility,
|
|
829
|
+
lastModified: object.lastModified,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
function assertUploadContentTypeSignature(upload, file, bytes) {
|
|
833
|
+
if (upload.file.contentTypeVerification === false)
|
|
834
|
+
return;
|
|
835
|
+
const declaredContentType = normalizeContentType(file.contentType);
|
|
836
|
+
const acceptedContentTypes = upload.file.contentTypes?.map(normalizeContentType) ?? [];
|
|
837
|
+
const detectedContentType = detectSupportedContentType(bytes);
|
|
838
|
+
if (detectedContentType &&
|
|
839
|
+
acceptedContentTypes.length > 0 &&
|
|
840
|
+
!acceptedContentTypes.includes(detectedContentType)) {
|
|
841
|
+
throw new InvalidUploadFileError({
|
|
842
|
+
status: 415,
|
|
843
|
+
message: `Upload "${upload.name}" does not accept detected content type "${detectedContentType}".`,
|
|
844
|
+
details: {
|
|
845
|
+
fileName: file.name,
|
|
846
|
+
declaredContentType,
|
|
847
|
+
detectedContentType,
|
|
848
|
+
acceptedContentTypes,
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if (hasContentTypeSignature(declaredContentType) &&
|
|
853
|
+
!matchesContentTypeSignature(declaredContentType, bytes)) {
|
|
854
|
+
throw new InvalidUploadFileError({
|
|
855
|
+
status: 415,
|
|
856
|
+
message: `Upload "${upload.name}" content type "${declaredContentType}" does not match the file bytes.`,
|
|
857
|
+
details: {
|
|
858
|
+
fileName: file.name,
|
|
859
|
+
expectedContentType: declaredContentType,
|
|
860
|
+
detectedContentType: detectedContentType ?? "unknown",
|
|
861
|
+
},
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function assertUploadChecksum(upload, file, actual) {
|
|
866
|
+
if (!file.checksum)
|
|
867
|
+
return;
|
|
868
|
+
if (file.checksum.value.toLowerCase() === actual.value)
|
|
869
|
+
return;
|
|
870
|
+
throw new InvalidUploadFileError({
|
|
871
|
+
message: `Upload "${upload.name}" checksum does not match "${file.name}".`,
|
|
872
|
+
details: {
|
|
873
|
+
fileName: file.name,
|
|
874
|
+
algorithm: "sha256",
|
|
875
|
+
expected: file.checksum.value.toLowerCase(),
|
|
876
|
+
actual: actual.value,
|
|
877
|
+
},
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
async function assertVerifiedFile(upload, args) {
|
|
881
|
+
if (!upload.verifyFile)
|
|
882
|
+
return;
|
|
883
|
+
const result = await upload.verifyFile(args);
|
|
884
|
+
const invalid = result === false ||
|
|
885
|
+
(typeof result === "object" &&
|
|
886
|
+
result !== null &&
|
|
887
|
+
"valid" in result &&
|
|
888
|
+
result.valid === false);
|
|
889
|
+
if (!invalid)
|
|
890
|
+
return;
|
|
891
|
+
throw new InvalidUploadFileError({
|
|
892
|
+
message: typeof result === "object" && result?.reason
|
|
893
|
+
? result.reason
|
|
894
|
+
: `Upload "${upload.name}" file "${args.file.name}" did not pass verification.`,
|
|
895
|
+
details: typeof result === "object" && "details" in result
|
|
896
|
+
? result.details
|
|
897
|
+
: {
|
|
898
|
+
fileName: args.file.name,
|
|
899
|
+
key: args.file.key,
|
|
900
|
+
},
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
async function readObjectPrefix(object, maxBytes) {
|
|
904
|
+
const reader = object.stream().getReader();
|
|
905
|
+
const chunks = [];
|
|
906
|
+
let total = 0;
|
|
907
|
+
try {
|
|
908
|
+
while (total < maxBytes) {
|
|
909
|
+
const result = await reader.read();
|
|
910
|
+
if (result.done)
|
|
911
|
+
break;
|
|
912
|
+
const remaining = maxBytes - total;
|
|
913
|
+
const chunk = result.value.byteLength > remaining
|
|
914
|
+
? result.value.slice(0, remaining)
|
|
915
|
+
: result.value;
|
|
916
|
+
chunks.push(chunk);
|
|
917
|
+
total += chunk.byteLength;
|
|
918
|
+
if (result.value.byteLength > remaining)
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
finally {
|
|
923
|
+
await reader.cancel().catch(() => undefined);
|
|
924
|
+
reader.releaseLock();
|
|
925
|
+
}
|
|
926
|
+
const bytes = new Uint8Array(total);
|
|
927
|
+
let offset = 0;
|
|
928
|
+
for (const chunk of chunks) {
|
|
929
|
+
bytes.set(chunk, offset);
|
|
930
|
+
offset += chunk.byteLength;
|
|
931
|
+
}
|
|
932
|
+
return bytes;
|
|
933
|
+
}
|
|
934
|
+
async function createUploadChecksum(bytes) {
|
|
935
|
+
if (!globalThis.crypto?.subtle) {
|
|
936
|
+
throw new InvalidUploadFileError({
|
|
937
|
+
message: "Upload checksum verification requires Web Crypto.",
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
941
|
+
new Uint8Array(buffer).set(bytes);
|
|
942
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", buffer);
|
|
943
|
+
return {
|
|
944
|
+
algorithm: "sha256",
|
|
945
|
+
value: bytesToHex(new Uint8Array(digest)),
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function bytesToHex(bytes) {
|
|
949
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
950
|
+
}
|
|
951
|
+
function isSha256Hex(value) {
|
|
952
|
+
return /^[a-f0-9]{64}$/i.test(value);
|
|
953
|
+
}
|
|
954
|
+
function normalizeContentType(contentType) {
|
|
955
|
+
return contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
956
|
+
}
|
|
957
|
+
function hasContentTypeSignature(contentType) {
|
|
958
|
+
return SIGNATURE_CONTENT_TYPES.has(normalizeContentType(contentType));
|
|
959
|
+
}
|
|
960
|
+
function matchesContentTypeSignature(contentType, bytes) {
|
|
961
|
+
const normalized = normalizeContentType(contentType);
|
|
962
|
+
switch (normalized) {
|
|
963
|
+
case "application/pdf":
|
|
964
|
+
return startsWithBytes(bytes, [0x25, 0x50, 0x44, 0x46, 0x2d]);
|
|
965
|
+
case "application/zip":
|
|
966
|
+
return (startsWithBytes(bytes, [0x50, 0x4b, 0x03, 0x04]) ||
|
|
967
|
+
startsWithBytes(bytes, [0x50, 0x4b, 0x05, 0x06]) ||
|
|
968
|
+
startsWithBytes(bytes, [0x50, 0x4b, 0x07, 0x08]));
|
|
969
|
+
case "image/gif":
|
|
970
|
+
return (startsWithAscii(bytes, "GIF87a") || startsWithAscii(bytes, "GIF89a"));
|
|
971
|
+
case "image/jpeg":
|
|
972
|
+
return startsWithBytes(bytes, [0xff, 0xd8, 0xff]);
|
|
973
|
+
case "image/png":
|
|
974
|
+
return startsWithBytes(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
975
|
+
case "image/svg+xml":
|
|
976
|
+
return looksLikeSvg(bytes);
|
|
977
|
+
case "image/webp":
|
|
978
|
+
return (startsWithAscii(bytes, "RIFF") &&
|
|
979
|
+
bytes.length >= 12 &&
|
|
980
|
+
asciiAt(bytes, 8, 4) === "WEBP");
|
|
981
|
+
default:
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function detectSupportedContentType(bytes) {
|
|
986
|
+
for (const contentType of SIGNATURE_CONTENT_TYPES) {
|
|
987
|
+
if (matchesContentTypeSignature(contentType, bytes))
|
|
988
|
+
return contentType;
|
|
989
|
+
}
|
|
990
|
+
return undefined;
|
|
991
|
+
}
|
|
992
|
+
const SIGNATURE_CONTENT_TYPES = new Set([
|
|
993
|
+
"application/pdf",
|
|
994
|
+
"application/zip",
|
|
995
|
+
"image/gif",
|
|
996
|
+
"image/jpeg",
|
|
997
|
+
"image/png",
|
|
998
|
+
"image/svg+xml",
|
|
999
|
+
"image/webp",
|
|
1000
|
+
]);
|
|
1001
|
+
function startsWithBytes(bytes, prefix) {
|
|
1002
|
+
if (bytes.length < prefix.length)
|
|
1003
|
+
return false;
|
|
1004
|
+
return prefix.every((byte, index) => bytes[index] === byte);
|
|
1005
|
+
}
|
|
1006
|
+
function startsWithAscii(bytes, prefix) {
|
|
1007
|
+
return asciiAt(bytes, 0, prefix.length) === prefix;
|
|
1008
|
+
}
|
|
1009
|
+
function asciiAt(bytes, offset, length) {
|
|
1010
|
+
return String.fromCharCode(...bytes.slice(offset, offset + length));
|
|
1011
|
+
}
|
|
1012
|
+
function looksLikeSvg(bytes) {
|
|
1013
|
+
const text = new TextDecoder()
|
|
1014
|
+
.decode(bytes)
|
|
1015
|
+
.replace(/^\uFEFF/, "")
|
|
1016
|
+
.trimStart()
|
|
1017
|
+
.toLowerCase();
|
|
1018
|
+
return (text.startsWith("<svg") ||
|
|
1019
|
+
(text.startsWith("<?xml") && text.includes("<svg")));
|
|
1020
|
+
}
|
|
618
1021
|
async function assertAuthorized(upload, args) {
|
|
619
1022
|
if (!upload.authorize) {
|
|
620
1023
|
if ((upload.access ?? "protected") === "public")
|
|
621
1024
|
return;
|
|
622
|
-
throw new
|
|
623
|
-
code: "UNAUTHORIZED_UPLOAD",
|
|
624
|
-
status: 403,
|
|
1025
|
+
throw new UnauthorizedUploadError({
|
|
625
1026
|
message: `Upload "${upload.name}" must declare authorize(...) or set access: "public".`,
|
|
626
1027
|
});
|
|
627
1028
|
}
|
|
@@ -633,9 +1034,7 @@ async function assertAuthorized(upload, args) {
|
|
|
633
1034
|
result.allowed === false);
|
|
634
1035
|
if (!denied)
|
|
635
1036
|
return;
|
|
636
|
-
throw new
|
|
637
|
-
code: "UNAUTHORIZED_UPLOAD",
|
|
638
|
-
status: 403,
|
|
1037
|
+
throw new UnauthorizedUploadError({
|
|
639
1038
|
message: typeof result === "object" && result?.reason
|
|
640
1039
|
? result.reason
|
|
641
1040
|
: `Upload "${upload.name}" is not authorized.`,
|
|
@@ -645,8 +1044,7 @@ function assertStoredObject(upload, file, object) {
|
|
|
645
1044
|
assertFiles(upload, [file]);
|
|
646
1045
|
if (upload.file.maxSizeBytes !== undefined &&
|
|
647
1046
|
object.size > upload.file.maxSizeBytes) {
|
|
648
|
-
throw new
|
|
649
|
-
code: "INVALID_UPLOAD_FILE",
|
|
1047
|
+
throw new InvalidUploadFileError({
|
|
650
1048
|
status: 413,
|
|
651
1049
|
message: `Uploaded object "${object.key}" exceeds the maximum file size.`,
|
|
652
1050
|
details: {
|
|
@@ -656,9 +1054,7 @@ function assertStoredObject(upload, file, object) {
|
|
|
656
1054
|
});
|
|
657
1055
|
}
|
|
658
1056
|
if (object.size !== file.size) {
|
|
659
|
-
throw new
|
|
660
|
-
code: "INVALID_UPLOAD_FILE",
|
|
661
|
-
status: 422,
|
|
1057
|
+
throw new InvalidUploadFileError({
|
|
662
1058
|
message: `Uploaded object "${object.key}" size does not match the prepared file.`,
|
|
663
1059
|
details: {
|
|
664
1060
|
expected: file.size,
|
|
@@ -668,10 +1064,9 @@ function assertStoredObject(upload, file, object) {
|
|
|
668
1064
|
}
|
|
669
1065
|
if (object.contentType &&
|
|
670
1066
|
file.contentType &&
|
|
671
|
-
object.contentType !==
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
status: 422,
|
|
1067
|
+
normalizeContentType(object.contentType) !==
|
|
1068
|
+
normalizeContentType(file.contentType)) {
|
|
1069
|
+
throw new InvalidUploadFileError({
|
|
675
1070
|
message: `Uploaded object "${object.key}" content type does not match the prepared file.`,
|
|
676
1071
|
details: {
|
|
677
1072
|
expected: file.contentType,
|
|
@@ -688,9 +1083,7 @@ function metadataFromFormData(formData) {
|
|
|
688
1083
|
return JSON.parse(metadata);
|
|
689
1084
|
}
|
|
690
1085
|
catch {
|
|
691
|
-
throw new
|
|
692
|
-
code: "INVALID_UPLOAD_BODY",
|
|
693
|
-
status: 400,
|
|
1086
|
+
throw new InvalidUploadBodyError({
|
|
694
1087
|
message: "Upload metadata must be valid JSON.",
|
|
695
1088
|
});
|
|
696
1089
|
}
|
|
@@ -699,18 +1092,17 @@ function filesFromFormData(formData) {
|
|
|
699
1092
|
const values = [...formData.getAll("file"), ...formData.getAll("files")];
|
|
700
1093
|
const files = values.filter((value) => value instanceof File);
|
|
701
1094
|
if (files.length === 0) {
|
|
702
|
-
throw new
|
|
703
|
-
code: "INVALID_UPLOAD_BODY",
|
|
704
|
-
status: 400,
|
|
1095
|
+
throw new InvalidUploadBodyError({
|
|
705
1096
|
message: 'Multipart upload must include at least one "file" field.',
|
|
706
1097
|
});
|
|
707
1098
|
}
|
|
708
1099
|
return files;
|
|
709
1100
|
}
|
|
710
1101
|
function fileIntentFromFile(file) {
|
|
1102
|
+
const contentType = normalizeContentType(file.type) || "application/octet-stream";
|
|
711
1103
|
return {
|
|
712
1104
|
name: file.name,
|
|
713
|
-
contentType
|
|
1105
|
+
contentType,
|
|
714
1106
|
size: file.size,
|
|
715
1107
|
};
|
|
716
1108
|
}
|