@hot-updater/aws 1.2.0-rc.8 → 1.2.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,8 +5,7 @@ let _aws_sdk_client_cloudfront = require("@aws-sdk/client-cloudfront");
5
5
  let _aws_sdk_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
6
6
  let _aws_sdk_client_ssm = require("@aws-sdk/client-ssm");
7
7
  let _aws_sdk_cloudfront_signer = require("@aws-sdk/cloudfront-signer");
8
- let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
9
- let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
8
+ let _hot_updater_storage_s3 = require("@hot-updater/storage-s3");
10
9
  let node_crypto = require("node:crypto");
11
10
  //#region src/cloudFrontInvalidation.ts
12
11
  const DEFAULT_POLL_INTERVAL_MS = 2e3;
@@ -519,156 +518,17 @@ const cloudFrontDownloadUrl = (config) => {
519
518
  };
520
519
  //#endregion
521
520
  //#region src/s3Storage.ts
522
- /** Devices download right after the update check that returns the URL. */
523
- const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
524
- const isObjectNotFoundError = (error) => error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey");
525
521
  function s3Storage(config) {
526
522
  const { bucketName, basePath, getDownloadUrl, credentials, ...s3Config } = config;
527
- const client = new _aws_sdk_client_s3.S3Client(applyS3RuntimeAwsConfig({
528
- ...s3Config,
529
- credentials: toSdkCredentials(credentials)
530
- }));
531
- const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
532
- const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(normalizedBasePath);
533
- const getListPrefix = (prefix = "") => {
534
- const normalizedPrefix = prefix.replace(/^\/+|\/+$/g, "");
535
- const value = [normalizedBasePath, normalizedPrefix].filter(Boolean).join("/");
536
- return value ? `${value}/` : "";
537
- };
538
- /**
539
- * The URI `put` returns for the key, or null for a key no `put` writes,
540
- * such as a folder marker that ends in "/".
541
- */
542
- const getListedStorageUri = (key) => {
543
- try {
544
- return (0, _hot_updater_plugin_core.createStorageUri)({
545
- bucket: bucketName,
546
- key,
547
- protocol: "s3"
548
- });
549
- } catch {
550
- return null;
551
- }
552
- };
553
- const getRelativeKey = (key) => {
554
- if (!normalizedBasePath) return key;
555
- const basePrefix = `${normalizedBasePath}/`;
556
- return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
557
- };
558
- const parseAndValidate = (storageUri) => {
559
- const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "s3");
560
- if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
561
- return parsed;
562
- };
563
- return (0, _hot_updater_plugin_core.createStorageAdapter)({
523
+ return (0, _hot_updater_storage_s3.createS3Storage)({
564
524
  name: "s3Storage",
565
- protocol: "s3",
566
- async listObjects(prefix) {
567
- const objects = [];
568
- let continuationToken;
569
- do {
570
- const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
571
- Bucket: bucketName,
572
- ContinuationToken: continuationToken,
573
- Prefix: getListPrefix(prefix)
574
- }));
575
- for (const object of response.Contents ?? []) {
576
- if (!object.Key) continue;
577
- const storageUri = getListedStorageUri(object.Key);
578
- if (storageUri === null) continue;
579
- objects.push({
580
- key: getRelativeKey(object.Key),
581
- lastModifiedAt: object.LastModified,
582
- size: object.Size ?? 0,
583
- storageUri
584
- });
585
- }
586
- continuationToken = response.NextContinuationToken;
587
- } while (continuationToken);
588
- return objects;
589
- },
590
- async deleteObjects(relativeKeys) {
591
- const keys = relativeKeys.map((key) => getStorageKey(key));
592
- for (let offset = 0; offset < keys.length; offset += 1e3) {
593
- const batch = keys.slice(offset, offset + 1e3);
594
- if (batch.length === 0) continue;
595
- const response = await client.send(new _aws_sdk_client_s3.DeleteObjectsCommand({
596
- Bucket: bucketName,
597
- Delete: {
598
- Objects: batch.map((Key) => ({ Key })),
599
- Quiet: true
600
- }
601
- }));
602
- if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
603
- }
604
- },
605
- async put({ key, body, contentLength, contentType }) {
606
- const storageKey = getStorageKey(key);
607
- const response = await new _aws_sdk_lib_storage.Upload({
608
- client,
609
- params: {
610
- Body: body,
611
- Bucket: bucketName,
612
- CacheControl: "max-age=31536000",
613
- ...contentLength === void 0 ? {} : { ContentLength: contentLength },
614
- ContentType: contentType,
615
- Key: storageKey
616
- }
617
- }).done();
618
- if (!response.Bucket || !response.Key) throw new Error("Upload failed");
619
- return { storageUri: (0, _hot_updater_plugin_core.createStorageUri)({
620
- bucket: bucketName,
621
- key: storageKey,
622
- protocol: "s3"
623
- }) };
624
- },
625
- async get({ storageUri }) {
626
- const { key } = parseAndValidate(storageUri);
627
- try {
628
- const response = await client.send(new _aws_sdk_client_s3.GetObjectCommand({
629
- Bucket: bucketName,
630
- Key: key
631
- }));
632
- if (!response.Body) return { response: null };
633
- const headers = new Headers();
634
- if (response.ContentType) headers.set("content-type", response.ContentType);
635
- if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
636
- return { response: new Response(response.Body.transformToWebStream(), { headers }) };
637
- } catch (error) {
638
- if (isObjectNotFoundError(error)) return { response: null };
639
- throw error;
640
- }
641
- },
642
- async getDownloadUrl({ storageUri }) {
643
- const { key } = parseAndValidate(storageUri);
644
- if (getDownloadUrl) return getDownloadUrl({ storageUri });
645
- const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
646
- return { url: await getSignedUrl(client, new _aws_sdk_client_s3.GetObjectCommand({
647
- Bucket: bucketName,
648
- Key: key
649
- }), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
650
- },
651
- async exists({ storageUri }) {
652
- const { key } = parseAndValidate(storageUri);
653
- try {
654
- await client.send(new _aws_sdk_client_s3.HeadObjectCommand({
655
- Bucket: bucketName,
656
- Key: key
657
- }));
658
- return { exists: true };
659
- } catch (error) {
660
- if (isObjectNotFoundError(error)) return { exists: false };
661
- throw error;
662
- }
663
- },
664
- async delete({ storageUri }) {
665
- const { key } = parseAndValidate(storageUri);
666
- await client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
667
- Bucket: bucketName,
668
- Key: key
669
- }));
670
- return { deleted: true };
671
- }
525
+ protocol: "s3"
526
+ }, {
527
+ ...applyS3RuntimeAwsConfig(s3Config),
528
+ credentials,
529
+ bucketName,
530
+ basePath,
531
+ getDownloadUrl
672
532
  });
673
533
  }
674
534
  //#endregion
@@ -693,7 +553,22 @@ const EXPIRED_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set([
693
553
  ]);
694
554
  /** Errors of the SDK finding no credentials, or an expired SSO session. */
695
555
  const NO_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set(["CredentialsProviderError", "TokenProviderError"]);
696
- const SIGN_IN_HINT = "In CI, set them with aws-actions/configure-aws-credentials; on your machine, run aws sso login --profile <profile> and set AWS_PROFILE=<profile>";
556
+ const SIGN_IN_HINT = ["In CI, set them with aws-actions/configure-aws-credentials.", "On your machine: aws sso login --profile <profile>, then set AWS_PROFILE."];
557
+ /** The widest line a message has: the CLI prints no wider. */
558
+ const LINE_WIDTH = 76;
559
+ /** `text`, such as AWS's own message, in lines that fit, broken at spaces. */
560
+ const wrap = (text) => {
561
+ const lines = [];
562
+ let line = "";
563
+ for (const word of text.split(/\s+/u).filter(Boolean)) if (line && line.length + 1 + word.length > LINE_WIDTH) {
564
+ lines.push(line);
565
+ line = word;
566
+ } else line = line ? `${line} ${word}` : word;
567
+ if (line) lines.push(line);
568
+ return lines;
569
+ };
570
+ /** An error of one line each, for a terminal. */
571
+ const failure = (lines, cause) => new Error(lines.join("\n"), cause === void 0 ? void 0 : { cause });
697
572
  const errorName = (error) => error instanceof Error ? error.name : "";
698
573
  const errorMessage = (error) => (error instanceof Error ? error.message : String(error)).replace(/\.+$/u, "");
699
574
  const resolveEndpoint = (endpoint) => {
@@ -708,7 +583,11 @@ const resolveEndpoint = (endpoint) => {
708
583
  "127.0.0.1",
709
584
  "[::1]"
710
585
  ])).has(url.hostname);
711
- if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash) throw new Error(`${NAME} endpoint must use HTTPS, or HTTP on a loopback address, without credentials, a query, or a fragment.`);
586
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash) throw failure([
587
+ `${NAME} can't use its endpoint.`,
588
+ "Use HTTPS, or HTTP on a loopback address.",
589
+ "Leave out credentials, a query, and a fragment."
590
+ ]);
712
591
  return url.toString();
713
592
  };
714
593
  /** Masks a value in the CLI's output, which loads `@hot-updater/cli-tools`. */
@@ -735,20 +614,20 @@ const checkCredentials = async (credentials) => {
735
614
  */
736
615
  const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region }) => {
737
616
  const checkSettings = () => {
738
- if (typeof region !== "string" || !REGION_PATTERN.test(region)) throw new Error(`${NAME} needs an AWS region, such as "us-east-1", and ${JSON.stringify(region)} is not one.`);
617
+ if (typeof region !== "string" || !REGION_PATTERN.test(region)) throw failure([`${NAME} can't use the region ${JSON.stringify(region)}.`, "Set an AWS region, such as \"us-east-1\"."]);
739
618
  if (prefix !== "" && !SECRET_ID_PATTERN.test(prefix)) throw new Error(`${NAME}'s prefix takes letters, digits, and /_+=.@- only.`);
740
619
  };
741
620
  const secretId = (name) => {
742
621
  checkSettings();
743
622
  const id = `${prefix}${name}`;
744
- if (!SECRET_ID_PATTERN.test(name) || id.length > MAX_SECRET_ID_LENGTH) throw new Error(`${NAME} cannot name a secret ${JSON.stringify(id)}: AWS takes up to ${MAX_SECRET_ID_LENGTH} letters, digits, and /_+=.@- characters.`);
623
+ if (!SECRET_ID_PATTERN.test(name) || id.length > MAX_SECRET_ID_LENGTH) throw failure([`${NAME} can't name a secret ${JSON.stringify(id)}.`, `AWS takes up to ${MAX_SECRET_ID_LENGTH} letters, digits, and /_+=.@- characters.`]);
745
624
  return id;
746
625
  };
747
626
  const resolveCredentials = () => {
748
627
  if (credentials !== void 0) return async () => checkCredentials(typeof credentials === "function" ? await credentials() : credentials);
749
628
  const accessKeyId = process.env["AWS_ACCESS_KEY_ID"];
750
629
  const secretAccessKey = process.env["AWS_SECRET_ACCESS_KEY"];
751
- if (Boolean(accessKeyId) !== Boolean(secretAccessKey)) throw new Error(`${NAME} found ${accessKeyId ? "AWS_ACCESS_KEY_ID" : "AWS_SECRET_ACCESS_KEY"} without ${accessKeyId ? "AWS_SECRET_ACCESS_KEY" : "AWS_ACCESS_KEY_ID"} in the environment: set both, or neither to use a profile.`);
630
+ if (Boolean(accessKeyId) !== Boolean(secretAccessKey)) throw failure([`${NAME} found ${accessKeyId ? "AWS_ACCESS_KEY_ID" : "AWS_SECRET_ACCESS_KEY"} without ${accessKeyId ? "AWS_SECRET_ACCESS_KEY" : "AWS_ACCESS_KEY_ID"}.`, "Set both, or neither to use a profile."]);
752
631
  };
753
632
  let opened;
754
633
  const open = () => {
@@ -771,12 +650,40 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
771
650
  const describeFailure = (verb, id, error, permissions) => {
772
651
  const name = errorName(error);
773
652
  const status = error?.$metadata?.httpStatusCode;
774
- const detail = `${name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}`;
775
- if (NO_CREDENTIAL_ERRORS.has(name)) return new Error(`${NAME} found no usable AWS credentials to ${verb} ${JSON.stringify(id)} (${errorMessage(error)}). ${SIGN_IN_HINT}.`, { cause: error });
776
- if (EXPIRED_CREDENTIAL_ERRORS.has(name)) return new Error(`AWS Secrets Manager refused the credentials to ${verb} ${JSON.stringify(id)}, ${detail}. They are expired or invalid: sign in again. ${SIGN_IN_HINT}.`, { cause: error });
777
- if (name === "AccessDeniedException") return new Error(`AWS Secrets Manager refused to ${verb} ${JSON.stringify(id)}, ${detail}. The AWS identity needs ${permissions} on it, and on a secret encrypted with a customer managed KMS key, kms:Decrypt to read and kms:GenerateDataKey to store.`, { cause: error });
778
- if (/marked for deletion|scheduled for deletion/iu.test(errorMessage(error))) return new Error(`AWS Secrets Manager cannot ${verb} ${JSON.stringify(id)}: it is scheduled for deletion. Restore it with aws secretsmanager restore-secret --secret-id ${id}, or wait until it is deleted.`, { cause: error });
779
- return new Error(`AWS Secrets Manager failed to ${verb} ${JSON.stringify(id)}, ${detail}.`, { cause: error });
653
+ const detail = wrap(`${name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}.`);
654
+ const secret = `Secret: ${id}`;
655
+ if (NO_CREDENTIAL_ERRORS.has(name)) return failure([
656
+ `${NAME} found no AWS credentials to ${verb} a secret.`,
657
+ secret,
658
+ ...wrap(`${errorMessage(error)}.`),
659
+ ...SIGN_IN_HINT
660
+ ], error);
661
+ if (EXPIRED_CREDENTIAL_ERRORS.has(name)) return failure([
662
+ `AWS Secrets Manager refused the sign-in to ${verb} a secret.`,
663
+ secret,
664
+ ...detail,
665
+ "They are expired or invalid: sign in again.",
666
+ ...SIGN_IN_HINT
667
+ ], error);
668
+ if (name === "AccessDeniedException") return failure([
669
+ `AWS Secrets Manager refused to ${verb} a secret.`,
670
+ secret,
671
+ ...detail,
672
+ `The AWS identity needs ${permissions} on it.`,
673
+ `With a customer managed KMS key, it needs ${verb === "read" ? "kms:Decrypt" : "kms:GenerateDataKey"} too.`
674
+ ], error);
675
+ if (/marked for deletion|scheduled for deletion/iu.test(errorMessage(error))) return failure([
676
+ `AWS Secrets Manager can't ${verb} a secret.`,
677
+ secret,
678
+ "It is scheduled for deletion. Restore it, or wait until it is gone:",
679
+ "aws secretsmanager restore-secret \\",
680
+ ` --secret-id ${id}`
681
+ ], error);
682
+ return failure([
683
+ `AWS Secrets Manager failed to ${verb} a secret.`,
684
+ secret,
685
+ ...detail
686
+ ], error);
780
687
  };
781
688
  const read = async ({ sdk, client }, id) => {
782
689
  let output;
@@ -806,8 +713,8 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
806
713
  },
807
714
  set: async (name, value) => {
808
715
  const id = secretId(name);
809
- if (typeof value !== "string" || value === "") throw new Error(`${NAME} cannot store an empty ${id}.`);
810
- if (Buffer.byteLength(value, "utf8") > MAX_SECRET_BYTES) throw new Error(`${NAME} cannot store ${id}: AWS takes up to ${MAX_SECRET_BYTES} bytes per secret.`);
716
+ if (typeof value !== "string" || value === "") throw new Error(`${NAME} can't store an empty ${id}.`);
717
+ if (Buffer.byteLength(value, "utf8") > MAX_SECRET_BYTES) throw failure([`${NAME} can't store ${id}.`, `AWS takes up to ${MAX_SECRET_BYTES} bytes per secret.`]);
811
718
  await mask([value]);
812
719
  const { sdk, client } = await open();
813
720
  const token = (0, node_crypto.randomUUID)();
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";
2
2
  import { EngineDatabase, Lazy, PluginTables, SecretsAdapter, StorageAdapter, StorageAdapterWith } from "@hot-updater/plugin-core";
3
3
  import { S3ClientConfig } from "@aws-sdk/client-s3";
4
+ import { S3StorageOperations } from "@hot-updater/storage-s3";
4
5
  //#region src/lazyCredentials.d.ts
5
6
  type SdkCredentials = S3ClientConfig["credentials"];
6
7
  /**
@@ -88,7 +89,6 @@ interface S3StorageConfig extends Omit<S3ClientConfig, "credentials"> {
88
89
  */
89
90
  getDownloadUrl?: StorageAdapter["getDownloadUrl"];
90
91
  }
91
- type S3StorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
92
92
  declare function s3Storage(config: S3StorageConfig): StorageAdapterWith<S3StorageOperations>;
93
93
  //#endregion
94
94
  //#region src/secretsManager.d.ts
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";
2
2
  import { EngineDatabase, Lazy, PluginTables, SecretsAdapter, StorageAdapter, StorageAdapterWith } from "@hot-updater/plugin-core";
3
+ import { S3StorageOperations } from "@hot-updater/storage-s3";
3
4
  import { S3ClientConfig } from "@aws-sdk/client-s3";
4
5
  //#region src/lazyCredentials.d.ts
5
6
  type SdkCredentials = S3ClientConfig["credentials"];
@@ -88,7 +89,6 @@ interface S3StorageConfig extends Omit<S3ClientConfig, "credentials"> {
88
89
  */
89
90
  getDownloadUrl?: StorageAdapter["getDownloadUrl"];
90
91
  }
91
- type S3StorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
92
92
  declare function s3Storage(config: S3StorageConfig): StorageAdapterWith<S3StorageOperations>;
93
93
  //#endregion
94
94
  //#region src/secretsManager.d.ts
package/dist/index.mjs CHANGED
@@ -1,11 +1,10 @@
1
1
  import { CreateTableCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DynamoDBClient, UpdateTimeToLiveCommand, waitUntilTableExists } from "@aws-sdk/client-dynamodb";
2
- import { BUNDLE_STORAGE_PREFIX, CONTENT_ADDRESSED_ASSET_PREFIX, createEngineDatabase, createKvAdapter, createStorageAdapter, createStorageKeyBuilder, createStorageUri, migrateCoreSchema, parseStorageUri, resolveLazy } from "@hot-updater/plugin-core";
2
+ import { BUNDLE_STORAGE_PREFIX, CONTENT_ADDRESSED_ASSET_PREFIX, createEngineDatabase, createKvAdapter, migrateCoreSchema, resolveLazy } from "@hot-updater/plugin-core";
3
3
  import { CloudFrontClient, CreateInvalidationCommand, GetInvalidationCommand } from "@aws-sdk/client-cloudfront";
4
4
  import { BatchGetCommand, BatchWriteCommand, DynamoDBDocumentClient, QueryCommand, TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
5
5
  import { SSM } from "@aws-sdk/client-ssm";
6
6
  import { getSignedUrl } from "@aws-sdk/cloudfront-signer";
7
- import { DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, S3Client } from "@aws-sdk/client-s3";
8
- import { Upload } from "@aws-sdk/lib-storage";
7
+ import { createS3Storage } from "@hot-updater/storage-s3";
9
8
  import { randomUUID } from "node:crypto";
10
9
  //#region src/cloudFrontInvalidation.ts
11
10
  const DEFAULT_POLL_INTERVAL_MS = 2e3;
@@ -518,156 +517,17 @@ const cloudFrontDownloadUrl = (config) => {
518
517
  };
519
518
  //#endregion
520
519
  //#region src/s3Storage.ts
521
- /** Devices download right after the update check that returns the URL. */
522
- const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
523
- const isObjectNotFoundError = (error) => error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey");
524
520
  function s3Storage(config) {
525
521
  const { bucketName, basePath, getDownloadUrl, credentials, ...s3Config } = config;
526
- const client = new S3Client(applyS3RuntimeAwsConfig({
527
- ...s3Config,
528
- credentials: toSdkCredentials(credentials)
529
- }));
530
- const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
531
- const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
532
- const getListPrefix = (prefix = "") => {
533
- const normalizedPrefix = prefix.replace(/^\/+|\/+$/g, "");
534
- const value = [normalizedBasePath, normalizedPrefix].filter(Boolean).join("/");
535
- return value ? `${value}/` : "";
536
- };
537
- /**
538
- * The URI `put` returns for the key, or null for a key no `put` writes,
539
- * such as a folder marker that ends in "/".
540
- */
541
- const getListedStorageUri = (key) => {
542
- try {
543
- return createStorageUri({
544
- bucket: bucketName,
545
- key,
546
- protocol: "s3"
547
- });
548
- } catch {
549
- return null;
550
- }
551
- };
552
- const getRelativeKey = (key) => {
553
- if (!normalizedBasePath) return key;
554
- const basePrefix = `${normalizedBasePath}/`;
555
- return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
556
- };
557
- const parseAndValidate = (storageUri) => {
558
- const parsed = parseStorageUri(storageUri, "s3");
559
- if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
560
- return parsed;
561
- };
562
- return createStorageAdapter({
522
+ return createS3Storage({
563
523
  name: "s3Storage",
564
- protocol: "s3",
565
- async listObjects(prefix) {
566
- const objects = [];
567
- let continuationToken;
568
- do {
569
- const response = await client.send(new ListObjectsV2Command({
570
- Bucket: bucketName,
571
- ContinuationToken: continuationToken,
572
- Prefix: getListPrefix(prefix)
573
- }));
574
- for (const object of response.Contents ?? []) {
575
- if (!object.Key) continue;
576
- const storageUri = getListedStorageUri(object.Key);
577
- if (storageUri === null) continue;
578
- objects.push({
579
- key: getRelativeKey(object.Key),
580
- lastModifiedAt: object.LastModified,
581
- size: object.Size ?? 0,
582
- storageUri
583
- });
584
- }
585
- continuationToken = response.NextContinuationToken;
586
- } while (continuationToken);
587
- return objects;
588
- },
589
- async deleteObjects(relativeKeys) {
590
- const keys = relativeKeys.map((key) => getStorageKey(key));
591
- for (let offset = 0; offset < keys.length; offset += 1e3) {
592
- const batch = keys.slice(offset, offset + 1e3);
593
- if (batch.length === 0) continue;
594
- const response = await client.send(new DeleteObjectsCommand({
595
- Bucket: bucketName,
596
- Delete: {
597
- Objects: batch.map((Key) => ({ Key })),
598
- Quiet: true
599
- }
600
- }));
601
- if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
602
- }
603
- },
604
- async put({ key, body, contentLength, contentType }) {
605
- const storageKey = getStorageKey(key);
606
- const response = await new Upload({
607
- client,
608
- params: {
609
- Body: body,
610
- Bucket: bucketName,
611
- CacheControl: "max-age=31536000",
612
- ...contentLength === void 0 ? {} : { ContentLength: contentLength },
613
- ContentType: contentType,
614
- Key: storageKey
615
- }
616
- }).done();
617
- if (!response.Bucket || !response.Key) throw new Error("Upload failed");
618
- return { storageUri: createStorageUri({
619
- bucket: bucketName,
620
- key: storageKey,
621
- protocol: "s3"
622
- }) };
623
- },
624
- async get({ storageUri }) {
625
- const { key } = parseAndValidate(storageUri);
626
- try {
627
- const response = await client.send(new GetObjectCommand({
628
- Bucket: bucketName,
629
- Key: key
630
- }));
631
- if (!response.Body) return { response: null };
632
- const headers = new Headers();
633
- if (response.ContentType) headers.set("content-type", response.ContentType);
634
- if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
635
- return { response: new Response(response.Body.transformToWebStream(), { headers }) };
636
- } catch (error) {
637
- if (isObjectNotFoundError(error)) return { response: null };
638
- throw error;
639
- }
640
- },
641
- async getDownloadUrl({ storageUri }) {
642
- const { key } = parseAndValidate(storageUri);
643
- if (getDownloadUrl) return getDownloadUrl({ storageUri });
644
- const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
645
- return { url: await getSignedUrl(client, new GetObjectCommand({
646
- Bucket: bucketName,
647
- Key: key
648
- }), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
649
- },
650
- async exists({ storageUri }) {
651
- const { key } = parseAndValidate(storageUri);
652
- try {
653
- await client.send(new HeadObjectCommand({
654
- Bucket: bucketName,
655
- Key: key
656
- }));
657
- return { exists: true };
658
- } catch (error) {
659
- if (isObjectNotFoundError(error)) return { exists: false };
660
- throw error;
661
- }
662
- },
663
- async delete({ storageUri }) {
664
- const { key } = parseAndValidate(storageUri);
665
- await client.send(new DeleteObjectCommand({
666
- Bucket: bucketName,
667
- Key: key
668
- }));
669
- return { deleted: true };
670
- }
524
+ protocol: "s3"
525
+ }, {
526
+ ...applyS3RuntimeAwsConfig(s3Config),
527
+ credentials,
528
+ bucketName,
529
+ basePath,
530
+ getDownloadUrl
671
531
  });
672
532
  }
673
533
  //#endregion
@@ -692,7 +552,22 @@ const EXPIRED_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set([
692
552
  ]);
693
553
  /** Errors of the SDK finding no credentials, or an expired SSO session. */
694
554
  const NO_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set(["CredentialsProviderError", "TokenProviderError"]);
695
- const SIGN_IN_HINT = "In CI, set them with aws-actions/configure-aws-credentials; on your machine, run aws sso login --profile <profile> and set AWS_PROFILE=<profile>";
555
+ const SIGN_IN_HINT = ["In CI, set them with aws-actions/configure-aws-credentials.", "On your machine: aws sso login --profile <profile>, then set AWS_PROFILE."];
556
+ /** The widest line a message has: the CLI prints no wider. */
557
+ const LINE_WIDTH = 76;
558
+ /** `text`, such as AWS's own message, in lines that fit, broken at spaces. */
559
+ const wrap = (text) => {
560
+ const lines = [];
561
+ let line = "";
562
+ for (const word of text.split(/\s+/u).filter(Boolean)) if (line && line.length + 1 + word.length > LINE_WIDTH) {
563
+ lines.push(line);
564
+ line = word;
565
+ } else line = line ? `${line} ${word}` : word;
566
+ if (line) lines.push(line);
567
+ return lines;
568
+ };
569
+ /** An error of one line each, for a terminal. */
570
+ const failure = (lines, cause) => new Error(lines.join("\n"), cause === void 0 ? void 0 : { cause });
696
571
  const errorName = (error) => error instanceof Error ? error.name : "";
697
572
  const errorMessage = (error) => (error instanceof Error ? error.message : String(error)).replace(/\.+$/u, "");
698
573
  const resolveEndpoint = (endpoint) => {
@@ -707,7 +582,11 @@ const resolveEndpoint = (endpoint) => {
707
582
  "127.0.0.1",
708
583
  "[::1]"
709
584
  ])).has(url.hostname);
710
- if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash) throw new Error(`${NAME} endpoint must use HTTPS, or HTTP on a loopback address, without credentials, a query, or a fragment.`);
585
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash) throw failure([
586
+ `${NAME} can't use its endpoint.`,
587
+ "Use HTTPS, or HTTP on a loopback address.",
588
+ "Leave out credentials, a query, and a fragment."
589
+ ]);
711
590
  return url.toString();
712
591
  };
713
592
  /** Masks a value in the CLI's output, which loads `@hot-updater/cli-tools`. */
@@ -734,20 +613,20 @@ const checkCredentials = async (credentials) => {
734
613
  */
735
614
  const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region }) => {
736
615
  const checkSettings = () => {
737
- if (typeof region !== "string" || !REGION_PATTERN.test(region)) throw new Error(`${NAME} needs an AWS region, such as "us-east-1", and ${JSON.stringify(region)} is not one.`);
616
+ if (typeof region !== "string" || !REGION_PATTERN.test(region)) throw failure([`${NAME} can't use the region ${JSON.stringify(region)}.`, "Set an AWS region, such as \"us-east-1\"."]);
738
617
  if (prefix !== "" && !SECRET_ID_PATTERN.test(prefix)) throw new Error(`${NAME}'s prefix takes letters, digits, and /_+=.@- only.`);
739
618
  };
740
619
  const secretId = (name) => {
741
620
  checkSettings();
742
621
  const id = `${prefix}${name}`;
743
- if (!SECRET_ID_PATTERN.test(name) || id.length > MAX_SECRET_ID_LENGTH) throw new Error(`${NAME} cannot name a secret ${JSON.stringify(id)}: AWS takes up to ${MAX_SECRET_ID_LENGTH} letters, digits, and /_+=.@- characters.`);
622
+ if (!SECRET_ID_PATTERN.test(name) || id.length > MAX_SECRET_ID_LENGTH) throw failure([`${NAME} can't name a secret ${JSON.stringify(id)}.`, `AWS takes up to ${MAX_SECRET_ID_LENGTH} letters, digits, and /_+=.@- characters.`]);
744
623
  return id;
745
624
  };
746
625
  const resolveCredentials = () => {
747
626
  if (credentials !== void 0) return async () => checkCredentials(typeof credentials === "function" ? await credentials() : credentials);
748
627
  const accessKeyId = process.env["AWS_ACCESS_KEY_ID"];
749
628
  const secretAccessKey = process.env["AWS_SECRET_ACCESS_KEY"];
750
- if (Boolean(accessKeyId) !== Boolean(secretAccessKey)) throw new Error(`${NAME} found ${accessKeyId ? "AWS_ACCESS_KEY_ID" : "AWS_SECRET_ACCESS_KEY"} without ${accessKeyId ? "AWS_SECRET_ACCESS_KEY" : "AWS_ACCESS_KEY_ID"} in the environment: set both, or neither to use a profile.`);
629
+ if (Boolean(accessKeyId) !== Boolean(secretAccessKey)) throw failure([`${NAME} found ${accessKeyId ? "AWS_ACCESS_KEY_ID" : "AWS_SECRET_ACCESS_KEY"} without ${accessKeyId ? "AWS_SECRET_ACCESS_KEY" : "AWS_ACCESS_KEY_ID"}.`, "Set both, or neither to use a profile."]);
751
630
  };
752
631
  let opened;
753
632
  const open = () => {
@@ -770,12 +649,40 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
770
649
  const describeFailure = (verb, id, error, permissions) => {
771
650
  const name = errorName(error);
772
651
  const status = error?.$metadata?.httpStatusCode;
773
- const detail = `${name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}`;
774
- if (NO_CREDENTIAL_ERRORS.has(name)) return new Error(`${NAME} found no usable AWS credentials to ${verb} ${JSON.stringify(id)} (${errorMessage(error)}). ${SIGN_IN_HINT}.`, { cause: error });
775
- if (EXPIRED_CREDENTIAL_ERRORS.has(name)) return new Error(`AWS Secrets Manager refused the credentials to ${verb} ${JSON.stringify(id)}, ${detail}. They are expired or invalid: sign in again. ${SIGN_IN_HINT}.`, { cause: error });
776
- if (name === "AccessDeniedException") return new Error(`AWS Secrets Manager refused to ${verb} ${JSON.stringify(id)}, ${detail}. The AWS identity needs ${permissions} on it, and on a secret encrypted with a customer managed KMS key, kms:Decrypt to read and kms:GenerateDataKey to store.`, { cause: error });
777
- if (/marked for deletion|scheduled for deletion/iu.test(errorMessage(error))) return new Error(`AWS Secrets Manager cannot ${verb} ${JSON.stringify(id)}: it is scheduled for deletion. Restore it with aws secretsmanager restore-secret --secret-id ${id}, or wait until it is deleted.`, { cause: error });
778
- return new Error(`AWS Secrets Manager failed to ${verb} ${JSON.stringify(id)}, ${detail}.`, { cause: error });
652
+ const detail = wrap(`${name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}.`);
653
+ const secret = `Secret: ${id}`;
654
+ if (NO_CREDENTIAL_ERRORS.has(name)) return failure([
655
+ `${NAME} found no AWS credentials to ${verb} a secret.`,
656
+ secret,
657
+ ...wrap(`${errorMessage(error)}.`),
658
+ ...SIGN_IN_HINT
659
+ ], error);
660
+ if (EXPIRED_CREDENTIAL_ERRORS.has(name)) return failure([
661
+ `AWS Secrets Manager refused the sign-in to ${verb} a secret.`,
662
+ secret,
663
+ ...detail,
664
+ "They are expired or invalid: sign in again.",
665
+ ...SIGN_IN_HINT
666
+ ], error);
667
+ if (name === "AccessDeniedException") return failure([
668
+ `AWS Secrets Manager refused to ${verb} a secret.`,
669
+ secret,
670
+ ...detail,
671
+ `The AWS identity needs ${permissions} on it.`,
672
+ `With a customer managed KMS key, it needs ${verb === "read" ? "kms:Decrypt" : "kms:GenerateDataKey"} too.`
673
+ ], error);
674
+ if (/marked for deletion|scheduled for deletion/iu.test(errorMessage(error))) return failure([
675
+ `AWS Secrets Manager can't ${verb} a secret.`,
676
+ secret,
677
+ "It is scheduled for deletion. Restore it, or wait until it is gone:",
678
+ "aws secretsmanager restore-secret \\",
679
+ ` --secret-id ${id}`
680
+ ], error);
681
+ return failure([
682
+ `AWS Secrets Manager failed to ${verb} a secret.`,
683
+ secret,
684
+ ...detail
685
+ ], error);
779
686
  };
780
687
  const read = async ({ sdk, client }, id) => {
781
688
  let output;
@@ -805,8 +712,8 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
805
712
  },
806
713
  set: async (name, value) => {
807
714
  const id = secretId(name);
808
- if (typeof value !== "string" || value === "") throw new Error(`${NAME} cannot store an empty ${id}.`);
809
- if (Buffer.byteLength(value, "utf8") > MAX_SECRET_BYTES) throw new Error(`${NAME} cannot store ${id}: AWS takes up to ${MAX_SECRET_BYTES} bytes per secret.`);
715
+ if (typeof value !== "string" || value === "") throw new Error(`${NAME} can't store an empty ${id}.`);
716
+ if (Buffer.byteLength(value, "utf8") > MAX_SECRET_BYTES) throw failure([`${NAME} can't store ${id}.`, `AWS takes up to ${MAX_SECRET_BYTES} bytes per secret.`]);
810
717
  await mask([value]);
811
718
  const { sdk, client } = await open();
812
719
  const token = randomUUID();
@@ -30,6 +30,7 @@ let _aws_sdk_client_cloudfront = require("@aws-sdk/client-cloudfront");
30
30
  let _aws_sdk_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
31
31
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
32
32
  let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
33
+ let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
33
34
  const SHA256_FILE_HASH_RE = /^[0-9a-f]{64}$/;
34
35
  const isContentAddressedAssetFileHash = (value) => {
35
36
  return typeof value === "string" && SHA256_FILE_HASH_RE.test(value);
@@ -4711,7 +4712,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
4711
4712
  };
4712
4713
  //#endregion
4713
4714
  //#region ../../packages/server/dist/version.mjs
4714
- const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.8";
4715
+ const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.9";
4715
4716
  //#endregion
4716
4717
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4717
4718
  /**
@@ -14304,7 +14305,7 @@ async apply() {
14304
14305
  * `credentials` as the AWS SDK takes them: access keys with a lazy field
14305
14306
  * become a provider, which the SDK calls once, on its first request.
14306
14307
  */
14307
- const toSdkCredentials = (credentials) => {
14308
+ const toSdkCredentials$1 = (credentials) => {
14308
14309
  if (credentials === void 0 || typeof credentials === "function") return credentials;
14309
14310
  const { accessKeyId, secretAccessKey, sessionToken, ...rest } = credentials;
14310
14311
  if (typeof accessKeyId !== "function" && typeof secretAccessKey !== "function" && typeof sessionToken !== "function") return credentials;
@@ -14321,7 +14322,7 @@ const toSdkCredentials = (credentials) => {
14321
14322
  const adapterOf = ({ tableName, credentials, ...clientConfig }) => {
14322
14323
  const client = new _aws_sdk_client_dynamodb.DynamoDBClient({
14323
14324
  ...clientConfig,
14324
- credentials: toSdkCredentials(credentials)
14325
+ credentials: toSdkCredentials$1(credentials)
14325
14326
  });
14326
14327
  const store = createDynamoDBStore({
14327
14328
  client,
@@ -14349,7 +14350,7 @@ const dynamoDB = (config) => {
14349
14350
  const { client, adapter } = adapterOf(config);
14350
14351
  const cloudFront = createUpdateRouteInvalidation({
14351
14352
  ...config,
14352
- credentials: toSdkCredentials(config.credentials)
14353
+ credentials: toSdkCredentials$1(config.credentials)
14353
14354
  });
14354
14355
  return {
14355
14356
  ...createEngineDatabase({
@@ -14367,21 +14368,49 @@ const dynamoDB = (config) => {
14367
14368
  };
14368
14369
  };
14369
14370
  //#endregion
14370
- //#region src/s3Storage.ts
14371
+ //#region ../storage-s3/dist/index.mjs
14372
+ /**
14373
+ * `credentials` as the AWS SDK takes them: access keys with a lazy field
14374
+ * become a provider, which the SDK calls once, on its first request.
14375
+ */
14376
+ const toSdkCredentials = (credentials) => {
14377
+ if (credentials === void 0 || typeof credentials === "function") return credentials;
14378
+ const { accessKeyId, secretAccessKey, sessionToken, ...rest } = credentials;
14379
+ if (typeof accessKeyId !== "function" && typeof secretAccessKey !== "function" && typeof sessionToken !== "function") return credentials;
14380
+ return async () => ({
14381
+ ...rest,
14382
+ accessKeyId: await resolveLazy(accessKeyId),
14383
+ secretAccessKey: await resolveLazy(secretAccessKey),
14384
+ ...sessionToken === void 0 ? {} : { sessionToken: await resolveLazy(sessionToken) }
14385
+ });
14386
+ };
14371
14387
  /** Devices download right after the update check that returns the URL. */
14372
- const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
14373
- const isObjectNotFoundError = (error) => error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey");
14374
- function s3Storage(config) {
14375
- const { bucketName, basePath, getDownloadUrl, credentials, ...s3Config } = config;
14376
- const client = new _aws_sdk_client_s3.S3Client(applyS3RuntimeAwsConfig({
14377
- ...s3Config,
14388
+ const DEFAULT_SIGNED_LIFETIME_SECONDS = 3600;
14389
+ /** SigV4's limit on a presigned URL. */
14390
+ const MAX_SIGNED_LIFETIME_SECONDS = 604800;
14391
+ /** DeleteObjects takes at most 1000 keys a request. */
14392
+ const DELETE_BATCH_SIZE = 1e3;
14393
+ const isObjectNotFoundError = (error) => {
14394
+ if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
14395
+ return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
14396
+ };
14397
+ const trimSlashes = (value) => value.replace(/^\/+|\/+$/g, "");
14398
+ /**
14399
+ * A storage adapter over a bucket's S3 API, which AWS S3, Cloudflare R2, and
14400
+ * Neon Object Storage all serve. Each provider package passes its own name,
14401
+ * URI protocol, and client defaults.
14402
+ */
14403
+ const createS3Storage = ({ name, protocol }, config) => {
14404
+ const { bucketName, basePath, getDownloadUrl, signedUrlExpiresIn = DEFAULT_SIGNED_LIFETIME_SECONDS, credentials, ...clientConfig } = config;
14405
+ if (!Number.isFinite(signedUrlExpiresIn) || signedUrlExpiresIn <= 0 || signedUrlExpiresIn > MAX_SIGNED_LIFETIME_SECONDS) throw new Error(`${name} signedUrlExpiresIn must be between 1 and ${MAX_SIGNED_LIFETIME_SECONDS} seconds (seven days).`);
14406
+ const client = new _aws_sdk_client_s3.S3Client({
14407
+ ...clientConfig,
14378
14408
  credentials: toSdkCredentials(credentials)
14379
- }));
14380
- const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
14409
+ });
14410
+ const normalizedBasePath = trimSlashes(basePath ?? "");
14381
14411
  const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
14382
14412
  const getListPrefix = (prefix = "") => {
14383
- const normalizedPrefix = prefix.replace(/^\/+|\/+$/g, "");
14384
- const value = [normalizedBasePath, normalizedPrefix].filter(Boolean).join("/");
14413
+ const value = [normalizedBasePath, trimSlashes(prefix)].filter(Boolean).join("/");
14385
14414
  return value ? `${value}/` : "";
14386
14415
  };
14387
14416
  /**
@@ -14393,7 +14422,7 @@ function s3Storage(config) {
14393
14422
  return createStorageUri({
14394
14423
  bucket: bucketName,
14395
14424
  key,
14396
- protocol: "s3"
14425
+ protocol
14397
14426
  });
14398
14427
  } catch {
14399
14428
  return null;
@@ -14405,55 +14434,16 @@ function s3Storage(config) {
14405
14434
  return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
14406
14435
  };
14407
14436
  const parseAndValidate = (storageUri) => {
14408
- const parsed = parseStorageUri(storageUri, "s3");
14437
+ const parsed = parseStorageUri(storageUri, protocol);
14409
14438
  if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
14410
14439
  return parsed;
14411
14440
  };
14412
14441
  return createStorageAdapter({
14413
- name: "s3Storage",
14414
- protocol: "s3",
14415
- async listObjects(prefix) {
14416
- const objects = [];
14417
- let continuationToken;
14418
- do {
14419
- const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
14420
- Bucket: bucketName,
14421
- ContinuationToken: continuationToken,
14422
- Prefix: getListPrefix(prefix)
14423
- }));
14424
- for (const object of response.Contents ?? []) {
14425
- if (!object.Key) continue;
14426
- const storageUri = getListedStorageUri(object.Key);
14427
- if (storageUri === null) continue;
14428
- objects.push({
14429
- key: getRelativeKey(object.Key),
14430
- lastModifiedAt: object.LastModified,
14431
- size: object.Size ?? 0,
14432
- storageUri
14433
- });
14434
- }
14435
- continuationToken = response.NextContinuationToken;
14436
- } while (continuationToken);
14437
- return objects;
14438
- },
14439
- async deleteObjects(relativeKeys) {
14440
- const keys = relativeKeys.map((key) => getStorageKey(key));
14441
- for (let offset = 0; offset < keys.length; offset += 1e3) {
14442
- const batch = keys.slice(offset, offset + 1e3);
14443
- if (batch.length === 0) continue;
14444
- const response = await client.send(new _aws_sdk_client_s3.DeleteObjectsCommand({
14445
- Bucket: bucketName,
14446
- Delete: {
14447
- Objects: batch.map((Key) => ({ Key })),
14448
- Quiet: true
14449
- }
14450
- }));
14451
- if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
14452
- }
14453
- },
14442
+ name,
14443
+ protocol,
14454
14444
  async put({ key, body, contentLength, contentType }) {
14455
14445
  const storageKey = getStorageKey(key);
14456
- const response = await new _aws_sdk_lib_storage.Upload({
14446
+ await new _aws_sdk_lib_storage.Upload({
14457
14447
  client,
14458
14448
  params: {
14459
14449
  Body: body,
@@ -14464,11 +14454,10 @@ function s3Storage(config) {
14464
14454
  Key: storageKey
14465
14455
  }
14466
14456
  }).done();
14467
- if (!response.Bucket || !response.Key) throw new Error("Upload failed");
14468
14457
  return { storageUri: createStorageUri({
14469
14458
  bucket: bucketName,
14470
14459
  key: storageKey,
14471
- protocol: "s3"
14460
+ protocol
14472
14461
  }) };
14473
14462
  },
14474
14463
  async get({ storageUri }) {
@@ -14491,11 +14480,10 @@ function s3Storage(config) {
14491
14480
  async getDownloadUrl({ storageUri }) {
14492
14481
  const { key } = parseAndValidate(storageUri);
14493
14482
  if (getDownloadUrl) return getDownloadUrl({ storageUri });
14494
- const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
14495
- return { url: await getSignedUrl(client, new _aws_sdk_client_s3.GetObjectCommand({
14483
+ return { url: await (0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.GetObjectCommand({
14496
14484
  Bucket: bucketName,
14497
14485
  Key: key
14498
- }), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
14486
+ }), { expiresIn: signedUrlExpiresIn }) };
14499
14487
  },
14500
14488
  async exists({ storageUri }) {
14501
14489
  const { key } = parseAndValidate(storageUri);
@@ -14517,8 +14505,61 @@ function s3Storage(config) {
14517
14505
  Key: key
14518
14506
  }));
14519
14507
  return { deleted: true };
14508
+ },
14509
+ async listObjects(prefix) {
14510
+ const objects = [];
14511
+ let continuationToken;
14512
+ do {
14513
+ const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
14514
+ Bucket: bucketName,
14515
+ ContinuationToken: continuationToken,
14516
+ Prefix: getListPrefix(prefix)
14517
+ }));
14518
+ for (const object of response.Contents ?? []) {
14519
+ if (!object.Key) continue;
14520
+ const storageUri = getListedStorageUri(object.Key);
14521
+ if (storageUri === null) continue;
14522
+ objects.push({
14523
+ key: getRelativeKey(object.Key),
14524
+ lastModifiedAt: object.LastModified,
14525
+ size: object.Size ?? 0,
14526
+ storageUri
14527
+ });
14528
+ }
14529
+ continuationToken = response.NextContinuationToken;
14530
+ } while (continuationToken);
14531
+ return objects;
14532
+ },
14533
+ async deleteObjects(relativeKeys) {
14534
+ const keys = relativeKeys.map((key) => getStorageKey(key));
14535
+ for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) {
14536
+ const batch = keys.slice(offset, offset + DELETE_BATCH_SIZE);
14537
+ const response = await client.send(new _aws_sdk_client_s3.DeleteObjectsCommand({
14538
+ Bucket: bucketName,
14539
+ Delete: {
14540
+ Objects: batch.map((Key) => ({ Key })),
14541
+ Quiet: true
14542
+ }
14543
+ }));
14544
+ if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s) from bucket "${bucketName}".`);
14545
+ }
14520
14546
  }
14521
14547
  });
14548
+ };
14549
+ //#endregion
14550
+ //#region src/s3Storage.ts
14551
+ function s3Storage(config) {
14552
+ const { bucketName, basePath, getDownloadUrl, credentials, ...s3Config } = config;
14553
+ return createS3Storage({
14554
+ name: "s3Storage",
14555
+ protocol: "s3"
14556
+ }, {
14557
+ ...applyS3RuntimeAwsConfig(s3Config),
14558
+ credentials,
14559
+ bucketName,
14560
+ basePath,
14561
+ getDownloadUrl
14562
+ });
14522
14563
  }
14523
14564
  //#endregion
14524
14565
  //#region lambda/index.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/aws",
3
3
  "type": "module",
4
- "version": "1.2.0-rc.8",
4
+ "version": "1.2.0-rc.9",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -45,9 +45,9 @@
45
45
  "es-toolkit": "^1.32.0",
46
46
  "execa": "9.5.2",
47
47
  "mime": "^4.0.4",
48
- "@hot-updater/protocol": "1.2.0-rc.8",
49
- "@hot-updater/test-utils": "1.2.0-rc.8",
50
- "@hot-updater/mock": "1.2.0-rc.8"
48
+ "@hot-updater/protocol": "1.2.0-rc.9",
49
+ "@hot-updater/mock": "1.2.0-rc.9",
50
+ "@hot-updater/test-utils": "1.2.0-rc.9"
51
51
  },
52
52
  "dependencies": {
53
53
  "@aws-sdk/client-cloudfront": "3.1066.0",
@@ -64,9 +64,10 @@
64
64
  "@aws-sdk/lib-dynamodb": "3.1066.0",
65
65
  "@aws-sdk/s3-request-presigner": "3.1066.0",
66
66
  "hono": "4.12.34",
67
- "@hot-updater/cli-tools": "1.2.0-rc.8",
68
- "@hot-updater/plugin-core": "1.2.0-rc.8",
69
- "@hot-updater/server": "1.2.0-rc.8"
67
+ "@hot-updater/cli-tools": "1.2.0-rc.9",
68
+ "@hot-updater/plugin-core": "1.2.0-rc.9",
69
+ "@hot-updater/server": "1.2.0-rc.9",
70
+ "@hot-updater/storage-s3": "1.2.0-rc.9"
70
71
  },
71
72
  "scripts": {
72
73
  "build": "tsdown",