@hot-updater/aws 0.35.8 → 0.35.10

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.
@@ -1,6 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { fromIni, fromNodeProviderChain, fromSSO } from "@aws-sdk/credential-providers";
3
- import { ConfigBuilder, colors, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, createZip, ensureInstallPackages, getCwd, link, makeEnv, p, transformEnv, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
2
+ import { ConfigBuilder, MissingInitInputsError, assertInitProviderInputs, colors, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, createZip, ensureInstallPackages, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, readHotUpdaterInitEnv, resolveInitProviderInput, transformEnv, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
4
3
  import { fileURLToPath } from "node:url";
5
4
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
6
5
  import { StringDecoder } from "node:string_decoder";
@@ -17,6 +16,7 @@ import { appendFileSync, createReadStream, createWriteStream, readFileSync, stat
17
16
  import { finished } from "node:stream/promises";
18
17
  import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
19
18
  import { Buffer as Buffer$1 } from "node:buffer";
19
+ import { fromIni, fromNodeProviderChain, fromSSO } from "@aws-sdk/credential-providers";
20
20
  import crypto from "crypto";
21
21
  import { CloudFront } from "@aws-sdk/client-cloudfront";
22
22
  import { IAM } from "@aws-sdk/client-iam";
@@ -6209,6 +6209,380 @@ createExeca(mapNode);
6209
6209
  createExeca(mapScriptAsync, {}, deepScriptOptions, setScriptSync);
6210
6210
  const { sendMessage, getOneMessage, getEachMessage, getCancelSignal } = getIpcExport();
6211
6211
  //#endregion
6212
+ //#region iac/init/index.ts
6213
+ const AWS_AUTH_MODES = [
6214
+ "local-session",
6215
+ "shared-profile",
6216
+ "sso",
6217
+ "account"
6218
+ ];
6219
+ const isAwsAuthMode = (value) => value !== void 0 && AWS_AUTH_MODES.some((mode) => mode === value);
6220
+ const AWS_REGION_VALUES = [
6221
+ "af-south-1",
6222
+ "ap-east-1",
6223
+ "ap-east-2",
6224
+ "ap-northeast-1",
6225
+ "ap-northeast-2",
6226
+ "ap-northeast-3",
6227
+ "ap-south-1",
6228
+ "ap-south-2",
6229
+ "ap-southeast-1",
6230
+ "ap-southeast-2",
6231
+ "ap-southeast-3",
6232
+ "ap-southeast-4",
6233
+ "ap-southeast-5",
6234
+ "ap-southeast-6",
6235
+ "ap-southeast-7",
6236
+ "ca-central-1",
6237
+ "ca-west-1",
6238
+ "cn-north-1",
6239
+ "cn-northwest-1",
6240
+ "eu-central-1",
6241
+ "eu-central-2",
6242
+ "eu-north-1",
6243
+ "eu-south-1",
6244
+ "eu-south-2",
6245
+ "eu-west-1",
6246
+ "eu-west-2",
6247
+ "eu-west-3",
6248
+ "il-central-1",
6249
+ "me-central-1",
6250
+ "me-south-1",
6251
+ "mx-central-1",
6252
+ "sa-east-1",
6253
+ "us-east-1",
6254
+ "us-east-2",
6255
+ "us-gov-east-1",
6256
+ "us-gov-west-1",
6257
+ "us-west-1",
6258
+ "us-west-2"
6259
+ ];
6260
+ const isAwsRegionValue = (value) => value !== void 0 && AWS_REGION_VALUES.some((region) => region === value);
6261
+ const initProvider = {
6262
+ label: "AWS S3 + Lambda@Edge",
6263
+ inputs: {
6264
+ authMode: {
6265
+ envKey: "HOT_UPDATER_AWS_AUTH_MODE",
6266
+ help: "AWS authentication mode: local-session, shared-profile, sso, or account",
6267
+ prompt: {
6268
+ message: "Select the mode to login to AWS",
6269
+ type: "select"
6270
+ },
6271
+ validate: isAwsAuthMode
6272
+ },
6273
+ profile: {
6274
+ envKey: "HOT_UPDATER_AWS_PROFILE",
6275
+ help: "AWS profile name",
6276
+ requiredWhen: (inputs) => inputs.authMode === "shared-profile" || inputs.authMode === "sso",
6277
+ requirementHint: "required for shared-profile and sso auth",
6278
+ prompt: {
6279
+ defaultValue: "default",
6280
+ message: "Enter the AWS profile name",
6281
+ placeholder: "default",
6282
+ type: "text"
6283
+ }
6284
+ },
6285
+ accessKeyId: {
6286
+ envKey: "HOT_UPDATER_S3_ACCESS_KEY_ID",
6287
+ help: "AWS access key ID",
6288
+ persistence: "with-consent",
6289
+ requiredWhen: (inputs) => inputs.authMode === "account",
6290
+ requirementHint: "required for account auth",
6291
+ prompt: {
6292
+ message: "Enter your AWS Access Key ID",
6293
+ placeholder: "AKIA...",
6294
+ type: "text"
6295
+ }
6296
+ },
6297
+ secretAccessKey: {
6298
+ envKey: "HOT_UPDATER_S3_SECRET_ACCESS_KEY",
6299
+ help: "AWS secret access key",
6300
+ persistence: "with-consent",
6301
+ requiredWhen: (inputs) => inputs.authMode === "account",
6302
+ requirementHint: "required for account auth",
6303
+ prompt: {
6304
+ message: "Enter your AWS Secret Access Key",
6305
+ type: "password"
6306
+ }
6307
+ },
6308
+ bucketName: {
6309
+ envKey: "HOT_UPDATER_S3_BUCKET_NAME",
6310
+ help: "S3 bucket name",
6311
+ prompt: {
6312
+ defaultValue: "hot-updater-storage",
6313
+ message: "Enter the name of the new S3 Bucket",
6314
+ placeholder: "hot-updater-storage",
6315
+ type: "text"
6316
+ }
6317
+ },
6318
+ bucketRegion: {
6319
+ envKey: "HOT_UPDATER_S3_REGION",
6320
+ help: "S3 bucket region",
6321
+ prompt: {
6322
+ message: "Enter AWS region for the S3 bucket",
6323
+ type: "select"
6324
+ },
6325
+ validate: isAwsRegionValue
6326
+ },
6327
+ lambdaName: {
6328
+ envKey: "HOT_UPDATER_AWS_LAMBDA_NAME",
6329
+ help: "Lambda@Edge function name",
6330
+ prompt: {
6331
+ defaultValue: "hot-updater-edge",
6332
+ message: "Enter the name of the Lambda@Edge function",
6333
+ placeholder: "hot-updater-edge",
6334
+ type: "text"
6335
+ }
6336
+ },
6337
+ distributionId: {
6338
+ envKey: "HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID",
6339
+ help: "Existing CloudFront distribution ID",
6340
+ optional: true
6341
+ },
6342
+ migrationApproved: {
6343
+ envKey: "HOT_UPDATER_AWS_MIGRATION_APPROVED",
6344
+ help: "Allow pending Hot Updater S3 migrations (true)",
6345
+ optional: true,
6346
+ prompt: {
6347
+ message: "Apply pending Hot Updater S3 migrations during init and future infrastructure updates?",
6348
+ type: "confirm"
6349
+ },
6350
+ validate: (value) => value === "true"
6351
+ }
6352
+ }
6353
+ };
6354
+ //#endregion
6355
+ //#region iac/regionLocationMap.ts
6356
+ const regionLocationMap = {
6357
+ "af-south-1": "Cape Town, South Africa",
6358
+ "ap-east-1": "Hong Kong",
6359
+ "ap-east-2": "Taipei, Taiwan",
6360
+ "ap-northeast-1": "Tokyo, Japan",
6361
+ "ap-northeast-2": "Seoul, South Korea",
6362
+ "ap-northeast-3": "Osaka, Japan",
6363
+ "ap-south-1": "Mumbai, India",
6364
+ "ap-south-2": "Hyderabad, India",
6365
+ "ap-southeast-1": "Singapore",
6366
+ "ap-southeast-2": "Sydney, Australia",
6367
+ "ap-southeast-3": "Jakarta, Indonesia",
6368
+ "ap-southeast-4": "Melbourne, Australia",
6369
+ "ap-southeast-5": "Auckland, New Zealand",
6370
+ "ap-southeast-6": "New Zealand",
6371
+ "ap-southeast-7": "Thailand",
6372
+ "ca-central-1": "Montreal, Canada",
6373
+ "ca-west-1": "Calgary, Canada",
6374
+ "cn-north-1": "Beijing, China",
6375
+ "cn-northwest-1": "Ningxia, China",
6376
+ "eu-central-1": "Frankfurt, Germany",
6377
+ "eu-north-1": "Stockholm, Sweden",
6378
+ "eu-south-1": "Milan, Italy",
6379
+ "eu-south-2": "Spain",
6380
+ "eu-west-1": "Ireland",
6381
+ "eu-west-2": "London, UK",
6382
+ "eu-west-3": "Paris, France",
6383
+ "il-central-1": "Tel Aviv, Israel",
6384
+ "me-central-1": "UAE",
6385
+ "me-south-1": "Bahrain",
6386
+ "mx-central-1": "Mexico",
6387
+ "sa-east-1": "São Paulo, Brazil",
6388
+ "us-east-1": "Virginia, USA",
6389
+ "us-east-2": "Ohio, USA",
6390
+ "us-gov-east-1": "US Gov East (Virginia, USA)",
6391
+ "us-gov-west-1": "US Gov West (Oregon, USA)",
6392
+ "us-west-1": "California, USA",
6393
+ "us-west-2": "Oregon, USA",
6394
+ "eu-central-2": "Zurich, Switzerland"
6395
+ };
6396
+ //#endregion
6397
+ //#region iac/awsInitInputs.ts
6398
+ const resolveAwsInitInputs = (existingEnv) => {
6399
+ const { inputs } = initProvider;
6400
+ const savedAuthMode = resolveInitProviderInput(existingEnv, inputs.authMode);
6401
+ return {
6402
+ accessKeyId: resolveInitProviderInput(existingEnv, inputs.accessKeyId),
6403
+ authMode: isAwsAuthMode(savedAuthMode) ? savedAuthMode : void 0,
6404
+ bucketName: resolveInitProviderInput(existingEnv, inputs.bucketName),
6405
+ bucketRegion: resolveInitProviderInput(existingEnv, inputs.bucketRegion),
6406
+ distributionId: resolveInitProviderInput(existingEnv, inputs.distributionId),
6407
+ lambdaName: resolveInitProviderInput(existingEnv, inputs.lambdaName),
6408
+ migrationApproved: resolveInitProviderInput(existingEnv, inputs.migrationApproved),
6409
+ profile: resolveInitProviderInput(existingEnv, inputs.profile),
6410
+ secretAccessKey: resolveInitProviderInput(existingEnv, inputs.secretAccessKey)
6411
+ };
6412
+ };
6413
+ const assertAwsNonInteractiveInputs = (inputs, nonInteractive) => {
6414
+ assertInitProviderInputs({
6415
+ inputs: {
6416
+ ...inputs,
6417
+ bucketRegion: inputs.bucketRegion && Object.hasOwn(regionLocationMap, inputs.bucketRegion) ? inputs.bucketRegion : void 0
6418
+ },
6419
+ provider: initProvider,
6420
+ strict: nonInteractive
6421
+ });
6422
+ };
6423
+ //#endregion
6424
+ //#region iac/awsAuth.ts
6425
+ const exitWithCredentialError = (error) => {
6426
+ if (error instanceof ExecaError) p.log.error(error.stdout || error.stderr || error.message);
6427
+ else p.log.error(error.message);
6428
+ process.exit(1);
6429
+ };
6430
+ const resolveAwsAuth = async (existingEnv, nonInteractive = false) => {
6431
+ const inputDefinitions = initProvider.inputs;
6432
+ const savedMode = resolveInitProviderInput(existingEnv, inputDefinitions.authMode);
6433
+ const mode = isAwsAuthMode(savedMode) ? savedMode : void 0;
6434
+ const savedProfile = resolveInitProviderInput(existingEnv, inputDefinitions.profile);
6435
+ const savedAccessKeyId = resolveInitProviderInput(existingEnv, inputDefinitions.accessKeyId);
6436
+ const savedSecretAccessKey = resolveInitProviderInput(existingEnv, inputDefinitions.secretAccessKey);
6437
+ if (nonInteractive && mode === "account" && savedAccessKeyId && savedSecretAccessKey) return {
6438
+ awsProfile: null,
6439
+ configAuthMode: { mode: "account" },
6440
+ credentials: {
6441
+ accessKeyId: savedAccessKeyId,
6442
+ secretAccessKey: savedSecretAccessKey
6443
+ },
6444
+ mode
6445
+ };
6446
+ const inputs = await p.group({
6447
+ mode: () => nonInteractive && mode ? Promise.resolve(mode) : p.select({
6448
+ initialValue: mode,
6449
+ message: inputDefinitions.authMode.prompt.message,
6450
+ options: [
6451
+ {
6452
+ label: "Current AWS CLI Session / Default Credential Chain",
6453
+ value: "local-session"
6454
+ },
6455
+ {
6456
+ label: "Shared AWS Profile",
6457
+ value: "shared-profile"
6458
+ },
6459
+ {
6460
+ label: "AWS SSO Login",
6461
+ value: "sso"
6462
+ },
6463
+ {
6464
+ label: "AWS Access Key ID & Secret Access Key",
6465
+ value: "account"
6466
+ }
6467
+ ]
6468
+ }),
6469
+ profile: ({ results }) => {
6470
+ if (results.mode !== "shared-profile" && results.mode !== "sso") return Promise.resolve(void 0);
6471
+ if (nonInteractive && savedProfile) return Promise.resolve(savedProfile);
6472
+ const prompt = inputDefinitions.profile.prompt;
6473
+ return p.text({
6474
+ ...getInitProviderTextPromptValues(prompt, savedProfile ?? process.env.AWS_PROFILE),
6475
+ message: prompt.message,
6476
+ validate: (value) => (value ?? "").trim() ? void 0 : "AWS profile name is required"
6477
+ });
6478
+ },
6479
+ accessKeyId: ({ results }) => {
6480
+ if (results.mode !== "account") return Promise.resolve(void 0);
6481
+ if (nonInteractive && savedAccessKeyId) return Promise.resolve(savedAccessKeyId);
6482
+ const prompt = inputDefinitions.accessKeyId.prompt;
6483
+ return p.text({
6484
+ ...getInitProviderTextPromptValues(prompt, savedAccessKeyId),
6485
+ message: prompt.message,
6486
+ validate: (value) => value ? void 0 : "Access Key ID is required"
6487
+ });
6488
+ },
6489
+ secretAccessKey: ({ results }) => {
6490
+ if (results.mode !== "account") return Promise.resolve(void 0);
6491
+ if (nonInteractive && savedSecretAccessKey) return Promise.resolve(savedSecretAccessKey);
6492
+ return p.password({
6493
+ message: inputDefinitions.secretAccessKey.prompt.message,
6494
+ validate: (value) => value ? void 0 : "Secret Access Key is required"
6495
+ });
6496
+ }
6497
+ }, { onCancel: () => process.exit(1) });
6498
+ switch (inputs.mode) {
6499
+ case "account":
6500
+ if (!inputs.accessKeyId || !inputs.secretAccessKey) {
6501
+ p.log.error("AWS access key credentials are required.");
6502
+ process.exit(1);
6503
+ }
6504
+ return {
6505
+ awsProfile: null,
6506
+ configAuthMode: { mode: "account" },
6507
+ credentials: {
6508
+ accessKeyId: inputs.accessKeyId,
6509
+ secretAccessKey: inputs.secretAccessKey
6510
+ },
6511
+ mode: inputs.mode
6512
+ };
6513
+ case "local-session": try {
6514
+ return {
6515
+ awsProfile: null,
6516
+ configAuthMode: {
6517
+ mode: "local",
6518
+ profile: null
6519
+ },
6520
+ credentials: await fromNodeProviderChain()(),
6521
+ mode: inputs.mode
6522
+ };
6523
+ } catch (error) {
6524
+ if (error instanceof Error) return exitWithCredentialError(error);
6525
+ throw error;
6526
+ }
6527
+ case "shared-profile": {
6528
+ if (!inputs.profile) {
6529
+ p.log.error("AWS profile is required.");
6530
+ process.exit(1);
6531
+ }
6532
+ const profile = inputs.profile.trim();
6533
+ try {
6534
+ return {
6535
+ awsProfile: profile,
6536
+ configAuthMode: {
6537
+ mode: "local",
6538
+ profile
6539
+ },
6540
+ credentials: await fromIni({ profile })(),
6541
+ mode: inputs.mode
6542
+ };
6543
+ } catch (error) {
6544
+ if (error instanceof Error) return exitWithCredentialError(error);
6545
+ throw error;
6546
+ }
6547
+ }
6548
+ case "sso": {
6549
+ if (!inputs.profile) {
6550
+ p.log.error("AWS SSO profile is required.");
6551
+ process.exit(1);
6552
+ }
6553
+ const profile = inputs.profile.trim();
6554
+ try {
6555
+ let credentials;
6556
+ try {
6557
+ credentials = await fromSSO({ profile })();
6558
+ } catch {
6559
+ if (nonInteractive) throw new MissingInitInputsError(["active AWS SSO session (`aws sso login`)"]);
6560
+ await execa("aws", [
6561
+ "sso",
6562
+ "login",
6563
+ "--profile",
6564
+ profile
6565
+ ], { stdio: "inherit" });
6566
+ credentials = await fromSSO({ profile })();
6567
+ }
6568
+ return {
6569
+ awsProfile: profile,
6570
+ configAuthMode: {
6571
+ mode: "sso",
6572
+ profile
6573
+ },
6574
+ credentials,
6575
+ mode: inputs.mode
6576
+ };
6577
+ } catch (error) {
6578
+ if (error instanceof MissingInitInputsError) throw error;
6579
+ if (error instanceof Error) return exitWithCredentialError(error);
6580
+ throw error;
6581
+ }
6582
+ }
6583
+ }
6584
+ };
6585
+ //#endregion
6212
6586
  //#region ../../node_modules/.pnpm/es-toolkit@1.47.0/node_modules/es-toolkit/dist/_internal/globalThis.mjs
6213
6587
  const globalThis_ = typeof globalThis === "object" && globalThis || typeof window === "object" && window || typeof self === "object" && self || typeof global === "object" && global || (function() {
6214
6588
  return this;
@@ -6591,6 +6965,16 @@ const buildDistributionConfig = (options) => sanitizeDistributionConfig({
6591
6965
  });
6592
6966
  //#endregion
6593
6967
  //#region iac/cloudfrontPagination.ts
6968
+ const collectPaginatedCloudFrontList = async ({ listPage }) => {
6969
+ const items = [];
6970
+ let marker;
6971
+ do {
6972
+ const page = await listPage(marker);
6973
+ items.push(...page.items);
6974
+ marker = page.nextMarker;
6975
+ } while (marker);
6976
+ return items;
6977
+ };
6594
6978
  const findInPaginatedCloudFrontList = async ({ listPage, matches }) => {
6595
6979
  let marker;
6596
6980
  do {
@@ -6670,6 +7054,11 @@ var CloudFrontManager = class {
6670
7054
  region: this.region,
6671
7055
  credentials: this.credentials
6672
7056
  });
7057
+ const selectedDistribution = options.distribution === void 0 ? await this.selectDistribution({
7058
+ bucketName: options.bucketName,
7059
+ distributionId: options.distributionId,
7060
+ nonInteractive: options.nonInteractive
7061
+ }) : options.distribution;
6673
7062
  let oacId;
6674
7063
  if (!options.functionArn.split(":")[4]) throw new Error("Failed to get AWS account ID");
6675
7064
  try {
@@ -6696,29 +7085,6 @@ var CloudFrontManager = class {
6696
7085
  } catch (error) {
6697
7086
  throw new Error(`Failed to get or create shared cache policy: ${error instanceof Error ? error.message : String(error)}`);
6698
7087
  }
6699
- const matchingDistributions = [];
6700
- try {
6701
- const items = (await cloudfrontClient.listDistributions({})).DistributionList?.Items || [];
6702
- for (const dist of items) if ((dist.Origins?.Items || []).some((origin) => origin.DomainName === bucketDomain)) matchingDistributions.push({
6703
- Id: dist.Id,
6704
- DomainName: dist.DomainName
6705
- });
6706
- } catch (error) {
6707
- console.error("Error listing CloudFront distributions:", error);
6708
- }
6709
- let selectedDistribution = null;
6710
- if (matchingDistributions.length === 1) selectedDistribution = matchingDistributions[0];
6711
- else if (matchingDistributions.length > 1) {
6712
- const selectedDistributionStr = await p.select({
6713
- message: "Multiple CloudFront distributions found. Please select one to use:",
6714
- options: matchingDistributions.map((dist) => ({
6715
- value: JSON.stringify(dist),
6716
- label: `${dist.Id} (${dist.DomainName})`
6717
- }))
6718
- });
6719
- if (p.isCancel(selectedDistributionStr)) process.exit(0);
6720
- selectedDistribution = JSON.parse(selectedDistributionStr);
6721
- }
6722
7088
  const newOverrides = buildDistributionConfigOverrides({
6723
7089
  bucketName: options.bucketName,
6724
7090
  bucketDomain,
@@ -6728,6 +7094,7 @@ var CloudFrontManager = class {
6728
7094
  sharedCachePolicyId
6729
7095
  });
6730
7096
  if (selectedDistribution) {
7097
+ await makeEnv({ HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID: selectedDistribution.Id });
6731
7098
  p.log.success(`Existing CloudFront distribution selected. Distribution ID: ${selectedDistribution.Id}.`);
6732
7099
  try {
6733
7100
  const { DistributionConfig, ETag } = await cloudfrontClient.getDistributionConfig({ Id: selectedDistribution.Id });
@@ -6772,6 +7139,7 @@ var CloudFrontManager = class {
6772
7139
  if (!distResp.Distribution?.Id || !distResp.Distribution?.DomainName) throw new Error("Failed to create CloudFront distribution: No ID or DomainName returned");
6773
7140
  const distributionId = distResp.Distribution.Id;
6774
7141
  const distributionDomain = distResp.Distribution.DomainName;
7142
+ await makeEnv({ HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID: distributionId });
6775
7143
  p.log.success(`Created new CloudFront distribution. Distribution ID: ${distributionId}`);
6776
7144
  let retryCount = 0;
6777
7145
  await p.tasks([{
@@ -6797,6 +7165,53 @@ var CloudFrontManager = class {
6797
7165
  throw error;
6798
7166
  }
6799
7167
  }
7168
+ async selectDistribution({ bucketName, distributionId, nonInteractive }) {
7169
+ const bucketDomain = `${bucketName}.s3.${this.region}.amazonaws.com`;
7170
+ const cloudfrontClient = new CloudFront({
7171
+ region: this.region,
7172
+ credentials: this.credentials
7173
+ });
7174
+ const distributions = await collectPaginatedCloudFrontList({ listPage: async (marker) => {
7175
+ const options = marker ? { Marker: marker } : {};
7176
+ const response = await cloudfrontClient.listDistributions(options);
7177
+ return {
7178
+ items: response.DistributionList?.Items ?? [],
7179
+ nextMarker: response.DistributionList?.NextMarker
7180
+ };
7181
+ } });
7182
+ const matchingDistributions = distributions.flatMap((distribution) => {
7183
+ return (distribution.Origins?.Items ?? []).some((origin) => origin.DomainName === bucketDomain) && distribution.Id && distribution.DomainName ? [{
7184
+ Id: distribution.Id,
7185
+ DomainName: distribution.DomainName
7186
+ }] : [];
7187
+ });
7188
+ const savedDistribution = matchingDistributions.find((distribution) => distribution.Id === distributionId);
7189
+ const savedDistributionExists = distributions.some((distribution) => distribution.Id === distributionId);
7190
+ if (nonInteractive && savedDistribution) return savedDistribution;
7191
+ if (distributionId && !savedDistribution) {
7192
+ if (matchingDistributions.length === 0 && !savedDistributionExists) {
7193
+ p.log.warn("Saved CloudFront distribution was not found. A new distribution will be created.");
7194
+ return null;
7195
+ }
7196
+ if (nonInteractive) throw new MissingInitInputsError(["HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID"]);
7197
+ p.log.warn("Saved CloudFront distribution was not found. Select a distribution again.");
7198
+ }
7199
+ if (matchingDistributions.length === 0) return null;
7200
+ if (nonInteractive) {
7201
+ if (!distributionId && matchingDistributions.length === 1) return matchingDistributions[0] ?? null;
7202
+ throw new MissingInitInputsError(["HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID"]);
7203
+ }
7204
+ const selectedDistributionId = await p.select({
7205
+ initialValue: savedDistribution?.Id ?? matchingDistributions[0]?.Id,
7206
+ message: "Select a CloudFront distribution:",
7207
+ options: matchingDistributions.map((distribution) => ({
7208
+ value: distribution.Id,
7209
+ label: `${distribution.Id} (${distribution.DomainName})`
7210
+ }))
7211
+ });
7212
+ if (p.isCancel(selectedDistributionId)) process.exit(0);
7213
+ return matchingDistributions.find((distribution) => distribution.Id === selectedDistributionId) ?? null;
7214
+ }
6800
7215
  };
6801
7216
  //#endregion
6802
7217
  //#region iac/iam.ts
@@ -6893,14 +7308,8 @@ var LambdaEdgeDeployer = class {
6893
7308
  constructor(credentials) {
6894
7309
  this.credentials = credentials;
6895
7310
  }
6896
- async deploy(lambdaRoleArn, config) {
7311
+ async deploy(lambdaRoleArn, lambdaName, config) {
6897
7312
  const cwd = getCwd();
6898
- const lambdaName = await p.text({
6899
- message: "Enter the name of the Lambda@Edge function",
6900
- defaultValue: "hot-updater-edge",
6901
- placeholder: "hot-updater-edge"
6902
- });
6903
- if (p.isCancel(lambdaName)) process.exit(1);
6904
7313
  const lambdaPath = __require.resolve("@hot-updater/aws/lambda");
6905
7314
  const { tmpDir, removeTmpDir } = await copyDirToTmp(path$1.dirname(lambdaPath));
6906
7315
  const indexPath = path$1.join(tmpDir, "index.cjs");
@@ -7297,48 +7706,6 @@ var Migration0001HotUpdater0_18_0 = class extends S3Migration {
7297
7706
  }
7298
7707
  };
7299
7708
  //#endregion
7300
- //#region iac/regionLocationMap.ts
7301
- const regionLocationMap = {
7302
- "af-south-1": "Cape Town, South Africa",
7303
- "ap-east-1": "Hong Kong",
7304
- "ap-east-2": "Taipei, Taiwan",
7305
- "ap-northeast-1": "Tokyo, Japan",
7306
- "ap-northeast-2": "Seoul, South Korea",
7307
- "ap-northeast-3": "Osaka, Japan",
7308
- "ap-south-1": "Mumbai, India",
7309
- "ap-south-2": "Hyderabad, India",
7310
- "ap-southeast-1": "Singapore",
7311
- "ap-southeast-2": "Sydney, Australia",
7312
- "ap-southeast-3": "Jakarta, Indonesia",
7313
- "ap-southeast-4": "Melbourne, Australia",
7314
- "ap-southeast-5": "Auckland, New Zealand",
7315
- "ap-southeast-6": "New Zealand",
7316
- "ap-southeast-7": "Thailand",
7317
- "ca-central-1": "Montreal, Canada",
7318
- "ca-west-1": "Calgary, Canada",
7319
- "cn-north-1": "Beijing, China",
7320
- "cn-northwest-1": "Ningxia, China",
7321
- "eu-central-1": "Frankfurt, Germany",
7322
- "eu-north-1": "Stockholm, Sweden",
7323
- "eu-south-1": "Milan, Italy",
7324
- "eu-south-2": "Spain",
7325
- "eu-west-1": "Ireland",
7326
- "eu-west-2": "London, UK",
7327
- "eu-west-3": "Paris, France",
7328
- "il-central-1": "Tel Aviv, Israel",
7329
- "me-central-1": "UAE",
7330
- "me-south-1": "Bahrain",
7331
- "mx-central-1": "Mexico",
7332
- "sa-east-1": "São Paulo, Brazil",
7333
- "us-east-1": "Virginia, USA",
7334
- "us-east-2": "Ohio, USA",
7335
- "us-gov-east-1": "US Gov East (Virginia, USA)",
7336
- "us-gov-west-1": "US Gov West (Oregon, USA)",
7337
- "us-west-1": "California, USA",
7338
- "us-west-2": "Oregon, USA",
7339
- "eu-central-2": "Zurich, Switzerland"
7340
- };
7341
- //#endregion
7342
7709
  //#region iac/s3.ts
7343
7710
  function normalizeBucketRegion(region) {
7344
7711
  if (region == null) return "us-east-1";
@@ -7356,7 +7723,7 @@ var S3Manager = class {
7356
7723
  credentials: this.credentials
7357
7724
  });
7358
7725
  const buckets = (await s3Client.listBuckets({})).Buckets ?? [];
7359
- return await Promise.all(buckets.filter((bucket) => bucket.Name).map(async (bucket) => {
7726
+ return await Promise.all(buckets.filter((bucket) => typeof bucket.Name === "string").map(async (bucket) => {
7360
7727
  const { LocationConstraint } = await s3Client.getBucketLocation({ Bucket: bucket.Name });
7361
7728
  return {
7362
7729
  name: bucket.Name,
@@ -7374,7 +7741,7 @@ var S3Manager = class {
7374
7741
  });
7375
7742
  p.log.info(`Created S3 bucket: ${bucketName}`);
7376
7743
  }
7377
- async runMigrations({ bucketName, region, migrations }) {
7744
+ async runMigrations({ approved, bucketName, nonInteractive, region, migrations }) {
7378
7745
  const migrator = new S3Migrator({
7379
7746
  s3: new S3({
7380
7747
  region,
@@ -7385,16 +7752,22 @@ var S3Manager = class {
7385
7752
  });
7386
7753
  const { pending } = await migrator.list();
7387
7754
  await migrator.migrate({ dryRun: true });
7388
- if (pending.length > 0) {
7389
- p.log.step("Pending migrations:");
7390
- for (const m of pending) p.log.step(`- ${m.name}`);
7391
- }
7392
- const confirm = await p.confirm({ message: "Do you want to continue?" });
7393
- if (p.isCancel(confirm) || !confirm) {
7394
- p.log.info("Migration cancelled.");
7395
- process.exit(1);
7755
+ if (pending.length === 0) return false;
7756
+ p.log.step("Pending migrations:");
7757
+ for (const migration of pending) p.log.step(`- ${migration.name}`);
7758
+ if (!nonInteractive || !approved) {
7759
+ if (nonInteractive) throw new MissingInitInputsError(["HOT_UPDATER_AWS_MIGRATION_APPROVED"]);
7760
+ const confirm = await p.confirm({
7761
+ initialValue: approved,
7762
+ message: "Do you want to continue?"
7763
+ });
7764
+ if (p.isCancel(confirm) || !confirm) {
7765
+ p.log.info("Migration cancelled.");
7766
+ process.exit(1);
7767
+ }
7396
7768
  }
7397
7769
  await migrator.migrate({ dryRun: false });
7770
+ return true;
7398
7771
  }
7399
7772
  async updateBucketPolicy({ bucketName, region, distributionId, accountId }) {
7400
7773
  const s3Client = new S3({
@@ -7590,118 +7963,34 @@ const checkIfAwsCliInstalled = async () => {
7590
7963
  return false;
7591
7964
  }
7592
7965
  };
7593
- const runInit = async ({ build }) => {
7966
+ const isAwsRegion = (value) => {
7967
+ return value !== void 0 && Object.hasOwn(regionLocationMap, value);
7968
+ };
7969
+ const runInit = async ({ build, envFile }) => {
7970
+ const nonInteractive = envFile !== void 0;
7971
+ const initEnvSources = await readHotUpdaterInitEnv(process.cwd(), envFile);
7972
+ const { managedEnv } = initEnvSources;
7973
+ const providerEnv = getHotUpdaterInitInputEnv(initEnvSources, nonInteractive);
7974
+ const savedInputs = resolveAwsInitInputs(providerEnv);
7975
+ assertAwsNonInteractiveInputs(savedInputs, nonInteractive);
7594
7976
  if (!await checkIfAwsCliInstalled()) {
7595
7977
  p.log.error(`AWS CLI is not installed. Please visit ${link("https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html")} for installation instructions`);
7596
7978
  process.exit(1);
7597
7979
  }
7598
- let credentials;
7599
- let configAuthMode = { mode: "account" };
7600
- let awsProfile = null;
7601
- const mode = await p.select({
7602
- message: "Select the mode to login to AWS",
7603
- options: [
7604
- {
7605
- label: "Current AWS CLI Session / Default Credential Chain",
7606
- value: "local-session"
7607
- },
7608
- {
7609
- label: "Shared AWS Profile",
7610
- value: "shared-profile"
7611
- },
7612
- {
7613
- label: "AWS SSO Login",
7614
- value: "sso"
7615
- },
7616
- {
7617
- label: "AWS Access Key ID & Secret Access Key",
7618
- value: "account"
7619
- }
7620
- ]
7621
- });
7622
- if (p.isCancel(mode)) process.exit(1);
7623
7980
  p.log.message(colors.blue("The following permissions are required:"));
7624
7981
  p.log.message(`${colors.blue("AmazonS3FullAccess")}: Create and read S3 buckets`);
7625
7982
  p.log.message(`${colors.blue("AWSLambda_FullAccess")}: Create and update Lambda functions`);
7626
7983
  p.log.message(`${colors.blue("CloudFrontFullAccess")}: Create and update CloudFront distributions`);
7627
7984
  p.log.message(`${colors.blue("IAMFullAccess")}: Get or create IAM roles for Lambda@Edge`);
7628
7985
  p.log.message(`${colors.blue("AmazonSSMFullAccess")}: Access to SSM Parameters for storing CloudFront key pairs`);
7629
- if (mode === "sso") try {
7630
- const profile = await p.text({
7631
- message: "Enter the SSO profile name",
7632
- defaultValue: "default",
7633
- placeholder: "default"
7634
- });
7635
- if (p.isCancel(profile)) process.exit(1);
7636
- awsProfile = profile;
7637
- configAuthMode = {
7638
- mode: "sso",
7639
- profile
7640
- };
7641
- await execa("aws", [
7642
- "sso",
7643
- "login",
7644
- "--profile",
7645
- profile
7646
- ], {
7647
- stdio: "inherit",
7648
- shell: true
7649
- });
7650
- credentials = await fromSSO({ profile })();
7651
- } catch (error) {
7652
- if (error instanceof ExecaError) p.log.error(error.stdout || error.stderr || error.message);
7653
- process.exit(1);
7654
- }
7655
- else if (mode === "local-session") try {
7656
- configAuthMode = {
7657
- mode: "local",
7658
- profile: null
7659
- };
7660
- credentials = await fromNodeProviderChain()();
7661
- } catch (error) {
7662
- if (error instanceof Error) p.log.error(error.message);
7663
- process.exit(1);
7664
- }
7665
- else if (mode === "shared-profile") try {
7666
- const profile = await p.text({
7667
- message: "Enter the AWS profile name",
7668
- defaultValue: process.env.AWS_PROFILE ?? "default",
7669
- placeholder: process.env.AWS_PROFILE ?? "default",
7670
- validate: (value) => (value ?? "").trim() ? void 0 : "AWS profile name is required"
7671
- });
7672
- if (p.isCancel(profile)) process.exit(1);
7673
- awsProfile = profile.trim();
7674
- configAuthMode = {
7675
- mode: "local",
7676
- profile: awsProfile
7677
- };
7678
- credentials = await fromIni({ profile: awsProfile })();
7679
- } catch (error) {
7680
- if (error instanceof Error) p.log.error(error.message);
7681
- process.exit(1);
7682
- }
7683
- else {
7684
- const creds = await p.group({
7685
- accessKeyId: () => p.text({
7686
- message: "Enter your AWS Access Key ID",
7687
- validate: (value) => value ? void 0 : "Access Key ID is required"
7688
- }),
7689
- secretAccessKey: () => p.password({
7690
- message: "Enter your AWS Secret Access Key",
7691
- validate: (value) => value ? void 0 : "Secret Access Key is required"
7692
- })
7693
- });
7694
- if (p.isCancel(creds)) process.exit(1);
7695
- credentials = {
7696
- accessKeyId: creds.accessKeyId,
7697
- secretAccessKey: creds.secretAccessKey
7698
- };
7699
- configAuthMode = { mode: "account" };
7700
- }
7701
- if (!credentials) {
7702
- p.log.error("Couldn't fetch the credentials.");
7703
- process.exit(1);
7704
- }
7986
+ const { awsProfile, configAuthMode, credentials, mode } = await resolveAwsAuth(providerEnv, nonInteractive);
7987
+ const resolvedAuthInputs = {
7988
+ ...savedInputs,
7989
+ accessKeyId: mode === "account" ? credentials.accessKeyId : savedInputs.accessKeyId,
7990
+ authMode: mode,
7991
+ profile: awsProfile ?? void 0,
7992
+ secretAccessKey: mode === "account" ? credentials.secretAccessKey : savedInputs.secretAccessKey
7993
+ };
7705
7994
  const s3Manager = new S3Manager(credentials);
7706
7995
  let availableBuckets = [];
7707
7996
  try {
@@ -7716,54 +8005,109 @@ const runInit = async ({ build }) => {
7716
8005
  throw e;
7717
8006
  }
7718
8007
  const createKey = `create/${Math.random().toString(36).substring(2, 15)}`;
7719
- let bucketName = await p.select({
7720
- message: "S3 Bucket List",
7721
- options: [...availableBuckets.map((bucket) => ({
7722
- value: bucket.name,
7723
- label: `${bucket.name} (${bucket.region})`
7724
- })), {
7725
- value: createKey,
7726
- label: "Create New S3 Bucket"
7727
- }]
7728
- });
7729
- if (p.isCancel(bucketName)) process.exit(1);
7730
- let bucketRegion = availableBuckets.find((bucket) => bucket.name === bucketName)?.region;
7731
- if (bucketName === createKey) {
7732
- const name = await p.text({
7733
- message: "Enter the name of the new S3 Bucket",
7734
- defaultValue: "hot-updater-storage",
7735
- placeholder: "hot-updater-storage"
7736
- });
7737
- if (p.isCancel(name)) process.exit(1);
7738
- bucketName = name;
7739
- const selectedRegion = await p.select({
7740
- message: "Enter AWS region for the S3 bucket",
8008
+ const savedBucketName = savedInputs.bucketName;
8009
+ const savedBucketRegion = savedInputs.bucketRegion;
8010
+ const existingBucket = availableBuckets.find((bucket) => bucket.name === savedBucketName);
8011
+ const createSavedBucket = nonInteractive && savedBucketName !== void 0 && isAwsRegion(savedBucketRegion) && !existingBucket;
8012
+ if (createSavedBucket && savedInputs.migrationApproved !== "true") throw new MissingInitInputsError(["HOT_UPDATER_AWS_MIGRATION_APPROVED"]);
8013
+ if (savedBucketName && !existingBucket && !createSavedBucket) p.log.warn("Saved S3 bucket was not found. Select a bucket again.");
8014
+ const savedLambdaName = savedInputs.lambdaName;
8015
+ const resourceInputs = await p.group({
8016
+ bucketSelection: () => {
8017
+ if (nonInteractive && existingBucket) return Promise.resolve(existingBucket.name);
8018
+ if (createSavedBucket) return Promise.resolve(createKey);
8019
+ return p.select({
8020
+ initialValue: existingBucket?.name ?? availableBuckets[0]?.name,
8021
+ message: "S3 Bucket List",
8022
+ options: [...availableBuckets.map((bucket) => ({
8023
+ value: bucket.name,
8024
+ label: `${bucket.name} (${bucket.region})`
8025
+ })), {
8026
+ value: createKey,
8027
+ label: "Create New S3 Bucket"
8028
+ }]
8029
+ });
8030
+ },
8031
+ bucketName: ({ results }) => results.bucketSelection === createKey ? createSavedBucket ? Promise.resolve(savedBucketName) : p.text({
8032
+ ...getInitProviderTextPromptValues(initProvider.inputs.bucketName.prompt, savedBucketName),
8033
+ message: initProvider.inputs.bucketName.prompt.message,
8034
+ validate: (value) => value ? void 0 : "S3 bucket name is required"
8035
+ }) : Promise.resolve(results.bucketSelection),
8036
+ bucketRegion: ({ results }) => results.bucketSelection === createKey ? createSavedBucket ? Promise.resolve(savedBucketRegion) : p.select({
8037
+ initialValue: isAwsRegion(savedBucketRegion) ? savedBucketRegion : void 0,
8038
+ message: initProvider.inputs.bucketRegion.prompt.message,
7741
8039
  options: Object.entries(regionLocationMap).map(([region, location]) => ({
7742
8040
  label: `${region} (${location})`,
7743
8041
  value: region
7744
8042
  }))
7745
- });
7746
- if (p.isCancel(selectedRegion)) process.exit(1);
7747
- bucketRegion = selectedRegion;
7748
- await s3Manager.createBucket(bucketName, bucketRegion);
8043
+ }) : Promise.resolve(availableBuckets.find((bucket) => bucket.name === results.bucketSelection)?.region),
8044
+ lambdaName: () => nonInteractive && savedLambdaName ? Promise.resolve(savedLambdaName) : p.text({
8045
+ ...getInitProviderTextPromptValues(initProvider.inputs.lambdaName.prompt, savedLambdaName),
8046
+ message: initProvider.inputs.lambdaName.prompt.message,
8047
+ validate: (value) => value ? void 0 : "Lambda function name is required"
8048
+ })
8049
+ }, { onCancel: () => process.exit(1) });
8050
+ const { bucketName, bucketRegion, lambdaName } = resourceInputs;
8051
+ if (!bucketName || !lambdaName) {
8052
+ p.log.error("AWS resource names are required.");
8053
+ process.exit(1);
7749
8054
  }
7750
- if (!bucketRegion) {
7751
- p.log.error("Failed to get S3 bucket region");
8055
+ if (!isAwsRegion(bucketRegion)) {
8056
+ p.log.error("AWS bucket region is required.");
7752
8057
  process.exit(1);
7753
8058
  }
8059
+ const cloudFrontManager = new CloudFrontManager(bucketRegion, credentials);
8060
+ const selectedDistribution = await cloudFrontManager.selectDistribution({
8061
+ bucketName,
8062
+ distributionId: savedInputs.distributionId,
8063
+ nonInteractive
8064
+ });
8065
+ const resolvedInputs = {
8066
+ ...resolvedAuthInputs,
8067
+ bucketName,
8068
+ bucketRegion,
8069
+ distributionId: selectedDistribution?.Id,
8070
+ lambdaName,
8071
+ migrationApproved: savedInputs.migrationApproved
8072
+ };
8073
+ const initEnv = getInitProviderEnvVars({
8074
+ includeConsentInputs: await confirmInitInputPersistence({
8075
+ existingEnv: managedEnv,
8076
+ inputs: resolvedInputs,
8077
+ nonInteractive,
8078
+ provider: initProvider
8079
+ }),
8080
+ inputs: resolvedInputs,
8081
+ provider: initProvider
8082
+ });
8083
+ const accessKeyEnvKey = initProvider.inputs.accessKeyId.envKey;
8084
+ const secretAccessKeyEnvKey = initProvider.inputs.secretAccessKey.envKey;
8085
+ await makeEnv({
8086
+ ...initEnv,
8087
+ ...initEnv[accessKeyEnvKey] ? { [accessKeyEnvKey]: {
8088
+ comment: "The current key may have excessive permissions. Update it with an S3FullAccess and CloudFrontFullAccess key.",
8089
+ value: initEnv[accessKeyEnvKey]
8090
+ } } : {},
8091
+ ...initEnv[secretAccessKeyEnvKey] ? { [secretAccessKeyEnvKey]: {
8092
+ comment: "The current key may have excessive permissions. Update it with an S3FullAccess and CloudFrontFullAccess key.",
8093
+ value: initEnv[secretAccessKeyEnvKey]
8094
+ } } : {}
8095
+ });
8096
+ if (resourceInputs.bucketSelection === createKey) await s3Manager.createBucket(bucketName, bucketRegion);
7754
8097
  p.log.info(`Selected S3 Bucket: ${bucketName} (${bucketRegion})`);
7755
- await s3Manager.runMigrations({
8098
+ if (await s3Manager.runMigrations({
8099
+ approved: savedInputs.migrationApproved === "true",
7756
8100
  bucketName,
8101
+ nonInteractive,
7757
8102
  region: bucketRegion,
7758
8103
  migrations: [new Migration0001HotUpdater0_13_0(), new Migration0001HotUpdater0_18_0()]
7759
- });
8104
+ }) && savedInputs.migrationApproved !== "true") await makeEnv({ [initProvider.inputs.migrationApproved.envKey]: "true" });
7760
8105
  const lambdaRoleArn = await new IAMManager(bucketRegion, credentials).createOrSelectRole();
7761
8106
  const keyPair = await new SSMKeyPairManager(bucketRegion, credentials).getOrCreateKeyPair(`/hot-updater/${bucketName}/keypair`);
7762
- const cloudFrontManager = new CloudFrontManager(bucketRegion, credentials);
7763
8107
  const { publicKeyId, keyGroupId } = await cloudFrontManager.getOrCreateKeyGroup(keyPair.publicKey);
7764
8108
  const lambdaEdgeDeployer = new LambdaEdgeDeployer(credentials);
7765
8109
  const ssmParameterName = `/hot-updater/${bucketName}/keypair`;
7766
- const { functionArn } = await lambdaEdgeDeployer.deploy(lambdaRoleArn, {
8110
+ const { functionArn } = await lambdaEdgeDeployer.deploy(lambdaRoleArn, lambdaName, {
7767
8111
  bucketName,
7768
8112
  publicKeyId,
7769
8113
  ssmParameterName,
@@ -7772,6 +8116,7 @@ const runInit = async ({ build }) => {
7772
8116
  const { distributionDomain, distributionId } = await cloudFrontManager.createOrUpdateDistribution({
7773
8117
  keyGroupId,
7774
8118
  bucketName,
8119
+ distribution: selectedDistribution,
7775
8120
  functionArn
7776
8121
  });
7777
8122
  const accountId = functionArn.split(":")[4];
@@ -7782,22 +8127,7 @@ const runInit = async ({ build }) => {
7782
8127
  accountId
7783
8128
  });
7784
8129
  const configWriteResult = await writeHotUpdaterConfig(getConfigScaffold(build, configAuthMode));
7785
- await makeEnv({
7786
- HOT_UPDATER_S3_BUCKET_NAME: bucketName,
7787
- HOT_UPDATER_S3_REGION: bucketRegion,
7788
- ...mode === "account" ? {
7789
- HOT_UPDATER_S3_ACCESS_KEY_ID: {
7790
- comment: "The current key may have excessive permissions. Update it with an S3FullAccess and CloudFrontFullAccess key.",
7791
- value: credentials.accessKeyId
7792
- },
7793
- HOT_UPDATER_S3_SECRET_ACCESS_KEY: {
7794
- comment: "The current key may have excessive permissions. Update it with an S3FullAccess and CloudFrontFullAccess key.",
7795
- value: credentials.secretAccessKey
7796
- }
7797
- } : {},
7798
- ...awsProfile !== null ? { HOT_UPDATER_AWS_PROFILE: awsProfile } : {},
7799
- HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID: distributionId
7800
- });
8130
+ await makeEnv({ [initProvider.inputs.distributionId.envKey]: distributionId });
7801
8131
  if (mode === "sso") await ensureInstallPackages({ devDependencies: ["@aws-sdk/credential-provider-sso"] });
7802
8132
  else if (mode === "local-session" || mode === "shared-profile") await ensureInstallPackages({ devDependencies: ["@aws-sdk/credential-providers"] });
7803
8133
  p.log.success("Generated '.env.hotupdater' file with AWS settings.");