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