@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.
- package/dist/iac/index.cjs +2670 -824
- package/dist/iac/index.d.cts +15 -1
- package/dist/iac/index.d.mts +15 -1
- package/dist/iac/index.mjs +2669 -825
- package/dist/index.cjs +1916 -1853
- package/dist/index.d.cts +37 -46
- package/dist/index.d.mts +39 -47
- package/dist/index.mjs +1916 -1828
- package/dist/init/index.cjs +13 -13
- package/dist/init/index.d.cts +13 -13
- package/dist/init/index.d.mts +13 -13
- package/dist/init/index.mjs +13 -13
- package/dist/lambda/index.cjs +6473 -4313
- package/dist/lambda/index.d.cts +4 -4
- package/package.json +9 -9
package/dist/iac/index.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6218
|
-
const
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
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
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
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
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
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
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
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
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
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
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
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
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
"
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
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
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
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: "
|
|
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: {
|
|
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
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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:
|
|
6941
|
-
Items:
|
|
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,
|
|
8782
|
+
DefaultCacheBehavior: mergeBehaviorWithExisting(distributionConfig.DefaultCacheBehavior, defaultCacheBehavior),
|
|
6946
8783
|
CacheBehaviors: {
|
|
6947
|
-
Quantity:
|
|
6948
|
-
Items:
|
|
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
|
|
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 ===
|
|
8848
|
+
matches: (policy) => policy.CachePolicy?.CachePolicyConfig?.Name === config.Name
|
|
7014
8849
|
}))?.CachePolicy?.Id;
|
|
7015
|
-
if (existingPolicyId)
|
|
7016
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
7196
|
-
|
|
7197
|
-
if (
|
|
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:
|
|
9086
|
+
initialValue: reusableSavedDistributionId ?? createNewDistribution,
|
|
7211
9087
|
message: "Select a CloudFront distribution:",
|
|
7212
|
-
options:
|
|
7213
|
-
value:
|
|
7214
|
-
label:
|
|
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"
|
|
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
|
|
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 =
|
|
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
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
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:
|
|
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.
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
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
|
|
7320
|
-
const
|
|
7321
|
-
const
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
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
|
|
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
|
|
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:
|
|
9451
|
+
Code: { ZipFile: zipFile },
|
|
7383
9452
|
Description: "Hot Updater Lambda@Edge function",
|
|
9453
|
+
MemorySize: LAMBDA_MEMORY_SIZE,
|
|
7384
9454
|
Publish: true,
|
|
7385
|
-
|
|
7386
|
-
|
|
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
|
|
9469
|
+
await this.waitForUpdate(lambdaClient, lambdaName);
|
|
7401
9470
|
await lambdaClient.updateFunctionConfiguration({
|
|
7402
9471
|
FunctionName: lambdaName,
|
|
7403
|
-
MemorySize:
|
|
7404
|
-
|
|
9472
|
+
MemorySize: LAMBDA_MEMORY_SIZE,
|
|
9473
|
+
Role: lambdaRoleArn,
|
|
9474
|
+
Timeout: LAMBDA_TIMEOUT_SECONDS
|
|
7405
9475
|
});
|
|
7406
|
-
|
|
7407
|
-
await
|
|
7408
|
-
|
|
7409
|
-
functionArn.
|
|
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
|
-
|
|
7796
|
-
|
|
7797
|
-
Statement: [{
|
|
7798
|
-
Sid:
|
|
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":
|
|
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:
|
|
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: ["
|
|
9676
|
+
named: ["dynamoDB"]
|
|
7891
9677
|
}],
|
|
7892
|
-
configString: `
|
|
7893
|
-
...
|
|
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: "
|
|
9688
|
+
name: "awsOptions",
|
|
7902
9689
|
strategy: "merge-object",
|
|
9690
|
+
replaceIncompatibleProperties: ["credentials"],
|
|
7903
9691
|
code: `
|
|
7904
|
-
const
|
|
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: "
|
|
9700
|
+
name: "awsOptions",
|
|
7914
9701
|
strategy: "merge-object",
|
|
9702
|
+
replaceIncompatibleProperties: ["credentials"],
|
|
7915
9703
|
code: authMode.profile ? `
|
|
7916
|
-
const
|
|
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
|
|
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: "
|
|
9716
|
+
name: "awsOptions",
|
|
7931
9717
|
strategy: "merge-object",
|
|
9718
|
+
replaceIncompatibleProperties: ["credentials"],
|
|
7932
9719
|
code: `
|
|
7933
|
-
const
|
|
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
|
-
|
|
7970
|
-
baseURL:
|
|
7971
|
-
|
|
7972
|
-
|
|
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
|
-
|
|
8088
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
9933
|
+
await prepareDynamoDBDeployment({
|
|
9934
|
+
credentials,
|
|
9935
|
+
region: bucketRegion,
|
|
9936
|
+
tableName: resolvedDynamoDBTableName
|
|
9937
|
+
});
|
|
9938
|
+
const databasePlugin = dynamoDB({
|
|
9939
|
+
credentials,
|
|
8119
9940
|
region: bucketRegion,
|
|
8120
|
-
|
|
8121
|
-
})
|
|
8122
|
-
|
|
8123
|
-
|
|
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
|
|
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}
|
|
8155
|
-
_hot_updater_cli_tools.p.note((0, _hot_updater_cli_tools.transformTemplate)(SOURCE_TEMPLATE, {
|
|
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;
|