@hot-updater/aws 0.36.6 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from "node:module";
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";
2
+ import { ConfigBuilder, InitError, MissingInitInputsError, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitProviderInputs, colors, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, createZip, ensureInstallPackages, formatApiKeyNote, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, readHotUpdaterInitEnv, resolveInitProviderInput, transformEnv, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
3
+ import { provisionApiKey } from "@hot-updater/server";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
5
6
  import { StringDecoder } from "node:string_decoder";
@@ -16,15 +17,19 @@ import { appendFileSync, createReadStream, createWriteStream, readFileSync, stat
16
17
  import { finished } from "node:stream/promises";
17
18
  import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
18
19
  import { Buffer as Buffer$1 } from "node:buffer";
20
+ import { CloudFront, CloudFrontClient, CreateInvalidationCommand, GetInvalidationCommand } from "@aws-sdk/client-cloudfront";
21
+ import { DynamoDB, DynamoDBClient, waitUntilTableExists } from "@aws-sdk/client-dynamodb";
22
+ import { BatchGetCommand, DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, ScanCommand, TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
23
+ import { createDatabasePlugin, isDatabaseMetadataObject } from "@hot-updater/plugin-core";
24
+ import { createDatabasePluginAdapter } from "@hot-updater/plugin-core/internal";
19
25
  import { fromIni, fromNodeProviderChain, fromSSO } from "@aws-sdk/credential-providers";
26
+ import { Lambda } from "@aws-sdk/client-lambda";
20
27
  import crypto from "crypto";
21
- import { CloudFront } from "@aws-sdk/client-cloudfront";
28
+ import { createHash } from "node:crypto";
22
29
  import { IAM } from "@aws-sdk/client-iam";
23
30
  import { STS } from "@aws-sdk/client-sts";
24
31
  import fs from "fs/promises";
25
- import { Lambda } from "@aws-sdk/client-lambda";
26
- import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, S3 } from "@aws-sdk/client-s3";
27
- import { Upload } from "@aws-sdk/lib-storage";
32
+ import { S3 } from "@aws-sdk/client-s3";
28
33
  import { ParameterNotFound, SSM } from "@aws-sdk/client-ssm";
29
34
  //#region \0rolldown/runtime.js
30
35
  var __create = Object.create;
@@ -51,7 +56,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
51
56
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
52
57
  //#endregion
53
58
  //#region ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
54
- function isPlainObject$1(value) {
59
+ function isPlainObject(value) {
55
60
  if (typeof value !== "object" || value === null) return false;
56
61
  const prototype = Object.getPrototypeOf(value);
57
62
  return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in value) && !(Symbol.iterator in value);
@@ -70,13 +75,13 @@ const normalizeFileUrl = (file) => file instanceof URL ? fileURLToPath(file) : f
70
75
  //#region ../../node_modules/.pnpm/execa@9.5.2/node_modules/execa/lib/methods/parameters.js
71
76
  const normalizeParameters = (rawFile, rawArguments = [], rawOptions = {}) => {
72
77
  const filePath = safeNormalizeFileUrl(rawFile, "First argument");
73
- const [commandArguments, options] = isPlainObject$1(rawArguments) ? [[], rawArguments] : [rawArguments, rawOptions];
78
+ const [commandArguments, options] = isPlainObject(rawArguments) ? [[], rawArguments] : [rawArguments, rawOptions];
74
79
  if (!Array.isArray(commandArguments)) throw new TypeError(`Second argument must be either an array of arguments or an options object: ${commandArguments}`);
75
80
  if (commandArguments.some((commandArgument) => typeof commandArgument === "object" && commandArgument !== null)) throw new TypeError(`Second argument must be an array of strings: ${commandArguments}`);
76
81
  const normalizedArguments = commandArguments.map(String);
77
82
  const nullByteArgument = normalizedArguments.find((normalizedArgument) => normalizedArgument.includes("\0"));
78
83
  if (nullByteArgument !== void 0) throw new TypeError(`Arguments cannot contain null bytes ("\\0"): ${nullByteArgument}`);
79
- if (!isPlainObject$1(options)) throw new TypeError(`Last argument must be an options object: ${options}`);
84
+ if (!isPlainObject(options)) throw new TypeError(`Last argument must be an options object: ${options}`);
80
85
  return [
81
86
  filePath,
82
87
  normalizedArguments,
@@ -200,7 +205,7 @@ const parseExpression = (expression) => {
200
205
  const typeOfExpression = typeof expression;
201
206
  if (typeOfExpression === "string") return expression;
202
207
  if (typeOfExpression === "number") return String(expression);
203
- if (isPlainObject$1(expression) && ("stdout" in expression || "isMaxBuffer" in expression)) return getSubprocessResult(expression);
208
+ if (isPlainObject(expression) && ("stdout" in expression || "isMaxBuffer" in expression)) return getSubprocessResult(expression);
204
209
  if (expression instanceof ChildProcess || Object.prototype.toString.call(expression) === "[object Promise]") throw new TypeError("Unexpected subprocess in template expression. Please use ${await subprocess} instead of ${subprocess}.");
205
210
  throw new TypeError(`Unexpected "${typeOfExpression}" in template expression`);
206
211
  };
@@ -236,7 +241,7 @@ const normalizeFdSpecificOption = (options, optionName) => {
236
241
  return addDefaultValue$1(normalizeFdSpecificValue(options[optionName], optionBaseArray, optionName), optionName);
237
242
  };
238
243
  const getStdioLength = ({ stdio }) => Array.isArray(stdio) ? Math.max(stdio.length, STANDARD_STREAMS_ALIASES.length) : STANDARD_STREAMS_ALIASES.length;
239
- const normalizeFdSpecificValue = (optionValue, optionArray, optionName) => isPlainObject$1(optionValue) ? normalizeOptionObject(optionValue, optionArray, optionName) : optionArray.fill(optionValue);
244
+ const normalizeFdSpecificValue = (optionValue, optionArray, optionName) => isPlainObject(optionValue) ? normalizeOptionObject(optionValue, optionArray, optionName) : optionArray.fill(optionValue);
240
245
  const normalizeOptionObject = (optionValue, optionArray, optionName) => {
241
246
  for (const fdName of Object.keys(optionValue).sort(compareFdName)) for (const fdNumber of parseFdName(fdName, optionName, optionArray)) optionArray[fdNumber] = optionValue[fdName];
242
247
  return optionArray;
@@ -3196,10 +3201,10 @@ const checkBooleanOption = (value, optionName) => {
3196
3201
  const isGenerator = (value) => isAsyncGenerator(value) || isSyncGenerator(value);
3197
3202
  const isAsyncGenerator = (value) => Object.prototype.toString.call(value) === "[object AsyncGeneratorFunction]";
3198
3203
  const isSyncGenerator = (value) => Object.prototype.toString.call(value) === "[object GeneratorFunction]";
3199
- const isTransformOptions = (value) => isPlainObject$1(value) && (value.transform !== void 0 || value.final !== void 0);
3204
+ const isTransformOptions = (value) => isPlainObject(value) && (value.transform !== void 0 || value.final !== void 0);
3200
3205
  const isUrl = (value) => Object.prototype.toString.call(value) === "[object URL]";
3201
3206
  const isRegularUrl = (value) => isUrl(value) && value.protocol !== "file:";
3202
- const isFilePathObject = (value) => isPlainObject$1(value) && Object.keys(value).length > 0 && Object.keys(value).every((key) => FILE_PATH_KEYS.has(key)) && isFilePathString(value.file);
3207
+ const isFilePathObject = (value) => isPlainObject(value) && Object.keys(value).length > 0 && Object.keys(value).every((key) => FILE_PATH_KEYS.has(key)) && isFilePathString(value.file);
3203
3208
  const FILE_PATH_KEYS = new Set(["file", "append"]);
3204
3209
  const isFilePathString = (file) => typeof file === "string";
3205
3210
  const isUnknownStdioString = (type, value) => type === "native" && typeof value === "string" && !KNOWN_STDIO_STRINGS.has(value);
@@ -3321,7 +3326,7 @@ const normalizeDuplex = ({ stdioItem, stdioItem: { value: { transform, transform
3321
3326
  };
3322
3327
  };
3323
3328
  const normalizeTransformStream = ({ stdioItem, stdioItem: { value }, index, newTransforms, direction }) => {
3324
- const { transform, objectMode } = isPlainObject$1(value) ? value : { transform: value };
3329
+ const { transform, objectMode } = isPlainObject(value) ? value : { transform: value };
3325
3330
  const { writableObjectMode, readableObjectMode } = getTransformObjectModes(objectMode, index, newTransforms, direction);
3326
3331
  return {
3327
3332
  ...stdioItem,
@@ -3333,7 +3338,7 @@ const normalizeTransformStream = ({ stdioItem, stdioItem: { value }, index, newT
3333
3338
  };
3334
3339
  };
3335
3340
  const normalizeGenerator = ({ stdioItem, stdioItem: { value }, index, newTransforms, direction, encoding }) => {
3336
- const { transform, final, binary: binaryOption = false, preserveNewlines = false, objectMode } = isPlainObject$1(value) ? value : { transform: value };
3341
+ const { transform, final, binary: binaryOption = false, preserveNewlines = false, objectMode } = isPlainObject(value) ? value : { transform: value };
3337
3342
  const binary = binaryOption || BINARY_ENCODINGS.has(encoding);
3338
3343
  const { writableObjectMode, readableObjectMode } = getTransformObjectModes(objectMode, index, newTransforms, direction);
3339
3344
  return {
@@ -5173,7 +5178,7 @@ const unpipeOnSignalAbort = async (unpipeSignal, { sourceStream, mergedStream, f
5173
5178
  //#endregion
5174
5179
  //#region ../../node_modules/.pnpm/execa@9.5.2/node_modules/execa/lib/pipe/setup.js
5175
5180
  const pipeToSubprocess = (sourceInfo, ...pipeArguments) => {
5176
- if (isPlainObject$1(pipeArguments[0])) return pipeToSubprocess.bind(void 0, {
5181
+ if (isPlainObject(pipeArguments[0])) return pipeToSubprocess.bind(void 0, {
5177
5182
  ...sourceInfo,
5178
5183
  boundOptions: {
5179
5184
  ...sourceInfo.boundOptions,
@@ -6107,7 +6112,7 @@ const mergeOptions = (boundOptions, options) => {
6107
6112
  };
6108
6113
  };
6109
6114
  const mergeOption = (optionName, boundOptionValue, optionValue) => {
6110
- if (DEEP_OPTIONS.has(optionName) && isPlainObject$1(boundOptionValue) && isPlainObject$1(optionValue)) return {
6115
+ if (DEEP_OPTIONS.has(optionName) && isPlainObject(boundOptionValue) && isPlainObject(optionValue)) return {
6111
6116
  ...boundOptionValue,
6112
6117
  ...optionValue
6113
6118
  };
@@ -6129,7 +6134,7 @@ const createExeca = (mapArguments, boundOptions, deepOptions, setBoundExeca) =>
6129
6134
  return boundExeca;
6130
6135
  };
6131
6136
  const callBoundExeca = ({ mapArguments, deepOptions = {}, boundOptions = {}, setBoundExeca, createNested }, firstArgument, ...nextArguments) => {
6132
- if (isPlainObject$1(firstArgument)) return createNested(mapArguments, mergeOptions(boundOptions, firstArgument), setBoundExeca);
6137
+ if (isPlainObject(firstArgument)) return createNested(mapArguments, mergeOptions(boundOptions, firstArgument), setBoundExeca);
6133
6138
  const { file, commandArguments, options, isSync } = parseArguments({
6134
6139
  mapArguments,
6135
6140
  firstArgument,
@@ -6209,203 +6214,1977 @@ createExeca(mapNode);
6209
6214
  createExeca(mapScriptAsync, {}, deepScriptOptions, setScriptSync);
6210
6215
  const { sendMessage, getOneMessage, getEachMessage, getCancelSignal } = getIpcExport();
6211
6216
  //#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"
6217
+ //#region src/cloudFrontInvalidation.ts
6218
+ const DEFAULT_POLL_INTERVAL_MS = 2e3;
6219
+ const DEFAULT_TIMEOUT_MS = 300 * 1e3;
6220
+ const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
6221
+ const invalidateCloudFront = async (client, distributionId, paths, options) => {
6222
+ if (paths.length === 0) return;
6223
+ const response = await client.send(new CreateInvalidationCommand({
6224
+ DistributionId: distributionId,
6225
+ InvalidationBatch: {
6226
+ CallerReference: `invalidation-${Date.now()}`,
6227
+ Paths: {
6228
+ Quantity: paths.length,
6229
+ Items: paths.map((path) => encodeURI(path))
6283
6230
  }
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"
6231
+ }
6232
+ }));
6233
+ if (!options.shouldWait || response.Invalidation?.Status === "Completed") return;
6234
+ const invalidationId = response.Invalidation?.Id;
6235
+ if (!invalidationId) throw new Error("CloudFront invalidation response is missing Invalidation.Id");
6236
+ const deadline = Date.now() + DEFAULT_TIMEOUT_MS;
6237
+ while (Date.now() < deadline) {
6238
+ await sleep(DEFAULT_POLL_INTERVAL_MS);
6239
+ if ((await client.send(new GetInvalidationCommand({
6240
+ DistributionId: distributionId,
6241
+ Id: invalidationId
6242
+ }))).Invalidation?.Status === "Completed") return;
6243
+ }
6244
+ throw new Error(`Timed out waiting for CloudFront invalidation ${invalidationId}.`);
6245
+ };
6246
+ //#endregion
6247
+ //#region src/dynamoDB.ts
6248
+ const DYNAMODB_MAX_METADATA_ITEM_BYTES = 8 * 1024;
6249
+ const DYNAMODB_MAX_CATALOG_ITEM_BYTES = 400 * 1024;
6250
+ var DynamoDBMetadataItemSizeError = class extends Error {
6251
+ name = "DynamoDBMetadataItemSizeError";
6252
+ constructor(byteLength) {
6253
+ super(`DynamoDB metadata item is ${byteLength} bytes; maximum is ${DYNAMODB_MAX_METADATA_ITEM_BYTES}`);
6254
+ this.byteLength = byteLength;
6255
+ }
6256
+ };
6257
+ const boundedDynamoDBMetadataItem = (item) => {
6258
+ const byteLength = new TextEncoder().encode(JSON.stringify(item)).byteLength;
6259
+ if (byteLength > 8192) throw new DynamoDBMetadataItemSizeError(byteLength);
6260
+ return item;
6261
+ };
6262
+ var DynamoDBCatalogItemSizeError = class extends Error {
6263
+ name = "DynamoDBCatalogItemSizeError";
6264
+ constructor(byteLength) {
6265
+ super(`DynamoDB catalog item is ${byteLength} bytes; maximum is ${DYNAMODB_MAX_CATALOG_ITEM_BYTES}`);
6266
+ this.byteLength = byteLength;
6267
+ }
6268
+ };
6269
+ const boundedDynamoDBCatalogItem = (item) => {
6270
+ const byteLength = new TextEncoder().encode(JSON.stringify(item)).byteLength;
6271
+ if (byteLength > 409600) throw new DynamoDBCatalogItemSizeError(byteLength);
6272
+ return item;
6273
+ };
6274
+ const DYNAMODB_CHANNEL_PARTITION = "channels";
6275
+ const DYNAMODB_CHANNEL_NAME_PARTITION = "_hot-updater#channel-names";
6276
+ var DynamoDBStoredItemError = class extends Error {
6277
+ name = "DynamoDBStoredItemError";
6278
+ constructor() {
6279
+ super("DynamoDB contains an invalid Hot Updater row");
6280
+ }
6281
+ };
6282
+ const field = (value, name) => Reflect.get(value, name);
6283
+ const isNullableString = (value) => value === null || typeof value === "string";
6284
+ const isByteSize = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
6285
+ const withLegacyArchiveByteSize = (value) => typeof value === "object" && value !== null && field(value, "archive_byte_size") === void 0 ? {
6286
+ ...value,
6287
+ archive_byte_size: 0
6288
+ } : value;
6289
+ const isBundleRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && (field(value, "platform") === "ios" || field(value, "platform") === "android") && typeof field(value, "file_hash") === "string" && isNullableString(field(value, "git_commit_hash")) && typeof field(value, "storage_uri") === "string" && isByteSize(field(value, "archive_byte_size")) && isDatabaseMetadataObject(field(value, "metadata")) && isNullableString(field(value, "manifest_storage_uri")) && isNullableString(field(value, "manifest_file_hash")) && isNullableString(field(value, "asset_base_storage_uri"));
6290
+ const isPatchRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && typeof field(value, "bundle_id") === "string" && typeof field(value, "base_bundle_id") === "string" && typeof field(value, "base_file_hash") === "string" && typeof field(value, "patch_file_hash") === "string" && typeof field(value, "patch_storage_uri") === "string" && isByteSize(field(value, "byte_size")) && typeof field(value, "order_index") === "number";
6291
+ const isChannelRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && typeof field(value, "name") === "string";
6292
+ const isReleaseRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && typeof field(value, "revision") === "number" && typeof field(value, "scope_key") === "string" && typeof field(value, "channel_id") === "string" && (field(value, "platform") === "ios" || field(value, "platform") === "android") && (field(value, "kind") === "BUNDLE" || field(value, "kind") === "EMBEDDED") && isNullableString(field(value, "bundle_id")) && (field(value, "strategy") === "APP_VERSION" || field(value, "strategy") === "FINGERPRINT") && isNullableString(field(value, "target_app_version")) && isNullableString(field(value, "fingerprint_hash")) && typeof field(value, "enabled") === "boolean" && typeof field(value, "should_force_update") === "boolean" && isNullableString(field(value, "message")) && typeof field(value, "rollout_cohort_count") === "number" && Array.isArray(field(value, "target_cohorts")) && field(value, "target_cohorts").every((cohort) => typeof cohort === "string") && (field(value, "operation") === "DEPLOY" || field(value, "operation") === "PROMOTE" || field(value, "operation") === "ROLLBACK") && isNullableString(field(value, "source_release_id")) && typeof field(value, "created_at_ms") === "number" && typeof field(value, "updated_at_ms") === "number";
6293
+ const isReleaseCatalogRow = (value) => typeof value === "object" && value !== null && typeof field(value, "scope_key") === "string" && typeof field(value, "catalog_id") === "string" && (field(value, "strategy") === "APP_VERSION" || field(value, "strategy") === "FINGERPRINT") && typeof field(value, "channel_id") === "string" && typeof field(value, "channel_key") === "string" && (field(value, "platform") === "ios" || field(value, "platform") === "android") && isNullableString(field(value, "fingerprint_hash")) && typeof field(value, "generation") === "number" && typeof field(value, "payload") === "string" && typeof field(value, "catalog_hash") === "string" && typeof field(value, "byte_size") === "number" && typeof field(value, "is_tombstone") === "boolean" && typeof field(value, "updated_at_ms") === "number";
6294
+ const parseDynamoDBItem = (value) => {
6295
+ const pk = value.pk;
6296
+ const sk = value.sk;
6297
+ const version = value.version;
6298
+ const relationCount = value.relation_count;
6299
+ const ownedPatchCount = value.owned_patch_count;
6300
+ const gsi1pk = value.gsi1pk;
6301
+ const gsi1sk = value.gsi1sk;
6302
+ const row = pk === "bundles" ? withLegacyArchiveByteSize(value.row) : value.row;
6303
+ if (typeof sk !== "string" || typeof version !== "number") throw new DynamoDBStoredItemError();
6304
+ if (pk === "channels" && typeof value.reference_count === "number" && isChannelRow(row)) return {
6305
+ pk,
6306
+ sk,
6307
+ version,
6308
+ reference_count: value.reference_count,
6309
+ row
6310
+ };
6311
+ if (typeof pk === "string" && pk.startsWith("release-scope#") && isReleaseRow(row)) return {
6312
+ pk,
6313
+ sk,
6314
+ version,
6315
+ row
6316
+ };
6317
+ if (pk === "release_catalogs" && isReleaseCatalogRow(row)) return {
6318
+ pk,
6319
+ sk,
6320
+ version,
6321
+ row
6322
+ };
6323
+ if (typeof gsi1pk !== "string" || typeof gsi1sk !== "string") throw new DynamoDBStoredItemError();
6324
+ if (pk === "bundles" && isBundleRow(row)) {
6325
+ if (typeof relationCount !== "number" || typeof ownedPatchCount !== "number") throw new DynamoDBStoredItemError();
6326
+ return {
6327
+ pk,
6328
+ sk,
6329
+ version,
6330
+ relation_count: relationCount,
6331
+ owned_patch_count: ownedPatchCount,
6332
+ gsi1pk,
6333
+ gsi1sk,
6334
+ row
6335
+ };
6336
+ }
6337
+ if (pk === "bundle_patches" && isPatchRow(row)) return {
6338
+ pk,
6339
+ sk,
6340
+ version,
6341
+ gsi1pk,
6342
+ gsi1sk,
6343
+ row
6344
+ };
6345
+ throw new DynamoDBStoredItemError();
6346
+ };
6347
+ const bundlePlatformPartition = (row) => `bundle#${row.platform}`;
6348
+ const patchOwnerPartition = (bundleId) => `patch-owner#${bundleId}`;
6349
+ const patchOrderKey = (row) => `${row.order_index.toString().padStart(10, "0")}#${row.id}`;
6350
+ const toDynamoDBBundleItem = (row, version = 1, relationCount = 0, ownedPatchCount = 0) => ({
6351
+ pk: "bundles",
6352
+ sk: row.id,
6353
+ version,
6354
+ relation_count: relationCount,
6355
+ owned_patch_count: ownedPatchCount,
6356
+ gsi1pk: bundlePlatformPartition(row),
6357
+ gsi1sk: row.id,
6358
+ row
6359
+ });
6360
+ const toDynamoDBPatchItem = (row, version = 1) => {
6361
+ const orderKey = patchOrderKey(row);
6362
+ return {
6363
+ pk: "bundle_patches",
6364
+ sk: row.id,
6365
+ version,
6366
+ gsi1pk: patchOwnerPartition(row.bundle_id),
6367
+ gsi1sk: orderKey,
6368
+ row
6369
+ };
6370
+ };
6371
+ const toDynamoDBChannelItem = (row, version = 1, referenceCount = 0) => ({
6372
+ pk: DYNAMODB_CHANNEL_PARTITION,
6373
+ sk: row.id,
6374
+ version,
6375
+ reference_count: referenceCount,
6376
+ row
6377
+ });
6378
+ const releaseScopePartition = (scopeKey) => `release-scope#${scopeKey}`;
6379
+ const toDynamoDBReleaseItem = (row, version = 1) => ({
6380
+ pk: releaseScopePartition(row.scope_key),
6381
+ sk: row.id,
6382
+ version,
6383
+ row
6384
+ });
6385
+ const toDynamoDBReleaseCatalogItem = (row, version = 1) => ({
6386
+ pk: "release_catalogs",
6387
+ sk: row.scope_key,
6388
+ version,
6389
+ row
6390
+ });
6391
+ const itemKey = (model, id) => ({
6392
+ pk: model,
6393
+ sk: id
6394
+ });
6395
+ const exactDynamoDBId = (where) => {
6396
+ if (where?.length !== 1) return void 0;
6397
+ const condition = where[0];
6398
+ if (Reflect.get(condition, "field") !== "id" || (Reflect.get(condition, "operator") ?? "eq") !== "eq" || Reflect.get(condition, "mode") === "insensitive") return;
6399
+ const value = Reflect.get(condition, "value");
6400
+ return typeof value === "string" ? value : void 0;
6401
+ };
6402
+ const exactDynamoDBPatchOwner = (where) => {
6403
+ const owners = exactDynamoDBPatchOwners(where);
6404
+ return owners?.length === 1 ? owners[0] : void 0;
6405
+ };
6406
+ const exactDynamoDBPatchOwners = (where) => {
6407
+ if (!where || where.some((condition) => Reflect.get(condition, "connector") === "OR")) return;
6408
+ const ownerConditions = where.filter((condition) => Reflect.get(condition, "field") === "bundle_id");
6409
+ if (ownerConditions.length !== 1) return void 0;
6410
+ const condition = ownerConditions[0];
6411
+ if (Reflect.get(condition, "mode") === "insensitive") return;
6412
+ const operator = Reflect.get(condition, "operator") ?? "eq";
6413
+ const value = Reflect.get(condition, "value");
6414
+ if (operator === "eq") return typeof value === "string" ? [value] : void 0;
6415
+ if (operator === "in" && Array.isArray(value) && value.every((id) => typeof id === "string")) return value;
6416
+ };
6417
+ const exactDynamoDBBundleIds = (where) => {
6418
+ if (!where || where.some((condition) => Reflect.get(condition, "connector") === "OR")) return;
6419
+ for (const condition of where) {
6420
+ if (Reflect.get(condition, "field") !== "id" || Reflect.get(condition, "mode") === "insensitive") continue;
6421
+ const operator = Reflect.get(condition, "operator") ?? "eq";
6422
+ const value = Reflect.get(condition, "value");
6423
+ if (operator === "eq" && typeof value === "string") return [value];
6424
+ if (operator === "in" && Array.isArray(value) && value.every((id) => typeof id === "string")) return value;
6425
+ }
6426
+ };
6427
+ const dynamoDBBundlePageStart = (where, direction) => {
6428
+ if ((where ?? []).some((condition) => Reflect.get(condition, "connector") === "OR")) return;
6429
+ let result;
6430
+ for (const condition of where ?? []) {
6431
+ if (Reflect.get(condition, "field") !== "id") continue;
6432
+ const operator = Reflect.get(condition, "operator") ?? "eq";
6433
+ const value = Reflect.get(condition, "value");
6434
+ if (!(direction === "asc" ? operator === "gt" || operator === "gte" : operator === "lt" || operator === "lte") || typeof value !== "string") continue;
6435
+ const inclusive = operator === "gte" || operator === "lte";
6436
+ if (result === void 0 || (direction === "asc" ? value > result.id : value < result.id) || value === result.id && !inclusive) result = {
6437
+ id: value,
6438
+ inclusive
6439
+ };
6440
+ }
6441
+ return result;
6442
+ };
6443
+ const parseStoredItem = (item) => parseDynamoDBItem(item.pk === "release_catalogs" ? boundedDynamoDBCatalogItem(item) : boundedDynamoDBMetadataItem(item));
6444
+ const queryItems = async (store, input) => {
6445
+ let exclusiveStartKey;
6446
+ const items = [];
6447
+ do {
6448
+ const sortCondition = input.minimumSortKey ? " AND #sortKey >= :minimumSortKey" : "";
6449
+ const page = await store.client.send(new QueryCommand({
6450
+ TableName: store.tableName,
6451
+ ...input.indexName ? { IndexName: input.indexName } : {},
6452
+ ...input.consistentRead ? { ConsistentRead: true } : {},
6453
+ ExclusiveStartKey: exclusiveStartKey,
6454
+ KeyConditionExpression: `#partitionKey = :partition${sortCondition}`,
6455
+ ExpressionAttributeNames: {
6456
+ "#partitionKey": input.indexName ? "gsi1pk" : "pk",
6457
+ ...input.minimumSortKey ? { "#sortKey": input.indexName ? "gsi1sk" : "sk" } : {}
6458
+ },
6459
+ ExpressionAttributeValues: {
6460
+ ":partition": input.partition,
6461
+ ...input.minimumSortKey ? { ":minimumSortKey": input.minimumSortKey } : {}
6295
6462
  }
6463
+ }));
6464
+ const pageItems = (page.Items ?? []).map(parseStoredItem);
6465
+ items.push(...pageItems);
6466
+ exclusiveStartKey = page.LastEvaluatedKey;
6467
+ } while (exclusiveStartKey !== void 0);
6468
+ return items;
6469
+ };
6470
+ const queryBundleItemsPage = async (store, input) => {
6471
+ if (input.limit === 0) return [];
6472
+ let exclusiveStartKey;
6473
+ const items = [];
6474
+ do {
6475
+ const startOperator = input.start?.inclusive ? input.direction === "asc" ? ">=" : "<=" : input.direction === "asc" ? ">" : "<";
6476
+ const page = await store.client.send(new QueryCommand({
6477
+ TableName: store.tableName,
6478
+ ExclusiveStartKey: exclusiveStartKey,
6479
+ KeyConditionExpression: input.start ? `#pk = :pk AND #sk ${startOperator} :start` : "#pk = :pk",
6480
+ ExpressionAttributeNames: {
6481
+ "#pk": "pk",
6482
+ ...input.start ? { "#sk": "sk" } : {}
6483
+ },
6484
+ ExpressionAttributeValues: {
6485
+ ":pk": "bundles",
6486
+ ...input.start ? { ":start": input.start.id } : {}
6487
+ },
6488
+ Limit: Math.max(input.limit - items.length, 100),
6489
+ ScanIndexForward: input.direction === "asc"
6490
+ }));
6491
+ for (const item of (page.Items ?? []).map(parseStoredItem)) {
6492
+ if (item.pk === "bundles" && input.matches(item.row)) items.push(item);
6493
+ if (items.length === input.limit) return items;
6494
+ }
6495
+ exclusiveStartKey = page.LastEvaluatedKey;
6496
+ } while (exclusiveStartKey !== void 0);
6497
+ return items;
6498
+ };
6499
+ const loadModelItems = (store, model) => queryItems(store, {
6500
+ partition: model,
6501
+ consistentRead: true
6502
+ });
6503
+ const loadModelItem = async (store, model, id) => {
6504
+ const { Item } = await store.client.send(new GetCommand({
6505
+ TableName: store.tableName,
6506
+ Key: itemKey(model, id),
6507
+ ConsistentRead: true
6508
+ }));
6509
+ return Item === void 0 ? void 0 : parseStoredItem(Item);
6510
+ };
6511
+ const loadBundleItem = async (store, id) => {
6512
+ const item = await loadModelItem(store, "bundles", id);
6513
+ return item?.pk === "bundles" ? item : void 0;
6514
+ };
6515
+ const loadPatchItem = async (store, id) => {
6516
+ const item = await loadModelItem(store, "bundle_patches", id);
6517
+ return item?.pk === "bundle_patches" ? item : void 0;
6518
+ };
6519
+ const loadChannelItem = async (store, id) => {
6520
+ const item = await loadModelItem(store, DYNAMODB_CHANNEL_PARTITION, id);
6521
+ return item?.pk === "channels" ? item : void 0;
6522
+ };
6523
+ const loadBundleItems = async (store) => {
6524
+ return (await loadModelItems(store, "bundles")).filter((item) => item.pk === "bundles");
6525
+ };
6526
+ const loadPatchItems = async (store) => {
6527
+ return (await loadModelItems(store, "bundle_patches")).filter((item) => item.pk === "bundle_patches");
6528
+ };
6529
+ const loadChannelItems = async (store) => {
6530
+ return (await loadModelItems(store, DYNAMODB_CHANNEL_PARTITION)).filter((item) => item.pk === DYNAMODB_CHANNEL_PARTITION);
6531
+ };
6532
+ const DYNAMODB_RELEASE_ID_PARTITION = "_hot-updater#release-scope-by-id";
6533
+ const releaseLocatorItem = (row) => ({
6534
+ pk: DYNAMODB_RELEASE_ID_PARTITION,
6535
+ sk: row.id,
6536
+ scope_key: row.scope_key
6537
+ });
6538
+ const loadReleaseItem = async (store, id) => {
6539
+ const locator = await store.client.send(new GetCommand({
6540
+ TableName: store.tableName,
6541
+ Key: {
6542
+ pk: DYNAMODB_RELEASE_ID_PARTITION,
6543
+ sk: id
6296
6544
  },
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
- }
6545
+ ConsistentRead: true
6546
+ }));
6547
+ if (locator.Item === void 0) return void 0;
6548
+ const scopeKey = locator.Item.scope_key;
6549
+ if (typeof scopeKey !== "string") throw new DynamoDBStoredItemError();
6550
+ const result = await store.client.send(new GetCommand({
6551
+ TableName: store.tableName,
6552
+ Key: {
6553
+ pk: releaseScopePartition(scopeKey),
6554
+ sk: id
6307
6555
  },
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
- }
6556
+ ConsistentRead: true
6557
+ }));
6558
+ if (result.Item === void 0) throw new DynamoDBStoredItemError();
6559
+ const item = parseStoredItem(result.Item);
6560
+ if (!isReleaseRow(item.row) || item.sk !== id) throw new DynamoDBStoredItemError();
6561
+ return item;
6562
+ };
6563
+ const loadReleaseItemsByScope = async (store, scopeKey) => (await queryItems(store, {
6564
+ partition: releaseScopePartition(scopeKey),
6565
+ consistentRead: true
6566
+ })).filter((item) => item.pk === releaseScopePartition(scopeKey) && isReleaseRow(item.row));
6567
+ const loadReleaseItems = async (store) => {
6568
+ let exclusiveStartKey;
6569
+ const items = [];
6570
+ do {
6571
+ const page = await store.client.send(new ScanCommand({
6572
+ TableName: store.tableName,
6573
+ ExclusiveStartKey: exclusiveStartKey,
6574
+ ConsistentRead: true,
6575
+ FilterExpression: "begins_with(#pk, :prefix)",
6576
+ ExpressionAttributeNames: { "#pk": "pk" },
6577
+ ExpressionAttributeValues: { ":prefix": "release-scope#" }
6578
+ }));
6579
+ for (const value of page.Items ?? []) {
6580
+ const item = parseStoredItem(value);
6581
+ if (isReleaseRow(item.row)) items.push(item);
6582
+ }
6583
+ exclusiveStartKey = page.LastEvaluatedKey;
6584
+ } while (exclusiveStartKey !== void 0);
6585
+ return items;
6586
+ };
6587
+ const loadReleaseCatalogItem = async (store, scopeKey) => {
6588
+ const result = await store.client.send(new GetCommand({
6589
+ TableName: store.tableName,
6590
+ Key: itemKey("release_catalogs", scopeKey),
6591
+ ConsistentRead: true
6592
+ }));
6593
+ if (result.Item === void 0) return void 0;
6594
+ const item = parseStoredItem(result.Item);
6595
+ return item.pk === "release_catalogs" ? item : void 0;
6596
+ };
6597
+ const loadReleaseCatalogItems = async (store) => (await queryItems(store, {
6598
+ partition: "release_catalogs",
6599
+ consistentRead: true
6600
+ })).filter((item) => item.pk === "release_catalogs");
6601
+ const loadChannelByName = async (store, name) => {
6602
+ const { Item } = await store.client.send(new GetCommand({
6603
+ TableName: store.tableName,
6604
+ Key: {
6605
+ pk: DYNAMODB_CHANNEL_NAME_PARTITION,
6606
+ sk: name
6317
6607
  },
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"
6608
+ ConsistentRead: true
6609
+ }));
6610
+ const id = Item?.channel_id;
6611
+ if (Item === void 0) return void 0;
6612
+ if (typeof id !== "string") throw new DynamoDBStoredItemError();
6613
+ const channel = await loadChannelItem(store, id);
6614
+ if (channel === void 0 || channel.row.name !== name) throw new DynamoDBStoredItemError();
6615
+ return channel;
6616
+ };
6617
+ const insertDynamoDBChannel = async (store, input) => {
6618
+ const existing = await loadChannelByName(store, input.row.name);
6619
+ if (existing !== void 0) return {
6620
+ row: existing.row,
6621
+ inserted: false
6622
+ };
6623
+ const existingId = await loadChannelItem(store, input.row.id);
6624
+ if (existingId !== void 0) {
6625
+ if (existingId.row.name === input.row.name) return {
6626
+ row: existingId.row,
6627
+ inserted: false
6628
+ };
6629
+ throw new DynamoDBStoredItemError();
6630
+ }
6631
+ try {
6632
+ await commitDynamoDBTransaction(store, [{ Put: {
6633
+ TableName: store.tableName,
6634
+ Item: boundedDynamoDBMetadataItem(toDynamoDBChannelItem(input.row)),
6635
+ ConditionExpression: "attribute_not_exists(#pk)",
6636
+ ExpressionAttributeNames: { "#pk": "pk" }
6637
+ } }, { Put: {
6638
+ TableName: store.tableName,
6639
+ Item: {
6640
+ pk: DYNAMODB_CHANNEL_NAME_PARTITION,
6641
+ sk: input.row.name,
6642
+ channel_id: input.row.id
6324
6643
  },
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"
6644
+ ConditionExpression: "attribute_not_exists(#pk)",
6645
+ ExpressionAttributeNames: { "#pk": "pk" }
6646
+ } }]);
6647
+ return {
6648
+ row: input.row,
6649
+ inserted: true
6650
+ };
6651
+ } catch (error) {
6652
+ const canonical = await loadChannelByName(store, input.row.name);
6653
+ if (canonical !== void 0) return {
6654
+ row: canonical.row,
6655
+ inserted: false
6656
+ };
6657
+ throw error;
6658
+ }
6659
+ };
6660
+ const deleteDynamoDBChannel = async (store, input) => {
6661
+ const current = await loadChannelItem(store, input.id);
6662
+ if (current === void 0) return {
6663
+ deleted: false,
6664
+ reason: "not_found"
6665
+ };
6666
+ if (current.reference_count !== 0) return {
6667
+ deleted: false,
6668
+ reason: "not_empty"
6669
+ };
6670
+ try {
6671
+ await commitDynamoDBTransaction(store, [{ Delete: {
6672
+ TableName: store.tableName,
6673
+ Key: itemKey(DYNAMODB_CHANNEL_PARTITION, current.sk),
6674
+ ConditionExpression: "#version = :version AND #referenceCount = :zero",
6675
+ ExpressionAttributeNames: {
6676
+ "#version": "version",
6677
+ "#referenceCount": "reference_count"
6678
+ },
6679
+ ExpressionAttributeValues: {
6680
+ ":version": current.version,
6681
+ ":zero": 0
6335
6682
  }
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"
6683
+ } }, { Delete: {
6684
+ TableName: store.tableName,
6685
+ Key: {
6686
+ pk: DYNAMODB_CHANNEL_NAME_PARTITION,
6687
+ sk: current.row.name
6349
6688
  },
6350
- validate: (value) => value === "true"
6689
+ ConditionExpression: "#channelId = :channelId",
6690
+ ExpressionAttributeNames: { "#channelId": "channel_id" },
6691
+ ExpressionAttributeValues: { ":channelId": current.sk }
6692
+ } }]);
6693
+ return { deleted: true };
6694
+ } catch (error) {
6695
+ const latest = await loadChannelItem(store, input.id);
6696
+ if (latest !== void 0 && latest.reference_count > 0) return {
6697
+ deleted: false,
6698
+ reason: "not_empty"
6699
+ };
6700
+ if (latest === void 0) return {
6701
+ deleted: false,
6702
+ reason: "not_found"
6703
+ };
6704
+ throw error;
6705
+ }
6706
+ };
6707
+ const queryOwnerPatchIds = async (store, indexName, bundleId) => {
6708
+ return (await queryItems(store, {
6709
+ partition: patchOwnerPartition(bundleId),
6710
+ indexName
6711
+ })).filter((item) => item.pk === "bundle_patches").map(({ sk }) => sk);
6712
+ };
6713
+ var DynamoDBTransactionLimitError = class extends Error {
6714
+ name = "DynamoDBTransactionLimitError";
6715
+ constructor(actionCount) {
6716
+ super(`DynamoDB transaction requires ${actionCount} actions; maximum is 100`);
6717
+ this.actionCount = actionCount;
6718
+ }
6719
+ };
6720
+ const metadataUpdate = (store, delta) => {
6721
+ const entries = Object.entries(delta).filter(([, value]) => value !== 0);
6722
+ if (entries.length === 0) return void 0;
6723
+ const names = {};
6724
+ const values = {};
6725
+ const updates = [];
6726
+ const conditions = [];
6727
+ for (const [model, change] of entries) {
6728
+ const name = model === "bundles" ? "bundles" : "patches";
6729
+ const valueName = model === "bundles" ? "bundle" : "patch";
6730
+ names[`#${name}`] = name;
6731
+ values[`:${valueName}Delta`] = change;
6732
+ updates.push(`#${name} :${valueName}Delta`);
6733
+ if (change < 0) {
6734
+ values[`:${valueName}Removal`] = -change;
6735
+ conditions.push(`#${name} >= :${valueName}Removal`);
6351
6736
  }
6352
6737
  }
6738
+ return { Update: {
6739
+ TableName: store.tableName,
6740
+ Key: {
6741
+ pk: "_hot-updater",
6742
+ sk: "limits.metadata"
6743
+ },
6744
+ UpdateExpression: `ADD ${updates.join(", ")}`,
6745
+ ...conditions.length > 0 ? { ConditionExpression: conditions.join(" AND ") } : {},
6746
+ ExpressionAttributeNames: names,
6747
+ ExpressionAttributeValues: values
6748
+ } };
6353
6749
  };
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"
6750
+ const updateBundleRelation = (store, bundleId, delta, ownedPatchDelta = 0) => {
6751
+ const names = {
6752
+ "#pk": "pk",
6753
+ "#version": "version"
6754
+ };
6755
+ const values = { ":one": 1 };
6756
+ const additions = [];
6757
+ let condition = "attribute_exists(#pk)";
6758
+ let update = "SET #version = #version + :one";
6759
+ if (delta !== 0) {
6760
+ names["#relationCount"] = "relation_count";
6761
+ values[":relationDelta"] = delta;
6762
+ additions.push("#relationCount :relationDelta");
6763
+ if (delta < 0) {
6764
+ values[":relationRemoval"] = -delta;
6765
+ condition += " AND #relationCount >= :relationRemoval";
6766
+ }
6767
+ }
6768
+ if (ownedPatchDelta !== 0) {
6769
+ names["#ownedPatchCount"] = "owned_patch_count";
6770
+ values[":ownedPatchDelta"] = ownedPatchDelta;
6771
+ additions.push("#ownedPatchCount :ownedPatchDelta");
6772
+ if (ownedPatchDelta < 0) {
6773
+ values[":ownedPatchRemoval"] = -ownedPatchDelta;
6774
+ condition += " AND #ownedPatchCount >= :ownedPatchRemoval";
6775
+ }
6776
+ }
6777
+ if (additions.length > 0) update += ` ADD ${additions.join(", ")}`;
6778
+ return { Update: {
6779
+ TableName: store.tableName,
6780
+ Key: itemKey("bundles", bundleId),
6781
+ ConditionExpression: condition,
6782
+ ExpressionAttributeNames: names,
6783
+ ExpressionAttributeValues: values,
6784
+ UpdateExpression: update
6785
+ } };
6395
6786
  };
6396
- //#endregion
6397
- //#region iac/awsInitInputs.ts
6398
- const resolveAwsInitInputs = (existingEnv) => {
6399
- const { inputs } = initProvider;
6400
- const savedAuthMode = resolveInitProviderInput(existingEnv, inputs.authMode);
6401
- return {
6787
+ const commitDynamoDBTransaction = async (store, actions) => {
6788
+ if (actions.length > 100) throw new DynamoDBTransactionLimitError(actions.length);
6789
+ await store.client.send(new TransactWriteCommand({ TransactItems: [...actions] }));
6790
+ };
6791
+ const DYNAMODB_BATCH_GET_ATTEMPTS = 5;
6792
+ const DYNAMODB_BATCH_GET_BASE_DELAY_MS = 25;
6793
+ var DynamoDBBatchGetExhaustedError = class extends Error {
6794
+ name = "DynamoDBBatchGetExhaustedError";
6795
+ constructor(unprocessedKeyCount) {
6796
+ super(`DynamoDB did not process ${unprocessedKeyCount} batch-get keys after ${DYNAMODB_BATCH_GET_ATTEMPTS} attempts`);
6797
+ this.unprocessedKeyCount = unprocessedKeyCount;
6798
+ }
6799
+ };
6800
+ const waitBeforeRetry = (attempt) => new Promise((resolve) => setTimeout(resolve, DYNAMODB_BATCH_GET_BASE_DELAY_MS * 2 ** attempt));
6801
+ const batchGetDynamoDBItems = async (store, keys) => {
6802
+ const items = [];
6803
+ for (let offset = 0; offset < keys.length; offset += 100) {
6804
+ let pending = keys.slice(offset, offset + 100);
6805
+ for (let attempt = 0; attempt < DYNAMODB_BATCH_GET_ATTEMPTS && pending.length > 0; attempt++) {
6806
+ const { Responses, UnprocessedKeys } = await store.client.send(new BatchGetCommand({ RequestItems: { [store.tableName]: {
6807
+ ConsistentRead: true,
6808
+ Keys: pending
6809
+ } } }));
6810
+ items.push(...Responses?.[store.tableName] ?? []);
6811
+ pending = UnprocessedKeys?.[store.tableName]?.Keys ?? [];
6812
+ if (pending.length > 0 && attempt + 1 < DYNAMODB_BATCH_GET_ATTEMPTS) await waitBeforeRetry(attempt);
6813
+ }
6814
+ if (pending.length > 0) throw new DynamoDBBatchGetExhaustedError(pending.length);
6815
+ }
6816
+ return items;
6817
+ };
6818
+ const parseKeyReadItem = (item) => parseDynamoDBItem(boundedDynamoDBMetadataItem(item));
6819
+ const loadBundleItemsById = async (store, ids) => {
6820
+ const uniqueIds = [...new Set(ids)];
6821
+ if (uniqueIds.length === 0) return [];
6822
+ return (await batchGetDynamoDBItems(store, uniqueIds.map((id) => itemKey("bundles", id)))).map(parseKeyReadItem).filter((item) => item.pk === "bundles");
6823
+ };
6824
+ const loadPatchItemsById = async (store, ids) => {
6825
+ const uniqueIds = [...new Set(ids)];
6826
+ if (uniqueIds.length === 0) return [];
6827
+ return (await batchGetDynamoDBItems(store, uniqueIds.map((id) => itemKey("bundle_patches", id)))).map(parseKeyReadItem).filter((item) => item.pk === "bundle_patches");
6828
+ };
6829
+ const loadMetadataCount = async (store, model) => {
6830
+ if (model !== "bundles" && model !== "bundle_patches") return void 0;
6831
+ const field = model === "bundles" ? "bundles" : "patches";
6832
+ const { Item } = await store.client.send(new GetCommand({
6833
+ TableName: store.tableName,
6834
+ Key: {
6835
+ pk: "_hot-updater",
6836
+ sk: "limits.metadata"
6837
+ },
6838
+ ConsistentRead: true,
6839
+ ProjectionExpression: "#count",
6840
+ ExpressionAttributeNames: { "#count": field }
6841
+ }));
6842
+ const count = Item?.[field];
6843
+ return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
6844
+ };
6845
+ var DynamoDBPatchIndexConsistencyError = class extends Error {
6846
+ name = "DynamoDBPatchIndexConsistencyError";
6847
+ constructor(bundleId, expectedCount) {
6848
+ super(`DynamoDB patch index did not converge to ${expectedCount} rows for bundle "${bundleId}"`);
6849
+ this.bundleId = bundleId;
6850
+ this.expectedCount = expectedCount;
6851
+ }
6852
+ };
6853
+ const waitForIndex = (attempt) => new Promise((resolve) => setTimeout(resolve, 20 * (attempt + 1)));
6854
+ const queryCompleteOwnerPatches = async (store, indexName, bundleId) => {
6855
+ const owner = await loadBundleItem(store, bundleId);
6856
+ if (!owner || owner.owned_patch_count === 0) return [];
6857
+ for (let attempt = 0; attempt < 3; attempt++) {
6858
+ const patchIds = await queryOwnerPatchIds(store, indexName, bundleId);
6859
+ const patchIdSet = new Set(patchIds);
6860
+ const patches = (await loadPatchItemsById(store, patchIds)).filter(({ row }) => row.bundle_id === bundleId);
6861
+ if (patchIds.length === owner.owned_patch_count && patchIdSet.size === owner.owned_patch_count && patches.length === owner.owned_patch_count && patches.every(({ sk }) => patchIdSet.has(sk))) return patches.map(({ row }) => row).sort((left, right) => left.order_index - right.order_index || left.id.localeCompare(right.id));
6862
+ if (attempt < 2) await waitForIndex(attempt);
6863
+ }
6864
+ throw new DynamoDBPatchIndexConsistencyError(bundleId, owner.owned_patch_count);
6865
+ };
6866
+ const queryCompleteOwnersPatches = async (store, indexName, bundleIds) => {
6867
+ const owners = (await loadBundleItemsById(store, bundleIds)).filter(({ owned_patch_count }) => owned_patch_count > 0);
6868
+ const firstOwner = owners[0];
6869
+ if (!firstOwner) return [];
6870
+ let failedOwner = firstOwner;
6871
+ for (let attempt = 0; attempt < 3; attempt++) {
6872
+ const candidateIdsByOwner = await Promise.all(owners.map(({ sk }) => queryOwnerPatchIds(store, indexName, sk)));
6873
+ const patches = await loadPatchItemsById(store, candidateIdsByOwner.flat());
6874
+ const patchesById = new Map(patches.map((patch) => [patch.sk, patch]));
6875
+ const mismatchedOwner = owners.find((owner, ownerIndex) => {
6876
+ const ids = candidateIdsByOwner[ownerIndex] ?? [];
6877
+ const uniqueIds = new Set(ids);
6878
+ return ids.length !== owner.owned_patch_count || uniqueIds.size !== owner.owned_patch_count || ids.some((id) => patchesById.get(id)?.row.bundle_id !== owner.sk);
6879
+ });
6880
+ if (!mismatchedOwner) return patches.map(({ row }) => row);
6881
+ failedOwner = mismatchedOwner;
6882
+ if (attempt < 2) await waitForIndex(attempt);
6883
+ }
6884
+ throw new DynamoDBPatchIndexConsistencyError(failedOwner.sk, failedOwner.owned_patch_count);
6885
+ };
6886
+ const createDynamoDBBundlePatchTable = (store, indexName) => ({ findByBundleIds: (bundleIds) => bundleIds.length === 0 ? Promise.resolve([]) : queryCompleteOwnersPatches(store, indexName, bundleIds) });
6887
+ const compare = (left, right) => {
6888
+ if (typeof left === "number" && typeof right === "number") return left - right;
6889
+ if (left == null) return right == null ? 0 : -1;
6890
+ if (right == null) return 1;
6891
+ return JSON.stringify(left).localeCompare(JSON.stringify(right));
6892
+ };
6893
+ const compareString = (actual, expected, mode, predicate) => {
6894
+ if (typeof actual !== "string") return false;
6895
+ return mode === "insensitive" ? predicate(actual.toLocaleLowerCase(), expected.toLocaleLowerCase()) : predicate(actual, expected);
6896
+ };
6897
+ const matchesCondition = (row, condition) => {
6898
+ const actual = Reflect.get(row, condition.field);
6899
+ const expected = Reflect.get(condition, "value");
6900
+ switch (Reflect.get(condition, "operator") ?? "eq") {
6901
+ case "eq": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value === query) : actual === expected;
6902
+ case "ne":
6903
+ if (actual == null) return false;
6904
+ return typeof expected === "string" ? !compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value === query) : actual !== expected;
6905
+ case "gt": return actual != null && compare(actual, expected) > 0;
6906
+ case "gte": return actual != null && compare(actual, expected) >= 0;
6907
+ case "lt": return actual != null && compare(actual, expected) < 0;
6908
+ case "lte": return actual != null && compare(actual, expected) <= 0;
6909
+ case "in": return Array.isArray(expected) && expected.some((candidate) => candidate === actual);
6910
+ case "not_in": return Array.isArray(expected) && (expected.length === 0 || actual != null && expected.every((candidate) => candidate !== actual));
6911
+ case "contains": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.includes(query)) : false;
6912
+ case "starts_with": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.startsWith(query)) : false;
6913
+ case "ends_with": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.endsWith(query)) : false;
6914
+ default: return false;
6915
+ }
6916
+ };
6917
+ const matchesDynamoDBWhere = (row, where) => {
6918
+ if (!where || where.length === 0) return true;
6919
+ let result = matchesCondition(row, where[0]);
6920
+ for (const condition of where.slice(1)) {
6921
+ const current = matchesCondition(row, condition);
6922
+ result = condition.connector === "OR" ? result || current : result && current;
6923
+ }
6924
+ return result;
6925
+ };
6926
+ const compareRows = (left, right, orderBy) => {
6927
+ for (const clause of orderBy) {
6928
+ const leftValue = Reflect.get(left, clause.field);
6929
+ const rightValue = Reflect.get(right, clause.field);
6930
+ if (leftValue == null || rightValue == null) {
6931
+ if (leftValue == null && rightValue == null) continue;
6932
+ const nulls = clause.nulls ?? (clause.direction === "asc" ? "last" : "first");
6933
+ const order = leftValue == null ? -1 : 1;
6934
+ return nulls === "first" ? order : -order;
6935
+ }
6936
+ const order = compare(leftValue, rightValue);
6937
+ if (order !== 0) return clause.direction === "asc" ? order : -order;
6938
+ }
6939
+ return 0;
6940
+ };
6941
+ const distinctKey = (row, distinctOn) => JSON.stringify(distinctOn.fields.map((field) => Reflect.get(row, field)));
6942
+ const queryDynamoDBRows = (rows, input) => {
6943
+ const filtered = rows.filter((row) => matchesDynamoDBWhere(row, input.where));
6944
+ const orderBy = input.orderBy;
6945
+ const ordered = orderBy ? filtered.toSorted((left, right) => compareRows(left, right, orderBy)) : filtered;
6946
+ const distinctOn = input.distinctOn;
6947
+ const distinct = distinctOn ? (() => {
6948
+ const seen = /* @__PURE__ */ new Set();
6949
+ return ordered.filter((row) => {
6950
+ const key = distinctKey(row, distinctOn);
6951
+ if (seen.has(key)) return false;
6952
+ seen.add(key);
6953
+ return true;
6954
+ });
6955
+ })() : ordered;
6956
+ const offset = input.offset ?? 0;
6957
+ return distinct.slice(offset, offset + (input.limit ?? 100));
6958
+ };
6959
+ const countDistinctDynamoDBRows = (rows, fields) => {
6960
+ if (fields === void 0) return rows.length;
6961
+ return new Set(rows.map((row) => JSON.stringify(fields.map((field) => Reflect.get(row, field))))).size;
6962
+ };
6963
+ const createDynamoDBBundle = async (store, row) => {
6964
+ const counter = metadataUpdate(store, { bundles: 1 });
6965
+ if (!counter) return;
6966
+ await commitDynamoDBTransaction(store, [counter, { Put: {
6967
+ TableName: store.tableName,
6968
+ Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row)),
6969
+ ConditionExpression: "attribute_not_exists(#pk)",
6970
+ ExpressionAttributeNames: { "#pk": "pk" }
6971
+ } }]);
6972
+ };
6973
+ const createDynamoDBPatch = async (store, row) => {
6974
+ const bundleIds = [...new Set([row.bundle_id, row.base_bundle_id])];
6975
+ const counter = metadataUpdate(store, { bundle_patches: 1 });
6976
+ if (!counter) return;
6977
+ await commitDynamoDBTransaction(store, [
6978
+ counter,
6979
+ ...bundleIds.map((bundleId) => updateBundleRelation(store, bundleId, 1, bundleId === row.bundle_id ? 1 : 0)),
6980
+ { Put: {
6981
+ TableName: store.tableName,
6982
+ Item: boundedDynamoDBMetadataItem(toDynamoDBPatchItem(row)),
6983
+ ConditionExpression: "attribute_not_exists(#pk)",
6984
+ ExpressionAttributeNames: { "#pk": "pk" }
6985
+ } }
6986
+ ]);
6987
+ };
6988
+ const replaceDynamoDBBundle = async (store, current, row) => {
6989
+ await store.client.send(new PutCommand({
6990
+ TableName: store.tableName,
6991
+ Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row, current.version + 1, current.relation_count, current.owned_patch_count)),
6992
+ ConditionExpression: "#version = :currentVersion",
6993
+ ExpressionAttributeNames: { "#version": "version" },
6994
+ ExpressionAttributeValues: { ":currentVersion": current.version }
6995
+ }));
6996
+ };
6997
+ const deleteAction = (store, item) => ({ Delete: {
6998
+ TableName: store.tableName,
6999
+ Key: {
7000
+ pk: item.pk,
7001
+ sk: item.sk
7002
+ },
7003
+ ConditionExpression: "#version = :currentVersion",
7004
+ ExpressionAttributeNames: { "#version": "version" },
7005
+ ExpressionAttributeValues: { ":currentVersion": item.version }
7006
+ } });
7007
+ const deleteDynamoDBPatch = async (store, item) => {
7008
+ const counter = metadataUpdate(store, { bundle_patches: -1 });
7009
+ if (!counter) return;
7010
+ await commitDynamoDBTransaction(store, [
7011
+ counter,
7012
+ ...[...new Set([item.row.bundle_id, item.row.base_bundle_id])].map((id) => updateBundleRelation(store, id, -1, id === item.row.bundle_id ? -1 : 0)),
7013
+ deleteAction(store, item)
7014
+ ]);
7015
+ };
7016
+ const deleteBundle = async (store, bundle, patches) => {
7017
+ const counter = metadataUpdate(store, {
7018
+ bundles: -1,
7019
+ bundle_patches: -patches.length
7020
+ });
7021
+ if (!counter) return;
7022
+ const otherEndpointCounts = /* @__PURE__ */ new Map();
7023
+ for (const { row } of patches) for (const id of new Set([row.bundle_id, row.base_bundle_id])) {
7024
+ if (id === bundle.sk) continue;
7025
+ const current = otherEndpointCounts.get(id) ?? {
7026
+ owned: 0,
7027
+ relations: 0
7028
+ };
7029
+ otherEndpointCounts.set(id, {
7030
+ owned: current.owned + (id === row.bundle_id ? 1 : 0),
7031
+ relations: current.relations + 1
7032
+ });
7033
+ }
7034
+ await commitDynamoDBTransaction(store, [
7035
+ counter,
7036
+ ...[...otherEndpointCounts].map(([id, count]) => updateBundleRelation(store, id, -count.relations, -count.owned)),
7037
+ ...patches.map((patch) => deleteAction(store, patch)),
7038
+ deleteAction(store, bundle)
7039
+ ]);
7040
+ };
7041
+ const deleteDynamoDBBundles = async (store, bundles, allPatches) => {
7042
+ let patches = [...allPatches];
7043
+ const versionIncrements = /* @__PURE__ */ new Map();
7044
+ for (const originalBundle of bundles) {
7045
+ const bundle = {
7046
+ ...originalBundle,
7047
+ version: originalBundle.version + (versionIncrements.get(originalBundle.sk) ?? 0)
7048
+ };
7049
+ const related = patches.filter(({ row }) => row.bundle_id === bundle.sk || row.base_bundle_id === bundle.sk);
7050
+ await deleteBundle(store, bundle, related);
7051
+ const updatedBundleIds = new Set(related.flatMap(({ row }) => [row.bundle_id, row.base_bundle_id]));
7052
+ updatedBundleIds.delete(bundle.sk);
7053
+ for (const id of updatedBundleIds) versionIncrements.set(id, (versionIncrements.get(id) ?? 0) + 1);
7054
+ const removedPatchIds = new Set(related.map(({ sk }) => sk));
7055
+ patches = patches.filter(({ sk }) => !removedPatchIds.has(sk));
7056
+ }
7057
+ };
7058
+ var DynamoDBUnsupportedModelError = class extends Error {
7059
+ name = "DynamoDBUnsupportedModelError";
7060
+ constructor() {
7061
+ super("DynamoDB received an unsupported database model");
7062
+ }
7063
+ };
7064
+ const distinctFields = (fields) => fields;
7065
+ const exactDynamoDBField = (where, fieldName) => {
7066
+ const conditions = (where ?? []).filter((condition) => Reflect.get(condition, "field") === fieldName);
7067
+ if (conditions.length !== 1) return void 0;
7068
+ const condition = conditions[0];
7069
+ if ((Reflect.get(condition, "operator") ?? "eq") !== "eq" || Reflect.get(condition, "mode") === "insensitive") return;
7070
+ const value = Reflect.get(condition, "value");
7071
+ return typeof value === "string" ? value : void 0;
7072
+ };
7073
+ const createDynamoDBCrud = (store, updateIndexName) => ({
7074
+ async create(input) {
7075
+ switch (input.model) {
7076
+ case "bundles":
7077
+ await createDynamoDBBundle(store, input.data);
7078
+ return input.data;
7079
+ case "bundle_patches":
7080
+ await createDynamoDBPatch(store, input.data);
7081
+ return input.data;
7082
+ case "releases": {
7083
+ const channel = await loadChannelItem(store, input.data.channel_id);
7084
+ if (channel === void 0) throw new DynamoDBStoredItemError();
7085
+ await commitDynamoDBTransaction(store, [
7086
+ updateChannelReferenceCount(store, channel, 1),
7087
+ { Put: {
7088
+ TableName: store.tableName,
7089
+ Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(input.data)),
7090
+ ConditionExpression: "attribute_not_exists(#pk)",
7091
+ ExpressionAttributeNames: { "#pk": "pk" }
7092
+ } },
7093
+ { Put: {
7094
+ TableName: store.tableName,
7095
+ Item: releaseLocatorItem(input.data),
7096
+ ConditionExpression: "attribute_not_exists(#pk)",
7097
+ ExpressionAttributeNames: { "#pk": "pk" }
7098
+ } }
7099
+ ]);
7100
+ return input.data;
7101
+ }
7102
+ case "release_catalogs":
7103
+ await store.client.send(new PutCommand({
7104
+ TableName: store.tableName,
7105
+ Item: boundedDynamoDBCatalogItem(toDynamoDBReleaseCatalogItem(input.data)),
7106
+ ConditionExpression: "attribute_not_exists(#pk)",
7107
+ ExpressionAttributeNames: { "#pk": "pk" }
7108
+ }));
7109
+ return input.data;
7110
+ case "channels": return (await insertDynamoDBChannel(store, {
7111
+ row: input.data,
7112
+ onConflict: "returnExisting"
7113
+ })).row;
7114
+ }
7115
+ throw new DynamoDBUnsupportedModelError();
7116
+ },
7117
+ async update(input) {
7118
+ if (input.model === "releases") {
7119
+ const id = exactDynamoDBId(input.where);
7120
+ if (id === void 0) throw new DynamoDBUnsupportedModelError();
7121
+ const current = await loadReleaseItem(store, id);
7122
+ if (current === void 0) return null;
7123
+ const updated = {
7124
+ ...current.row,
7125
+ ...input.update
7126
+ };
7127
+ const nextPartition = releaseScopePartition(updated.scope_key);
7128
+ const moved = current.pk !== nextPartition;
7129
+ await commitDynamoDBTransaction(store, [
7130
+ ...moved ? [{ Delete: {
7131
+ TableName: store.tableName,
7132
+ Key: {
7133
+ pk: current.pk,
7134
+ sk: current.sk
7135
+ },
7136
+ ConditionExpression: "#version = :version",
7137
+ ExpressionAttributeNames: { "#version": "version" },
7138
+ ExpressionAttributeValues: { ":version": current.version }
7139
+ } }] : [],
7140
+ { Put: {
7141
+ TableName: store.tableName,
7142
+ Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(updated, current.version + 1)),
7143
+ ConditionExpression: moved ? "attribute_not_exists(#pk)" : "#version = :version",
7144
+ ExpressionAttributeNames: moved ? { "#pk": "pk" } : { "#version": "version" },
7145
+ ...moved ? {} : { ExpressionAttributeValues: { ":version": current.version } }
7146
+ } },
7147
+ { Put: {
7148
+ TableName: store.tableName,
7149
+ Item: releaseLocatorItem(updated),
7150
+ ConditionExpression: "#scopeKey = :scopeKey",
7151
+ ExpressionAttributeNames: { "#scopeKey": "scope_key" },
7152
+ ExpressionAttributeValues: { ":scopeKey": current.row.scope_key }
7153
+ } }
7154
+ ]);
7155
+ return updated;
7156
+ }
7157
+ if (input.model === "release_catalogs") {
7158
+ const scopeKey = exactDynamoDBField(input.where, "scope_key");
7159
+ if (scopeKey === void 0) throw new DynamoDBUnsupportedModelError();
7160
+ const current = await loadReleaseCatalogItem(store, scopeKey);
7161
+ if (current === void 0) return null;
7162
+ const updated = {
7163
+ ...current.row,
7164
+ ...input.update
7165
+ };
7166
+ await store.client.send(new PutCommand({
7167
+ TableName: store.tableName,
7168
+ Item: boundedDynamoDBCatalogItem(toDynamoDBReleaseCatalogItem(updated, current.version + 1)),
7169
+ ConditionExpression: "#version = :version",
7170
+ ExpressionAttributeNames: { "#version": "version" },
7171
+ ExpressionAttributeValues: { ":version": current.version }
7172
+ }));
7173
+ return updated;
7174
+ }
7175
+ if (input.model !== "bundles") throw new DynamoDBUnsupportedModelError();
7176
+ const id = exactDynamoDBId(input.where);
7177
+ const current = id === void 0 ? (await loadBundleItems(store)).find(({ row }) => matchesDynamoDBWhere(row, input.where)) : await loadBundleItem(store, id);
7178
+ if (!current) return null;
7179
+ const updated = {
7180
+ ...current.row,
7181
+ ...input.update
7182
+ };
7183
+ await replaceDynamoDBBundle(store, current, updated);
7184
+ return updated;
7185
+ },
7186
+ async delete(input) {
7187
+ if (input.model === "channels") {
7188
+ const id = exactDynamoDBId(input.where);
7189
+ if (id !== void 0) await deleteDynamoDBChannel(store, { id });
7190
+ return;
7191
+ }
7192
+ if (input.model === "bundle_patches") {
7193
+ const items = (await loadPatchItems(store)).filter(({ row }) => matchesDynamoDBWhere(row, input.where));
7194
+ for (const item of items) await deleteDynamoDBPatch(store, item);
7195
+ return;
7196
+ }
7197
+ if (input.model === "releases") {
7198
+ const items = (await loadReleaseItems(store)).filter(({ row }) => matchesDynamoDBWhere(row, input.where));
7199
+ for (const item of items) {
7200
+ const channel = await loadChannelItem(store, item.row.channel_id);
7201
+ if (channel === void 0) throw new DynamoDBStoredItemError();
7202
+ await commitDynamoDBTransaction(store, [
7203
+ updateChannelReferenceCount(store, channel, -1),
7204
+ deleteAction(store, item),
7205
+ { Delete: {
7206
+ TableName: store.tableName,
7207
+ Key: {
7208
+ pk: DYNAMODB_RELEASE_ID_PARTITION,
7209
+ sk: item.sk
7210
+ },
7211
+ ConditionExpression: "#scopeKey = :scopeKey",
7212
+ ExpressionAttributeNames: { "#scopeKey": "scope_key" },
7213
+ ExpressionAttributeValues: { ":scopeKey": item.row.scope_key }
7214
+ } }
7215
+ ]);
7216
+ }
7217
+ return;
7218
+ }
7219
+ if (input.model !== "bundles") throw new DynamoDBUnsupportedModelError();
7220
+ const bundleItems = (await loadBundleItems(store)).filter(({ row }) => matchesDynamoDBWhere(row, input.where));
7221
+ if (bundleItems.length === 0) return;
7222
+ await deleteDynamoDBBundles(store, bundleItems, await loadPatchItems(store));
7223
+ },
7224
+ async count(input) {
7225
+ if ((input.where === void 0 || input.where.length === 0) && input.distinct === void 0) {
7226
+ const count = await loadMetadataCount(store, input.model);
7227
+ if (count !== void 0) return count;
7228
+ }
7229
+ switch (input.model) {
7230
+ case "bundles": return countDistinctDynamoDBRows((await loadBundleItems(store)).map(({ row }) => row).filter((row) => matchesDynamoDBWhere(row, input.where)), distinctFields(input.distinct));
7231
+ case "bundle_patches": {
7232
+ const ownerId = exactDynamoDBPatchOwner(input.where);
7233
+ return countDistinctDynamoDBRows((ownerId ? await queryCompleteOwnerPatches(store, updateIndexName, ownerId) : (await loadPatchItems(store)).map(({ row }) => row)).filter((row) => matchesDynamoDBWhere(row, input.where)), distinctFields(input.distinct));
7234
+ }
7235
+ case "releases": {
7236
+ const scopeKey = exactDynamoDBField(input.where, "scope_key");
7237
+ return countDistinctDynamoDBRows((scopeKey === void 0 ? await loadReleaseItems(store) : await loadReleaseItemsByScope(store, scopeKey)).map(({ row }) => row).filter((row) => matchesDynamoDBWhere(row, input.where)), distinctFields(input.distinct));
7238
+ }
7239
+ }
7240
+ throw new DynamoDBUnsupportedModelError();
7241
+ },
7242
+ async findOne(input) {
7243
+ const id = exactDynamoDBId(input.where);
7244
+ switch (input.model) {
7245
+ case "bundles":
7246
+ if (id !== void 0) return (await loadBundleItem(store, id))?.row ?? null;
7247
+ return (await loadBundleItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7248
+ case "bundle_patches":
7249
+ if (id !== void 0) return (await loadPatchItem(store, id))?.row ?? null;
7250
+ {
7251
+ const ownerId = exactDynamoDBPatchOwner(input.where);
7252
+ if (ownerId === void 0) break;
7253
+ return (await queryCompleteOwnerPatches(store, updateIndexName, ownerId)).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7254
+ }
7255
+ break;
7256
+ case "channels":
7257
+ if (id !== void 0) return (await loadChannelItem(store, id))?.row ?? null;
7258
+ return (await loadChannelItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7259
+ case "releases":
7260
+ if (id !== void 0) return (await loadReleaseItem(store, id))?.row ?? null;
7261
+ {
7262
+ const scopeKey = exactDynamoDBField(input.where, "scope_key");
7263
+ return (scopeKey === void 0 ? await loadReleaseItems(store) : await loadReleaseItemsByScope(store, scopeKey)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7264
+ }
7265
+ case "release_catalogs": {
7266
+ const scopeKey = exactDynamoDBField(input.where, "scope_key");
7267
+ if (scopeKey !== void 0) return (await loadReleaseCatalogItem(store, scopeKey))?.row ?? null;
7268
+ return (await loadReleaseCatalogItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7269
+ }
7270
+ }
7271
+ if (input.model === "bundle_patches") return (await loadPatchItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7272
+ throw new DynamoDBUnsupportedModelError();
7273
+ },
7274
+ async findMany(input) {
7275
+ if (input.limit === 0) return [];
7276
+ switch (input.model) {
7277
+ case "bundles": {
7278
+ const ids = exactDynamoDBBundleIds(input.where);
7279
+ if (ids !== void 0) return queryDynamoDBRows((await loadBundleItemsById(store, ids)).map(({ row }) => row), input);
7280
+ const orderBy = input.orderBy;
7281
+ const direction = orderBy?.[0]?.direction;
7282
+ if ((input.offset ?? 0) === 0 && input.distinctOn === void 0 && orderBy?.length === 1 && orderBy[0]?.field === "id" && (direction === "asc" || direction === "desc")) return (await queryBundleItemsPage(store, {
7283
+ direction,
7284
+ limit: input.limit ?? 100,
7285
+ matches: (row) => matchesDynamoDBWhere(row, input.where),
7286
+ start: dynamoDBBundlePageStart(input.where, direction)
7287
+ })).map(({ row }) => row);
7288
+ return queryDynamoDBRows((await loadBundleItems(store)).map(({ row }) => row), input);
7289
+ }
7290
+ case "bundle_patches": {
7291
+ const ownerIds = exactDynamoDBPatchOwners(input.where);
7292
+ if (ownerIds !== void 0) {
7293
+ const ownerId = ownerIds.length === 1 ? ownerIds[0] : void 0;
7294
+ return queryDynamoDBRows(ownerId !== void 0 ? await queryCompleteOwnerPatches(store, updateIndexName, ownerId) : await queryCompleteOwnersPatches(store, updateIndexName, ownerIds), input);
7295
+ }
7296
+ return queryDynamoDBRows((await loadPatchItems(store)).map(({ row }) => row), input);
7297
+ }
7298
+ case "channels": return queryDynamoDBRows((await loadChannelItems(store)).map(({ row }) => row), input);
7299
+ case "releases": {
7300
+ const scopeKey = exactDynamoDBField(input.where, "scope_key");
7301
+ return queryDynamoDBRows((scopeKey === void 0 ? await loadReleaseItems(store) : await loadReleaseItemsByScope(store, scopeKey)).map(({ row }) => row), input);
7302
+ }
7303
+ case "release_catalogs": return queryDynamoDBRows((await loadReleaseCatalogItems(store)).map(({ row }) => row), input);
7304
+ }
7305
+ throw new DynamoDBUnsupportedModelError();
7306
+ },
7307
+ insertChannel: (input) => insertDynamoDBChannel(store, input),
7308
+ deleteChannel: (input) => deleteDynamoDBChannel(store, input)
7309
+ });
7310
+ var DynamoDBDuplicatePatchError = class extends Error {
7311
+ name = "DynamoDBDuplicatePatchError";
7312
+ constructor(patchId) {
7313
+ super(`DynamoDB bundle mutation contains duplicate patch "${patchId}"`);
7314
+ this.patchId = patchId;
7315
+ }
7316
+ };
7317
+ const putNewBundle = (store, row, relationCount, ownedPatchCount = relationCount) => ({ Put: {
7318
+ TableName: store.tableName,
7319
+ Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row, 1, relationCount, ownedPatchCount)),
7320
+ ConditionExpression: "attribute_not_exists(#pk)",
7321
+ ExpressionAttributeNames: { "#pk": "pk" }
7322
+ } });
7323
+ const putUpdatedBundle = (store, current, row, relationCount, ownedPatchCount) => ({ Put: {
7324
+ TableName: store.tableName,
7325
+ Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row, current.version + 1, relationCount, ownedPatchCount)),
7326
+ ConditionExpression: "#version = :currentVersion",
7327
+ ExpressionAttributeNames: { "#version": "version" },
7328
+ ExpressionAttributeValues: { ":currentVersion": current.version }
7329
+ } });
7330
+ const putPatch = (store, row, current) => ({ Put: {
7331
+ TableName: store.tableName,
7332
+ Item: boundedDynamoDBMetadataItem(toDynamoDBPatchItem(row, (current?.version ?? 0) + 1)),
7333
+ ConditionExpression: current ? "#version = :currentVersion" : "attribute_not_exists(#pk)",
7334
+ ExpressionAttributeNames: current ? { "#version": "version" } : { "#pk": "pk" },
7335
+ ...current ? { ExpressionAttributeValues: { ":currentVersion": current.version } } : {}
7336
+ } });
7337
+ const deletePatch = (store, item) => ({ Delete: {
7338
+ TableName: store.tableName,
7339
+ Key: itemKey(item.pk, item.sk),
7340
+ ConditionExpression: "#version = :currentVersion",
7341
+ ExpressionAttributeNames: { "#version": "version" },
7342
+ ExpressionAttributeValues: { ":currentVersion": item.version }
7343
+ } });
7344
+ var DynamoDBCommitStateError = class extends Error {
7345
+ name = "DynamoDBCommitStateError";
7346
+ constructor(message) {
7347
+ super(message);
7348
+ }
7349
+ };
7350
+ const rowsEqual = (left, right) => JSON.stringify(left) === JSON.stringify(right);
7351
+ const putChannel = (store, row, current, referenceCount) => ({ Put: {
7352
+ TableName: store.tableName,
7353
+ Item: boundedDynamoDBMetadataItem(toDynamoDBChannelItem(row, (current?.version ?? 0) + 1, referenceCount)),
7354
+ ConditionExpression: current ? "#version = :version" : "attribute_not_exists(#pk)",
7355
+ ExpressionAttributeNames: current ? { "#version": "version" } : { "#pk": "pk" },
7356
+ ...current ? { ExpressionAttributeValues: { ":version": current.version } } : {}
7357
+ } });
7358
+ const deleteChannelActions = (store, current) => [{ Delete: {
7359
+ TableName: store.tableName,
7360
+ Key: itemKey(DYNAMODB_CHANNEL_PARTITION, current.sk),
7361
+ ConditionExpression: "#version = :version",
7362
+ ExpressionAttributeNames: { "#version": "version" },
7363
+ ExpressionAttributeValues: { ":version": current.version }
7364
+ } }, { Delete: {
7365
+ TableName: store.tableName,
7366
+ Key: {
7367
+ pk: DYNAMODB_CHANNEL_NAME_PARTITION,
7368
+ sk: current.row.name
7369
+ },
7370
+ ConditionExpression: "#channelId = :channelId",
7371
+ ExpressionAttributeNames: { "#channelId": "channel_id" },
7372
+ ExpressionAttributeValues: { ":channelId": current.sk }
7373
+ } }];
7374
+ const updateChannelReferenceCount = (store, current, delta) => ({ Update: {
7375
+ TableName: store.tableName,
7376
+ Key: itemKey(DYNAMODB_CHANNEL_PARTITION, current.sk),
7377
+ ConditionExpression: "#row.#name = :name" + (delta < 0 ? " AND #referenceCount >= :removal" : ""),
7378
+ UpdateExpression: "SET #version = #version + :one ADD #referenceCount :delta",
7379
+ ExpressionAttributeNames: {
7380
+ "#row": "row",
7381
+ "#name": "name",
7382
+ "#version": "version",
7383
+ "#referenceCount": "reference_count"
7384
+ },
7385
+ ExpressionAttributeValues: {
7386
+ ":name": current.row.name,
7387
+ ":one": 1,
7388
+ ":delta": delta,
7389
+ ...delta < 0 ? { ":removal": -delta } : {}
7390
+ }
7391
+ } });
7392
+ const loadApiKeyByHash = async (store, hash) => {
7393
+ const { Item } = await store.client.send(new GetCommand({
7394
+ TableName: store.tableName,
7395
+ Key: {
7396
+ pk: DYNAMODB_API_KEY_HASH_PARTITION,
7397
+ sk: hash
7398
+ },
7399
+ ConsistentRead: true
7400
+ }));
7401
+ if (Item === void 0) return null;
7402
+ const id = Item.api_key_id;
7403
+ if (typeof id !== "string") throw new DynamoDBStoredItemError();
7404
+ return loadApiKey(store, id);
7405
+ };
7406
+ const compileAndCommitDynamoDBChanges = async (store, input) => {
7407
+ const [originalBundleItems, originalPatchItems, originalChannelItems, originalReleaseItems, originalReleaseCatalogItems] = await Promise.all([
7408
+ loadBundleItems(store),
7409
+ loadPatchItems(store),
7410
+ loadChannelItems(store),
7411
+ loadReleaseItems(store),
7412
+ loadReleaseCatalogItems(store)
7413
+ ]);
7414
+ const bundles = new Map(originalBundleItems.map(({ sk, row }) => [sk, row]));
7415
+ const patches = new Map(originalPatchItems.map(({ sk, row }) => [sk, row]));
7416
+ const channels = new Map(originalChannelItems.map(({ sk, row }) => [sk, row]));
7417
+ const channelNames = new Map(originalChannelItems.map(({ sk, row }) => [row.name, sk]));
7418
+ const releases = new Map(originalReleaseItems.map(({ sk, row }) => [sk, row]));
7419
+ const releaseCatalogs = new Map(originalReleaseCatalogItems.map(({ sk, row }) => [sk, row]));
7420
+ const originalApiKeys = /* @__PURE__ */ new Map();
7421
+ const apiKeys = /* @__PURE__ */ new Map();
7422
+ const apiKeyHashes = /* @__PURE__ */ new Map();
7423
+ const analytics = /* @__PURE__ */ new Map();
7424
+ const rememberApiKey = (value) => {
7425
+ if (value === null || apiKeys.has(value.row.id)) return;
7426
+ originalApiKeys.set(value.row.id, value);
7427
+ apiKeys.set(value.row.id, value);
7428
+ apiKeyHashes.set(value.row.hash, value.row.id);
7429
+ };
7430
+ const requireReleaseReferences = (row) => {
7431
+ if (!channels.has(row.channel_id)) throw new DynamoDBCommitStateError(`Release "${row.id}" references an invalid channel`);
7432
+ if (row.bundle_id !== null) {
7433
+ const bundle = bundles.get(row.bundle_id);
7434
+ if (bundle === void 0 || bundle.platform !== row.platform) throw new DynamoDBCommitStateError(`Release "${row.id}" references an invalid bundle`);
7435
+ }
7436
+ };
7437
+ for (const expectation of input.expectations ?? []) {
7438
+ const actualVersion = expectation.model === "releases" ? releases.get(expectation.id)?.revision ?? null : releaseCatalogs.get(expectation.scopeKey)?.generation ?? null;
7439
+ const expectedVersion = expectation.model === "releases" ? expectation.revision : expectation.generation;
7440
+ if (actualVersion !== expectedVersion) return {
7441
+ committed: false,
7442
+ conflict: {
7443
+ actualVersion,
7444
+ changeIndex: -1,
7445
+ expectedVersion,
7446
+ key: expectation.model === "releases" ? expectation.id : expectation.scopeKey,
7447
+ model: expectation.model,
7448
+ reason: "version_conflict"
7449
+ }
7450
+ };
7451
+ }
7452
+ for (const [changeIndex, change] of input.changes.entries()) switch (change.model) {
7453
+ case "channels":
7454
+ if (change.operation === "insert") {
7455
+ if (channelNames.get(change.row.name) !== void 0) break;
7456
+ if (channels.get(change.row.id) !== void 0) throw new DynamoDBCommitStateError(`Channel id "${change.row.id}" already exists`);
7457
+ channels.set(change.row.id, change.row);
7458
+ channelNames.set(change.row.name, change.row.id);
7459
+ } else {
7460
+ const current = channels.get(change.where.id);
7461
+ if (current === void 0) break;
7462
+ if ([...releases.values()].some(({ channel_id }) => channel_id === change.where.id)) return {
7463
+ committed: false,
7464
+ conflict: {
7465
+ changeIndex,
7466
+ reason: "referenced"
7467
+ }
7468
+ };
7469
+ channels.delete(change.where.id);
7470
+ channelNames.delete(current.name);
7471
+ }
7472
+ break;
7473
+ case "bundles":
7474
+ if (change.operation === "insert") {
7475
+ if (bundles.has(change.row.id)) throw new DynamoDBCommitStateError(`Bundle "${change.row.id}" already exists`);
7476
+ bundles.set(change.row.id, change.row);
7477
+ } else {
7478
+ const current = bundles.get(change.where.id);
7479
+ if (current === void 0) {
7480
+ if (change.operation === "update") return {
7481
+ committed: false,
7482
+ conflict: {
7483
+ changeIndex,
7484
+ reason: "not_found"
7485
+ }
7486
+ };
7487
+ break;
7488
+ }
7489
+ if (change.operation === "update") {
7490
+ const row = {
7491
+ ...current,
7492
+ ...change.update
7493
+ };
7494
+ bundles.set(row.id, row);
7495
+ } else {
7496
+ if ([...releases.values()].some(({ bundle_id }) => bundle_id === change.where.id)) return {
7497
+ committed: false,
7498
+ conflict: {
7499
+ changeIndex,
7500
+ reason: "referenced"
7501
+ }
7502
+ };
7503
+ bundles.delete(change.where.id);
7504
+ for (const [id, patch] of patches) if (patch.bundle_id === change.where.id || patch.base_bundle_id === change.where.id) patches.delete(id);
7505
+ }
7506
+ }
7507
+ break;
7508
+ case "releases":
7509
+ if (change.operation === "insert") {
7510
+ requireReleaseReferences(change.row);
7511
+ if (releases.has(change.row.id)) throw new DynamoDBCommitStateError(`Release "${change.row.id}" already exists`);
7512
+ releases.set(change.row.id, change.row);
7513
+ } else {
7514
+ const current = releases.get(change.where.id);
7515
+ if (current === void 0) {
7516
+ if (change.operation === "update") return {
7517
+ committed: false,
7518
+ conflict: {
7519
+ changeIndex,
7520
+ reason: "not_found"
7521
+ }
7522
+ };
7523
+ break;
7524
+ }
7525
+ if (change.operation === "update") {
7526
+ const row = {
7527
+ ...current,
7528
+ ...change.update
7529
+ };
7530
+ requireReleaseReferences(row);
7531
+ releases.set(row.id, row);
7532
+ } else releases.delete(change.where.id);
7533
+ }
7534
+ break;
7535
+ case "releaseCatalogs":
7536
+ if (!channels.has(change.row.channel_id)) throw new DynamoDBCommitStateError(`Release catalog "${change.row.scope_key}" references an invalid channel`);
7537
+ releaseCatalogs.set(change.row.scope_key, change.row);
7538
+ break;
7539
+ case "bundlePatches":
7540
+ if (change.operation === "insert") {
7541
+ if (!bundles.has(change.row.bundle_id) || !bundles.has(change.row.base_bundle_id)) throw new DynamoDBCommitStateError(`Patch "${change.row.id}" references a missing bundle`);
7542
+ if (patches.has(change.row.id)) throw new DynamoDBDuplicatePatchError(change.row.id);
7543
+ patches.set(change.row.id, change.row);
7544
+ } else for (const [id, patch] of patches) if (patch.bundle_id === change.where.bundleId) patches.delete(id);
7545
+ break;
7546
+ case "analytics": {
7547
+ const key = analyticsSortKey(change.row);
7548
+ if (analytics.has(key)) throw new DynamoDBCommitStateError(`Analytics event "${change.row.id}" is duplicated`);
7549
+ analytics.set(key, change.row);
7550
+ break;
7551
+ }
7552
+ case "apiKeys":
7553
+ if (change.operation === "insert") {
7554
+ rememberApiKey(await loadApiKeyByHash(store, change.row.hash));
7555
+ if (apiKeyHashes.has(change.row.hash)) break;
7556
+ rememberApiKey(await loadApiKey(store, change.row.id));
7557
+ if (apiKeys.has(change.row.id)) throw new DynamoDBCommitStateError(`API key id "${change.row.id}" already exists`);
7558
+ const inserted = {
7559
+ row: change.row,
7560
+ version: 1
7561
+ };
7562
+ apiKeys.set(change.row.id, inserted);
7563
+ apiKeyHashes.set(change.row.hash, change.row.id);
7564
+ } else {
7565
+ rememberApiKey(await loadApiKey(store, change.where.id));
7566
+ const current = apiKeys.get(change.where.id);
7567
+ if (current === void 0) return {
7568
+ committed: false,
7569
+ conflict: {
7570
+ changeIndex,
7571
+ reason: "not_found"
7572
+ }
7573
+ };
7574
+ apiKeys.set(change.where.id, {
7575
+ row: {
7576
+ ...current.row,
7577
+ revoked_at_ms: change.update.revokedAtMs
7578
+ },
7579
+ version: current.version
7580
+ });
7581
+ }
7582
+ break;
7583
+ }
7584
+ const relationCounts = /* @__PURE__ */ new Map();
7585
+ const ownedPatchCounts = /* @__PURE__ */ new Map();
7586
+ for (const patch of patches.values()) {
7587
+ for (const id of new Set([patch.bundle_id, patch.base_bundle_id])) relationCounts.set(id, (relationCounts.get(id) ?? 0) + 1);
7588
+ ownedPatchCounts.set(patch.bundle_id, (ownedPatchCounts.get(patch.bundle_id) ?? 0) + 1);
7589
+ }
7590
+ const channelReferenceCounts = /* @__PURE__ */ new Map();
7591
+ for (const release of releases.values()) channelReferenceCounts.set(release.channel_id, (channelReferenceCounts.get(release.channel_id) ?? 0) + 1);
7592
+ const actions = [];
7593
+ const originalChannels = new Map(originalChannelItems.map((item) => [item.sk, item]));
7594
+ for (const original of originalChannelItems) if (!channels.has(original.sk)) actions.push(...deleteChannelActions(store, original));
7595
+ for (const [id, row] of channels) {
7596
+ const original = originalChannels.get(id);
7597
+ const referenceCount = channelReferenceCounts.get(id) ?? 0;
7598
+ if (original === void 0) {
7599
+ actions.push(putChannel(store, row, original, referenceCount));
7600
+ actions.push({ Put: {
7601
+ TableName: store.tableName,
7602
+ Item: {
7603
+ pk: DYNAMODB_CHANNEL_NAME_PARTITION,
7604
+ sk: row.name,
7605
+ channel_id: id
7606
+ },
7607
+ ConditionExpression: "attribute_not_exists(#pk)",
7608
+ ExpressionAttributeNames: { "#pk": "pk" }
7609
+ } });
7610
+ } else {
7611
+ if (!rowsEqual(original.row, row)) throw new DynamoDBCommitStateError(`Channel "${id}" cannot be updated`);
7612
+ const delta = referenceCount - original.reference_count;
7613
+ if (delta !== 0) actions.push(updateChannelReferenceCount(store, original, delta));
7614
+ }
7615
+ }
7616
+ const originalBundles = new Map(originalBundleItems.map((item) => [item.sk, item]));
7617
+ for (const original of originalBundleItems) if (!bundles.has(original.sk)) actions.push(deleteAction(store, original));
7618
+ for (const [id, row] of bundles) {
7619
+ const original = originalBundles.get(id);
7620
+ const relationCount = relationCounts.get(id) ?? 0;
7621
+ const ownedPatchCount = ownedPatchCounts.get(id) ?? 0;
7622
+ if (original === void 0) actions.push(putNewBundle(store, row, relationCount, ownedPatchCount));
7623
+ else if (!rowsEqual(original.row, row)) actions.push(putUpdatedBundle(store, original, row, relationCount, ownedPatchCount));
7624
+ else {
7625
+ const relationDelta = relationCount - original.relation_count;
7626
+ const ownedPatchDelta = ownedPatchCount - original.owned_patch_count;
7627
+ if (relationDelta !== 0 || ownedPatchDelta !== 0) actions.push(updateBundleRelation(store, id, relationDelta, ownedPatchDelta));
7628
+ }
7629
+ }
7630
+ const originalPatches = new Map(originalPatchItems.map((item) => [item.sk, item]));
7631
+ for (const original of originalPatchItems) if (!patches.has(original.sk)) actions.push(deletePatch(store, original));
7632
+ for (const [id, row] of patches) {
7633
+ const original = originalPatches.get(id);
7634
+ if (original === void 0 || !rowsEqual(original.row, row)) actions.push(putPatch(store, row, original));
7635
+ }
7636
+ const originalReleases = new Map(originalReleaseItems.map((item) => [item.sk, item]));
7637
+ for (const original of originalReleaseItems) {
7638
+ if (releases.get(original.sk) !== void 0) continue;
7639
+ actions.push(deleteAction(store, original));
7640
+ actions.push({ Delete: {
7641
+ TableName: store.tableName,
7642
+ Key: {
7643
+ pk: DYNAMODB_RELEASE_ID_PARTITION,
7644
+ sk: original.sk
7645
+ },
7646
+ ConditionExpression: "#scopeKey = :scopeKey",
7647
+ ExpressionAttributeNames: { "#scopeKey": "scope_key" },
7648
+ ExpressionAttributeValues: { ":scopeKey": original.row.scope_key }
7649
+ } });
7650
+ }
7651
+ for (const [id, row] of releases) {
7652
+ const original = originalReleases.get(id);
7653
+ if (original === void 0) {
7654
+ actions.push({ Put: {
7655
+ TableName: store.tableName,
7656
+ Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(row)),
7657
+ ConditionExpression: "attribute_not_exists(#pk)",
7658
+ ExpressionAttributeNames: { "#pk": "pk" }
7659
+ } });
7660
+ actions.push({ Put: {
7661
+ TableName: store.tableName,
7662
+ Item: releaseLocatorItem(row),
7663
+ ConditionExpression: "attribute_not_exists(#pk)",
7664
+ ExpressionAttributeNames: { "#pk": "pk" }
7665
+ } });
7666
+ } else if (!rowsEqual(original.row, row)) {
7667
+ const moved = original.row.scope_key !== row.scope_key;
7668
+ if (moved) actions.push(deleteAction(store, original));
7669
+ actions.push({ Put: {
7670
+ TableName: store.tableName,
7671
+ Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(row, original.version + 1)),
7672
+ ConditionExpression: moved ? "attribute_not_exists(#pk)" : "#version = :version",
7673
+ ExpressionAttributeNames: moved ? { "#pk": "pk" } : { "#version": "version" },
7674
+ ...moved ? {} : { ExpressionAttributeValues: { ":version": original.version } }
7675
+ } });
7676
+ if (moved) actions.push({ Put: {
7677
+ TableName: store.tableName,
7678
+ Item: releaseLocatorItem(row),
7679
+ ConditionExpression: "#scopeKey = :scopeKey",
7680
+ ExpressionAttributeNames: { "#scopeKey": "scope_key" },
7681
+ ExpressionAttributeValues: { ":scopeKey": original.row.scope_key }
7682
+ } });
7683
+ }
7684
+ }
7685
+ const originalReleaseCatalogs = new Map(originalReleaseCatalogItems.map((item) => [item.sk, item]));
7686
+ for (const [scopeKey, row] of releaseCatalogs) {
7687
+ const original = originalReleaseCatalogs.get(scopeKey);
7688
+ if (original === void 0 || !rowsEqual(original.row, row)) actions.push({ Put: {
7689
+ TableName: store.tableName,
7690
+ Item: boundedDynamoDBCatalogItem(toDynamoDBReleaseCatalogItem(row, (original?.version ?? 0) + 1)),
7691
+ ConditionExpression: original ? "#version = :version" : "attribute_not_exists(#pk)",
7692
+ ExpressionAttributeNames: original ? { "#version": "version" } : { "#pk": "pk" },
7693
+ ...original ? { ExpressionAttributeValues: { ":version": original.version } } : {}
7694
+ } });
7695
+ }
7696
+ const changedReleaseIds = new Set([...new Set([...originalReleases.keys(), ...releases.keys()])].filter((id) => {
7697
+ const original = originalReleases.get(id)?.row;
7698
+ const next = releases.get(id);
7699
+ return original === void 0 || next === void 0 || !rowsEqual(original, next);
7700
+ }));
7701
+ const changedCatalogScopes = new Set([...releaseCatalogs].flatMap(([scopeKey, row]) => {
7702
+ const original = originalReleaseCatalogs.get(scopeKey)?.row;
7703
+ return original === void 0 || !rowsEqual(original, row) ? [scopeKey] : [];
7704
+ }));
7705
+ for (const expectation of input.expectations ?? []) {
7706
+ if (expectation.model === "releases" && !changedReleaseIds.has(expectation.id)) {
7707
+ const original = originalReleases.get(expectation.id);
7708
+ actions.push({ ConditionCheck: {
7709
+ TableName: store.tableName,
7710
+ Key: original === void 0 ? {
7711
+ pk: DYNAMODB_RELEASE_ID_PARTITION,
7712
+ sk: expectation.id
7713
+ } : {
7714
+ pk: original.pk,
7715
+ sk: original.sk
7716
+ },
7717
+ ConditionExpression: original === void 0 ? "attribute_not_exists(#pk)" : "#version = :version",
7718
+ ExpressionAttributeNames: original === void 0 ? { "#pk": "pk" } : { "#version": "version" },
7719
+ ...original === void 0 ? {} : { ExpressionAttributeValues: { ":version": original.version } }
7720
+ } });
7721
+ }
7722
+ if (expectation.model === "releaseCatalogs" && !changedCatalogScopes.has(expectation.scopeKey)) {
7723
+ const original = originalReleaseCatalogs.get(expectation.scopeKey);
7724
+ actions.push({ ConditionCheck: {
7725
+ TableName: store.tableName,
7726
+ Key: itemKey("release_catalogs", expectation.scopeKey),
7727
+ ConditionExpression: original === void 0 ? "attribute_not_exists(#pk)" : "#version = :version",
7728
+ ExpressionAttributeNames: original === void 0 ? { "#pk": "pk" } : { "#version": "version" },
7729
+ ...original === void 0 ? {} : { ExpressionAttributeValues: { ":version": original.version } }
7730
+ } });
7731
+ }
7732
+ }
7733
+ const counter = metadataUpdate(store, {
7734
+ bundles: bundles.size - originalBundles.size,
7735
+ bundle_patches: patches.size - originalPatches.size
7736
+ });
7737
+ if (counter !== void 0) actions.push(counter);
7738
+ for (const [key, row] of analytics) actions.push({ Put: {
7739
+ TableName: store.tableName,
7740
+ Item: boundedDynamoDBMetadataItem({
7741
+ pk: DYNAMODB_ANALYTICS_PARTITION,
7742
+ sk: key,
7743
+ version: 1,
7744
+ row
7745
+ }),
7746
+ ConditionExpression: "attribute_not_exists(#pk)",
7747
+ ExpressionAttributeNames: { "#pk": "pk" }
7748
+ } });
7749
+ for (const [id, current] of apiKeys) {
7750
+ const original = originalApiKeys.get(id);
7751
+ if (original !== void 0 && rowsEqual(original.row, current.row)) continue;
7752
+ actions.push({ Put: {
7753
+ TableName: store.tableName,
7754
+ Item: boundedDynamoDBMetadataItem(apiKeyItem(current.row, original === void 0 ? 1 : original.version + 1)),
7755
+ ConditionExpression: original ? "#version = :version" : "attribute_not_exists(#pk)",
7756
+ ExpressionAttributeNames: original ? { "#version": "version" } : { "#pk": "pk" },
7757
+ ...original ? { ExpressionAttributeValues: { ":version": original.version } } : {}
7758
+ } });
7759
+ if (original === void 0) actions.push({ Put: {
7760
+ TableName: store.tableName,
7761
+ Item: {
7762
+ pk: DYNAMODB_API_KEY_HASH_PARTITION,
7763
+ sk: current.row.hash,
7764
+ api_key_id: current.row.id
7765
+ },
7766
+ ConditionExpression: "attribute_not_exists(#pk)",
7767
+ ExpressionAttributeNames: { "#pk": "pk" }
7768
+ } });
7769
+ }
7770
+ if (actions.length > 0) await commitDynamoDBTransaction(store, actions);
7771
+ return { committed: true };
7772
+ };
7773
+ const isDynamoDBTransactionConflict = (error) => typeof error === "object" && error !== null && Reflect.get(error, "name") === "TransactionCanceledException";
7774
+ const createDynamoDBCommit = (store) => async (input) => {
7775
+ try {
7776
+ return await compileAndCommitDynamoDBChanges(store, input);
7777
+ } catch (error) {
7778
+ if (!isDynamoDBTransactionConflict(error)) throw error;
7779
+ return compileAndCommitDynamoDBChanges(store, input);
7780
+ }
7781
+ };
7782
+ const DYNAMODB_ANALYTICS_PARTITION = "bundle_events";
7783
+ const DYNAMODB_API_KEY_PARTITION = "api_keys";
7784
+ const DYNAMODB_API_KEY_HASH_PARTITION = "_hot-updater#api-key-hashes";
7785
+ const hasValidBundleEventShape = (value) => {
7786
+ const type = field(value, "type");
7787
+ const fromBundleId = field(value, "from_bundle_id");
7788
+ const updateStrategy = field(value, "update_strategy");
7789
+ return (type === "UPDATE_APPLIED" || type === "RECOVERED" || type === "RELEASE_ADOPTED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null;
7790
+ };
7791
+ const isBundleEventRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && typeof field(value, "install_id") === "string" && isNullableString(field(value, "from_release_id")) && isNullableString(field(value, "to_release_id")) && typeof field(value, "to_bundle_id") === "string" && (field(value, "platform") === "ios" || field(value, "platform") === "android") && typeof field(value, "received_at_ms") === "number" && hasValidBundleEventShape(value);
7792
+ const isApiKeyRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && typeof field(value, "hash") === "string" && typeof field(value, "name") === "string" && typeof field(value, "prefix") === "string" && field(value, "role") === "client" && typeof field(value, "created_at_ms") === "number" && (field(value, "revoked_at_ms") === null || typeof field(value, "revoked_at_ms") === "number");
7793
+ const parseOfficialRowItem = (value, partition, isRow) => {
7794
+ if (value.pk !== partition || typeof value.sk !== "string" || typeof value.version !== "number" || !isRow(value.row)) throw new DynamoDBStoredItemError();
7795
+ return {
7796
+ row: value.row,
7797
+ version: value.version
7798
+ };
7799
+ };
7800
+ const timestampSortKey = (timestampMs) => Math.trunc(timestampMs).toString().padStart(16, "0");
7801
+ const analyticsSortKey = (row) => `${timestampSortKey(row.received_at_ms)}#${row.id}`;
7802
+ const createDynamoDBAnalyticsTable = (store) => ({
7803
+ async append(row) {
7804
+ await store.client.send(new PutCommand({
7805
+ TableName: store.tableName,
7806
+ Item: boundedDynamoDBMetadataItem({
7807
+ pk: DYNAMODB_ANALYTICS_PARTITION,
7808
+ sk: analyticsSortKey(row),
7809
+ version: 1,
7810
+ row
7811
+ }),
7812
+ ConditionExpression: "attribute_not_exists(#pk)",
7813
+ ExpressionAttributeNames: { "#pk": "pk" }
7814
+ }));
7815
+ },
7816
+ async scan(input) {
7817
+ if (input.limit <= 0) return [];
7818
+ if (input.after !== void 0 && input.after.receivedAtMs >= input.beforeReceivedAtMs) return [];
7819
+ const rows = [];
7820
+ const afterKey = input.after === void 0 ? void 0 : `${timestampSortKey(input.after.receivedAtMs)}#${input.after.id}`;
7821
+ let exclusiveStartKey;
7822
+ do {
7823
+ const page = await store.client.send(new QueryCommand({
7824
+ TableName: store.tableName,
7825
+ ExclusiveStartKey: exclusiveStartKey,
7826
+ KeyConditionExpression: afterKey === void 0 ? "#pk = :pk AND #sk < :before" : "#pk = :pk AND #sk BETWEEN :after AND :before",
7827
+ ExpressionAttributeNames: {
7828
+ "#pk": "pk",
7829
+ "#sk": "sk"
7830
+ },
7831
+ ExpressionAttributeValues: {
7832
+ ":before": `${timestampSortKey(input.beforeReceivedAtMs)}#`,
7833
+ ":pk": DYNAMODB_ANALYTICS_PARTITION,
7834
+ ...afterKey === void 0 ? {} : { ":after": afterKey }
7835
+ },
7836
+ Limit: input.limit - rows.length + (afterKey === void 0 ? 0 : 1),
7837
+ ScanIndexForward: true
7838
+ }));
7839
+ for (const item of page.Items ?? []) {
7840
+ if (item.sk === afterKey) continue;
7841
+ rows.push(parseOfficialRowItem(item, DYNAMODB_ANALYTICS_PARTITION, isBundleEventRow).row);
7842
+ if (rows.length === input.limit) return rows;
7843
+ }
7844
+ exclusiveStartKey = page.LastEvaluatedKey;
7845
+ } while (exclusiveStartKey !== void 0);
7846
+ return rows;
7847
+ }
7848
+ });
7849
+ const apiKeyItem = (row, version) => ({
7850
+ pk: DYNAMODB_API_KEY_PARTITION,
7851
+ sk: row.id,
7852
+ version,
7853
+ row
7854
+ });
7855
+ const loadApiKey = async (store, id) => {
7856
+ const result = await store.client.send(new GetCommand({
7857
+ TableName: store.tableName,
7858
+ Key: {
7859
+ pk: DYNAMODB_API_KEY_PARTITION,
7860
+ sk: id
7861
+ },
7862
+ ConsistentRead: true
7863
+ }));
7864
+ return result.Item === void 0 ? null : parseOfficialRowItem(result.Item, DYNAMODB_API_KEY_PARTITION, isApiKeyRow);
7865
+ };
7866
+ const createDynamoDBApiKeyTable = (store) => {
7867
+ const findByHash = async (hash) => {
7868
+ const lookup = await store.client.send(new GetCommand({
7869
+ TableName: store.tableName,
7870
+ Key: {
7871
+ pk: DYNAMODB_API_KEY_HASH_PARTITION,
7872
+ sk: hash
7873
+ },
7874
+ ConsistentRead: true
7875
+ }));
7876
+ const id = lookup.Item?.api_key_id;
7877
+ if (lookup.Item === void 0) return null;
7878
+ if (typeof id !== "string") throw new DynamoDBStoredItemError();
7879
+ return (await loadApiKey(store, id))?.row ?? null;
7880
+ };
7881
+ return {
7882
+ async create(row) {
7883
+ if (await findByHash(row.hash) !== null) return "existing";
7884
+ try {
7885
+ await commitDynamoDBTransaction(store, [{ Put: {
7886
+ TableName: store.tableName,
7887
+ Item: boundedDynamoDBMetadataItem(apiKeyItem(row, 1)),
7888
+ ConditionExpression: "attribute_not_exists(#pk)",
7889
+ ExpressionAttributeNames: { "#pk": "pk" }
7890
+ } }, { Put: {
7891
+ TableName: store.tableName,
7892
+ Item: {
7893
+ pk: DYNAMODB_API_KEY_HASH_PARTITION,
7894
+ sk: row.hash,
7895
+ api_key_id: row.id
7896
+ },
7897
+ ConditionExpression: "attribute_not_exists(#pk)",
7898
+ ExpressionAttributeNames: { "#pk": "pk" }
7899
+ } }]);
7900
+ return "created";
7901
+ } catch (error) {
7902
+ if (await findByHash(row.hash) !== null) return "existing";
7903
+ throw error;
7904
+ }
7905
+ },
7906
+ findByHash,
7907
+ async list() {
7908
+ const rows = [];
7909
+ let exclusiveStartKey;
7910
+ do {
7911
+ const page = await store.client.send(new QueryCommand({
7912
+ TableName: store.tableName,
7913
+ ExclusiveStartKey: exclusiveStartKey,
7914
+ KeyConditionExpression: "#pk = :pk",
7915
+ ExpressionAttributeNames: { "#pk": "pk" },
7916
+ ExpressionAttributeValues: { ":pk": DYNAMODB_API_KEY_PARTITION },
7917
+ ConsistentRead: true
7918
+ }));
7919
+ rows.push(...(page.Items ?? []).map((item) => parseOfficialRowItem(item, DYNAMODB_API_KEY_PARTITION, isApiKeyRow).row));
7920
+ exclusiveStartKey = page.LastEvaluatedKey;
7921
+ } while (exclusiveStartKey !== void 0);
7922
+ return rows.sort((left, right) => right.created_at_ms - left.created_at_ms || left.id.localeCompare(right.id));
7923
+ },
7924
+ async revoke({ id, revokedAtMs }) {
7925
+ const current = await loadApiKey(store, id);
7926
+ if (current === null) return null;
7927
+ const row = {
7928
+ ...current.row,
7929
+ revoked_at_ms: revokedAtMs
7930
+ };
7931
+ await store.client.send(new PutCommand({
7932
+ TableName: store.tableName,
7933
+ Item: boundedDynamoDBMetadataItem(apiKeyItem(row, current.version + 1)),
7934
+ ConditionExpression: "#version = :currentVersion",
7935
+ ExpressionAttributeNames: { "#version": "version" },
7936
+ ExpressionAttributeValues: { ":currentVersion": current.version }
7937
+ }));
7938
+ return row;
7939
+ }
7940
+ };
7941
+ };
7942
+ const DYNAMODB_UPDATE_INDEX_NAME = "hot-updater-update-index";
7943
+ const dynamoDB = (config) => {
7944
+ const { apiBasePath = "/release-catalogs", cloudfrontDistributionId, shouldWaitForInvalidation = false, tableName, ...clientConfig } = config;
7945
+ const client = DynamoDBDocumentClient.from(new DynamoDBClient(clientConfig), { marshallOptions: { removeUndefinedValues: true } });
7946
+ const cloudFront = cloudfrontDistributionId ? new CloudFrontClient({
7947
+ credentials: clientConfig.credentials,
7948
+ region: clientConfig.region
7949
+ }) : null;
7950
+ const store = {
7951
+ client,
7952
+ tableName
7953
+ };
7954
+ const crud = createDynamoDBCrud(store, DYNAMODB_UPDATE_INDEX_NAME);
7955
+ const invalidateUpdateRoutes = async () => {
7956
+ if (!cloudFront || !cloudfrontDistributionId) return;
7957
+ try {
7958
+ await invalidateCloudFront(cloudFront, cloudfrontDistributionId, [`${apiBasePath.replace(/\/+$/, "")}/*`], { shouldWait: shouldWaitForInvalidation });
7959
+ } catch (error) {
7960
+ console.warn("[hot-updater/aws] CloudFront invalidation failed; continuing without cache invalidation.", {
7961
+ distributionId: cloudfrontDistributionId,
7962
+ error: error instanceof Error ? error.message : "Unknown error"
7963
+ });
7964
+ }
7965
+ };
7966
+ const adapter = createDatabasePluginAdapter("dynamoDB", {
7967
+ ...crud,
7968
+ commit: createDynamoDBCommit(store),
7969
+ dispose: async () => {
7970
+ client.destroy();
7971
+ cloudFront?.destroy();
7972
+ }
7973
+ });
7974
+ return createDatabasePlugin({
7975
+ name: "dynamoDB",
7976
+ models: {
7977
+ ...adapter.models,
7978
+ bundlePatches: createDynamoDBBundlePatchTable(store, DYNAMODB_UPDATE_INDEX_NAME),
7979
+ analytics: createDynamoDBAnalyticsTable(store),
7980
+ apiKeys: createDynamoDBApiKeyTable(store)
7981
+ },
7982
+ async commit(input) {
7983
+ const result = await adapter.commit(input);
7984
+ if (result.committed && input.changes.some(({ model }) => model === "bundles" || model === "bundlePatches")) await invalidateUpdateRoutes();
7985
+ return result;
7986
+ },
7987
+ dispose: adapter.dispose
7988
+ });
7989
+ };
7990
+ //#endregion
7991
+ //#region iac/init/index.ts
7992
+ const AWS_AUTH_MODES = [
7993
+ "local-session",
7994
+ "shared-profile",
7995
+ "sso",
7996
+ "account"
7997
+ ];
7998
+ const isAwsAuthMode = (value) => value !== void 0 && AWS_AUTH_MODES.some((mode) => mode === value);
7999
+ const AWS_REGION_VALUES = [
8000
+ "af-south-1",
8001
+ "ap-east-1",
8002
+ "ap-east-2",
8003
+ "ap-northeast-1",
8004
+ "ap-northeast-2",
8005
+ "ap-northeast-3",
8006
+ "ap-south-1",
8007
+ "ap-south-2",
8008
+ "ap-southeast-1",
8009
+ "ap-southeast-2",
8010
+ "ap-southeast-3",
8011
+ "ap-southeast-4",
8012
+ "ap-southeast-5",
8013
+ "ap-southeast-6",
8014
+ "ap-southeast-7",
8015
+ "ca-central-1",
8016
+ "ca-west-1",
8017
+ "cn-north-1",
8018
+ "cn-northwest-1",
8019
+ "eu-central-1",
8020
+ "eu-central-2",
8021
+ "eu-north-1",
8022
+ "eu-south-1",
8023
+ "eu-south-2",
8024
+ "eu-west-1",
8025
+ "eu-west-2",
8026
+ "eu-west-3",
8027
+ "il-central-1",
8028
+ "me-central-1",
8029
+ "me-south-1",
8030
+ "mx-central-1",
8031
+ "sa-east-1",
8032
+ "us-east-1",
8033
+ "us-east-2",
8034
+ "us-gov-east-1",
8035
+ "us-gov-west-1",
8036
+ "us-west-1",
8037
+ "us-west-2"
8038
+ ];
8039
+ const isAwsRegionValue = (value) => value !== void 0 && AWS_REGION_VALUES.some((region) => region === value);
8040
+ const initProvider = {
8041
+ label: "AWS + Lambda@Edge",
8042
+ inputs: {
8043
+ dynamodbTableName: {
8044
+ envKey: "HOT_UPDATER_DYNAMODB_TABLE_NAME",
8045
+ help: "DynamoDB metadata table name",
8046
+ prompt: {
8047
+ defaultValue: "hot-updater-v1",
8048
+ message: "Enter the DynamoDB table name",
8049
+ placeholder: "hot-updater-v1",
8050
+ type: "text"
8051
+ }
8052
+ },
8053
+ authMode: {
8054
+ envKey: "HOT_UPDATER_AWS_AUTH_MODE",
8055
+ help: "AWS authentication mode: local-session, shared-profile, sso, or account",
8056
+ prompt: {
8057
+ message: "Select the mode to login to AWS",
8058
+ type: "select"
8059
+ },
8060
+ validate: isAwsAuthMode
8061
+ },
8062
+ profile: {
8063
+ envKey: "HOT_UPDATER_AWS_PROFILE",
8064
+ help: "AWS profile name",
8065
+ requiredWhen: (inputs) => inputs.authMode === "shared-profile" || inputs.authMode === "sso",
8066
+ requirementHint: "required for shared-profile and sso auth",
8067
+ prompt: {
8068
+ defaultValue: "default",
8069
+ message: "Enter the AWS profile name",
8070
+ placeholder: "default",
8071
+ type: "text"
8072
+ }
8073
+ },
8074
+ accessKeyId: {
8075
+ envKey: "HOT_UPDATER_S3_ACCESS_KEY_ID",
8076
+ help: "AWS access key ID",
8077
+ persistence: "with-consent",
8078
+ requiredWhen: (inputs) => inputs.authMode === "account",
8079
+ requirementHint: "required for account auth",
8080
+ prompt: {
8081
+ message: "Enter your AWS Access Key ID",
8082
+ placeholder: "AKIA...",
8083
+ type: "text"
8084
+ }
8085
+ },
8086
+ secretAccessKey: {
8087
+ envKey: "HOT_UPDATER_S3_SECRET_ACCESS_KEY",
8088
+ help: "AWS secret access key",
8089
+ persistence: "with-consent",
8090
+ requiredWhen: (inputs) => inputs.authMode === "account",
8091
+ requirementHint: "required for account auth",
8092
+ prompt: {
8093
+ message: "Enter your AWS Secret Access Key",
8094
+ type: "password"
8095
+ }
8096
+ },
8097
+ bucketName: {
8098
+ envKey: "HOT_UPDATER_S3_BUCKET_NAME",
8099
+ help: "S3 bucket name",
8100
+ prompt: {
8101
+ defaultValue: "hot-updater-storage",
8102
+ message: "Enter the name of the new S3 Bucket",
8103
+ placeholder: "hot-updater-storage",
8104
+ type: "text"
8105
+ }
8106
+ },
8107
+ bucketRegion: {
8108
+ envKey: "HOT_UPDATER_S3_REGION",
8109
+ help: "S3 bucket region",
8110
+ prompt: {
8111
+ message: "Enter AWS region for the S3 bucket",
8112
+ type: "select"
8113
+ },
8114
+ validate: isAwsRegionValue
8115
+ },
8116
+ lambdaName: {
8117
+ envKey: "HOT_UPDATER_AWS_LAMBDA_NAME",
8118
+ help: "Lambda@Edge function name",
8119
+ prompt: {
8120
+ defaultValue: "hot-updater-v1-edge",
8121
+ message: "Enter the name of the Lambda@Edge function",
8122
+ placeholder: "hot-updater-v1-edge",
8123
+ type: "text"
8124
+ }
8125
+ },
8126
+ distributionId: {
8127
+ envKey: "HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID",
8128
+ help: "Existing CloudFront distribution ID",
8129
+ optional: true
8130
+ }
8131
+ }
8132
+ };
8133
+ //#endregion
8134
+ //#region iac/regionLocationMap.ts
8135
+ const regionLocationMap = {
8136
+ "af-south-1": "Cape Town, South Africa",
8137
+ "ap-east-1": "Hong Kong",
8138
+ "ap-east-2": "Taipei, Taiwan",
8139
+ "ap-northeast-1": "Tokyo, Japan",
8140
+ "ap-northeast-2": "Seoul, South Korea",
8141
+ "ap-northeast-3": "Osaka, Japan",
8142
+ "ap-south-1": "Mumbai, India",
8143
+ "ap-south-2": "Hyderabad, India",
8144
+ "ap-southeast-1": "Singapore",
8145
+ "ap-southeast-2": "Sydney, Australia",
8146
+ "ap-southeast-3": "Jakarta, Indonesia",
8147
+ "ap-southeast-4": "Melbourne, Australia",
8148
+ "ap-southeast-5": "Auckland, New Zealand",
8149
+ "ap-southeast-6": "New Zealand",
8150
+ "ap-southeast-7": "Thailand",
8151
+ "ca-central-1": "Montreal, Canada",
8152
+ "ca-west-1": "Calgary, Canada",
8153
+ "cn-north-1": "Beijing, China",
8154
+ "cn-northwest-1": "Ningxia, China",
8155
+ "eu-central-1": "Frankfurt, Germany",
8156
+ "eu-north-1": "Stockholm, Sweden",
8157
+ "eu-south-1": "Milan, Italy",
8158
+ "eu-south-2": "Spain",
8159
+ "eu-west-1": "Ireland",
8160
+ "eu-west-2": "London, UK",
8161
+ "eu-west-3": "Paris, France",
8162
+ "il-central-1": "Tel Aviv, Israel",
8163
+ "me-central-1": "UAE",
8164
+ "me-south-1": "Bahrain",
8165
+ "mx-central-1": "Mexico",
8166
+ "sa-east-1": "São Paulo, Brazil",
8167
+ "us-east-1": "Virginia, USA",
8168
+ "us-east-2": "Ohio, USA",
8169
+ "us-gov-east-1": "US Gov East (Virginia, USA)",
8170
+ "us-gov-west-1": "US Gov West (Oregon, USA)",
8171
+ "us-west-1": "California, USA",
8172
+ "us-west-2": "Oregon, USA",
8173
+ "eu-central-2": "Zurich, Switzerland"
8174
+ };
8175
+ //#endregion
8176
+ //#region iac/awsInitInputs.ts
8177
+ const resolveAwsInitInputs = (existingEnv) => {
8178
+ const { inputs } = initProvider;
8179
+ const savedAuthMode = resolveInitProviderInput(existingEnv, inputs.authMode);
8180
+ return {
6402
8181
  accessKeyId: resolveInitProviderInput(existingEnv, inputs.accessKeyId),
6403
8182
  authMode: isAwsAuthMode(savedAuthMode) ? savedAuthMode : void 0,
6404
8183
  bucketName: resolveInitProviderInput(existingEnv, inputs.bucketName),
6405
8184
  bucketRegion: resolveInitProviderInput(existingEnv, inputs.bucketRegion),
6406
8185
  distributionId: resolveInitProviderInput(existingEnv, inputs.distributionId),
8186
+ dynamodbTableName: resolveInitProviderInput(existingEnv, inputs.dynamodbTableName),
6407
8187
  lambdaName: resolveInitProviderInput(existingEnv, inputs.lambdaName),
6408
- migrationApproved: resolveInitProviderInput(existingEnv, inputs.migrationApproved),
6409
8188
  profile: resolveInitProviderInput(existingEnv, inputs.profile),
6410
8189
  secretAccessKey: resolveInitProviderInput(existingEnv, inputs.secretAccessKey)
6411
8190
  };
@@ -6583,6 +8362,109 @@ const resolveAwsAuth = async (existingEnv, nonInteractive = false) => {
6583
8362
  }
6584
8363
  };
6585
8364
  //#endregion
8365
+ //#region iac/awsInfrastructureNames.ts
8366
+ const getAwsV1SsmParameterName = (lambdaName) => `/hot-updater/v1/${lambdaName}/keypair`;
8367
+ //#endregion
8368
+ //#region iac/awsInfrastructureState.ts
8369
+ const AWS_DISTRIBUTION_VERSION_PATHS = ["/version", "/api/check-update/version"];
8370
+ const readAwsDistributionGeneration = (payload) => {
8371
+ if (typeof payload !== "object" || payload === null) return "unknown";
8372
+ if ("infrastructureGeneration" in payload) return payload.infrastructureGeneration === 1 ? "v1" : "unknown";
8373
+ return "version" in payload && typeof payload.version === "string" ? "v0" : "unknown";
8374
+ };
8375
+ const resolveAwsDistributionGeneration = async ({ domainName, fetchImpl = fetch }) => {
8376
+ const generations = await Promise.all(AWS_DISTRIBUTION_VERSION_PATHS.map(async (path) => {
8377
+ try {
8378
+ const response = await fetchImpl(`https://${domainName}${path}`);
8379
+ if (!response.ok) return "unknown";
8380
+ return readAwsDistributionGeneration(await response.json().catch(() => void 0));
8381
+ } catch {
8382
+ return "unknown";
8383
+ }
8384
+ }));
8385
+ if (generations.includes("v1")) return "v1";
8386
+ if (generations.includes("v0")) return "v0";
8387
+ return "unknown";
8388
+ };
8389
+ const assertAwsInfrastructureGeneration = async (input) => {
8390
+ const versionUrl = `https://${input.domainName}/version`;
8391
+ await assertInfrastructureGenerationAtUrl({
8392
+ fetchImpl: input.fetchImpl,
8393
+ provider: "AWS",
8394
+ resource: versionUrl,
8395
+ versionUrl
8396
+ });
8397
+ };
8398
+ const createVersionEvent = () => ({ Records: [{ cf: {
8399
+ config: {
8400
+ distributionDomainName: "hot-updater-init.invalid",
8401
+ distributionId: "hot-updater-init",
8402
+ eventType: "origin-request",
8403
+ requestId: "hot-updater-init"
8404
+ },
8405
+ request: {
8406
+ clientIp: "127.0.0.1",
8407
+ headers: { host: [{
8408
+ key: "host",
8409
+ value: "hot-updater-init.invalid"
8410
+ }] },
8411
+ method: "GET",
8412
+ querystring: "",
8413
+ uri: "/version"
8414
+ }
8415
+ } }] });
8416
+ const readLambdaResponsePayload = (payload) => {
8417
+ if (!payload) return void 0;
8418
+ const response = JSON.parse(new TextDecoder().decode(payload));
8419
+ if (typeof response !== "object" || response === null) return void 0;
8420
+ const status = Reflect.get(response, "status");
8421
+ if (status !== 200 && status !== "200") {
8422
+ if (status === 404 || status === "404") return void 0;
8423
+ throw new InitError(`Could not verify the AWS Lambda infrastructure generation: HTTP ${String(status)}`);
8424
+ }
8425
+ const body = Reflect.get(response, "body");
8426
+ if (typeof body === "string") return JSON.parse(body);
8427
+ if (typeof body !== "object" || body === null) return void 0;
8428
+ const data = Reflect.get(body, "data");
8429
+ const encoding = Reflect.get(body, "encoding");
8430
+ if (typeof data !== "string") return void 0;
8431
+ return JSON.parse(encoding === "base64" ? Buffer.from(data, "base64").toString("utf8") : data);
8432
+ };
8433
+ const assertAwsLambdaCanInitialize = async ({ credentials, lambdaClient = new Lambda({
8434
+ credentials,
8435
+ region: "us-east-1"
8436
+ }), lambdaName }) => {
8437
+ try {
8438
+ await lambdaClient.getFunctionConfiguration({ FunctionName: lambdaName });
8439
+ } catch (error) {
8440
+ if (error instanceof Error && error.name === "ResourceNotFoundException") return;
8441
+ throw new InitError(`Could not check whether AWS Lambda ${lambdaName} already exists: ${error instanceof Error ? error.message : String(error)}`);
8442
+ }
8443
+ const resource = `Lambda function ${lambdaName}`;
8444
+ let result;
8445
+ try {
8446
+ result = await lambdaClient.invoke({
8447
+ FunctionName: lambdaName,
8448
+ Payload: Buffer.from(JSON.stringify(createVersionEvent()))
8449
+ });
8450
+ } catch (error) {
8451
+ throw new InitError(`Could not verify the AWS infrastructure generation at ${resource}: ${error instanceof Error ? error.message : String(error)}`);
8452
+ }
8453
+ if (result.FunctionError) throw new InitError(`Could not verify the AWS infrastructure generation at ${resource}: ${result.FunctionError}`);
8454
+ let payload;
8455
+ try {
8456
+ payload = readLambdaResponsePayload(result.Payload);
8457
+ } catch (error) {
8458
+ if (error instanceof InitError) throw error;
8459
+ payload = void 0;
8460
+ }
8461
+ assertInfrastructureGenerationPayload({
8462
+ payload,
8463
+ provider: "AWS",
8464
+ resource
8465
+ });
8466
+ };
8467
+ //#endregion
6586
8468
  //#region ../../node_modules/.pnpm/es-toolkit@1.47.0/node_modules/es-toolkit/dist/_internal/globalThis.mjs
6587
8469
  const globalThis_ = typeof globalThis === "object" && globalThis || typeof window === "object" && window || typeof self === "object" && self || typeof global === "object" && global || (function() {
6588
8470
  return this;
@@ -6628,185 +8510,34 @@ var AbortError = class extends DOMException$1 {
6628
8510
  * const { signal } = controller;
6629
8511
  *
6630
8512
  * setTimeout(() => controller.abort(), 50); // Will cancel the delay after 50ms
6631
- * try {
6632
- * await delay(100, { signal });
6633
- * } catch (error) {
6634
- * console.error(error); // Will log 'AbortError'
6635
- * }
6636
- * }
6637
- */
6638
- function delay(ms, { signal } = {}) {
6639
- return new Promise((resolve, reject) => {
6640
- const abortError = () => {
6641
- reject(new AbortError());
6642
- };
6643
- const abortHandler = () => {
6644
- clearTimeout(timeoutId);
6645
- abortError();
6646
- };
6647
- if (signal?.aborted) return abortError();
6648
- const timeoutId = setTimeout(() => {
6649
- signal?.removeEventListener("abort", abortHandler);
6650
- resolve();
6651
- }, ms);
6652
- signal?.addEventListener("abort", abortHandler, { once: true });
6653
- });
6654
- }
6655
- //#endregion
6656
- //#region ../../node_modules/.pnpm/es-toolkit@1.47.0/node_modules/es-toolkit/dist/predicate/isPlainObject.mjs
6657
- /**
6658
- * Checks if a given value is a plain object.
6659
- *
6660
- * @param {object} value - The value to check.
6661
- * @returns {value is Record<PropertyKey, any>} - True if the value is a plain object, otherwise false.
6662
- *
6663
- * @example
6664
- * ```typescript
6665
- * // ✅👇 True
6666
- *
6667
- * isPlainObject({ }); // ✅
6668
- * isPlainObject({ key: 'value' }); // ✅
6669
- * isPlainObject({ key: new Date() }); // ✅
6670
- * isPlainObject(new Object()); // ✅
6671
- * isPlainObject(Object.create(null)); // ✅
6672
- * isPlainObject({ nested: { key: true} }); // ✅
6673
- * isPlainObject(new Proxy({}, {})); // ✅
6674
- * isPlainObject({ [Symbol('tag')]: 'A' }); // ✅
6675
- *
6676
- * // ✅👇 (cross-realms, node context, workers, ...)
6677
- * const runInNewContext = await import('node:vm').then(
6678
- * (mod) => mod.runInNewContext
6679
- * );
6680
- * isPlainObject(runInNewContext('({})')); // ✅
6681
- *
6682
- * // ❌👇 False
6683
- *
6684
- * class Test { };
6685
- * isPlainObject(new Test()) // ❌
6686
- * isPlainObject(10); // ❌
6687
- * isPlainObject(null); // ❌
6688
- * isPlainObject('hello'); // ❌
6689
- * isPlainObject([]); // ❌
6690
- * isPlainObject(new Date()); // ❌
6691
- * isPlainObject(new Uint8Array([1])); // ❌
6692
- * isPlainObject(Buffer.from('ABC')); // ❌
6693
- * isPlainObject(Promise.resolve({})); // ❌
6694
- * isPlainObject(Object.create({})); // ❌
6695
- * isPlainObject(new (class Cls {})); // ❌
6696
- * isPlainObject(globalThis); // ❌,
6697
- * ```
6698
- */
6699
- function isPlainObject(value) {
6700
- if (!value || typeof value !== "object") return false;
6701
- const proto = Object.getPrototypeOf(value);
6702
- if (!(proto === null || proto === Object.prototype || Object.getPrototypeOf(proto) === null)) return false;
6703
- return Object.prototype.toString.call(value) === "[object Object]";
6704
- }
6705
- //#endregion
6706
- //#region ../../node_modules/.pnpm/es-toolkit@1.47.0/node_modules/es-toolkit/dist/_internal/isUnsafeProperty.mjs
6707
- /**
6708
- * Checks if a property key is unsafe to modify directly.
6709
- *
6710
- * This function is used in functions like `merge` to prevent prototype pollution attacks
6711
- * by identifying property keys that could modify the object's prototype chain or constructor.
6712
- *
6713
- * @param key - The property key to check
6714
- * @returns `true` if the property is unsafe to modify directly, `false` otherwise
6715
- * @internal
6716
- */
6717
- function isUnsafeProperty(key) {
6718
- return key === "__proto__";
6719
- }
6720
- //#endregion
6721
- //#region ../../node_modules/.pnpm/es-toolkit@1.47.0/node_modules/es-toolkit/dist/object/merge.mjs
6722
- /**
6723
- * Merges the properties of the source object into the target object.
6724
- *
6725
- * This function performs a deep merge, meaning nested objects and arrays are merged recursively.
6726
- * If a property in the source object is an array or an object and the corresponding property in the target object is also an array or object, they will be merged.
6727
- * If a property in the source object is undefined, it will not overwrite a defined property in the target object.
6728
- *
6729
- * Note that this function mutates the target object.
6730
- *
6731
- * @param {T} target - The target object into which the source object properties will be merged. This object is modified in place.
6732
- * @param {S} source - The source object whose properties will be merged into the target object.
6733
- * @returns {T & S} The updated target object with properties from the source object merged in.
6734
- *
6735
- * @template T - Type of the target object.
6736
- * @template S - Type of the source object.
6737
- *
6738
- * @example
6739
- * const target = { a: 1, b: { x: 1, y: 2 } };
6740
- * const source = { b: { y: 3, z: 4 }, c: 5 };
6741
- *
6742
- * const result = merge(target, source);
6743
- * console.log(result);
6744
- * // Output: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
6745
- *
6746
- * @example
6747
- * const target = { a: [1, 2], b: { x: 1 } };
6748
- * const source = { a: [3], b: { y: 2 } };
6749
- *
6750
- * const result = merge(target, source);
6751
- * console.log(result);
6752
- * // Output: { a: [3, 2], b: { x: 1, y: 2 } }
6753
- *
6754
- * @example
6755
- * const target = { a: null };
6756
- * const source = { a: [1, 2, 3] };
6757
- *
6758
- * const result = merge(target, source);
6759
- * console.log(result);
6760
- * // Output: { a: [1, 2, 3] }
6761
- */
6762
- function merge(target, source) {
6763
- const sourceKeys = Object.keys(source);
6764
- for (let i = 0; i < sourceKeys.length; i++) {
6765
- const key = sourceKeys[i];
6766
- if (isUnsafeProperty(key)) continue;
6767
- const sourceValue = source[key];
6768
- const targetValue = target[key];
6769
- if (isMergeableValue(sourceValue) && isMergeableValue(targetValue)) target[key] = merge(targetValue, sourceValue);
6770
- else if (Array.isArray(sourceValue)) target[key] = merge([], sourceValue);
6771
- else if (isPlainObject(sourceValue)) target[key] = merge({}, sourceValue);
6772
- else if (targetValue === void 0 || sourceValue !== void 0) target[key] = sourceValue;
6773
- }
6774
- return target;
6775
- }
6776
- function isMergeableValue(value) {
6777
- return isPlainObject(value) || Array.isArray(value);
6778
- }
6779
- //#endregion
6780
- //#region ../../node_modules/.pnpm/es-toolkit@1.47.0/node_modules/es-toolkit/dist/object/omit.mjs
6781
- /**
6782
- * Creates a new object with specified keys omitted.
6783
- *
6784
- * This function takes an object and an array of keys, and returns a new object that
6785
- * excludes the properties corresponding to the specified keys.
6786
- *
6787
- * @template T - The type of object.
6788
- * @template K - The type of keys in object.
6789
- * @param {T} obj - The object to omit keys from.
6790
- * @param {K[]} keys - An array of keys to be omitted from the object.
6791
- * @returns {Omit<T, K>} A new object with the specified keys omitted.
6792
- *
6793
- * @example
6794
- * const obj = { a: 1, b: 2, c: 3 };
6795
- * const result = omit(obj, ['b', 'c']);
6796
- * // result will be { a: 1 }
8513
+ * try {
8514
+ * await delay(100, { signal });
8515
+ * } catch (error) {
8516
+ * console.error(error); // Will log 'AbortError'
8517
+ * }
8518
+ * }
6797
8519
  */
6798
- function omit(obj, keys) {
6799
- const result = { ...obj };
6800
- for (let i = 0; i < keys.length; i++) {
6801
- const key = keys[i];
6802
- delete result[key];
6803
- }
6804
- return result;
8520
+ function delay(ms, { signal } = {}) {
8521
+ return new Promise((resolve, reject) => {
8522
+ const abortError = () => {
8523
+ reject(new AbortError());
8524
+ };
8525
+ const abortHandler = () => {
8526
+ clearTimeout(timeoutId);
8527
+ abortError();
8528
+ };
8529
+ if (signal?.aborted) return abortError();
8530
+ const timeoutId = setTimeout(() => {
8531
+ signal?.removeEventListener("abort", abortHandler);
8532
+ resolve();
8533
+ }, ms);
8534
+ signal?.addEventListener("abort", abortHandler, { once: true });
8535
+ });
6805
8536
  }
6806
8537
  //#endregion
6807
8538
  //#region iac/cloudfrontDistributionConfig.ts
6808
8539
  const HOT_UPDATER_SHARED_CACHE_POLICY_CONFIG = {
6809
- Name: "HotUpdaterOriginCacheControl",
8540
+ Name: "HotUpdaterOriginCacheControlV2",
6810
8541
  Comment: "Honor origin Cache-Control without forwarding viewer Host/cookies/query strings",
6811
8542
  DefaultTTL: 0,
6812
8543
  MaxTTL: 31536e3,
@@ -6814,11 +8545,50 @@ const HOT_UPDATER_SHARED_CACHE_POLICY_CONFIG = {
6814
8545
  ParametersInCacheKeyAndForwardedToOrigin: {
6815
8546
  EnableAcceptEncodingBrotli: true,
6816
8547
  EnableAcceptEncodingGzip: true,
6817
- HeadersConfig: { HeaderBehavior: "none" },
8548
+ HeadersConfig: {
8549
+ HeaderBehavior: "whitelist",
8550
+ Headers: {
8551
+ Quantity: 1,
8552
+ Items: ["x-api-key"]
8553
+ }
8554
+ },
8555
+ CookiesConfig: { CookieBehavior: "none" },
8556
+ QueryStringsConfig: { QueryStringBehavior: "none" }
8557
+ }
8558
+ };
8559
+ const HOT_UPDATER_RELEASE_CATALOG_CACHE_POLICY_CONFIG = {
8560
+ Name: "HotUpdaterReleaseCatalogV1",
8561
+ Comment: "Cache Release catalogs by canonical path, API key, and encoding",
8562
+ DefaultTTL: 0,
8563
+ MaxTTL: 5,
8564
+ MinTTL: 0,
8565
+ ParametersInCacheKeyAndForwardedToOrigin: {
8566
+ EnableAcceptEncodingBrotli: true,
8567
+ EnableAcceptEncodingGzip: true,
8568
+ HeadersConfig: {
8569
+ HeaderBehavior: "whitelist",
8570
+ Headers: {
8571
+ Quantity: 1,
8572
+ Items: ["x-api-key"]
8573
+ }
8574
+ },
6818
8575
  CookiesConfig: { CookieBehavior: "none" },
6819
8576
  QueryStringsConfig: { QueryStringBehavior: "none" }
6820
8577
  }
6821
8578
  };
8579
+ const HOT_UPDATER_ORIGIN_REQUEST_POLICY_CONFIG = {
8580
+ Name: "HotUpdaterManagedApiOriginRequestV2",
8581
+ Comment: "Forward managed API bodies, query strings, and the API key",
8582
+ HeadersConfig: {
8583
+ HeaderBehavior: "whitelist",
8584
+ Headers: {
8585
+ Quantity: 2,
8586
+ Items: ["content-type", "x-api-key"]
8587
+ }
8588
+ },
8589
+ CookiesConfig: { CookieBehavior: "none" },
8590
+ QueryStringsConfig: { QueryStringBehavior: "all" }
8591
+ };
6822
8592
  const READ_ONLY_METHODS = {
6823
8593
  Quantity: 2,
6824
8594
  Items: ["HEAD", "GET"],
@@ -6827,6 +8597,19 @@ const READ_ONLY_METHODS = {
6827
8597
  Items: ["HEAD", "GET"]
6828
8598
  }
6829
8599
  };
8600
+ const API_METHODS = {
8601
+ Quantity: 7,
8602
+ Items: [
8603
+ "HEAD",
8604
+ "DELETE",
8605
+ "POST",
8606
+ "GET",
8607
+ "OPTIONS",
8608
+ "PUT",
8609
+ "PATCH"
8610
+ ],
8611
+ CachedMethods: READ_ONLY_METHODS.CachedMethods
8612
+ };
6830
8613
  const EMPTY_FUNCTION_ASSOCIATIONS = { Quantity: 0 };
6831
8614
  const EMPTY_LAMBDA_FUNCTION_ASSOCIATIONS = { Quantity: 0 };
6832
8615
  const HOT_UPDATER_BEHAVIOR_BASE = {
@@ -6837,9 +8620,14 @@ const HOT_UPDATER_BEHAVIOR_BASE = {
6837
8620
  FieldLevelEncryptionId: "",
6838
8621
  AllowedMethods: READ_ONLY_METHODS
6839
8622
  };
6840
- const HOT_UPDATER_CACHE_BEHAVIOR_PATH = "/api/check-update/*";
8623
+ const HOT_UPDATER_CACHE_BEHAVIOR_PATHS = [
8624
+ "/events",
8625
+ "/artifacts/*",
8626
+ "/version"
8627
+ ];
8628
+ const HOT_UPDATER_RELEASE_CATALOG_BEHAVIOR_PATHS = ["/release-catalogs/*"];
6841
8629
  const omitLegacyCacheFields = (value) => {
6842
- const { ForwardedValues: _forwardedValues, MinTTL: _minTTL, DefaultTTL: _defaultTTL, MaxTTL: _maxTTL, OriginRequestPolicyId: _originRequestPolicyId, ...rest } = value;
8630
+ const { ForwardedValues: _forwardedValues, MinTTL: _minTTL, DefaultTTL: _defaultTTL, MaxTTL: _maxTTL, ...rest } = value;
6843
8631
  return rest;
6844
8632
  };
6845
8633
  const sanitizeDefaultBehavior = (behavior) => ({
@@ -6864,6 +8652,7 @@ const buildOriginRequestLambdaAssociations = (functionArn) => ({
6864
8652
  Quantity: 1,
6865
8653
  Items: [{
6866
8654
  EventType: "origin-request",
8655
+ IncludeBody: true,
6867
8656
  LambdaFunctionARN: functionArn
6868
8657
  }]
6869
8658
  });
@@ -6890,13 +8679,16 @@ const buildDefaultCacheBehavior = (options) => ({
6890
8679
  });
6891
8680
  const buildCacheBehavior = (options) => ({
6892
8681
  ...buildSharedBehavior(options.bucketName),
6893
- PathPattern: HOT_UPDATER_CACHE_BEHAVIOR_PATH,
8682
+ AllowedMethods: API_METHODS,
8683
+ PathPattern: options.pathPattern,
6894
8684
  CachePolicyId: options.sharedCachePolicyId,
8685
+ OriginRequestPolicyId: options.originRequestPolicyId,
6895
8686
  LambdaFunctionAssociations: buildOriginRequestLambdaAssociations(options.functionArn)
6896
8687
  });
6897
8688
  const mergeOriginWithExisting = (existingOrigin, overrideOrigin) => ({
6898
8689
  ...existingOrigin,
6899
8690
  ...overrideOrigin,
8691
+ Id: existingOrigin?.Id ?? overrideOrigin.Id,
6900
8692
  CustomHeaders: existingOrigin?.CustomHeaders ?? { Quantity: 0 }
6901
8693
  });
6902
8694
  const mergeBehaviorWithExisting = (existingBehavior, overrideBehavior) => ({
@@ -6905,6 +8697,33 @@ const mergeBehaviorWithExisting = (existingBehavior, overrideBehavior) => ({
6905
8697
  LambdaFunctionAssociations: overrideBehavior.LambdaFunctionAssociations ?? existingBehavior?.LambdaFunctionAssociations ?? EMPTY_LAMBDA_FUNCTION_ASSOCIATIONS,
6906
8698
  FunctionAssociations: overrideBehavior.FunctionAssociations ?? existingBehavior?.FunctionAssociations ?? EMPTY_FUNCTION_ASSOCIATIONS
6907
8699
  });
8700
+ const mergeOrigins = (existing, overrides) => [...existing.map((existingOrigin) => {
8701
+ const override = overrides.find((candidate) => candidate.Id === existingOrigin.Id || candidate.DomainName === existingOrigin.DomainName);
8702
+ return override ? mergeOriginWithExisting(existingOrigin, override) : existingOrigin;
8703
+ }), ...overrides.filter((override) => !existing.some((candidate) => candidate.Id === override.Id || candidate.DomainName === override.DomainName))];
8704
+ const mergeCacheBehaviors = (existing, overrides) => {
8705
+ const additions = overrides.filter((override) => !existing.some((candidate) => candidate.PathPattern === override.PathPattern));
8706
+ const mergedExisting = existing.map((existingBehavior) => {
8707
+ const override = overrides.find((candidate) => candidate.PathPattern === existingBehavior.PathPattern);
8708
+ return override ? mergeBehaviorWithExisting(existingBehavior, override) : existingBehavior;
8709
+ });
8710
+ return additions.reduce((behaviors, addition) => {
8711
+ const additionPrefix = (addition.PathPattern ?? "").split(/[?*]/, 1)[0] ?? "";
8712
+ const broaderBehaviorIndex = behaviors.findIndex((behavior) => {
8713
+ const behaviorPattern = behavior.PathPattern ?? "";
8714
+ const wildcardIndex = behaviorPattern.indexOf("*");
8715
+ if (wildcardIndex !== behaviorPattern.length - 1 || behaviorPattern.includes("?")) return false;
8716
+ const behaviorPrefix = behaviorPattern.slice(0, wildcardIndex);
8717
+ return behaviorPrefix.length < additionPrefix.length && additionPrefix.startsWith(behaviorPrefix);
8718
+ });
8719
+ if (broaderBehaviorIndex === -1) return [...behaviors, addition];
8720
+ return [
8721
+ ...behaviors.slice(0, broaderBehaviorIndex),
8722
+ addition,
8723
+ ...behaviors.slice(broaderBehaviorIndex)
8724
+ ];
8725
+ }, mergedExisting);
8726
+ };
6908
8727
  const buildDistributionConfigOverrides = (options) => ({
6909
8728
  Origins: {
6910
8729
  Quantity: 1,
@@ -6920,29 +8739,45 @@ const buildDistributionConfigOverrides = (options) => ({
6920
8739
  sharedCachePolicyId: options.sharedCachePolicyId
6921
8740
  }),
6922
8741
  CacheBehaviors: {
6923
- Quantity: 1,
6924
- Items: [buildCacheBehavior({
8742
+ Quantity: HOT_UPDATER_RELEASE_CATALOG_BEHAVIOR_PATHS.length + HOT_UPDATER_CACHE_BEHAVIOR_PATHS.length,
8743
+ Items: [...HOT_UPDATER_RELEASE_CATALOG_BEHAVIOR_PATHS.map((pathPattern) => buildCacheBehavior({
8744
+ bucketName: options.bucketName,
8745
+ functionArn: options.functionArn,
8746
+ originRequestPolicyId: options.originRequestPolicyId,
8747
+ pathPattern,
8748
+ sharedCachePolicyId: options.releaseCatalogCachePolicyId
8749
+ })), ...HOT_UPDATER_CACHE_BEHAVIOR_PATHS.map((pathPattern) => buildCacheBehavior({
6925
8750
  bucketName: options.bucketName,
6926
8751
  functionArn: options.functionArn,
8752
+ originRequestPolicyId: options.originRequestPolicyId,
8753
+ pathPattern,
6927
8754
  sharedCachePolicyId: options.sharedCachePolicyId
6928
- })]
8755
+ }))]
6929
8756
  }
6930
8757
  });
6931
8758
  const applyDistributionConfigOverrides = (distributionConfig, overrides) => {
8759
+ const managedOrigin = overrides.Origins.Items?.[0];
8760
+ const targetOriginId = (managedOrigin ? distributionConfig.Origins?.Items?.find((origin) => origin.Id === managedOrigin.Id || origin.DomainName === managedOrigin.DomainName) : void 0)?.Id ?? managedOrigin?.Id;
8761
+ const defaultCacheBehavior = targetOriginId ? {
8762
+ ...overrides.DefaultCacheBehavior,
8763
+ TargetOriginId: targetOriginId
8764
+ } : overrides.DefaultCacheBehavior;
8765
+ const cacheBehaviorOverrides = (overrides.CacheBehaviors.Items ?? []).map((behavior) => targetOriginId ? {
8766
+ ...behavior,
8767
+ TargetOriginId: targetOriginId
8768
+ } : behavior);
8769
+ const origins = mergeOrigins(distributionConfig.Origins?.Items ?? [], overrides.Origins.Items ?? []);
8770
+ const cacheBehaviors = mergeCacheBehaviors(distributionConfig.CacheBehaviors?.Items ?? [], cacheBehaviorOverrides);
6932
8771
  return sanitizeDistributionConfig({
6933
8772
  ...distributionConfig,
6934
8773
  Origins: {
6935
- Quantity: overrides.Origins.Quantity,
6936
- Items: (overrides.Origins.Items ?? []).map((overrideOrigin) => {
6937
- return mergeOriginWithExisting((distributionConfig.Origins?.Items ?? []).find((origin) => origin.Id === overrideOrigin.Id || origin.DomainName === overrideOrigin.DomainName), overrideOrigin);
6938
- })
8774
+ Quantity: origins.length,
8775
+ Items: origins
6939
8776
  },
6940
- DefaultCacheBehavior: mergeBehaviorWithExisting(distributionConfig.DefaultCacheBehavior, overrides.DefaultCacheBehavior),
8777
+ DefaultCacheBehavior: mergeBehaviorWithExisting(distributionConfig.DefaultCacheBehavior, defaultCacheBehavior),
6941
8778
  CacheBehaviors: {
6942
- Quantity: overrides.CacheBehaviors.Quantity,
6943
- Items: (overrides.CacheBehaviors.Items ?? []).map((overrideBehavior) => {
6944
- return mergeBehaviorWithExisting((distributionConfig.CacheBehaviors?.Items ?? []).find((behavior) => behavior.PathPattern === overrideBehavior.PathPattern), overrideBehavior);
6945
- })
8779
+ Quantity: cacheBehaviors.length,
8780
+ Items: cacheBehaviors
6946
8781
  }
6947
8782
  });
6948
8783
  };
@@ -6993,7 +8828,7 @@ var CloudFrontManager = class {
6993
8828
  this.region = region;
6994
8829
  this.credentials = credentials;
6995
8830
  }
6996
- async getOrCreateSharedCachePolicy(cloudfrontClient) {
8831
+ async getOrCreateCachePolicy(cloudfrontClient, config) {
6997
8832
  const existingPolicyId = (await findInPaginatedCloudFrontList({
6998
8833
  listPage: async (marker) => {
6999
8834
  const listPoliciesResponse = await cloudfrontClient.listCachePolicies({
@@ -7005,13 +8840,41 @@ var CloudFrontManager = class {
7005
8840
  nextMarker: listPoliciesResponse.CachePolicyList?.NextMarker
7006
8841
  };
7007
8842
  },
7008
- matches: (policy) => policy.CachePolicy?.CachePolicyConfig?.Name === HOT_UPDATER_SHARED_CACHE_POLICY_CONFIG.Name
8843
+ matches: (policy) => policy.CachePolicy?.CachePolicyConfig?.Name === config.Name
7009
8844
  }))?.CachePolicy?.Id;
7010
- if (existingPolicyId) return existingPolicyId;
7011
- const cachePolicyId = (await cloudfrontClient.createCachePolicy({ CachePolicyConfig: HOT_UPDATER_SHARED_CACHE_POLICY_CONFIG })).CachePolicy?.Id;
8845
+ if (existingPolicyId) {
8846
+ const currentPolicy = await cloudfrontClient.getCachePolicy({ Id: existingPolicyId });
8847
+ if (!currentPolicy.ETag) throw new Error("Failed to read shared cache policy ETag");
8848
+ await cloudfrontClient.updateCachePolicy({
8849
+ CachePolicyConfig: config,
8850
+ Id: existingPolicyId,
8851
+ IfMatch: currentPolicy.ETag
8852
+ });
8853
+ return existingPolicyId;
8854
+ }
8855
+ const cachePolicyId = (await cloudfrontClient.createCachePolicy({ CachePolicyConfig: config })).CachePolicy?.Id;
7012
8856
  if (!cachePolicyId) throw new Error("Failed to create shared cache policy");
7013
8857
  return cachePolicyId;
7014
8858
  }
8859
+ async getOrCreateOriginRequestPolicy(cloudfrontClient) {
8860
+ const existingPolicyId = (await findInPaginatedCloudFrontList({
8861
+ listPage: async (marker) => {
8862
+ const response = await cloudfrontClient.listOriginRequestPolicies({
8863
+ Type: "custom",
8864
+ ...marker ? { Marker: marker } : {}
8865
+ });
8866
+ return {
8867
+ items: response.OriginRequestPolicyList?.Items ?? [],
8868
+ nextMarker: response.OriginRequestPolicyList?.NextMarker
8869
+ };
8870
+ },
8871
+ matches: (policy) => policy.OriginRequestPolicy?.OriginRequestPolicyConfig?.Name === HOT_UPDATER_ORIGIN_REQUEST_POLICY_CONFIG.Name
8872
+ }))?.OriginRequestPolicy?.Id;
8873
+ if (existingPolicyId) return existingPolicyId;
8874
+ const policyId = (await cloudfrontClient.createOriginRequestPolicy({ OriginRequestPolicyConfig: HOT_UPDATER_ORIGIN_REQUEST_POLICY_CONFIG })).OriginRequestPolicy?.Id;
8875
+ if (!policyId) throw new Error("Failed to create origin request policy");
8876
+ return policyId;
8877
+ }
7015
8878
  async getOrCreateKeyGroup(publicKey) {
7016
8879
  const publicKeyHash = crypto.createHash("sha256").update(publicKey).digest("hex").slice(0, 16);
7017
8880
  const cloudfrontClient = new CloudFront({
@@ -7079,11 +8942,17 @@ var CloudFrontManager = class {
7079
8942
  }
7080
8943
  if (!oacId) throw new Error("Failed to get Origin Access Control ID");
7081
8944
  const bucketDomain = `${options.bucketName}.s3.${this.region}.amazonaws.com`;
8945
+ let releaseCatalogCachePolicyId;
7082
8946
  let sharedCachePolicyId;
8947
+ let originRequestPolicyId;
7083
8948
  try {
7084
- sharedCachePolicyId = await this.getOrCreateSharedCachePolicy(cloudfrontClient);
8949
+ [sharedCachePolicyId, releaseCatalogCachePolicyId, originRequestPolicyId] = await Promise.all([
8950
+ this.getOrCreateCachePolicy(cloudfrontClient, HOT_UPDATER_SHARED_CACHE_POLICY_CONFIG),
8951
+ this.getOrCreateCachePolicy(cloudfrontClient, HOT_UPDATER_RELEASE_CATALOG_CACHE_POLICY_CONFIG),
8952
+ this.getOrCreateOriginRequestPolicy(cloudfrontClient)
8953
+ ]);
7085
8954
  } catch (error) {
7086
- throw new Error(`Failed to get or create shared cache policy: ${error instanceof Error ? error.message : String(error)}`);
8955
+ throw new Error(`Failed to get or create CloudFront request policies: ${error instanceof Error ? error.message : String(error)}`);
7087
8956
  }
7088
8957
  const newOverrides = buildDistributionConfigOverrides({
7089
8958
  bucketName: options.bucketName,
@@ -7091,6 +8960,8 @@ var CloudFrontManager = class {
7091
8960
  functionArn: options.functionArn,
7092
8961
  keyGroupId: options.keyGroupId,
7093
8962
  oacId,
8963
+ originRequestPolicyId,
8964
+ releaseCatalogCachePolicyId,
7094
8965
  sharedCachePolicyId
7095
8966
  });
7096
8967
  if (selectedDistribution) {
@@ -7111,8 +8982,8 @@ var CloudFrontManager = class {
7111
8982
  InvalidationBatch: {
7112
8983
  CallerReference: (/* @__PURE__ */ new Date()).toISOString(),
7113
8984
  Paths: {
7114
- Quantity: 1,
7115
- Items: ["/*"]
8985
+ Quantity: HOT_UPDATER_CACHE_BEHAVIOR_PATHS.length + HOT_UPDATER_RELEASE_CATALOG_BEHAVIOR_PATHS.length,
8986
+ Items: [...HOT_UPDATER_CACHE_BEHAVIOR_PATHS, ...HOT_UPDATER_RELEASE_CATALOG_BEHAVIOR_PATHS]
7116
8987
  }
7117
8988
  }
7118
8989
  });
@@ -7132,6 +9003,8 @@ var CloudFrontManager = class {
7132
9003
  functionArn: options.functionArn,
7133
9004
  keyGroupId: options.keyGroupId,
7134
9005
  oacId,
9006
+ originRequestPolicyId,
9007
+ releaseCatalogCachePolicyId,
7135
9008
  sharedCachePolicyId
7136
9009
  });
7137
9010
  try {
@@ -7187,33 +9060,165 @@ var CloudFrontManager = class {
7187
9060
  });
7188
9061
  const savedDistribution = matchingDistributions.find((distribution) => distribution.Id === distributionId);
7189
9062
  const savedDistributionExists = distributions.some((distribution) => distribution.Id === distributionId);
7190
- if (nonInteractive && savedDistribution) return savedDistribution;
7191
- if (distributionId && !savedDistribution) {
7192
- if (matchingDistributions.length === 0 && !savedDistributionExists) {
9063
+ if (nonInteractive) {
9064
+ if (savedDistribution) return savedDistribution;
9065
+ if (!distributionId) return null;
9066
+ if (!savedDistributionExists) {
7193
9067
  p.log.warn("Saved CloudFront distribution was not found. A new distribution will be created.");
7194
9068
  return null;
7195
9069
  }
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
9070
  throw new MissingInitInputsError(["HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID"]);
7203
9071
  }
9072
+ if (distributionId && !savedDistribution) p.log.warn(savedDistributionExists ? "Saved CloudFront distribution does not use the selected S3 bucket. Select a distribution again." : "Saved CloudFront distribution was not found. A new distribution will be created unless another distribution is selected.");
9073
+ if (matchingDistributions.length === 0) return null;
9074
+ const legacyDistributionIds = /* @__PURE__ */ new Set();
9075
+ await Promise.all(matchingDistributions.map(async (distribution) => {
9076
+ if (await resolveAwsDistributionGeneration({ domainName: distribution.DomainName }) === "v0") legacyDistributionIds.add(distribution.Id);
9077
+ }));
9078
+ const createNewDistribution = "__create-new-cloudfront-distribution__";
9079
+ const reusableSavedDistributionId = savedDistribution && !legacyDistributionIds.has(savedDistribution.Id) ? savedDistribution.Id : void 0;
7204
9080
  const selectedDistributionId = await p.select({
7205
- initialValue: savedDistribution?.Id ?? matchingDistributions[0]?.Id,
9081
+ initialValue: reusableSavedDistributionId ?? createNewDistribution,
7206
9082
  message: "Select a CloudFront distribution:",
7207
- options: matchingDistributions.map((distribution) => ({
7208
- value: distribution.Id,
7209
- label: `${distribution.Id} (${distribution.DomainName})`
7210
- }))
9083
+ options: [{
9084
+ value: createNewDistribution,
9085
+ label: "Create New CloudFront Distribution"
9086
+ }, ...matchingDistributions.map((distribution) => {
9087
+ const isLegacy = legacyDistributionIds.has(distribution.Id);
9088
+ return {
9089
+ value: distribution.Id,
9090
+ label: `${distribution.Id} (${distribution.DomainName})${isLegacy ? " (v0, deprecated)" : ""}`,
9091
+ disabled: isLegacy
9092
+ };
9093
+ })]
7211
9094
  });
7212
9095
  if (p.isCancel(selectedDistributionId)) process.exit(0);
9096
+ if (selectedDistributionId === createNewDistribution) return null;
7213
9097
  return matchingDistributions.find((distribution) => distribution.Id === selectedDistributionId) ?? null;
7214
9098
  }
7215
9099
  };
7216
9100
  //#endregion
9101
+ //#region iac/dynamodb.ts
9102
+ const DYNAMODB_DESCRIBE_TABLE_ACTION = "dynamodb:DescribeTable";
9103
+ var DynamoDBTableSchemaError = class extends Error {
9104
+ name = "DynamoDBTableSchemaError";
9105
+ constructor(tableName) {
9106
+ super(`DynamoDB table "${tableName}" does not match the Hot Updater key and index schema`);
9107
+ this.tableName = tableName;
9108
+ }
9109
+ };
9110
+ var DynamoDBPermissionError = class extends InitError {
9111
+ name = "DynamoDBPermissionError";
9112
+ requiredAction = DYNAMODB_DESCRIBE_TABLE_ACTION;
9113
+ constructor(tableName, region, cause) {
9114
+ super([
9115
+ `AWS credentials cannot access DynamoDB table "${tableName}" in ${region}.`,
9116
+ `Required permission: ${DYNAMODB_DESCRIBE_TABLE_ACTION}`,
9117
+ `AWS error: ${cause.message}`,
9118
+ "Ask your AWS administrator to grant this permission, or attach the AmazonDynamoDBFullAccess_v2 managed policy, to the identity used for init.",
9119
+ "For AWS IAM Identity Center, update the assigned permission set and refresh the SSO session.",
9120
+ "Then rerun `hot-updater init`."
9121
+ ].join("\n"), { cause });
9122
+ this.tableName = tableName;
9123
+ this.region = region;
9124
+ }
9125
+ };
9126
+ const primaryKeySchema = [{
9127
+ AttributeName: "pk",
9128
+ KeyType: "HASH"
9129
+ }, {
9130
+ AttributeName: "sk",
9131
+ KeyType: "RANGE"
9132
+ }];
9133
+ const updateIndexKeySchema = [{
9134
+ AttributeName: "gsi1pk",
9135
+ KeyType: "HASH"
9136
+ }, {
9137
+ AttributeName: "gsi1sk",
9138
+ KeyType: "RANGE"
9139
+ }];
9140
+ const keyAttributes = [
9141
+ "pk",
9142
+ "sk",
9143
+ "gsi1pk",
9144
+ "gsi1sk"
9145
+ ];
9146
+ const onDemandThroughput = {
9147
+ MaxReadRequestUnits: 4e3,
9148
+ MaxWriteRequestUnits: 100
9149
+ };
9150
+ const hasKeySchema = (actual, expected) => actual?.length === expected.length && expected.every(({ AttributeName, KeyType }) => actual.some((key) => key.AttributeName === AttributeName && key.KeyType === KeyType));
9151
+ const hasExpectedSchema = (table) => {
9152
+ const updateIndex = table?.GlobalSecondaryIndexes?.find(({ IndexName }) => IndexName === DYNAMODB_UPDATE_INDEX_NAME);
9153
+ return keyAttributes.every((attributeName) => table?.AttributeDefinitions?.some(({ AttributeName, AttributeType }) => AttributeName === attributeName && AttributeType === "S")) && table?.BillingModeSummary?.BillingMode === "PAY_PER_REQUEST" && table.OnDemandThroughput?.MaxReadRequestUnits === onDemandThroughput.MaxReadRequestUnits && table.OnDemandThroughput?.MaxWriteRequestUnits === onDemandThroughput.MaxWriteRequestUnits && hasKeySchema(table?.KeySchema, primaryKeySchema) && hasKeySchema(updateIndex?.KeySchema, updateIndexKeySchema) && updateIndex?.OnDemandThroughput?.MaxReadRequestUnits === onDemandThroughput.MaxReadRequestUnits && updateIndex?.OnDemandThroughput?.MaxWriteRequestUnits === onDemandThroughput.MaxWriteRequestUnits && updateIndex?.Projection?.ProjectionType === "ALL";
9154
+ };
9155
+ const isResourceNotFound = (error) => typeof error === "object" && error !== null && Reflect.get(error, "name") === "ResourceNotFoundException";
9156
+ var DynamoDBManager = class {
9157
+ client;
9158
+ constructor(region, credentials) {
9159
+ this.region = region;
9160
+ this.client = new DynamoDB({
9161
+ credentials,
9162
+ region
9163
+ });
9164
+ }
9165
+ async ensureTable(tableName) {
9166
+ try {
9167
+ const { Table } = await this.client.describeTable({ TableName: tableName });
9168
+ if (!hasExpectedSchema(Table)) throw new DynamoDBTableSchemaError(tableName);
9169
+ await this.ensureLifecycle(tableName);
9170
+ return;
9171
+ } catch (error) {
9172
+ if (error instanceof Error && error.name === "AccessDeniedException" && error.message.includes(DYNAMODB_DESCRIBE_TABLE_ACTION)) throw new DynamoDBPermissionError(tableName, this.region, error);
9173
+ if (!isResourceNotFound(error)) throw error;
9174
+ }
9175
+ await this.client.createTable({
9176
+ AttributeDefinitions: [
9177
+ {
9178
+ AttributeName: "pk",
9179
+ AttributeType: "S"
9180
+ },
9181
+ {
9182
+ AttributeName: "sk",
9183
+ AttributeType: "S"
9184
+ },
9185
+ {
9186
+ AttributeName: "gsi1pk",
9187
+ AttributeType: "S"
9188
+ },
9189
+ {
9190
+ AttributeName: "gsi1sk",
9191
+ AttributeType: "S"
9192
+ }
9193
+ ],
9194
+ BillingMode: "PAY_PER_REQUEST",
9195
+ DeletionProtectionEnabled: true,
9196
+ GlobalSecondaryIndexes: [{
9197
+ IndexName: DYNAMODB_UPDATE_INDEX_NAME,
9198
+ KeySchema: [...updateIndexKeySchema],
9199
+ Projection: { ProjectionType: "ALL" },
9200
+ OnDemandThroughput: onDemandThroughput
9201
+ }],
9202
+ KeySchema: [...primaryKeySchema],
9203
+ OnDemandThroughput: onDemandThroughput,
9204
+ TableName: tableName
9205
+ });
9206
+ await waitUntilTableExists({
9207
+ client: this.client,
9208
+ maxWaitTime: 120
9209
+ }, { TableName: tableName });
9210
+ await this.ensureLifecycle(tableName);
9211
+ }
9212
+ async ensureLifecycle(tableName) {
9213
+ const { ContinuousBackupsDescription } = await this.client.describeContinuousBackups({ TableName: tableName });
9214
+ if (ContinuousBackupsDescription?.PointInTimeRecoveryDescription?.PointInTimeRecoveryStatus === "ENABLED") return;
9215
+ await this.client.updateContinuousBackups({
9216
+ PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true },
9217
+ TableName: tableName
9218
+ });
9219
+ }
9220
+ };
9221
+ //#endregion
7217
9222
  //#region iac/iam.ts
7218
9223
  var IAMManager = class {
7219
9224
  region;
@@ -7225,12 +9230,82 @@ var IAMManager = class {
7225
9230
  async ensureManagedPolicies(iamClient, roleName) {
7226
9231
  const attachedPolicies = await iamClient.listAttachedRolePolicies({ RoleName: roleName });
7227
9232
  const attachedPolicyArns = new Set((attachedPolicies.AttachedPolicies ?? []).map((policy) => policy.PolicyArn));
7228
- for (const policyArn of ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"]) if (!attachedPolicyArns.has(policyArn)) await iamClient.attachRolePolicy({
9233
+ for (const policyArn of ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]) if (!attachedPolicyArns.has(policyArn)) await iamClient.attachRolePolicy({
7229
9234
  RoleName: roleName,
7230
9235
  PolicyArn: policyArn
7231
9236
  });
7232
9237
  }
7233
- async createOrSelectRole() {
9238
+ async ensureDynamoDBPolicy(iamClient, roleName, accountId, tableName) {
9239
+ const tableArn = `arn:aws:dynamodb:${this.region}:${accountId}:table/${tableName}`;
9240
+ await iamClient.putRolePolicy({
9241
+ PolicyDocument: JSON.stringify({
9242
+ Version: "2012-10-17",
9243
+ Statement: [{
9244
+ Action: ["dynamodb:Query"],
9245
+ Effect: "Allow",
9246
+ Resource: [`${tableArn}/index/${DYNAMODB_UPDATE_INDEX_NAME}`]
9247
+ }, {
9248
+ Action: [
9249
+ "dynamodb:BatchGetItem",
9250
+ "dynamodb:DeleteItem",
9251
+ "dynamodb:GetItem",
9252
+ "dynamodb:PutItem",
9253
+ "dynamodb:Query",
9254
+ "dynamodb:TransactWriteItems",
9255
+ "dynamodb:UpdateItem"
9256
+ ],
9257
+ Condition: { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [
9258
+ "_hot-updater",
9259
+ "bundles",
9260
+ "bundle_patches",
9261
+ DYNAMODB_CHANNEL_PARTITION,
9262
+ DYNAMODB_CHANNEL_NAME_PARTITION,
9263
+ DYNAMODB_ANALYTICS_PARTITION,
9264
+ DYNAMODB_API_KEY_PARTITION,
9265
+ DYNAMODB_API_KEY_HASH_PARTITION
9266
+ ] } },
9267
+ Effect: "Allow",
9268
+ Resource: [tableArn]
9269
+ }]
9270
+ }),
9271
+ PolicyName: "HotUpdaterDynamoDBReadAccess",
9272
+ RoleName: roleName
9273
+ });
9274
+ }
9275
+ async ensureS3Policy(iamClient, roleName, bucketName) {
9276
+ await iamClient.putRolePolicy({
9277
+ PolicyDocument: JSON.stringify({
9278
+ Version: "2012-10-17",
9279
+ Statement: [{
9280
+ Action: ["s3:ListBucket"],
9281
+ Effect: "Allow",
9282
+ Resource: [`arn:aws:s3:::${bucketName}`]
9283
+ }, {
9284
+ Action: ["s3:GetObject"],
9285
+ Effect: "Allow",
9286
+ Resource: [`arn:aws:s3:::${bucketName}/*`]
9287
+ }]
9288
+ }),
9289
+ PolicyName: "HotUpdaterS3ReadAccess",
9290
+ RoleName: roleName
9291
+ });
9292
+ }
9293
+ async ensureSsmPolicy(iamClient, roleName, accountId, parameterName) {
9294
+ const parameterPath = parameterName.replace(/^\/+/, "");
9295
+ await iamClient.putRolePolicy({
9296
+ PolicyDocument: JSON.stringify({
9297
+ Version: "2012-10-17",
9298
+ Statement: [{
9299
+ Effect: "Allow",
9300
+ Action: ["ssm:GetParameter"],
9301
+ Resource: `arn:aws:ssm:${this.region}:${accountId}:parameter/${parameterPath}`
9302
+ }]
9303
+ }),
9304
+ PolicyName: "HotUpdaterSSMAccess",
9305
+ RoleName: roleName
9306
+ });
9307
+ }
9308
+ async createOrSelectRole(options) {
7234
9309
  const iamClient = new IAM({
7235
9310
  region: this.region,
7236
9311
  credentials: this.credentials
@@ -7248,50 +9323,35 @@ var IAMManager = class {
7248
9323
  Action: "sts:AssumeRole"
7249
9324
  }]
7250
9325
  });
7251
- const roleName = "hot-updater-edge-role";
7252
- const ssmPolicyDocument = JSON.stringify({
7253
- Version: "2012-10-17",
7254
- Statement: [{
7255
- Effect: "Allow",
7256
- Action: ["ssm:GetParameter"],
7257
- Resource: `arn:aws:ssm:*:${accountId}:parameter/hot-updater/*`
7258
- }]
7259
- });
9326
+ const roleName = `hot-updater-edge-${createHash("sha256").update(options.lambdaName).digest("hex").slice(0, 16)}`;
7260
9327
  try {
7261
9328
  const { Role: existingRole } = await iamClient.getRole({ RoleName: roleName });
7262
9329
  if (existingRole?.Arn) {
7263
9330
  await this.ensureManagedPolicies(iamClient, roleName);
7264
- try {
7265
- await iamClient.putRolePolicy({
7266
- RoleName: roleName,
7267
- PolicyName: "HotUpdaterSSMAccess",
7268
- PolicyDocument: ssmPolicyDocument
7269
- });
7270
- p.log.info("Updated SSM access policy for existing IAM role");
7271
- } catch {
7272
- p.log.warn("Failed to update SSM policy, continuing anyway");
7273
- }
9331
+ await this.ensureS3Policy(iamClient, roleName, options.bucketName);
9332
+ await this.ensureSsmPolicy(iamClient, roleName, accountId, options.ssmParameterName);
9333
+ await this.ensureDynamoDBPolicy(iamClient, roleName, accountId, options.dynamodbTableName);
7274
9334
  p.log.info(`Using existing IAM role: ${roleName} (${existingRole.Arn})`);
7275
9335
  return existingRole.Arn;
7276
9336
  }
7277
- } catch {
9337
+ } catch (error) {
9338
+ if (!(error instanceof Error && error.name === "NoSuchEntityException")) throw error;
7278
9339
  try {
7279
9340
  const createRoleResp = await iamClient.createRole({
7280
9341
  RoleName: roleName,
7281
9342
  AssumeRolePolicyDocument: assumeRolePolicyDocument,
7282
- Description: "Role for Lambda@Edge to access S3 and SSM"
9343
+ Description: `Role for Hot Updater Lambda@Edge ${options.lambdaName}`
7283
9344
  });
7284
9345
  if (!createRoleResp.Role?.Arn) throw new Error("Failed to create IAM role: No ARN returned");
7285
9346
  const lambdaRoleArn = createRoleResp.Role.Arn;
7286
9347
  p.log.info(`Created IAM role: ${roleName} (${lambdaRoleArn})`);
7287
9348
  await this.ensureManagedPolicies(iamClient, roleName);
7288
9349
  p.log.info(`Attached managed policies to ${roleName}`);
7289
- await iamClient.putRolePolicy({
7290
- RoleName: roleName,
7291
- PolicyName: "HotUpdaterSSMAccess",
7292
- PolicyDocument: ssmPolicyDocument
7293
- });
7294
- p.log.info(`Added SSM access inline policy to ${roleName}`);
9350
+ await this.ensureS3Policy(iamClient, roleName, options.bucketName);
9351
+ await this.ensureSsmPolicy(iamClient, roleName, accountId, options.ssmParameterName);
9352
+ p.log.info(`Added resource-scoped policies to ${roleName}`);
9353
+ await this.ensureDynamoDBPolicy(iamClient, roleName, accountId, options.dynamodbTableName);
9354
+ p.log.info(`Added DynamoDB read policy to ${roleName}`);
7295
9355
  return lambdaRoleArn;
7296
9356
  } catch (createError) {
7297
9357
  if (createError instanceof Error) p.log.error(`Error setting up IAM role: ${createError.message}`);
@@ -7302,40 +9362,48 @@ var IAMManager = class {
7302
9362
  }
7303
9363
  };
7304
9364
  //#endregion
7305
- //#region iac/lambdaAsset.ts
7306
- /**
7307
- * Resolves the directory holding the bundled Lambda@Edge runtime.
7308
- * Kept in its own module so it can be mocked in tests without requiring a
7309
- * built `dist/`.
7310
- */
7311
- const resolveLambdaDir = () => path$1.dirname(__require.resolve("@hot-updater/aws/lambda"));
7312
- //#endregion
7313
9365
  //#region iac/lambdaEdge.ts
7314
- const LAMBDA_EDGE_MEMORY_SIZE = 256;
7315
- const LAMBDA_EDGE_TIMEOUT = 10;
7316
- const waitForFunctionUpdate = async (lambdaClient, lambdaName) => {
7317
- while (true) {
7318
- try {
7319
- const status = await lambdaClient.getFunctionConfiguration({ FunctionName: lambdaName });
7320
- if (status.LastUpdateStatus === "Successful") return;
7321
- if (status.LastUpdateStatus === "Failed") throw new Error(`Lambda update failed: ${status.LastUpdateStatusReason}`);
7322
- } catch (err) {
7323
- if (!(err instanceof Error && err.name === "ResourceConflictException")) throw err;
7324
- }
7325
- await new Promise((resolve) => setTimeout(resolve, 2e3));
9366
+ const LAMBDA_MEMORY_SIZE = 256;
9367
+ const LAMBDA_TIMEOUT_SECONDS = 10;
9368
+ const LAMBDA_ROLE_PROPAGATION_MAX_ATTEMPTS = 10;
9369
+ const LAMBDA_ROLE_PROPAGATION_RETRY_DELAY_MS = 2e3;
9370
+ const isLambdaRolePropagationError = (error) => error instanceof Error && error.name === "InvalidParameterValueException" && /role defined for the function cannot be assumed by Lambda/i.test(error.message);
9371
+ const createFunctionWithRolePropagationRetry = async (createFunction, message) => {
9372
+ for (let attempt = 1; attempt <= LAMBDA_ROLE_PROPAGATION_MAX_ATTEMPTS; attempt++) try {
9373
+ return await createFunction();
9374
+ } catch (error) {
9375
+ if (!isLambdaRolePropagationError(error) || attempt === LAMBDA_ROLE_PROPAGATION_MAX_ATTEMPTS) throw error;
9376
+ message("Waiting for IAM role to become assumable by Lambda...");
9377
+ await new Promise((resolve) => setTimeout(resolve, LAMBDA_ROLE_PROPAGATION_RETRY_DELAY_MS));
7326
9378
  }
9379
+ throw new Error("Failed to create Lambda function");
7327
9380
  };
7328
9381
  var LambdaEdgeDeployer = class {
7329
9382
  credentials;
7330
9383
  constructor(credentials) {
7331
9384
  this.credentials = credentials;
7332
9385
  }
9386
+ async waitForUpdate(lambdaClient, lambdaName) {
9387
+ while (true) {
9388
+ try {
9389
+ const status = await lambdaClient.getFunctionConfiguration({ FunctionName: lambdaName });
9390
+ if (status.LastUpdateStatus === "Successful") return;
9391
+ if (status.LastUpdateStatus === "Failed") throw new Error(`Lambda update failed: ${status.LastUpdateStatusReason}`);
9392
+ } catch (error) {
9393
+ if (!(error instanceof Error) || error.name !== "ResourceConflictException") throw error;
9394
+ }
9395
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
9396
+ }
9397
+ }
7333
9398
  async deploy(lambdaRoleArn, lambdaName, config) {
7334
9399
  const cwd = getCwd();
7335
- const { tmpDir, removeTmpDir } = await copyDirToTmp(resolveLambdaDir());
9400
+ const lambdaPath = __require.resolve("@hot-updater/aws/lambda");
9401
+ const { tmpDir, removeTmpDir } = await copyDirToTmp(path$1.dirname(lambdaPath));
7336
9402
  const indexPath = path$1.join(tmpDir, "index.cjs");
7337
9403
  const code = transformEnv(indexPath, {
7338
9404
  CLOUDFRONT_KEY_PAIR_ID: config.publicKeyId,
9405
+ DYNAMODB_REGION: config.dynamodbRegion,
9406
+ DYNAMODB_TABLE_NAME: config.dynamodbTableName,
7339
9407
  SSM_PARAMETER_NAME: config.ssmParameterName,
7340
9408
  SSM_REGION: config.ssmRegion,
7341
9409
  S3_BUCKET_NAME: config.bucketName
@@ -7369,17 +9437,18 @@ var LambdaEdgeDeployer = class {
7369
9437
  title: "Creating or Updating Lambda function",
7370
9438
  task: async (message) => {
7371
9439
  try {
7372
- const createResp = await lambdaClient.createFunction({
9440
+ const zipFile = await fs.readFile(zipFilePath);
9441
+ const createResp = await createFunctionWithRolePropagationRetry(() => lambdaClient.createFunction({
7373
9442
  FunctionName: lambdaName,
7374
9443
  Runtime: "nodejs22.x",
7375
9444
  Role: lambdaRoleArn,
7376
9445
  Handler: "index.handler",
7377
- Code: { ZipFile: await fs.readFile(zipFilePath) },
9446
+ Code: { ZipFile: zipFile },
7378
9447
  Description: "Hot Updater Lambda@Edge function",
9448
+ MemorySize: LAMBDA_MEMORY_SIZE,
7379
9449
  Publish: true,
7380
- MemorySize: LAMBDA_EDGE_MEMORY_SIZE,
7381
- Timeout: LAMBDA_EDGE_TIMEOUT
7382
- });
9450
+ Timeout: LAMBDA_TIMEOUT_SECONDS
9451
+ }), message);
7383
9452
  functionArn.arn = createResp.FunctionArn || null;
7384
9453
  functionArn.version = createResp.Version || "1";
7385
9454
  return `Created Lambda "${lambdaName}" function`;
@@ -7392,17 +9461,17 @@ var LambdaEdgeDeployer = class {
7392
9461
  Publish: false
7393
9462
  });
7394
9463
  message("Waiting for Lambda function update to complete...");
7395
- await waitForFunctionUpdate(lambdaClient, lambdaName);
9464
+ await this.waitForUpdate(lambdaClient, lambdaName);
7396
9465
  await lambdaClient.updateFunctionConfiguration({
7397
9466
  FunctionName: lambdaName,
7398
- MemorySize: LAMBDA_EDGE_MEMORY_SIZE,
7399
- Timeout: LAMBDA_EDGE_TIMEOUT
9467
+ MemorySize: LAMBDA_MEMORY_SIZE,
9468
+ Role: lambdaRoleArn,
9469
+ Timeout: LAMBDA_TIMEOUT_SECONDS
7400
9470
  });
7401
- message("Waiting for Lambda configuration update to complete...");
7402
- await waitForFunctionUpdate(lambdaClient, lambdaName);
7403
- const publishResp = await lambdaClient.publishVersion({ FunctionName: lambdaName });
7404
- functionArn.arn = publishResp.FunctionArn || null;
7405
- functionArn.version = publishResp.Version || "1";
9471
+ await this.waitForUpdate(lambdaClient, lambdaName);
9472
+ const published = await lambdaClient.publishVersion({ FunctionName: lambdaName });
9473
+ functionArn.arn = published.FunctionArn || null;
9474
+ functionArn.version = published.Version || null;
7406
9475
  } else {
7407
9476
  if (error instanceof Error) p.log.error(`Failed to create or update Lambda function: ${error.message}`);
7408
9477
  throw error;
@@ -7437,287 +9506,6 @@ var LambdaEdgeDeployer = class {
7437
9506
  }
7438
9507
  };
7439
9508
  //#endregion
7440
- //#region iac/migrations/migrator.ts
7441
- /**
7442
- * S3Migration
7443
- * Base class that each migration script extends.
7444
- * Provides common S3-related methods (getKeys, readJson, updateFile, moveFile, backup, rollback, etc.)
7445
- */
7446
- var S3Migration = class {
7447
- name;
7448
- s3;
7449
- bucketName;
7450
- dryRun = false;
7451
- backupMapping = /* @__PURE__ */ new Map();
7452
- async doUpdateFile(key, content, { cacheControl } = {}) {
7453
- const normalizedKey = key.startsWith("/") ? key.substring(1) : key;
7454
- await new Upload({
7455
- client: this.s3,
7456
- params: {
7457
- Bucket: this.bucketName,
7458
- Key: normalizedKey,
7459
- Body: content,
7460
- CacheControl: cacheControl
7461
- }
7462
- }).done();
7463
- }
7464
- async getKeys(prefix) {
7465
- const keys = [];
7466
- let continuationToken;
7467
- do {
7468
- const command = new ListObjectsV2Command({
7469
- Bucket: this.bucketName,
7470
- Prefix: prefix.startsWith("/") ? prefix.substring(1) : prefix,
7471
- ContinuationToken: continuationToken
7472
- });
7473
- const data = await this.s3.send(command);
7474
- if (data.Contents) {
7475
- for (const item of data.Contents) if (item.Key) keys.push(item.Key);
7476
- }
7477
- continuationToken = data?.NextContinuationToken;
7478
- } while (continuationToken);
7479
- return keys;
7480
- }
7481
- async readFile(key) {
7482
- try {
7483
- const command = new GetObjectCommand({
7484
- Bucket: this.bucketName,
7485
- Key: key.startsWith("/") ? key.substring(1) : key
7486
- });
7487
- const response = await this.s3.send(command);
7488
- if (response.Body) return await response.Body.transformToString();
7489
- return null;
7490
- } catch (error) {
7491
- console.error(colors.red(`Error reading file ${key}:`), error);
7492
- return null;
7493
- }
7494
- }
7495
- async readJson(key) {
7496
- const content = await this.readFile(key);
7497
- if (content) try {
7498
- return JSON.parse(content);
7499
- } catch (e) {
7500
- console.error(colors.red(`Error parsing JSON from ${key}:`), e);
7501
- }
7502
- return null;
7503
- }
7504
- async backupFile(key) {
7505
- if (this.dryRun) return;
7506
- const content = await this.readFile(key);
7507
- if (content !== null) {
7508
- const backupKey = `backup/${this.name}/${key}`;
7509
- await this.doUpdateFile(backupKey, content);
7510
- this.backupMapping.set(key, backupKey);
7511
- }
7512
- }
7513
- async updateFile(key, content, { cacheControl } = {}) {
7514
- const normalizedKey = key.startsWith("/") ? key.substring(1) : key;
7515
- if (this.dryRun) {
7516
- console.log(colors.yellow(`[DRY RUN] Updated ${colors.bold(normalizedKey)}`));
7517
- return;
7518
- }
7519
- if (await this.readFile(key) !== null) await this.backupFile(key);
7520
- await this.doUpdateFile(normalizedKey, content, { cacheControl });
7521
- console.log(colors.green(`Updated ${colors.bold(normalizedKey)}`));
7522
- }
7523
- async moveFile(from, to) {
7524
- if (this.dryRun) {
7525
- console.log(colors.yellow(`[DRY RUN] ${colors.bold(from)} -> ${colors.bold(to)}`));
7526
- return;
7527
- }
7528
- await this.backupFile(from);
7529
- try {
7530
- const copyCommand = new CopyObjectCommand({
7531
- Bucket: this.bucketName,
7532
- CopySource: `${this.bucketName}/${from}`,
7533
- Key: to
7534
- });
7535
- await this.s3.send(copyCommand);
7536
- } catch (error) {
7537
- console.error(colors.red(`Error copying file from ${from} to ${to}:`), error);
7538
- throw error;
7539
- }
7540
- try {
7541
- const deleteCommand = new DeleteObjectCommand({
7542
- Bucket: this.bucketName,
7543
- Key: from
7544
- });
7545
- await this.s3.send(deleteCommand);
7546
- } catch (error) {
7547
- if (error?.message?.includes("NoSuchKey")) console.warn(colors.yellow(`Key ${from} not found during deletion, ignoring.`));
7548
- else {
7549
- console.error(colors.red(`Error deleting file ${from}:`), error);
7550
- throw error;
7551
- }
7552
- }
7553
- console.log(colors.green(`${colors.bold(from)} -> ${colors.bold(to)}`));
7554
- }
7555
- async deleteBackupFile(backupKey) {
7556
- if (this.dryRun) return;
7557
- try {
7558
- const deleteCommand = new DeleteObjectCommand({
7559
- Bucket: this.bucketName,
7560
- Key: backupKey
7561
- });
7562
- await this.s3.send(deleteCommand);
7563
- } catch (error) {
7564
- console.error(colors.red(`Error deleting backup file ${backupKey}:`), error);
7565
- }
7566
- }
7567
- async cleanupBackups() {
7568
- if (this.backupMapping.size === 0) return;
7569
- for (const backupKey of this.backupMapping.values()) await this.deleteBackupFile(backupKey);
7570
- this.backupMapping.clear();
7571
- }
7572
- async rollback() {
7573
- console.log(colors.magenta(`Starting rollback for migration ${this.name}...`));
7574
- for (const [originalKey, backupKey] of this.backupMapping.entries()) {
7575
- const backupContent = await this.readFile(backupKey);
7576
- if (backupContent !== null) {
7577
- console.log(colors.blue(`Restoring backup for ${originalKey} from ${backupKey}`));
7578
- await this.doUpdateFile(originalKey, backupContent);
7579
- } else console.error(colors.red(`Failed to read backup for ${originalKey} at ${backupKey}`));
7580
- }
7581
- console.log(colors.green(`Rollback completed for migration ${this.name}.`));
7582
- }
7583
- };
7584
- /**
7585
- * S3Migrator
7586
- * Manages migration records and sequentially executes the provided migration scripts.
7587
- */
7588
- var S3Migrator = class {
7589
- s3;
7590
- bucketName;
7591
- migrationRecordKey = "migrate.json";
7592
- migrationRecords = [];
7593
- migrations;
7594
- constructor(options) {
7595
- this.bucketName = options.bucketName;
7596
- this.s3 = options.s3;
7597
- this.migrations = options.migrations;
7598
- }
7599
- async loadMigrationRecords() {
7600
- try {
7601
- const command = new GetObjectCommand({
7602
- Bucket: this.bucketName,
7603
- Key: this.migrationRecordKey
7604
- });
7605
- const response = await this.s3.send(command);
7606
- if (response.Body) {
7607
- const bodyContents = await response.Body.transformToString();
7608
- try {
7609
- this.migrationRecords = JSON.parse(bodyContents);
7610
- } catch (jsonError) {
7611
- console.error(colors.red("Failed to parse migration records JSON:"), jsonError);
7612
- this.migrationRecords = [];
7613
- }
7614
- }
7615
- } catch (error) {
7616
- if (error.Code === "NoSuchKey" || error.name === "NoSuchKey" || error.message === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) this.migrationRecords = [];
7617
- else throw error;
7618
- }
7619
- }
7620
- async saveMigrationRecords(dryRun) {
7621
- if (dryRun) return;
7622
- const body = JSON.stringify(this.migrationRecords, null, 2);
7623
- await new Upload({
7624
- client: this.s3,
7625
- params: {
7626
- Bucket: this.bucketName,
7627
- Key: this.migrationRecordKey,
7628
- Body: body
7629
- }
7630
- }).done();
7631
- }
7632
- /**
7633
- * Returns a JSON object containing applied and pending migrations.
7634
- */
7635
- async list() {
7636
- await this.loadMigrationRecords();
7637
- return {
7638
- applied: this.migrationRecords.map((record) => ({
7639
- name: record.name,
7640
- appliedAt: record.appliedAt
7641
- })),
7642
- pending: this.migrations.filter((migration) => !this.migrationRecords.some((record) => record.name === migration.name)).map((migration) => ({ name: migration.name }))
7643
- };
7644
- }
7645
- async migrate({ dryRun }) {
7646
- await this.loadMigrationRecords();
7647
- for (const migration of this.migrations) {
7648
- if (this.migrationRecords.some((record) => record.name === migration.name)) continue;
7649
- console.log(colors.magenta(`Applying migration ${migration.name}...`));
7650
- migration.s3 = this.s3;
7651
- migration.bucketName = this.bucketName;
7652
- migration.dryRun = dryRun;
7653
- try {
7654
- await migration.migrate();
7655
- if (!dryRun) {
7656
- this.migrationRecords.push({
7657
- name: migration.name,
7658
- appliedAt: (/* @__PURE__ */ new Date()).toISOString()
7659
- });
7660
- console.log(colors.green(`${colors.bold(migration.name)} applied successfully.`));
7661
- await migration.cleanupBackups();
7662
- } else console.log(colors.yellow(`[DRY RUN] ${colors.bold(migration.name)} applied successfully`));
7663
- } catch (error) {
7664
- console.error(colors.red(`Migration ${migration.name} failed. Initiating rollback...`), error);
7665
- await migration.rollback();
7666
- throw error;
7667
- }
7668
- }
7669
- await this.saveMigrationRecords(dryRun);
7670
- if (!dryRun) console.log(colors.blue("All migrations applied."));
7671
- }
7672
- };
7673
- //#endregion
7674
- //#region iac/migrations/Migration0001HotUpdater0_13_0.ts
7675
- /**
7676
- * Migration0001HotUpdater0_13_0
7677
- * 1. Moves all files starting with ios/ or android/ to under production/ directory.
7678
- * 2. Reads update.json files in the production/ path, removes the fileUrl property from each item, merges { channel: "production" }, and updates them.
7679
- */
7680
- var Migration0001HotUpdater0_13_0 = class extends S3Migration {
7681
- name = "hot-updater_0.13.0";
7682
- async migrate() {
7683
- const keysToMove = (await this.getKeys("")).filter((key) => !key.startsWith("production/") && /^(ios|android)\//.test(key));
7684
- for (const key of keysToMove) {
7685
- const newKey = `production/${key}`;
7686
- await this.moveFile(key, newKey);
7687
- }
7688
- const updateKeys = (await this.getKeys("production/")).filter((key) => key.endsWith("update.json"));
7689
- for (const key of updateKeys) {
7690
- const data = await this.readJson(key);
7691
- if (data && Array.isArray(data)) {
7692
- const updatedData = data.map((item) => merge(omit(item, ["fileUrl"]), { channel: "production" }));
7693
- await this.updateFile(key, JSON.stringify(updatedData));
7694
- }
7695
- }
7696
- }
7697
- };
7698
- //#endregion
7699
- //#region iac/migrations/Migration0001HotUpdater0_18_0.ts
7700
- /**
7701
- * Migration0001HotUpdater0_18_0
7702
- * Adds storageUri to all update.json files.
7703
- */
7704
- var Migration0001HotUpdater0_18_0 = class extends S3Migration {
7705
- name = "hot-updater_0.18.0";
7706
- async migrate() {
7707
- const updateKeys = (await this.getKeys("")).filter((key) => key.endsWith("update.json"));
7708
- for (const key of updateKeys) {
7709
- const data = await this.readJson(key);
7710
- if (data && Array.isArray(data)) {
7711
- const updatedData = data.map((item) => merge(item, {
7712
- storageUri: `s3://${this.bucketName}/${item.id}/bundle.zip`,
7713
- metadata: {}
7714
- }));
7715
- await this.updateFile(key, JSON.stringify(updatedData));
7716
- }
7717
- }
7718
- }
7719
- };
7720
- //#endregion
7721
9509
  //#region iac/s3.ts
7722
9510
  function normalizeBucketRegion(region) {
7723
9511
  if (region == null) return "us-east-1";
@@ -7753,49 +9541,44 @@ var S3Manager = class {
7753
9541
  });
7754
9542
  p.log.info(`Created S3 bucket: ${bucketName}`);
7755
9543
  }
7756
- async runMigrations({ approved, bucketName, nonInteractive, region, migrations }) {
7757
- const migrator = new S3Migrator({
7758
- s3: new S3({
7759
- region,
7760
- credentials: this.credentials
7761
- }),
7762
- bucketName,
7763
- migrations
7764
- });
7765
- const { pending } = await migrator.list();
7766
- await migrator.migrate({ dryRun: true });
7767
- if (pending.length === 0) return false;
7768
- p.log.step("Pending migrations:");
7769
- for (const migration of pending) p.log.step(`- ${migration.name}`);
7770
- if (!nonInteractive || !approved) {
7771
- if (nonInteractive) throw new MissingInitInputsError(["HOT_UPDATER_AWS_MIGRATION_APPROVED"]);
7772
- const confirm = await p.confirm({
7773
- initialValue: approved,
7774
- message: "Do you want to continue?"
7775
- });
7776
- if (p.isCancel(confirm) || !confirm) {
7777
- p.log.info("Migration cancelled.");
7778
- process.exit(1);
7779
- }
7780
- }
7781
- await migrator.migrate({ dryRun: false });
7782
- return true;
7783
- }
7784
9544
  async updateBucketPolicy({ bucketName, region, distributionId, accountId }) {
7785
9545
  const s3Client = new S3({
7786
9546
  region,
7787
9547
  credentials: this.credentials
7788
9548
  });
9549
+ let existingPolicy = {};
9550
+ try {
9551
+ const { Policy } = await s3Client.getBucketPolicy({ Bucket: bucketName });
9552
+ if (Policy) {
9553
+ const parsed = JSON.parse(Policy);
9554
+ if (typeof parsed !== "object" || parsed === null) throw new Error("Existing S3 bucket policy is not a JSON object");
9555
+ existingPolicy = Object.fromEntries(Object.entries(parsed));
9556
+ }
9557
+ } catch (error) {
9558
+ if (typeof error !== "object" || error === null || Reflect.get(error, "name") !== "NoSuchBucketPolicy") throw error;
9559
+ }
9560
+ const policyStatements = Array.isArray(existingPolicy.Statement) ? existingPolicy.Statement : typeof existingPolicy.Statement === "object" && existingPolicy.Statement !== null ? [existingPolicy.Statement] : [];
9561
+ const sourceArn = `arn:aws:cloudfront::${accountId}:distribution/${distributionId}`;
9562
+ const statementId = `AllowHotUpdaterCloudFrontRead${distributionId.replace(/[^A-Za-z0-9]/g, "")}`;
9563
+ const existingStatements = policyStatements.filter((statement) => {
9564
+ if (typeof statement !== "object" || statement === null) return true;
9565
+ const sid = Reflect.get(statement, "Sid");
9566
+ if (sid === statementId) return false;
9567
+ if (sid !== "AllowCloudFrontServicePrincipal" && sid !== "AllowHotUpdaterCloudFrontRead") return true;
9568
+ const condition = Reflect.get(statement, "Condition");
9569
+ const stringEquals = typeof condition === "object" && condition !== null ? Reflect.get(condition, "StringEquals") : void 0;
9570
+ return !(typeof stringEquals === "object" && stringEquals !== null && Reflect.get(stringEquals, "AWS:SourceArn") === sourceArn);
9571
+ });
7789
9572
  const bucketPolicy = {
7790
- Version: "2008-10-17",
7791
- Id: "PolicyForCloudFrontPrivateContent",
7792
- Statement: [{
7793
- Sid: "AllowCloudFrontServicePrincipal",
9573
+ ...existingPolicy,
9574
+ Version: typeof existingPolicy.Version === "string" ? existingPolicy.Version : "2012-10-17",
9575
+ Statement: [...existingStatements, {
9576
+ Sid: statementId,
7794
9577
  Effect: "Allow",
7795
9578
  Principal: { Service: "cloudfront.amazonaws.com" },
7796
9579
  Action: "s3:GetObject",
7797
9580
  Resource: `arn:aws:s3:::${bucketName}/*`,
7798
- Condition: { StringEquals: { "AWS:SourceArn": `arn:aws:cloudfront::${accountId}:distribution/${distributionId}` } }
9581
+ Condition: { StringEquals: { "AWS:SourceArn": sourceArn } }
7799
9582
  }]
7800
9583
  };
7801
9584
  await s3Client.putBucketPolicy({
@@ -7877,15 +9660,19 @@ const getConfigScaffold = (build, authMode) => {
7877
9660
  pkg: "@hot-updater/aws",
7878
9661
  named: ["s3Storage"]
7879
9662
  }],
7880
- configString: "s3Storage(commonOptions)"
9663
+ configString: `s3Storage({
9664
+ ...awsOptions,
9665
+ bucketName: process.env.HOT_UPDATER_S3_BUCKET_NAME!,
9666
+ })`
7881
9667
  };
7882
9668
  const databaseConfig = {
7883
9669
  imports: [{
7884
9670
  pkg: "@hot-updater/aws",
7885
- named: ["s3Database"]
9671
+ named: ["dynamoDB"]
7886
9672
  }],
7887
- configString: `s3Database({
7888
- ...commonOptions,
9673
+ configString: `dynamoDB({
9674
+ ...awsOptions,
9675
+ tableName: process.env.HOT_UPDATER_DYNAMODB_TABLE_NAME!,
7889
9676
  cloudfrontDistributionId: process.env.HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID!,
7890
9677
  })`
7891
9678
  };
@@ -7893,11 +9680,11 @@ const getConfigScaffold = (build, authMode) => {
7893
9680
  switch (authMode.mode) {
7894
9681
  case "sso":
7895
9682
  helperStatements = [{
7896
- name: "commonOptions",
9683
+ name: "awsOptions",
7897
9684
  strategy: "merge-object",
9685
+ replaceIncompatibleProperties: ["credentials"],
7898
9686
  code: `
7899
- const commonOptions = {
7900
- bucketName: process.env.HOT_UPDATER_S3_BUCKET_NAME!,
9687
+ const awsOptions = {
7901
9688
  region: process.env.HOT_UPDATER_S3_REGION!,
7902
9689
  credentials: fromSSO({ profile: process.env.HOT_UPDATER_AWS_PROFILE! }),
7903
9690
  };`.trim()
@@ -7905,16 +9692,15 @@ const commonOptions = {
7905
9692
  break;
7906
9693
  case "local":
7907
9694
  helperStatements = [{
7908
- name: "commonOptions",
9695
+ name: "awsOptions",
7909
9696
  strategy: "merge-object",
9697
+ replaceIncompatibleProperties: ["credentials"],
7910
9698
  code: authMode.profile ? `
7911
- const commonOptions = {
7912
- bucketName: process.env.HOT_UPDATER_S3_BUCKET_NAME!,
9699
+ const awsOptions = {
7913
9700
  region: process.env.HOT_UPDATER_S3_REGION!,
7914
9701
  credentials: fromIni({ profile: process.env.HOT_UPDATER_AWS_PROFILE! }),
7915
9702
  };`.trim() : `
7916
- const commonOptions = {
7917
- bucketName: process.env.HOT_UPDATER_S3_BUCKET_NAME!,
9703
+ const awsOptions = {
7918
9704
  region: process.env.HOT_UPDATER_S3_REGION!,
7919
9705
  credentials: fromNodeProviderChain(),
7920
9706
  };`.trim()
@@ -7922,11 +9708,11 @@ const commonOptions = {
7922
9708
  break;
7923
9709
  case "account":
7924
9710
  helperStatements = [{
7925
- name: "commonOptions",
9711
+ name: "awsOptions",
7926
9712
  strategy: "merge-object",
9713
+ replaceIncompatibleProperties: ["credentials"],
7927
9714
  code: `
7928
- const commonOptions = {
7929
- bucketName: process.env.HOT_UPDATER_S3_BUCKET_NAME!,
9715
+ const awsOptions = {
7930
9716
  region: process.env.HOT_UPDATER_S3_REGION!,
7931
9717
  credentials: {
7932
9718
  accessKeyId: process.env.HOT_UPDATER_S3_ACCESS_KEY_ID!,
@@ -7958,13 +9744,19 @@ const SOURCE_TEMPLATE = `// Add this to your App.tsx
7958
9744
  import { HotUpdater } from "@hot-updater/react-native";
7959
9745
 
7960
9746
  function App() {
7961
- return null; // Replace with your app root
9747
+ return null; // Replace with your app root.
7962
9748
  }
7963
9749
 
7964
- export default HotUpdater.wrap({
7965
- baseURL: "%%source%%",
7966
- updateStrategy: "appVersion", // or "fingerprint"
7967
- })(App);`;
9750
+ HotUpdater.init({
9751
+ baseURL: %%source%%,
9752
+ requestHeaders: {
9753
+ "x-api-key": %%apiKey%%,
9754
+ },
9755
+ });
9756
+
9757
+ // Call HotUpdater.checkForUpdate({ updateStrategy: "appVersion" })
9758
+ // when your app is ready to check.
9759
+ export default App;`;
7968
9760
  //#endregion
7969
9761
  //#region iac/index.ts
7970
9762
  const checkIfAwsCliInstalled = async () => {
@@ -7978,6 +9770,16 @@ const checkIfAwsCliInstalled = async () => {
7978
9770
  const isAwsRegion = (value) => {
7979
9771
  return value !== void 0 && Object.hasOwn(regionLocationMap, value);
7980
9772
  };
9773
+ const prepareDynamoDBDeployment = async (input) => {
9774
+ return new DynamoDBManager(input.region, input.credentials).ensureTable(input.tableName);
9775
+ };
9776
+ const prepareDynamoDBApiKey = async (input) => {
9777
+ return (await provisionApiKey({
9778
+ apiKeys: input.apiKeys,
9779
+ existingApiKey: input.existingApiKey,
9780
+ name: "AWS init"
9781
+ })).apiKey;
9782
+ };
7981
9783
  const runInit = async ({ build, envFile }) => {
7982
9784
  const nonInteractive = envFile !== void 0;
7983
9785
  const initEnvSources = await readHotUpdaterInitEnv(process.cwd(), envFile);
@@ -7995,6 +9797,7 @@ const runInit = async ({ build, envFile }) => {
7995
9797
  p.log.message(`${colors.blue("CloudFrontFullAccess")}: Create and update CloudFront distributions`);
7996
9798
  p.log.message(`${colors.blue("IAMFullAccess")}: Get or create IAM roles for Lambda@Edge`);
7997
9799
  p.log.message(`${colors.blue("AmazonSSMFullAccess")}: Access to SSM Parameters for storing CloudFront key pairs`);
9800
+ p.log.message(`${colors.blue("AmazonDynamoDBFullAccess_v2")}: Create and configure the metadata table`);
7998
9801
  const { awsProfile, configAuthMode, credentials, mode } = await resolveAwsAuth(providerEnv, nonInteractive);
7999
9802
  const resolvedAuthInputs = {
8000
9803
  ...savedInputs,
@@ -8021,9 +9824,9 @@ const runInit = async ({ build, envFile }) => {
8021
9824
  const savedBucketRegion = savedInputs.bucketRegion;
8022
9825
  const existingBucket = availableBuckets.find((bucket) => bucket.name === savedBucketName);
8023
9826
  const createSavedBucket = nonInteractive && savedBucketName !== void 0 && isAwsRegion(savedBucketRegion) && !existingBucket;
8024
- if (createSavedBucket && savedInputs.migrationApproved !== "true") throw new MissingInitInputsError(["HOT_UPDATER_AWS_MIGRATION_APPROVED"]);
8025
9827
  if (savedBucketName && !existingBucket && !createSavedBucket) p.log.warn("Saved S3 bucket was not found. Select a bucket again.");
8026
9828
  const savedLambdaName = savedInputs.lambdaName;
9829
+ const savedDynamoDBTableName = savedInputs.dynamodbTableName;
8027
9830
  const resourceInputs = await p.group({
8028
9831
  bucketSelection: () => {
8029
9832
  if (nonInteractive && existingBucket) return Promise.resolve(existingBucket.name);
@@ -8053,13 +9856,18 @@ const runInit = async ({ build, envFile }) => {
8053
9856
  value: region
8054
9857
  }))
8055
9858
  }) : Promise.resolve(availableBuckets.find((bucket) => bucket.name === results.bucketSelection)?.region),
9859
+ dynamodbTableName: () => nonInteractive && savedDynamoDBTableName ? Promise.resolve(savedDynamoDBTableName) : p.text({
9860
+ ...getInitProviderTextPromptValues(initProvider.inputs.dynamodbTableName.prompt, savedDynamoDBTableName),
9861
+ message: initProvider.inputs.dynamodbTableName.prompt.message,
9862
+ validate: (value) => value ? void 0 : "DynamoDB table name is required"
9863
+ }),
8056
9864
  lambdaName: () => nonInteractive && savedLambdaName ? Promise.resolve(savedLambdaName) : p.text({
8057
9865
  ...getInitProviderTextPromptValues(initProvider.inputs.lambdaName.prompt, savedLambdaName),
8058
9866
  message: initProvider.inputs.lambdaName.prompt.message,
8059
9867
  validate: (value) => value ? void 0 : "Lambda function name is required"
8060
9868
  })
8061
9869
  }, { onCancel: () => process.exit(1) });
8062
- const { bucketName, bucketRegion, lambdaName } = resourceInputs;
9870
+ const { bucketName, bucketRegion, dynamodbTableName, lambdaName } = resourceInputs;
8063
9871
  if (!bucketName || !lambdaName) {
8064
9872
  p.log.error("AWS resource names are required.");
8065
9873
  process.exit(1);
@@ -8068,19 +9876,29 @@ const runInit = async ({ build, envFile }) => {
8068
9876
  p.log.error("AWS bucket region is required.");
8069
9877
  process.exit(1);
8070
9878
  }
9879
+ const resolvedDynamoDBTableName = typeof dynamodbTableName === "string" ? dynamodbTableName : void 0;
9880
+ if (!resolvedDynamoDBTableName) {
9881
+ p.log.error("AWS DynamoDB table name is required.");
9882
+ process.exit(1);
9883
+ }
9884
+ await assertAwsLambdaCanInitialize({
9885
+ credentials,
9886
+ lambdaName
9887
+ });
8071
9888
  const cloudFrontManager = new CloudFrontManager(bucketRegion, credentials);
8072
9889
  const selectedDistribution = await cloudFrontManager.selectDistribution({
8073
9890
  bucketName,
8074
9891
  distributionId: savedInputs.distributionId,
8075
9892
  nonInteractive
8076
9893
  });
9894
+ if (selectedDistribution?.DomainName) await assertAwsInfrastructureGeneration({ domainName: selectedDistribution.DomainName });
8077
9895
  const resolvedInputs = {
8078
9896
  ...resolvedAuthInputs,
8079
9897
  bucketName,
8080
9898
  bucketRegion,
8081
9899
  distributionId: selectedDistribution?.Id,
8082
- lambdaName,
8083
- migrationApproved: savedInputs.migrationApproved
9900
+ dynamodbTableName: resolvedDynamoDBTableName,
9901
+ lambdaName
8084
9902
  };
8085
9903
  const initEnv = getInitProviderEnvVars({
8086
9904
  includeConsentInputs: await confirmInitInputPersistence({
@@ -8097,30 +9915,51 @@ const runInit = async ({ build, envFile }) => {
8097
9915
  await makeEnv({
8098
9916
  ...initEnv,
8099
9917
  ...initEnv[accessKeyEnvKey] ? { [accessKeyEnvKey]: {
8100
- comment: "The current key may have excessive permissions. Update it with an S3FullAccess and CloudFrontFullAccess key.",
9918
+ comment: "The current key may have excessive permissions. Replace it with least-privilege S3, DynamoDB, and CloudFront permissions.",
8101
9919
  value: initEnv[accessKeyEnvKey]
8102
9920
  } } : {},
8103
9921
  ...initEnv[secretAccessKeyEnvKey] ? { [secretAccessKeyEnvKey]: {
8104
- comment: "The current key may have excessive permissions. Update it with an S3FullAccess and CloudFrontFullAccess key.",
9922
+ comment: "The current key may have excessive permissions. Replace it with least-privilege S3, DynamoDB, and CloudFront permissions.",
8105
9923
  value: initEnv[secretAccessKeyEnvKey]
8106
9924
  } } : {}
8107
9925
  });
8108
9926
  if (resourceInputs.bucketSelection === createKey) await s3Manager.createBucket(bucketName, bucketRegion);
8109
9927
  p.log.info(`Selected S3 Bucket: ${bucketName} (${bucketRegion})`);
8110
- if (await s3Manager.runMigrations({
8111
- approved: savedInputs.migrationApproved === "true",
8112
- bucketName,
8113
- nonInteractive,
9928
+ await prepareDynamoDBDeployment({
9929
+ credentials,
9930
+ region: bucketRegion,
9931
+ tableName: resolvedDynamoDBTableName
9932
+ });
9933
+ const databasePlugin = dynamoDB({
9934
+ credentials,
8114
9935
  region: bucketRegion,
8115
- migrations: [new Migration0001HotUpdater0_13_0(), new Migration0001HotUpdater0_18_0()]
8116
- }) && savedInputs.migrationApproved !== "true") await makeEnv({ [initProvider.inputs.migrationApproved.envKey]: "true" });
8117
- const lambdaRoleArn = await new IAMManager(bucketRegion, credentials).createOrSelectRole();
8118
- const keyPair = await new SSMKeyPairManager(bucketRegion, credentials).getOrCreateKeyPair(`/hot-updater/${bucketName}/keypair`);
9936
+ tableName: resolvedDynamoDBTableName
9937
+ });
9938
+ let apiKey;
9939
+ try {
9940
+ apiKey = await prepareDynamoDBApiKey({
9941
+ apiKeys: databasePlugin.models.apiKeys,
9942
+ existingApiKey: providerEnv.HOT_UPDATER_API_KEY
9943
+ });
9944
+ await makeEnv({ HOT_UPDATER_API_KEY: apiKey });
9945
+ } finally {
9946
+ await databasePlugin.dispose?.();
9947
+ }
9948
+ p.log.info(`Using DynamoDB table: ${resolvedDynamoDBTableName} (${bucketRegion})`);
9949
+ const iamManager = new IAMManager(bucketRegion, credentials);
9950
+ const ssmParameterName = getAwsV1SsmParameterName(lambdaName);
9951
+ const lambdaRoleArn = await iamManager.createOrSelectRole({
9952
+ bucketName,
9953
+ dynamodbTableName: resolvedDynamoDBTableName,
9954
+ lambdaName,
9955
+ ssmParameterName
9956
+ });
9957
+ const keyPair = await new SSMKeyPairManager(bucketRegion, credentials).getOrCreateKeyPair(ssmParameterName);
8119
9958
  const { publicKeyId, keyGroupId } = await cloudFrontManager.getOrCreateKeyGroup(keyPair.publicKey);
8120
- const lambdaEdgeDeployer = new LambdaEdgeDeployer(credentials);
8121
- const ssmParameterName = `/hot-updater/${bucketName}/keypair`;
8122
- const { functionArn } = await lambdaEdgeDeployer.deploy(lambdaRoleArn, lambdaName, {
9959
+ const { functionArn } = await new LambdaEdgeDeployer(credentials).deploy(lambdaRoleArn, lambdaName, {
8123
9960
  bucketName,
9961
+ dynamodbRegion: bucketRegion,
9962
+ dynamodbTableName: resolvedDynamoDBTableName,
8124
9963
  publicKeyId,
8125
9964
  ssmParameterName,
8126
9965
  ssmRegion: bucketRegion
@@ -8146,10 +9985,15 @@ const runInit = async ({ build, envFile }) => {
8146
9985
  if (configWriteResult.status === "created") p.log.success("Generated 'hot-updater.config.ts' file with AWS settings.");
8147
9986
  else if (configWriteResult.status === "merged") p.log.success("Updated 'hot-updater.config.ts' file with AWS settings.");
8148
9987
  else p.log.warn(`Kept existing 'hot-updater.config.ts' unchanged: ${configWriteResult.reason}`);
8149
- const sourceUrl = `https://${distributionDomain}/api/check-update`;
8150
- p.note(transformTemplate(SOURCE_TEMPLATE, { source: sourceUrl }));
9988
+ const sourceUrl = `https://${distributionDomain}`;
9989
+ p.note(transformTemplate(SOURCE_TEMPLATE, {
9990
+ apiKey: JSON.stringify(apiKey),
9991
+ source: JSON.stringify(sourceUrl)
9992
+ }));
9993
+ p.note(formatApiKeyNote(apiKey), "API Key");
9994
+ p.log.message("Store this API key separately in a secure place.");
8151
9995
  p.log.message(`Next step: ${link("https://hot-updater.dev/docs/managed/aws#step-4-changeenv-file-optional")}`);
8152
9996
  p.log.success("Done! 🎉");
8153
9997
  };
8154
9998
  //#endregion
8155
- export { runInit };
9999
+ export { prepareDynamoDBApiKey, prepareDynamoDBDeployment, runInit };