@hot-updater/aws 0.36.7 → 1.0.0-rc.1

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