@dimah-s3/server 0.8.5 → 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.
Files changed (72) hide show
  1. package/dist/adapters/express.js +13 -1
  2. package/dist/adapters/express.js.map +1 -1
  3. package/dist/adapters/fastify.js +13 -1
  4. package/dist/adapters/fastify.js.map +1 -1
  5. package/dist/adapters/node.js +13 -1
  6. package/dist/adapters/node.js.map +1 -1
  7. package/dist/adapters/svelte-kit.d.ts +2 -0
  8. package/dist/adapters/svelte-kit.d.ts.map +1 -1
  9. package/dist/adapters/svelte-kit.js.map +1 -1
  10. package/dist/adapters/types.d.ts.map +1 -1
  11. package/dist/api/assert-feature-enabled.d.ts.map +1 -1
  12. package/dist/api/bind-endpoints.d.ts.map +1 -1
  13. package/dist/api/context.d.ts.map +1 -1
  14. package/dist/api/index.d.ts +2 -2
  15. package/dist/api/index.d.ts.map +1 -1
  16. package/dist/api/index.js +218 -147
  17. package/dist/api/index.js.map +1 -1
  18. package/dist/{router.d.ts → api/router.d.ts} +3 -2
  19. package/dist/api/router.d.ts.map +1 -0
  20. package/dist/api/routes/confirm.d.ts.map +1 -1
  21. package/dist/api/routes/delete.d.ts.map +1 -1
  22. package/dist/api/routes/download.d.ts.map +1 -1
  23. package/dist/api/routes/index.d.ts +1 -7
  24. package/dist/api/routes/index.d.ts.map +1 -1
  25. package/dist/api/routes/multipart/abort.d.ts +1 -5
  26. package/dist/api/routes/multipart/abort.d.ts.map +1 -1
  27. package/dist/api/routes/multipart/complete.d.ts.map +1 -1
  28. package/dist/api/routes/multipart/init.d.ts +0 -2
  29. package/dist/api/routes/multipart/init.d.ts.map +1 -1
  30. package/dist/api/routes/multipart/list-parts.d.ts.map +1 -1
  31. package/dist/api/routes/multipart/part.d.ts.map +1 -1
  32. package/dist/api/routes/upload.d.ts.map +1 -1
  33. package/dist/errors.d.ts +4 -2
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/helpers/expires.d.ts +6 -0
  36. package/dist/helpers/expires.d.ts.map +1 -0
  37. package/dist/helpers/features.d.ts +34 -0
  38. package/dist/helpers/features.d.ts.map +1 -0
  39. package/dist/helpers/head-object.d.ts +11 -0
  40. package/dist/helpers/head-object.d.ts.map +1 -0
  41. package/dist/{internal-helpers.d.ts → helpers/hooks.d.ts} +1 -4
  42. package/dist/helpers/hooks.d.ts.map +1 -0
  43. package/dist/helpers/index.d.ts +12 -1
  44. package/dist/helpers/index.d.ts.map +1 -1
  45. package/dist/helpers/is-aws-not-found.d.ts +3 -1
  46. package/dist/helpers/is-aws-not-found.d.ts.map +1 -1
  47. package/dist/helpers/list-parts.d.ts +8 -0
  48. package/dist/helpers/list-parts.d.ts.map +1 -0
  49. package/dist/helpers/request.d.ts +3 -0
  50. package/dist/helpers/request.d.ts.map +1 -0
  51. package/dist/helpers/resolve-request-acl.d.ts +10 -0
  52. package/dist/helpers/resolve-request-acl.d.ts.map +1 -0
  53. package/dist/helpers/resolve-target.d.ts +2 -33
  54. package/dist/helpers/resolve-target.d.ts.map +1 -1
  55. package/dist/index.d.ts +1 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +188 -111
  58. package/dist/index.js.map +1 -1
  59. package/dist/plugin/apply-plugins.d.ts.map +1 -1
  60. package/dist/plugin/types.d.ts +5 -5
  61. package/dist/plugin/types.d.ts.map +1 -1
  62. package/dist/plugins.js +184 -104
  63. package/dist/plugins.js.map +1 -1
  64. package/dist/types/config.d.ts +27 -9
  65. package/dist/types/config.d.ts.map +1 -1
  66. package/dist/types/hook-contexts.d.ts +8 -8
  67. package/dist/types/hook-contexts.d.ts.map +1 -1
  68. package/dist/types/index.d.ts +1 -1
  69. package/dist/types/index.d.ts.map +1 -1
  70. package/package.json +6 -6
  71. package/dist/internal-helpers.d.ts.map +0 -1
  72. package/dist/router.d.ts.map +0 -1
package/dist/plugins.js CHANGED
@@ -1,35 +1,10 @@
1
- import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, parseFileName, buildContentDisposition } from '@dimah-s3/core';
2
- import { HeadObjectCommand, DeleteObjectCommand, GetObjectCommand, AbortMultipartUploadCommand, ListPartsCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand, GetObjectAclCommand } from '@aws-sdk/client-s3';
1
+ import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, parseFileName, S3_MAX_EXPIRES_IN, buildContentDisposition, S3_MAX_POST_OBJECT_BYTES, S3_DEFAULT_EXPIRES_IN } from '@dimah-s3/core';
2
+ import { DeleteObjectCommand, GetObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand, GetObjectAclCommand, ListPartsCommand, CompleteMultipartUploadCommand, HeadObjectCommand, AbortMultipartUploadCommand } from '@aws-sdk/client-s3';
3
3
  import { createMiddleware, createEndpoint } from 'better-call';
4
4
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
5
5
  import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
6
6
 
7
7
  // src/plugin/apply-plugins.ts
8
-
9
- // src/helpers/is-aws-not-found.ts
10
- function isAwsNotFound(err) {
11
- const e = err;
12
- return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.$metadata?.httpStatusCode === 404;
13
- }
14
- var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
15
- async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
16
- const controller = new AbortController();
17
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
18
- try {
19
- const result = await s3.send(
20
- new GetObjectAclCommand({ Bucket: bucket, Key: key }),
21
- { abortSignal: controller.signal }
22
- );
23
- const isPublic = result.Grants?.some(
24
- (g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
25
- );
26
- return isPublic ? "public-read" : "private";
27
- } catch {
28
- return void 0;
29
- } finally {
30
- clearTimeout(timeout);
31
- }
32
- }
33
8
  var errors = {
34
9
  notFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.NOT_FOUND),
35
10
  unauthorized: () => DimahS3Error.from("UNAUTHORIZED", S3_ERROR_CODES.UNAUTHORIZED),
@@ -64,13 +39,69 @@ var errors = {
64
39
  "BAD_REQUEST",
65
40
  S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART
66
41
  ),
42
+ multipartPartMissing: (partNumber) => DimahS3Error.from("BAD_REQUEST", {
43
+ code: S3_ERROR_CODES.MULTIPART_PART_MISSING.code,
44
+ message: S3_ERROR_CODES.MULTIPART_PART_MISSING.message.replaceAll(
45
+ "{partNumber}",
46
+ String(partNumber)
47
+ ),
48
+ params: { partNumber }
49
+ }),
50
+ payloadTooLarge: (message = S3_ERROR_CODES.PAYLOAD_TOO_LARGE.message) => DimahS3Error.from("PAYLOAD_TOO_LARGE", {
51
+ ...S3_ERROR_CODES.PAYLOAD_TOO_LARGE,
52
+ message
53
+ }),
67
54
  validationError: (message) => DimahS3Error.from("BAD_REQUEST", {
68
55
  code: S3_ERROR_CODES.VALIDATION_ERROR.code,
69
56
  message
70
57
  })
71
58
  };
72
59
 
73
- // src/internal-helpers.ts
60
+ // src/helpers/is-aws-not-found.ts
61
+ function isAwsNotFound(err) {
62
+ const e = err;
63
+ return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.name === "NoSuchUpload" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.Code === "NoSuchUpload" || e?.$metadata?.httpStatusCode === 404;
64
+ }
65
+ async function sendOrObjectNotFound(send) {
66
+ try {
67
+ return await send();
68
+ } catch (err) {
69
+ if (isAwsNotFound(err)) throw errors.objectNotFound();
70
+ throw err;
71
+ }
72
+ }
73
+ var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
74
+ async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
75
+ const controller = new AbortController();
76
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
77
+ try {
78
+ const result = await s3.send(
79
+ new GetObjectAclCommand({ Bucket: bucket, Key: key }),
80
+ { abortSignal: controller.signal }
81
+ );
82
+ const isPublic = result.Grants?.some(
83
+ (g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
84
+ );
85
+ return isPublic ? "public-read" : "private";
86
+ } catch {
87
+ return void 0;
88
+ } finally {
89
+ clearTimeout(timeout);
90
+ }
91
+ }
92
+
93
+ // src/helpers/resolve-request-acl.ts
94
+ function resolveRequestAcl(policy, requested) {
95
+ if (policy?.acl) return policy.acl;
96
+ if (policy?.allowClientAcl && requested) return requested;
97
+ return "private";
98
+ }
99
+ function normalizeExpiresIn(value, maxExpiresIn = S3_MAX_EXPIRES_IN) {
100
+ const n = Number(value);
101
+ const expires = Number.isFinite(n) && n > 0 ? Math.floor(n) : S3_DEFAULT_EXPIRES_IN;
102
+ const max = Number.isFinite(maxExpiresIn) && maxExpiresIn > 0 ? Math.floor(maxExpiresIn) : S3_MAX_EXPIRES_IN;
103
+ return Math.min(expires, max);
104
+ }
74
105
  async function runHook(hook, context) {
75
106
  if (!hook) return;
76
107
  try {
@@ -96,10 +127,8 @@ async function runLifecycleHook(hook, context) {
96
127
  throw errors.internalError();
97
128
  }
98
129
  }
99
- function normalizeExpiresIn(value) {
100
- const n = Number(value);
101
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;
102
- }
130
+
131
+ // src/helpers/request.ts
103
132
  function requestFromHeaders(headers) {
104
133
  return new Request("http://dimah-s3.local", { headers });
105
134
  }
@@ -155,11 +184,56 @@ async function resolveRequestTarget(config, policy, input) {
155
184
  });
156
185
  return { key, bucket };
157
186
  }
187
+ async function headObjectOrNotFound(client, bucket, key) {
188
+ return sendOrObjectNotFound(
189
+ () => client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))
190
+ );
191
+ }
192
+ async function headObjectAfterMultipartComplete(client, bucket, key) {
193
+ const send = () => sendOrObjectNotFound(
194
+ () => client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))
195
+ );
196
+ let head = await send();
197
+ for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
198
+ await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
199
+ head = await send();
200
+ }
201
+ return head;
202
+ }
203
+ function requireContentLength(head) {
204
+ if (typeof head.ContentLength !== "number") {
205
+ throw errors.internalError();
206
+ }
207
+ return head.ContentLength;
208
+ }
209
+ async function listAllParts(client, input) {
210
+ const parts = [];
211
+ let partNumberMarker;
212
+ let isTruncated = true;
213
+ while (isTruncated) {
214
+ const response = await sendOrObjectNotFound(
215
+ () => client.send(
216
+ new ListPartsCommand({
217
+ Bucket: input.bucket,
218
+ Key: input.key,
219
+ UploadId: input.uploadId,
220
+ ...partNumberMarker ? { PartNumberMarker: partNumberMarker } : {}
221
+ })
222
+ )
223
+ );
224
+ parts.push(...response.Parts ?? []);
225
+ isTruncated = response.IsTruncated === true;
226
+ partNumberMarker = isTruncated ? response.NextPartNumberMarker : void 0;
227
+ }
228
+ return parts;
229
+ }
230
+
231
+ // src/helpers/features.ts
158
232
  function normalizeFeature(value) {
159
233
  if (value === void 0) return void 0;
160
234
  if (value === false) return { enabled: false };
161
235
  if (value === true) return { enabled: true };
162
- return { ...value, enabled: value.enabled ?? true };
236
+ return { ...value, enabled: true };
163
237
  }
164
238
  function isFeatureEnabled(config, feature) {
165
239
  return config[feature]?.enabled === true;
@@ -178,7 +252,10 @@ function applyMultipartDefault(config, multipartInput) {
178
252
  multipart: {
179
253
  ...next.multipart,
180
254
  prefix: next.multipart.prefix ?? next.upload.prefix,
181
- resolveKey: next.multipart.resolveKey ?? next.upload.resolveKey
255
+ resolveKey: next.multipart.resolveKey ?? next.upload.resolveKey,
256
+ acl: next.multipart.acl ?? next.upload.acl,
257
+ allowClientAcl: next.multipart.allowClientAcl ?? next.upload.allowClientAcl,
258
+ requireFileSize: next.multipart.requireFileSize ?? next.upload.requireFileSize
182
259
  }
183
260
  };
184
261
  }
@@ -192,6 +269,13 @@ function normalizeFeatures(config) {
192
269
  multipart: normalizeFeature(config.multipart)
193
270
  };
194
271
  }
272
+ function assertExclusiveBucketFlags(config) {
273
+ if (config.allowClientBucket && config.buckets?.length) {
274
+ throw new Error(
275
+ "dimahS3: set either allowClientBucket or buckets, not both. allowClientBucket honors any client-sent bucket; buckets is an allowlist."
276
+ );
277
+ }
278
+ }
195
279
 
196
280
  // src/api/assert-feature-enabled.ts
197
281
  function assertFeatureEnabled(config, feature) {
@@ -255,9 +339,7 @@ async function handleConfirm(config, input, request) {
255
339
  key,
256
340
  bucket
257
341
  });
258
- const head = await config.client.send(
259
- new HeadObjectCommand({ Bucket: bucket, Key: key })
260
- );
342
+ const head = await headObjectOrNotFound(config.client, bucket, key);
261
343
  const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
262
344
  const fileName = parseFileName(head.ContentDisposition);
263
345
  const context = {
@@ -265,7 +347,7 @@ async function handleConfirm(config, input, request) {
265
347
  key,
266
348
  bucket,
267
349
  contentType: head.ContentType,
268
- contentLength: head.ContentLength ?? 0,
350
+ contentLength: requireContentLength(head),
269
351
  eTag: head.ETag?.replace(/"/g, ""),
270
352
  metadata: head.Metadata,
271
353
  acl,
@@ -306,16 +388,7 @@ async function handleDelete(config, input, request) {
306
388
  key,
307
389
  bucket
308
390
  });
309
- try {
310
- await config.client.send(
311
- new HeadObjectCommand({ Bucket: bucket, Key: key })
312
- );
313
- } catch (err) {
314
- if (isAwsNotFound(err)) {
315
- throw errors.objectNotFound();
316
- }
317
- throw err;
318
- }
391
+ await headObjectOrNotFound(config.client, bucket, key);
319
392
  await config.client.send(
320
393
  new DeleteObjectCommand({ Bucket: bucket, Key: key })
321
394
  );
@@ -337,7 +410,7 @@ async function handleDownload(config, input, request) {
337
410
  bucket: input.bucket,
338
411
  fileName: input.fileName
339
412
  });
340
- const expiresIn = normalizeExpiresIn(input.expiresIn);
413
+ const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
341
414
  const fileName = input.fileName;
342
415
  await runHook(config.download?.guard, {
343
416
  request,
@@ -345,16 +418,7 @@ async function handleDownload(config, input, request) {
345
418
  bucket,
346
419
  fileName
347
420
  });
348
- try {
349
- await config.client.send(
350
- new HeadObjectCommand({ Bucket: bucket, Key: key })
351
- );
352
- } catch (err) {
353
- if (isAwsNotFound(err)) {
354
- throw errors.objectNotFound();
355
- }
356
- throw err;
357
- }
421
+ await headObjectOrNotFound(config.client, bucket, key);
358
422
  const url = await getSignedUrl(
359
423
  config.client,
360
424
  new GetObjectCommand({
@@ -395,12 +459,14 @@ async function handleAbort(config, input, request) {
395
459
  bucket,
396
460
  uploadId
397
461
  });
398
- await config.client.send(
399
- new AbortMultipartUploadCommand({
400
- Bucket: bucket,
401
- Key: key,
402
- UploadId: uploadId
403
- })
462
+ await sendOrObjectNotFound(
463
+ () => config.client.send(
464
+ new AbortMultipartUploadCommand({
465
+ Bucket: bucket,
466
+ Key: key,
467
+ UploadId: uploadId
468
+ })
469
+ )
404
470
  );
405
471
  await runLifecycleHook(config.multipart?.onAbort, {
406
472
  request,
@@ -434,32 +500,34 @@ async function handleComplete(config, input, request) {
434
500
  uploadId,
435
501
  parts: partRefs
436
502
  });
437
- const listed = await config.client.send(
438
- new ListPartsCommand({ Bucket: bucket, Key: key, UploadId: uploadId })
439
- );
440
- const listedParts = listed.Parts ?? [];
503
+ const listedParts = await listAllParts(config.client, {
504
+ bucket,
505
+ key,
506
+ uploadId
507
+ });
441
508
  const completeParts = parts.map((partNumber) => {
442
509
  const found = listedParts.find((p) => p.PartNumber === partNumber);
443
- return { PartNumber: partNumber, ETag: found?.ETag ?? "" };
510
+ if (!found?.ETag) {
511
+ throw errors.multipartPartMissing(partNumber);
512
+ }
513
+ return { PartNumber: partNumber, ETag: found.ETag };
444
514
  });
445
- const completeResult = await config.client.send(
446
- new CompleteMultipartUploadCommand({
447
- Bucket: bucket,
448
- Key: key,
449
- UploadId: uploadId,
450
- MultipartUpload: { Parts: completeParts }
451
- })
515
+ const completeResult = await sendOrObjectNotFound(
516
+ () => config.client.send(
517
+ new CompleteMultipartUploadCommand({
518
+ Bucket: bucket,
519
+ Key: key,
520
+ UploadId: uploadId,
521
+ MultipartUpload: { Parts: completeParts }
522
+ })
523
+ )
452
524
  );
453
- let head = await config.client.send(
454
- new HeadObjectCommand({ Bucket: bucket, Key: key })
525
+ const head = await headObjectAfterMultipartComplete(
526
+ config.client,
527
+ bucket,
528
+ key
455
529
  );
456
- for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
457
- await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
458
- head = await config.client.send(
459
- new HeadObjectCommand({ Bucket: bucket, Key: key })
460
- );
461
- }
462
- const contentLength = head.ContentLength ?? 0;
530
+ const contentLength = requireContentLength(head);
463
531
  const contentType = head.ContentType;
464
532
  const eTag = (head.ETag ?? completeResult.ETag ?? "").replace(/"/g, "");
465
533
  const metadata = head.Metadata ?? {};
@@ -511,7 +579,7 @@ async function handleMultipartInit(config, input, request) {
511
579
  fileName: input.fileName,
512
580
  contentType: input.contentType
513
581
  });
514
- const acl = input.acl === "public-read" ? "public-read" : "private";
582
+ const acl = resolveRequestAcl(config.multipart, input.acl);
515
583
  const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : void 0;
516
584
  if (config.multipart?.requireFileSize && fileSize === void 0) {
517
585
  throw errors.fileSizeRequiredMultipart();
@@ -520,7 +588,11 @@ async function handleMultipartInit(config, input, request) {
520
588
  request,
521
589
  key,
522
590
  bucket,
523
- fileSize
591
+ fileSize,
592
+ contentType: input.contentType,
593
+ metadata: input.metadata,
594
+ acl,
595
+ fileName: input.fileName
524
596
  });
525
597
  const { UploadId } = await config.client.send(
526
598
  new CreateMultipartUploadCommand({
@@ -532,6 +604,9 @@ async function handleMultipartInit(config, input, request) {
532
604
  ACL: acl
533
605
  })
534
606
  );
607
+ if (!UploadId) {
608
+ throw errors.internalError();
609
+ }
535
610
  await runLifecycleHook(config.multipart?.onInit, {
536
611
  request,
537
612
  key,
@@ -571,14 +646,8 @@ async function handleListParts(config, input, request) {
571
646
  bucket,
572
647
  uploadId
573
648
  });
574
- const response = await config.client.send(
575
- new ListPartsCommand({
576
- Bucket: bucket,
577
- Key: key,
578
- UploadId: uploadId
579
- })
580
- );
581
- const parts = (response.Parts ?? []).map((p) => ({
649
+ const listed = await listAllParts(config.client, { bucket, key, uploadId });
650
+ const parts = listed.map((p) => ({
582
651
  partNumber: p.PartNumber ?? 0,
583
652
  size: p.Size ?? 0,
584
653
  eTag: (p.ETag ?? "").replace(/"/g, "")
@@ -608,7 +677,7 @@ async function handleSignPart(config, input, request) {
608
677
  });
609
678
  const uploadId = input.uploadId;
610
679
  const partNumber = input.partNumber;
611
- const expiresIn = normalizeExpiresIn(input.expiresIn);
680
+ const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
612
681
  const partSize = typeof input.partSize === "number" && input.partSize > 0 ? Math.floor(input.partSize) : null;
613
682
  await runHook(config.multipart?.partGuard, {
614
683
  request,
@@ -649,6 +718,14 @@ var multipartPart = createS3Endpoint(
649
718
  return handleSignPart(ctx.context.config, ctx.body, ctx.context.request);
650
719
  }
651
720
  );
721
+ function putMetadataHeaders(metadata) {
722
+ if (!metadata) return {};
723
+ const headers = {};
724
+ for (const [k, v] of Object.entries(metadata)) {
725
+ headers[`x-amz-meta-${k}`] = v;
726
+ }
727
+ return headers;
728
+ }
652
729
  async function handleUpload(config, input, request) {
653
730
  const { key, bucket } = await resolveRequestTarget(config, config.upload, {
654
731
  request,
@@ -657,10 +734,13 @@ async function handleUpload(config, input, request) {
657
734
  fileName: input.fileName,
658
735
  contentType: input.contentType
659
736
  });
660
- const expiresIn = normalizeExpiresIn(input.expiresIn);
661
- const acl = input.acl === "public-read" ? "public-read" : "private";
737
+ const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
738
+ const acl = resolveRequestAcl(config.upload, input.acl);
662
739
  const contentType = input.contentType ?? "application/octet-stream";
663
740
  const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : null;
741
+ if (config.upload?.requireFileSize && fileSize === null) {
742
+ throw errors.fileSizeRequiredUpload();
743
+ }
664
744
  await runHook(config.upload?.guard, {
665
745
  request,
666
746
  key,
@@ -672,12 +752,10 @@ async function handleUpload(config, input, request) {
672
752
  fileName: input.fileName
673
753
  });
674
754
  const method = config.upload?.method ?? "POST";
675
- if (method === "PUT" && config.upload?.requireFileSize && fileSize === null) {
676
- throw errors.fileSizeRequiredUpload();
677
- }
678
755
  if (method === "PUT") {
679
756
  const putHeaders = {
680
- "Content-Type": contentType
757
+ "Content-Type": contentType,
758
+ ...putMetadataHeaders(input.metadata)
681
759
  };
682
760
  if (input.fileName) {
683
761
  putHeaders["Content-Disposition"] = buildContentDisposition(
@@ -691,6 +769,7 @@ async function handleUpload(config, input, request) {
691
769
  Key: key,
692
770
  ContentType: contentType,
693
771
  ACL: acl,
772
+ Metadata: input.metadata,
694
773
  ...input.fileName ? { ContentDisposition: buildContentDisposition(input.fileName) } : {},
695
774
  ...fileSize !== null ? { ContentLength: fileSize } : {}
696
775
  }),
@@ -736,7 +815,7 @@ async function handleUpload(config, input, request) {
736
815
  {
737
816
  Bucket: bucket,
738
817
  Key: key,
739
- Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, Number.MAX_SAFE_INTEGER]],
818
+ Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, S3_MAX_POST_OBJECT_BYTES]],
740
819
  Fields: fields,
741
820
  Expires: expiresIn
742
821
  }
@@ -938,6 +1017,7 @@ function applyPlugins(config) {
938
1017
  }
939
1018
  }
940
1019
  const { plugins: _omit, ...rest } = config;
1020
+ assertExclusiveBucketFlags(rest);
941
1021
  for (const plugin of plugins) {
942
1022
  plugin.init?.({ config: rest, getPlugin });
943
1023
  }