@hot-updater/aws 1.2.0-rc.7 → 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.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 } 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;
@@ -314,10 +313,30 @@ async apply() {
314
313
  };
315
314
  };
316
315
  //#endregion
316
+ //#region src/lazyCredentials.ts
317
+ /**
318
+ * `credentials` as the AWS SDK takes them: access keys with a lazy field
319
+ * become a provider, which the SDK calls once, on its first request.
320
+ */
321
+ const toSdkCredentials = (credentials) => {
322
+ if (credentials === void 0 || typeof credentials === "function") return credentials;
323
+ const { accessKeyId, secretAccessKey, sessionToken, ...rest } = credentials;
324
+ if (typeof accessKeyId !== "function" && typeof secretAccessKey !== "function" && typeof sessionToken !== "function") return credentials;
325
+ return async () => ({
326
+ ...rest,
327
+ accessKeyId: await resolveLazy(accessKeyId),
328
+ secretAccessKey: await resolveLazy(secretAccessKey),
329
+ ...sessionToken === void 0 ? {} : { sessionToken: await resolveLazy(sessionToken) }
330
+ });
331
+ };
332
+ //#endregion
317
333
  //#region src/dynamoDB.ts
318
334
  /** The DynamoDB client ignores the CloudFront and batching settings. */
319
- const adapterOf = ({ tableName, ...clientConfig }) => {
320
- const client = new DynamoDBClient(clientConfig);
335
+ const adapterOf = ({ tableName, credentials, ...clientConfig }) => {
336
+ const client = new DynamoDBClient({
337
+ ...clientConfig,
338
+ credentials: toSdkCredentials(credentials)
339
+ });
321
340
  const store = createDynamoDBStore({
322
341
  client,
323
342
  tableName
@@ -352,7 +371,10 @@ const marked = (target) => ({ [DATABASE_TARGET]: target });
352
371
  */
353
372
  const dynamoDB = (config) => {
354
373
  const { client, adapter } = adapterOf(config);
355
- const cloudFront = createUpdateRouteInvalidation(config);
374
+ const cloudFront = createUpdateRouteInvalidation({
375
+ ...config,
376
+ credentials: toSdkCredentials(config.credentials)
377
+ });
356
378
  return {
357
379
  ...createEngineDatabase({
358
380
  name: "dynamoDB",
@@ -495,153 +517,17 @@ const cloudFrontDownloadUrl = (config) => {
495
517
  };
496
518
  //#endregion
497
519
  //#region src/s3Storage.ts
498
- /** Devices download right after the update check that returns the URL. */
499
- const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
500
- const isObjectNotFoundError = (error) => error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey");
501
520
  function s3Storage(config) {
502
- const { bucketName, basePath, getDownloadUrl, ...s3Config } = config;
503
- const client = new S3Client(applyS3RuntimeAwsConfig(s3Config));
504
- const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
505
- const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
506
- const getListPrefix = (prefix = "") => {
507
- const normalizedPrefix = prefix.replace(/^\/+|\/+$/g, "");
508
- const value = [normalizedBasePath, normalizedPrefix].filter(Boolean).join("/");
509
- return value ? `${value}/` : "";
510
- };
511
- /**
512
- * The URI `put` returns for the key, or null for a key no `put` writes,
513
- * such as a folder marker that ends in "/".
514
- */
515
- const getListedStorageUri = (key) => {
516
- try {
517
- return createStorageUri({
518
- bucket: bucketName,
519
- key,
520
- protocol: "s3"
521
- });
522
- } catch {
523
- return null;
524
- }
525
- };
526
- const getRelativeKey = (key) => {
527
- if (!normalizedBasePath) return key;
528
- const basePrefix = `${normalizedBasePath}/`;
529
- return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
530
- };
531
- const parseAndValidate = (storageUri) => {
532
- const parsed = parseStorageUri(storageUri, "s3");
533
- if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
534
- return parsed;
535
- };
536
- return createStorageAdapter({
521
+ const { bucketName, basePath, getDownloadUrl, credentials, ...s3Config } = config;
522
+ return createS3Storage({
537
523
  name: "s3Storage",
538
- protocol: "s3",
539
- async listObjects(prefix) {
540
- const objects = [];
541
- let continuationToken;
542
- do {
543
- const response = await client.send(new ListObjectsV2Command({
544
- Bucket: bucketName,
545
- ContinuationToken: continuationToken,
546
- Prefix: getListPrefix(prefix)
547
- }));
548
- for (const object of response.Contents ?? []) {
549
- if (!object.Key) continue;
550
- const storageUri = getListedStorageUri(object.Key);
551
- if (storageUri === null) continue;
552
- objects.push({
553
- key: getRelativeKey(object.Key),
554
- lastModifiedAt: object.LastModified,
555
- size: object.Size ?? 0,
556
- storageUri
557
- });
558
- }
559
- continuationToken = response.NextContinuationToken;
560
- } while (continuationToken);
561
- return objects;
562
- },
563
- async deleteObjects(relativeKeys) {
564
- const keys = relativeKeys.map((key) => getStorageKey(key));
565
- for (let offset = 0; offset < keys.length; offset += 1e3) {
566
- const batch = keys.slice(offset, offset + 1e3);
567
- if (batch.length === 0) continue;
568
- const response = await client.send(new DeleteObjectsCommand({
569
- Bucket: bucketName,
570
- Delete: {
571
- Objects: batch.map((Key) => ({ Key })),
572
- Quiet: true
573
- }
574
- }));
575
- if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
576
- }
577
- },
578
- async put({ key, body, contentLength, contentType }) {
579
- const storageKey = getStorageKey(key);
580
- const response = await new Upload({
581
- client,
582
- params: {
583
- Body: body,
584
- Bucket: bucketName,
585
- CacheControl: "max-age=31536000",
586
- ...contentLength === void 0 ? {} : { ContentLength: contentLength },
587
- ContentType: contentType,
588
- Key: storageKey
589
- }
590
- }).done();
591
- if (!response.Bucket || !response.Key) throw new Error("Upload failed");
592
- return { storageUri: createStorageUri({
593
- bucket: bucketName,
594
- key: storageKey,
595
- protocol: "s3"
596
- }) };
597
- },
598
- async get({ storageUri }) {
599
- const { key } = parseAndValidate(storageUri);
600
- try {
601
- const response = await client.send(new GetObjectCommand({
602
- Bucket: bucketName,
603
- Key: key
604
- }));
605
- if (!response.Body) return { response: null };
606
- const headers = new Headers();
607
- if (response.ContentType) headers.set("content-type", response.ContentType);
608
- if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
609
- return { response: new Response(response.Body.transformToWebStream(), { headers }) };
610
- } catch (error) {
611
- if (isObjectNotFoundError(error)) return { response: null };
612
- throw error;
613
- }
614
- },
615
- async getDownloadUrl({ storageUri }) {
616
- const { key } = parseAndValidate(storageUri);
617
- if (getDownloadUrl) return getDownloadUrl({ storageUri });
618
- const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
619
- return { url: await getSignedUrl(client, new GetObjectCommand({
620
- Bucket: bucketName,
621
- Key: key
622
- }), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
623
- },
624
- async exists({ storageUri }) {
625
- const { key } = parseAndValidate(storageUri);
626
- try {
627
- await client.send(new HeadObjectCommand({
628
- Bucket: bucketName,
629
- Key: key
630
- }));
631
- return { exists: true };
632
- } catch (error) {
633
- if (isObjectNotFoundError(error)) return { exists: false };
634
- throw error;
635
- }
636
- },
637
- async delete({ storageUri }) {
638
- const { key } = parseAndValidate(storageUri);
639
- await client.send(new DeleteObjectCommand({
640
- Bucket: bucketName,
641
- Key: key
642
- }));
643
- return { deleted: true };
644
- }
524
+ protocol: "s3"
525
+ }, {
526
+ ...applyS3RuntimeAwsConfig(s3Config),
527
+ credentials,
528
+ bucketName,
529
+ basePath,
530
+ getDownloadUrl
645
531
  });
646
532
  }
647
533
  //#endregion
@@ -666,7 +552,22 @@ const EXPIRED_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set([
666
552
  ]);
667
553
  /** Errors of the SDK finding no credentials, or an expired SSO session. */
668
554
  const NO_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set(["CredentialsProviderError", "TokenProviderError"]);
669
- 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 });
670
571
  const errorName = (error) => error instanceof Error ? error.name : "";
671
572
  const errorMessage = (error) => (error instanceof Error ? error.message : String(error)).replace(/\.+$/u, "");
672
573
  const resolveEndpoint = (endpoint) => {
@@ -681,7 +582,11 @@ const resolveEndpoint = (endpoint) => {
681
582
  "127.0.0.1",
682
583
  "[::1]"
683
584
  ])).has(url.hostname);
684
- 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
+ ]);
685
590
  return url.toString();
686
591
  };
687
592
  /** Masks a value in the CLI's output, which loads `@hot-updater/cli-tools`. */
@@ -708,20 +613,20 @@ const checkCredentials = async (credentials) => {
708
613
  */
709
614
  const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region }) => {
710
615
  const checkSettings = () => {
711
- 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\"."]);
712
617
  if (prefix !== "" && !SECRET_ID_PATTERN.test(prefix)) throw new Error(`${NAME}'s prefix takes letters, digits, and /_+=.@- only.`);
713
618
  };
714
619
  const secretId = (name) => {
715
620
  checkSettings();
716
621
  const id = `${prefix}${name}`;
717
- 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.`]);
718
623
  return id;
719
624
  };
720
625
  const resolveCredentials = () => {
721
626
  if (credentials !== void 0) return async () => checkCredentials(typeof credentials === "function" ? await credentials() : credentials);
722
627
  const accessKeyId = process.env["AWS_ACCESS_KEY_ID"];
723
628
  const secretAccessKey = process.env["AWS_SECRET_ACCESS_KEY"];
724
- 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."]);
725
630
  };
726
631
  let opened;
727
632
  const open = () => {
@@ -744,12 +649,40 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
744
649
  const describeFailure = (verb, id, error, permissions) => {
745
650
  const name = errorName(error);
746
651
  const status = error?.$metadata?.httpStatusCode;
747
- const detail = `${name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}`;
748
- 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 });
749
- 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 });
750
- 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 });
751
- 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 });
752
- 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);
753
686
  };
754
687
  const read = async ({ sdk, client }, id) => {
755
688
  let output;
@@ -779,8 +712,8 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
779
712
  },
780
713
  set: async (name, value) => {
781
714
  const id = secretId(name);
782
- if (typeof value !== "string" || value === "") throw new Error(`${NAME} cannot store an empty ${id}.`);
783
- 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.`]);
784
717
  await mask([value]);
785
718
  const { sdk, client } = await open();
786
719
  const token = randomUUID();
@@ -6579,10 +6579,30 @@ async apply() {
6579
6579
  };
6580
6580
  };
6581
6581
  //#endregion
6582
+ //#region src/lazyCredentials.ts
6583
+ /**
6584
+ * `credentials` as the AWS SDK takes them: access keys with a lazy field
6585
+ * become a provider, which the SDK calls once, on its first request.
6586
+ */
6587
+ const toSdkCredentials = (credentials) => {
6588
+ if (credentials === void 0 || typeof credentials === "function") return credentials;
6589
+ const { accessKeyId, secretAccessKey, sessionToken, ...rest } = credentials;
6590
+ if (typeof accessKeyId !== "function" && typeof secretAccessKey !== "function" && typeof sessionToken !== "function") return credentials;
6591
+ return async () => ({
6592
+ ...rest,
6593
+ accessKeyId: await (0, _hot_updater_plugin_core.resolveLazy)(accessKeyId),
6594
+ secretAccessKey: await (0, _hot_updater_plugin_core.resolveLazy)(secretAccessKey),
6595
+ ...sessionToken === void 0 ? {} : { sessionToken: await (0, _hot_updater_plugin_core.resolveLazy)(sessionToken) }
6596
+ });
6597
+ };
6598
+ //#endregion
6582
6599
  //#region src/dynamoDB.ts
6583
6600
  /** The DynamoDB client ignores the CloudFront and batching settings. */
6584
- const adapterOf = ({ tableName, ...clientConfig }) => {
6585
- const client = new _aws_sdk_client_dynamodb.DynamoDBClient(clientConfig);
6601
+ const adapterOf = ({ tableName, credentials, ...clientConfig }) => {
6602
+ const client = new _aws_sdk_client_dynamodb.DynamoDBClient({
6603
+ ...clientConfig,
6604
+ credentials: toSdkCredentials(credentials)
6605
+ });
6586
6606
  const store = createDynamoDBStore({
6587
6607
  client,
6588
6608
  tableName
@@ -6617,7 +6637,10 @@ const marked = (target) => ({ [DATABASE_TARGET]: target });
6617
6637
  */
6618
6638
  const dynamoDB = (config) => {
6619
6639
  const { client, adapter } = adapterOf(config);
6620
- const cloudFront = createUpdateRouteInvalidation(config);
6640
+ const cloudFront = createUpdateRouteInvalidation({
6641
+ ...config,
6642
+ credentials: toSdkCredentials(config.credentials)
6643
+ });
6621
6644
  return {
6622
6645
  ...(0, _hot_updater_plugin_core.createEngineDatabase)({
6623
6646
  name: "dynamoDB",
@@ -8419,10 +8442,11 @@ var SSMKeyPairManager = class {
8419
8442
  //#region iac/templates.ts
8420
8443
  /**
8421
8444
  * The config init writes. With the `resources` init set up, it names them,
8422
- * and an account's access keys are read with `secretEnv`, from the
8445
+ * and an account's access keys are read with `secret()`, from the
8423
8446
  * environment and `.env.hotupdater`, then the config's `secrets` adapter.
8424
- * Without them, as the agent scaffold writes it, it reads every value from
8425
- * the environment. A profile or SSO sign-in is each person's own.
8447
+ * Without them, as the agent scaffold writes it before the agent creates the
8448
+ * resources, it reads their names from the environment, where the agent
8449
+ * records them. A profile or SSO sign-in is each person's own.
8426
8450
  */
8427
8451
  const getConfigScaffold = (build, authMode, resources) => {
8428
8452
  const value = (envKey, resolved) => resolved === void 0 ? `process.env.${envKey}!` : JSON.stringify(resolved);
@@ -8478,31 +8502,19 @@ const awsOptions = {
8478
8502
  };`.trim()
8479
8503
  }];
8480
8504
  break;
8481
- case "account":
8482
- helperStatements = [{
8483
- name: "awsOptions",
8484
- strategy: "merge-object",
8485
- replaceIncompatibleProperties: ["credentials"],
8486
- code: resources === void 0 ? `
8505
+ case "account": helperStatements = [{
8506
+ name: "awsOptions",
8507
+ strategy: "merge-object",
8508
+ replaceIncompatibleProperties: ["credentials"],
8509
+ code: `
8487
8510
  const awsOptions = {
8488
8511
  region: ${region},
8489
8512
  credentials: {
8490
- accessKeyId: process.env.HOT_UPDATER_S3_ACCESS_KEY_ID!,
8491
- secretAccessKey: process.env.HOT_UPDATER_S3_SECRET_ACCESS_KEY!,
8492
- },
8493
- };`.trim() : `
8494
- const awsOptions = {
8495
- region: ${region},
8496
- credentials: async () => {
8497
- const env = await deploySecrets();
8498
- return {
8499
- accessKeyId: env.HOT_UPDATER_S3_ACCESS_KEY_ID,
8500
- secretAccessKey: env.HOT_UPDATER_S3_SECRET_ACCESS_KEY,
8501
- };
8513
+ accessKeyId: secret("HOT_UPDATER_S3_ACCESS_KEY_ID"),
8514
+ secretAccessKey: secret("HOT_UPDATER_S3_SECRET_ACCESS_KEY"),
8502
8515
  },
8503
8516
  };`.trim()
8504
- }];
8505
- if (resources !== void 0) helperStatements.unshift((0, _hot_updater_cli_tools.secretEnvHelper)("deploySecrets", ["HOT_UPDATER_S3_ACCESS_KEY_ID", "HOT_UPDATER_S3_SECRET_ACCESS_KEY"]));
8517
+ }];
8506
8518
  }
8507
8519
  const builder = new _hot_updater_cli_tools.ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setPlugins({
8508
8520
  imports: [{
@@ -8528,9 +8540,9 @@ const awsOptions = {
8528
8540
  named: [authMode.profile ? "fromIni" : "fromNodeProviderChain"]
8529
8541
  });
8530
8542
  break;
8531
- case "account": if (resources !== void 0) builder.addImport({
8543
+ case "account": builder.addImport({
8532
8544
  pkg: "hot-updater",
8533
- named: ["secretEnv"]
8545
+ named: ["secret"]
8534
8546
  });
8535
8547
  }
8536
8548
  return (0, _hot_updater_cli_tools.createHotUpdaterConfigScaffoldFromBuilder)(builder, { helperStatements });
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { ConfigBuilder, InitError, MissingInitInputsError, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitProviderInputs, colors, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, createZip, ensureInstallPackages, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, printAppSetup, provisionClientCredential, readHotUpdaterInitEnv, resolveInitProviderInput, secretEnvHelper, transformEnv, writeHotUpdaterFiles } from "@hot-updater/cli-tools";
2
+ import { ConfigBuilder, InitError, MissingInitInputsError, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitProviderInputs, colors, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, createZip, ensureInstallPackages, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, printAppSetup, provisionClientCredential, readHotUpdaterInitEnv, resolveInitProviderInput, transformEnv, writeHotUpdaterFiles } from "@hot-updater/cli-tools";
3
3
  import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
@@ -18,7 +18,7 @@ import { finished } from "node:stream/promises";
18
18
  import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
19
19
  import { Buffer as Buffer$1 } from "node:buffer";
20
20
  import { CreateTableCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DynamoDB, DynamoDBClient, UpdateTimeToLiveCommand, waitUntilTableExists } from "@aws-sdk/client-dynamodb";
21
- import { SETTINGS_TABLE, aggregateBatchingTables, createEngineDatabase, createKvAdapter, encodeKvKey, migrateCoreSchema, toolingTargetOf } from "@hot-updater/plugin-core";
21
+ import { SETTINGS_TABLE, aggregateBatchingTables, createEngineDatabase, createKvAdapter, encodeKvKey, migrateCoreSchema, resolveLazy, toolingTargetOf } from "@hot-updater/plugin-core";
22
22
  import { CloudFront, CloudFrontClient, CreateInvalidationCommand, GetInvalidationCommand } from "@aws-sdk/client-cloudfront";
23
23
  import { BatchGetCommand, BatchWriteCommand, DynamoDBDocumentClient, QueryCommand, TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
24
24
  import { fromIni, fromNodeProviderChain, fromSSO } from "@aws-sdk/credential-providers";
@@ -6574,10 +6574,30 @@ async apply() {
6574
6574
  };
6575
6575
  };
6576
6576
  //#endregion
6577
+ //#region src/lazyCredentials.ts
6578
+ /**
6579
+ * `credentials` as the AWS SDK takes them: access keys with a lazy field
6580
+ * become a provider, which the SDK calls once, on its first request.
6581
+ */
6582
+ const toSdkCredentials = (credentials) => {
6583
+ if (credentials === void 0 || typeof credentials === "function") return credentials;
6584
+ const { accessKeyId, secretAccessKey, sessionToken, ...rest } = credentials;
6585
+ if (typeof accessKeyId !== "function" && typeof secretAccessKey !== "function" && typeof sessionToken !== "function") return credentials;
6586
+ return async () => ({
6587
+ ...rest,
6588
+ accessKeyId: await resolveLazy(accessKeyId),
6589
+ secretAccessKey: await resolveLazy(secretAccessKey),
6590
+ ...sessionToken === void 0 ? {} : { sessionToken: await resolveLazy(sessionToken) }
6591
+ });
6592
+ };
6593
+ //#endregion
6577
6594
  //#region src/dynamoDB.ts
6578
6595
  /** The DynamoDB client ignores the CloudFront and batching settings. */
6579
- const adapterOf = ({ tableName, ...clientConfig }) => {
6580
- const client = new DynamoDBClient(clientConfig);
6596
+ const adapterOf = ({ tableName, credentials, ...clientConfig }) => {
6597
+ const client = new DynamoDBClient({
6598
+ ...clientConfig,
6599
+ credentials: toSdkCredentials(credentials)
6600
+ });
6581
6601
  const store = createDynamoDBStore({
6582
6602
  client,
6583
6603
  tableName
@@ -6612,7 +6632,10 @@ const marked = (target) => ({ [DATABASE_TARGET]: target });
6612
6632
  */
6613
6633
  const dynamoDB = (config) => {
6614
6634
  const { client, adapter } = adapterOf(config);
6615
- const cloudFront = createUpdateRouteInvalidation(config);
6635
+ const cloudFront = createUpdateRouteInvalidation({
6636
+ ...config,
6637
+ credentials: toSdkCredentials(config.credentials)
6638
+ });
6616
6639
  return {
6617
6640
  ...createEngineDatabase({
6618
6641
  name: "dynamoDB",
@@ -8414,10 +8437,11 @@ var SSMKeyPairManager = class {
8414
8437
  //#region iac/templates.ts
8415
8438
  /**
8416
8439
  * The config init writes. With the `resources` init set up, it names them,
8417
- * and an account's access keys are read with `secretEnv`, from the
8440
+ * and an account's access keys are read with `secret()`, from the
8418
8441
  * environment and `.env.hotupdater`, then the config's `secrets` adapter.
8419
- * Without them, as the agent scaffold writes it, it reads every value from
8420
- * the environment. A profile or SSO sign-in is each person's own.
8442
+ * Without them, as the agent scaffold writes it before the agent creates the
8443
+ * resources, it reads their names from the environment, where the agent
8444
+ * records them. A profile or SSO sign-in is each person's own.
8421
8445
  */
8422
8446
  const getConfigScaffold = (build, authMode, resources) => {
8423
8447
  const value = (envKey, resolved) => resolved === void 0 ? `process.env.${envKey}!` : JSON.stringify(resolved);
@@ -8473,31 +8497,19 @@ const awsOptions = {
8473
8497
  };`.trim()
8474
8498
  }];
8475
8499
  break;
8476
- case "account":
8477
- helperStatements = [{
8478
- name: "awsOptions",
8479
- strategy: "merge-object",
8480
- replaceIncompatibleProperties: ["credentials"],
8481
- code: resources === void 0 ? `
8500
+ case "account": helperStatements = [{
8501
+ name: "awsOptions",
8502
+ strategy: "merge-object",
8503
+ replaceIncompatibleProperties: ["credentials"],
8504
+ code: `
8482
8505
  const awsOptions = {
8483
8506
  region: ${region},
8484
8507
  credentials: {
8485
- accessKeyId: process.env.HOT_UPDATER_S3_ACCESS_KEY_ID!,
8486
- secretAccessKey: process.env.HOT_UPDATER_S3_SECRET_ACCESS_KEY!,
8487
- },
8488
- };`.trim() : `
8489
- const awsOptions = {
8490
- region: ${region},
8491
- credentials: async () => {
8492
- const env = await deploySecrets();
8493
- return {
8494
- accessKeyId: env.HOT_UPDATER_S3_ACCESS_KEY_ID,
8495
- secretAccessKey: env.HOT_UPDATER_S3_SECRET_ACCESS_KEY,
8496
- };
8508
+ accessKeyId: secret("HOT_UPDATER_S3_ACCESS_KEY_ID"),
8509
+ secretAccessKey: secret("HOT_UPDATER_S3_SECRET_ACCESS_KEY"),
8497
8510
  },
8498
8511
  };`.trim()
8499
- }];
8500
- if (resources !== void 0) helperStatements.unshift(secretEnvHelper("deploySecrets", ["HOT_UPDATER_S3_ACCESS_KEY_ID", "HOT_UPDATER_S3_SECRET_ACCESS_KEY"]));
8512
+ }];
8501
8513
  }
8502
8514
  const builder = new ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setPlugins({
8503
8515
  imports: [{
@@ -8523,9 +8535,9 @@ const awsOptions = {
8523
8535
  named: [authMode.profile ? "fromIni" : "fromNodeProviderChain"]
8524
8536
  });
8525
8537
  break;
8526
- case "account": if (resources !== void 0) builder.addImport({
8538
+ case "account": builder.addImport({
8527
8539
  pkg: "hot-updater",
8528
- named: ["secretEnv"]
8540
+ named: ["secret"]
8529
8541
  });
8530
8542
  }
8531
8543
  return createHotUpdaterConfigScaffoldFromBuilder(builder, { helperStatements });