lean-s3 0.7.1 → 0.7.3

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.d.ts CHANGED
@@ -57,7 +57,7 @@ type DeleteObjectsError = {
57
57
  message: string;
58
58
  versionId: string;
59
59
  };
60
- interface S3FilePresignOptions {
60
+ interface S3FilePresignOptions extends OverridableS3ClientOptions {
61
61
  contentHash?: Buffer;
62
62
  /** Seconds. */
63
63
  expiresIn?: number;
@@ -67,6 +67,27 @@ interface S3FilePresignOptions {
67
67
  acl?: Acl;
68
68
  /** `Content-Type` of the file. */
69
69
  type?: string;
70
+ /**
71
+ * Headers to set on the response of the S3 service.
72
+ */
73
+ response?: {
74
+ /**
75
+ * Used to set the file name that browsers display when downloading the file.
76
+ *
77
+ * @example
78
+ * ```js
79
+ * client.presign("foo.jpg", {
80
+ * response: {
81
+ * contentDisposition: {
82
+ * type: "attachment",
83
+ * filename: "download.jpg",
84
+ * },
85
+ * },
86
+ * });
87
+ * ```
88
+ */
89
+ contentDisposition?: ContentDisposition;
90
+ };
70
91
  }
71
92
  type ListObjectsOptions = {
72
93
  bucket?: string;
@@ -311,9 +332,21 @@ declare class S3Client {
311
332
  * expiresIn: 3600 // 1 hour
312
333
  * });
313
334
  * ```
335
+ *
336
+ * @example
337
+ * ```js
338
+ * client.presign("foo.jpg", {
339
+ * expiresIn: 3600 // 1 hour
340
+ * response: {
341
+ * contentDisposition: {
342
+ * type: "attachment",
343
+ * filename: "download.jpg",
344
+ * },
345
+ * },
346
+ * });
347
+ * ```
314
348
  */
315
- presign(path: string, { method, expiresIn, // TODO: Maybe rename this to expiresInSeconds
316
- storageClass, contentLength, type, acl, region: regionOverride, bucket: bucketOverride, endpoint: endpointOverride, }?: S3FilePresignOptions & OverridableS3ClientOptions): string;
349
+ presign(path: string, optio2ns?: S3FilePresignOptions): string;
317
350
  createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<CreateMultipartUploadResult>;
318
351
  /**
319
352
  * @remarks Uses [`ListMultipartUploads`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html).
@@ -538,5 +571,16 @@ type BucketInfo = {
538
571
  dataRedundancy?: string;
539
572
  type?: string;
540
573
  };
574
+ /**
575
+ * Represents valid values for the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Disposition) header.
576
+ */
577
+ type ContentDisposition = AttachmentContentDisposition | InlineContentDisposition;
578
+ type InlineContentDisposition = {
579
+ type: "inline";
580
+ };
581
+ type AttachmentContentDisposition = {
582
+ type: "attachment";
583
+ filename?: string;
584
+ };
541
585
 
542
- export { type AbortMultipartUploadOptions, type Acl, type BucketCorsRule, type BucketCorsRules, type BucketCreationOptions, type BucketDeletionOptions, type BucketExistsOptions, type BucketInfo, type BucketLocationInfo, type ByteSource, type ChecksumAlgorithm, type ChecksumType, type CompleteMultipartUploadOptions, type CompleteMultipartUploadResult, type CreateFileInstanceOptions, type CreateMultipartUploadOptions, type CreateMultipartUploadResult, type DeleteBucketCorsOptions, type DeleteObjectsError, type DeleteObjectsOptions, type DeleteObjectsResult, type GetBucketCorsOptions, type GetBucketCorsResult, type HttpMethod, type ListMultipartUploadsOptions, type ListMultipartUploadsResult, type ListObjectsIteratingOptions, type ListObjectsOptions, type ListObjectsResult, type ListPartsOptions, type ListPartsResult, type MultipartUpload, type MultipartUploadPart, type OverridableS3ClientOptions, type PresignableHttpMethod, type PutBucketCorsOptions, S3BucketEntry, S3Client, type S3ClientOptions, S3Error, type S3ErrorOptions, S3File, type S3FileDeleteOptions, type S3FileExistsOptions, type S3FilePresignOptions, type S3FileWriteOptions, S3Stat, type S3StatOptions, type StorageClass, type UndiciBodyInit, type UploadPartOptions, type UploadPartResult };
586
+ export { type AbortMultipartUploadOptions, type Acl, type AttachmentContentDisposition, type BucketCorsRule, type BucketCorsRules, type BucketCreationOptions, type BucketDeletionOptions, type BucketExistsOptions, type BucketInfo, type BucketLocationInfo, type ByteSource, type ChecksumAlgorithm, type ChecksumType, type CompleteMultipartUploadOptions, type CompleteMultipartUploadResult, type ContentDisposition, type CreateFileInstanceOptions, type CreateMultipartUploadOptions, type CreateMultipartUploadResult, type DeleteBucketCorsOptions, type DeleteObjectsError, type DeleteObjectsOptions, type DeleteObjectsResult, type GetBucketCorsOptions, type GetBucketCorsResult, type HttpMethod, type InlineContentDisposition, type ListMultipartUploadsOptions, type ListMultipartUploadsResult, type ListObjectsIteratingOptions, type ListObjectsOptions, type ListObjectsResult, type ListPartsOptions, type ListPartsResult, type MultipartUpload, type MultipartUploadPart, type OverridableS3ClientOptions, type PresignableHttpMethod, type PutBucketCorsOptions, S3BucketEntry, S3Client, type S3ClientOptions, S3Error, type S3ErrorOptions, S3File, type S3FileDeleteOptions, type S3FileExistsOptions, type S3FilePresignOptions, type S3FileWriteOptions, S3Stat, type S3StatOptions, type StorageClass, type UndiciBodyInit, type UploadPartOptions, type UploadPartResult };
package/dist/index.js CHANGED
@@ -202,7 +202,7 @@ function buildRequestUrl(endpoint, bucket, region, path) {
202
202
  const result = new URL(endpointWithBucketAndRegion);
203
203
  const pathPrefix = result.pathname.endsWith("/") ? result.pathname : `${result.pathname}/`;
204
204
  const pathSuffix = replacedBucket ? normalizePath(path) : `${normalizedBucket}/${normalizePath(path)}`;
205
- result.pathname = pathPrefix + pathSuffix.replaceAll(":", "%3A").replaceAll("+", "%2B").replaceAll("(", "%28").replaceAll(")", "%29").replaceAll(",", "%2C");
205
+ result.pathname = pathPrefix + pathSuffix.replaceAll(":", "%3A").replaceAll("+", "%2B").replaceAll("(", "%28").replaceAll(")", "%29").replaceAll(",", "%2C").replaceAll("'", "%27").replaceAll("*", "%2A");
206
206
  return result;
207
207
  }
208
208
  function replaceDomainPlaceholders(endpoint, bucket, region) {
@@ -346,6 +346,32 @@ function ensureValidPath(path) {
346
346
  return path;
347
347
  }
348
348
 
349
+ // src/assertNever.ts
350
+ function assertNever(v) {
351
+ throw new TypeError(`Expected value not to have type ${typeof v}`);
352
+ }
353
+
354
+ // src/encode.ts
355
+ function getContentDispositionHeader(value) {
356
+ switch (value.type) {
357
+ case "inline":
358
+ return "inline";
359
+ case "attachment": {
360
+ const { filename } = value;
361
+ if (typeof filename === "undefined") {
362
+ return "attachment";
363
+ }
364
+ const encoded = encodeURIComponent(filename);
365
+ return `attachment;filename="${encoded}";filename*=UTF-8''${encoded}`;
366
+ }
367
+ default:
368
+ assertNever(value);
369
+ }
370
+ }
371
+ function encodeURIComponentExtended(value) {
372
+ return encodeURIComponent(value).replaceAll(":", "%3A").replaceAll("+", "%2B").replaceAll("(", "%28").replaceAll(")", "%29").replaceAll(",", "%2C").replaceAll("'", "%27").replaceAll("*", "%2A");
373
+ }
374
+
349
375
  // src/S3Client.ts
350
376
  var write = Symbol("write");
351
377
  var stream = Symbol("stream");
@@ -471,50 +497,58 @@ var S3Client = class {
471
497
  * expiresIn: 3600 // 1 hour
472
498
  * });
473
499
  * ```
500
+ *
501
+ * @example
502
+ * ```js
503
+ * client.presign("foo.jpg", {
504
+ * expiresIn: 3600 // 1 hour
505
+ * response: {
506
+ * contentDisposition: {
507
+ * type: "attachment",
508
+ * filename: "download.jpg",
509
+ * },
510
+ * },
511
+ * });
512
+ * ```
474
513
  */
475
- presign(path, {
476
- method = "GET",
477
- expiresIn = 3600,
478
- // TODO: Maybe rename this to expiresInSeconds
479
- storageClass,
480
- contentLength,
481
- type,
482
- acl,
483
- region: regionOverride,
484
- bucket: bucketOverride,
485
- endpoint: endpointOverride
486
- } = {}) {
514
+ presign(path, optio2ns = {}) {
515
+ const contentLength = optio2ns.contentLength ?? void 0;
487
516
  if (typeof contentLength === "number") {
488
517
  if (contentLength < 0) {
489
518
  throw new RangeError("`contentLength` must be >= 0.");
490
519
  }
491
520
  }
521
+ const method = optio2ns.method ?? "GET";
522
+ const contentType = optio2ns.type ?? void 0;
523
+ const region = optio2ns.region ?? this.#options.region;
524
+ const bucket = optio2ns.bucket ?? this.#options.bucket;
525
+ const endpoint = optio2ns.endpoint ?? this.#options.endpoint;
526
+ const responseOptions = optio2ns.response;
527
+ const contentDisposition = responseOptions?.contentDisposition;
528
+ const responseContentDisposition = contentDisposition ? getContentDispositionHeader(contentDisposition) : void 0;
529
+ const res = buildRequestUrl(endpoint, bucket, region, path);
492
530
  const now2 = /* @__PURE__ */ new Date();
493
531
  const date = getAmzDate(now2);
494
- const options = this.#options;
495
- const region = regionOverride ?? options.region;
496
- const bucket = bucketOverride ?? options.bucket;
497
- const endpoint = endpointOverride ?? options.endpoint;
498
- const res = buildRequestUrl(endpoint, bucket, region, path);
499
532
  const query = buildSearchParams(
500
- `${options.accessKeyId}/${date.date}/${region}/s3/aws4_request`,
533
+ `${this.#options.accessKeyId}/${date.date}/${region}/s3/aws4_request`,
501
534
  date,
502
- expiresIn,
503
- typeof contentLength === "number" || typeof type === "string" ? typeof contentLength === "number" && typeof type === "string" ? "content-length;content-type;host" : typeof contentLength === "number" ? "content-length;host" : typeof type === "string" ? "content-type;host" : "" : "host",
535
+ optio2ns.expiresIn ?? 3600,
536
+ typeof contentLength === "number" || typeof contentType === "string" ? typeof contentLength === "number" && typeof contentType === "string" ? "content-length;content-type;host" : typeof contentLength === "number" ? "content-length;host" : typeof contentType === "string" ? "content-type;host" : "" : "host",
504
537
  unsignedPayload,
505
- storageClass,
506
- options.sessionToken,
507
- acl
538
+ optio2ns.storageClass,
539
+ this.#options.sessionToken,
540
+ optio2ns.acl,
541
+ responseContentDisposition
508
542
  );
509
- const dataDigest = typeof contentLength === "number" || typeof type === "string" ? createCanonicalDataDigest(
543
+ const dataDigest = typeof contentLength === "number" || typeof contentType === "string" ? createCanonicalDataDigest(
510
544
  method,
511
545
  res.pathname,
512
546
  query,
513
- typeof contentLength === "number" && typeof type === "string" ? {
547
+ typeof contentLength === "number" && typeof contentType === "string" ? {
514
548
  "content-length": String(contentLength),
515
- "content-type": type,
549
+ "content-type": contentType,
516
550
  host: res.host
517
- } : typeof contentLength === "number" ? { "content-length": String(contentLength), host: res.host } : typeof type === "string" ? { "content-type": type, host: res.host } : {},
551
+ } : typeof contentLength === "number" ? { "content-length": String(contentLength), host: res.host } : typeof contentType === "string" ? { "content-type": contentType, host: res.host } : {},
518
552
  unsignedPayload
519
553
  ) : createCanonicalDataDigestHostOnly(
520
554
  method,
@@ -525,8 +559,8 @@ var S3Client = class {
525
559
  const signingKey = this.#keyCache.computeIfAbsent(
526
560
  date,
527
561
  region,
528
- options.accessKeyId,
529
- options.secretAccessKey
562
+ this.#options.accessKeyId,
563
+ this.#options.secretAccessKey
530
564
  );
531
565
  const signature = signCanonicalDataHash(
532
566
  signingKey,
@@ -1409,7 +1443,7 @@ var S3Client = class {
1409
1443
  });
1410
1444
  }
1411
1445
  };
1412
- function buildSearchParams(amzCredential, date, expiresIn, headerList, contentHashStr, storageClass, sessionToken, acl) {
1446
+ function buildSearchParams(amzCredential, date, expiresIn, headerList, contentHashStr, storageClass, sessionToken, acl, responseContentDisposition) {
1413
1447
  let res = "";
1414
1448
  if (acl) {
1415
1449
  res += `X-Amz-Acl=${encodeURIComponent(acl)}&`;
@@ -1418,7 +1452,7 @@ function buildSearchParams(amzCredential, date, expiresIn, headerList, contentHa
1418
1452
  if (contentHashStr) {
1419
1453
  res += `&X-Amz-Content-Sha256=${contentHashStr}`;
1420
1454
  }
1421
- res += `&X-Amz-Credential=${encodeURIComponent(amzCredential)}`;
1455
+ res += `&X-Amz-Credential=${encodeURIComponentExtended(amzCredential)}`;
1422
1456
  res += `&X-Amz-Date=${date.dateTime}`;
1423
1457
  res += `&X-Amz-Expires=${expiresIn}`;
1424
1458
  if (sessionToken) {
@@ -1428,6 +1462,9 @@ function buildSearchParams(amzCredential, date, expiresIn, headerList, contentHa
1428
1462
  if (storageClass) {
1429
1463
  res += `&X-Amz-Storage-Class=${storageClass}`;
1430
1464
  }
1465
+ if (responseContentDisposition) {
1466
+ res += `&response-content-disposition=${encodeURIComponentExtended(responseContentDisposition)}`;
1467
+ }
1431
1468
  return res;
1432
1469
  }
1433
1470
  function ensureParsedXml(text) {
@@ -1447,11 +1484,6 @@ function ensureParsedXml(text) {
1447
1484
  }
1448
1485
  }
1449
1486
 
1450
- // src/assertNever.ts
1451
- function assertNever(v) {
1452
- throw new TypeError(`Expected value not to have type ${typeof v}`);
1453
- }
1454
-
1455
1487
  // src/S3File.ts
1456
1488
  var S3File = class _S3File {
1457
1489
  #client;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "lean-s3",
3
3
  "author": "Niklas Mollenhauer",
4
4
  "license": "MIT",
5
- "version": "0.7.1",
5
+ "version": "0.7.3",
6
6
  "description": "A server-side S3 API for the regular user.",
7
7
  "keywords": [
8
8
  "s3",