@hot-updater/aws 0.27.1 → 0.29.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 +426 -690
- package/dist/iac/{index.js → index.mjs} +304 -548
- package/dist/index.cjs +179 -59
- package/dist/index.d.cts +37 -4
- package/dist/index.d.mts +62 -0
- package/dist/{index.js → index.mjs} +155 -33
- package/dist/lambda/index.cjs +5819 -6848
- package/dist/lambda/index.d.cts +4 -1
- package/package.json +15 -13
- package/dist/index.d.ts +0 -29
- package/dist/lambda/dist-cjs-Mm7FDWb8.cjs +0 -128
- package/dist/lambda/event-streams-0D7SB645.cjs +0 -204
- /package/dist/iac/{index.d.ts → index.d.mts} +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { CloudFrontClient, CreateInvalidationCommand } from "@aws-sdk/client-cloudfront";
|
|
1
|
+
import { CloudFrontClient, CreateInvalidationCommand, GetInvalidationCommand } from "@aws-sdk/client-cloudfront";
|
|
2
2
|
import { DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, ListObjectsV2Command, NoSuchKey, S3Client } from "@aws-sdk/client-s3";
|
|
3
3
|
import { Upload } from "@aws-sdk/lib-storage";
|
|
4
4
|
import { createBlobDatabasePlugin, createStorageKeyBuilder, createStoragePlugin, getContentType, parseStorageUri } from "@hot-updater/plugin-core";
|
|
5
5
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
6
6
|
import fs from "fs/promises";
|
|
7
7
|
import path from "path";
|
|
8
|
-
|
|
8
|
+
import { SSM } from "@aws-sdk/client-ssm";
|
|
9
|
+
import { getSignedUrl as getSignedUrl$1 } from "@aws-sdk/cloudfront-signer";
|
|
9
10
|
//#region ../../node_modules/.pnpm/mime@4.0.4/node_modules/mime/dist/types/other.js
|
|
10
11
|
const types$1 = {
|
|
11
12
|
"application/prs.cww": ["cww"],
|
|
@@ -819,8 +820,6 @@ const types$1 = {
|
|
|
819
820
|
"x-conference/x-cooltalk": ["ice"]
|
|
820
821
|
};
|
|
821
822
|
Object.freeze(types$1);
|
|
822
|
-
var other_default = types$1;
|
|
823
|
-
|
|
824
823
|
//#endregion
|
|
825
824
|
//#region ../../node_modules/.pnpm/mime@4.0.4/node_modules/mime/dist/types/standard.js
|
|
826
825
|
const types = {
|
|
@@ -1266,11 +1265,9 @@ const types = {
|
|
|
1266
1265
|
"video/webm": ["webm"]
|
|
1267
1266
|
};
|
|
1268
1267
|
Object.freeze(types);
|
|
1269
|
-
var standard_default = types;
|
|
1270
|
-
|
|
1271
1268
|
//#endregion
|
|
1272
1269
|
//#region ../../node_modules/.pnpm/mime@4.0.4/node_modules/mime/dist/src/Mime.js
|
|
1273
|
-
var __classPrivateFieldGet =
|
|
1270
|
+
var __classPrivateFieldGet = function(receiver, state, kind, f) {
|
|
1274
1271
|
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
1275
1272
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
1276
1273
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
@@ -1304,11 +1301,11 @@ var Mime = class {
|
|
|
1304
1301
|
}
|
|
1305
1302
|
return this;
|
|
1306
1303
|
}
|
|
1307
|
-
getType(path
|
|
1308
|
-
if (typeof path
|
|
1309
|
-
const last = path
|
|
1304
|
+
getType(path) {
|
|
1305
|
+
if (typeof path !== "string") return null;
|
|
1306
|
+
const last = path.replace(/^.*[/\\]/, "").toLowerCase();
|
|
1310
1307
|
const ext = last.replace(/^.*\./, "").toLowerCase();
|
|
1311
|
-
const hasPath = last.length < path
|
|
1308
|
+
const hasPath = last.length < path.length;
|
|
1312
1309
|
if (!(ext.length < last.length - 1) && hasPath) return null;
|
|
1313
1310
|
return __classPrivateFieldGet(this, _Mime_extensionToType, "f").get(ext) ?? null;
|
|
1314
1311
|
}
|
|
@@ -1337,12 +1334,44 @@ var Mime = class {
|
|
|
1337
1334
|
}
|
|
1338
1335
|
};
|
|
1339
1336
|
_Mime_extensionToType = /* @__PURE__ */ new WeakMap(), _Mime_typeToExtension = /* @__PURE__ */ new WeakMap(), _Mime_typeToExtensions = /* @__PURE__ */ new WeakMap();
|
|
1340
|
-
var Mime_default = Mime;
|
|
1341
|
-
|
|
1342
1337
|
//#endregion
|
|
1343
1338
|
//#region ../../node_modules/.pnpm/mime@4.0.4/node_modules/mime/dist/src/index.js
|
|
1344
|
-
var src_default = new
|
|
1345
|
-
|
|
1339
|
+
var src_default = new Mime(types, types$1)._freeze();
|
|
1340
|
+
//#endregion
|
|
1341
|
+
//#region src/runtimeAwsConfig.ts
|
|
1342
|
+
const truthyValues = new Set([
|
|
1343
|
+
"1",
|
|
1344
|
+
"true",
|
|
1345
|
+
"yes",
|
|
1346
|
+
"on"
|
|
1347
|
+
]);
|
|
1348
|
+
const isTruthy = (value) => {
|
|
1349
|
+
if (!value) return false;
|
|
1350
|
+
return truthyValues.has(value.toLowerCase());
|
|
1351
|
+
};
|
|
1352
|
+
const getAwsEndpointUrl = () => {
|
|
1353
|
+
return process.env.AWS_ENDPOINT_URL?.trim() || void 0;
|
|
1354
|
+
};
|
|
1355
|
+
const shouldForcePathStyle = (forcePathStyle, endpoint) => {
|
|
1356
|
+
if (forcePathStyle !== void 0) return forcePathStyle;
|
|
1357
|
+
if (isTruthy(process.env.AWS_S3_FORCE_PATH_STYLE)) return true;
|
|
1358
|
+
return endpoint !== void 0;
|
|
1359
|
+
};
|
|
1360
|
+
const applyS3RuntimeAwsConfig = (config) => {
|
|
1361
|
+
const endpoint = config.endpoint ?? getAwsEndpointUrl();
|
|
1362
|
+
return {
|
|
1363
|
+
...config,
|
|
1364
|
+
endpoint,
|
|
1365
|
+
forcePathStyle: shouldForcePathStyle(config.forcePathStyle, endpoint)
|
|
1366
|
+
};
|
|
1367
|
+
};
|
|
1368
|
+
const applySsmRuntimeAwsConfig = (config) => {
|
|
1369
|
+
const endpoint = config.endpoint ?? getAwsEndpointUrl();
|
|
1370
|
+
return {
|
|
1371
|
+
...config,
|
|
1372
|
+
endpoint
|
|
1373
|
+
};
|
|
1374
|
+
};
|
|
1346
1375
|
//#endregion
|
|
1347
1376
|
//#region src/utils/streamToString.ts
|
|
1348
1377
|
const streamToString = (stream) => {
|
|
@@ -1353,9 +1382,11 @@ const streamToString = (stream) => {
|
|
|
1353
1382
|
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
1354
1383
|
});
|
|
1355
1384
|
};
|
|
1356
|
-
|
|
1357
1385
|
//#endregion
|
|
1358
1386
|
//#region src/s3Database.ts
|
|
1387
|
+
const DEFAULT_INVALIDATION_POLL_INTERVAL_MS = 2e3;
|
|
1388
|
+
const DEFAULT_INVALIDATION_TIMEOUT_MS = 300 * 1e3;
|
|
1389
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1359
1390
|
/**
|
|
1360
1391
|
* Loads JSON data from S3.
|
|
1361
1392
|
* Returns null if NoSuchKey error occurs.
|
|
@@ -1413,25 +1444,49 @@ async function deleteObjectInS3(client, bucketName, key) {
|
|
|
1413
1444
|
/**
|
|
1414
1445
|
* Invalidates CloudFront cache for the given paths.
|
|
1415
1446
|
*/
|
|
1416
|
-
async function invalidateCloudFront(client, distributionId, paths) {
|
|
1447
|
+
async function invalidateCloudFront(client, distributionId, paths, options) {
|
|
1417
1448
|
if (paths.length === 0) return;
|
|
1418
1449
|
const timestamp = Date.now();
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1450
|
+
try {
|
|
1451
|
+
const response = await client.send(new CreateInvalidationCommand({
|
|
1452
|
+
DistributionId: distributionId,
|
|
1453
|
+
InvalidationBatch: {
|
|
1454
|
+
CallerReference: `invalidation-${timestamp}`,
|
|
1455
|
+
Paths: {
|
|
1456
|
+
Quantity: paths.length,
|
|
1457
|
+
Items: paths
|
|
1458
|
+
}
|
|
1426
1459
|
}
|
|
1460
|
+
}));
|
|
1461
|
+
if (!options?.shouldWaitForInvalidation) return;
|
|
1462
|
+
const invalidationId = response.Invalidation?.Id;
|
|
1463
|
+
if (!invalidationId) throw new Error("CloudFront invalidation response is missing Invalidation.Id");
|
|
1464
|
+
if (response.Invalidation?.Status === "Completed") return;
|
|
1465
|
+
const timeoutMs = DEFAULT_INVALIDATION_TIMEOUT_MS;
|
|
1466
|
+
const pollIntervalMs = DEFAULT_INVALIDATION_POLL_INTERVAL_MS;
|
|
1467
|
+
const deadline = Date.now() + timeoutMs;
|
|
1468
|
+
while (Date.now() < deadline) {
|
|
1469
|
+
await sleep(pollIntervalMs);
|
|
1470
|
+
if ((await client.send(new GetInvalidationCommand({
|
|
1471
|
+
DistributionId: distributionId,
|
|
1472
|
+
Id: invalidationId
|
|
1473
|
+
}))).Invalidation?.Status === "Completed") return;
|
|
1427
1474
|
}
|
|
1428
|
-
|
|
1475
|
+
throw new Error(`Timed out waiting for CloudFront invalidation ${invalidationId} to complete after ${timeoutMs}ms`);
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
if (options?.shouldWaitForInvalidation) throw error;
|
|
1478
|
+
const message = error instanceof Error ? error.message : "Unknown invalidation error";
|
|
1479
|
+
console.warn(`[hot-updater/aws] CloudFront invalidation failed for distribution ${distributionId}; continuing without cache invalidation.`, {
|
|
1480
|
+
error: message,
|
|
1481
|
+
paths
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1429
1484
|
}
|
|
1430
1485
|
const s3Database = createBlobDatabasePlugin({
|
|
1431
1486
|
name: "s3Database",
|
|
1432
1487
|
factory: (config) => {
|
|
1433
|
-
const { bucketName, cloudfrontDistributionId, apiBasePath = "/api/check-update"
|
|
1434
|
-
const client = new S3Client(s3Config);
|
|
1488
|
+
const { bucketName, cloudfrontDistributionId, apiBasePath = "/api/check-update", shouldWaitForInvalidation = false, ...s3Config } = config;
|
|
1489
|
+
const client = new S3Client(applyS3RuntimeAwsConfig(s3Config));
|
|
1435
1490
|
const cloudfrontClient = cloudfrontDistributionId ? new CloudFrontClient({
|
|
1436
1491
|
credentials: s3Config.credentials,
|
|
1437
1492
|
region: s3Config.region
|
|
@@ -1443,21 +1498,20 @@ const s3Database = createBlobDatabasePlugin({
|
|
|
1443
1498
|
uploadObject: (key, data) => uploadJsonToS3(client, bucketName, key, data),
|
|
1444
1499
|
deleteObject: (key) => deleteObjectInS3(client, bucketName, key),
|
|
1445
1500
|
invalidatePaths: (pathsToInvalidate) => {
|
|
1446
|
-
if (cloudfrontClient && cloudfrontDistributionId && pathsToInvalidate.length > 0) return invalidateCloudFront(cloudfrontClient, cloudfrontDistributionId, pathsToInvalidate);
|
|
1501
|
+
if (cloudfrontClient && cloudfrontDistributionId && pathsToInvalidate.length > 0) return invalidateCloudFront(cloudfrontClient, cloudfrontDistributionId, pathsToInvalidate, { shouldWaitForInvalidation });
|
|
1447
1502
|
return Promise.resolve();
|
|
1448
1503
|
}
|
|
1449
1504
|
};
|
|
1450
1505
|
}
|
|
1451
1506
|
});
|
|
1452
|
-
|
|
1453
1507
|
//#endregion
|
|
1454
1508
|
//#region src/s3Storage.ts
|
|
1455
1509
|
const s3Storage = createStoragePlugin({
|
|
1456
1510
|
name: "s3Storage",
|
|
1457
1511
|
supportedProtocol: "s3",
|
|
1458
1512
|
factory: (config) => {
|
|
1459
|
-
const { bucketName
|
|
1460
|
-
const client = new S3Client(s3Config);
|
|
1513
|
+
const { bucketName, ...s3Config } = config;
|
|
1514
|
+
const client = new S3Client(applyS3RuntimeAwsConfig(s3Config));
|
|
1461
1515
|
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
1462
1516
|
return {
|
|
1463
1517
|
async delete(storageUri) {
|
|
@@ -1518,6 +1572,74 @@ const s3Storage = createStoragePlugin({
|
|
|
1518
1572
|
};
|
|
1519
1573
|
}
|
|
1520
1574
|
});
|
|
1521
|
-
|
|
1522
1575
|
//#endregion
|
|
1523
|
-
|
|
1576
|
+
//#region src/withCloudFrontSignedUrl.ts
|
|
1577
|
+
const ONE_YEAR_IN_SECONDS = 3600 * 24 * 365;
|
|
1578
|
+
const privateKeyCache = /* @__PURE__ */ new Map();
|
|
1579
|
+
const getPrivateKeyFromSsm = async (region, parameterName) => {
|
|
1580
|
+
if (!region) throw new Error(`Invalid AWS region format: ${region}. Expected format like 'us-east-1' or 'ap-southeast-1'`);
|
|
1581
|
+
const parameter = (await new SSM(applySsmRuntimeAwsConfig({ region })).getParameter({
|
|
1582
|
+
Name: parameterName,
|
|
1583
|
+
WithDecryption: true
|
|
1584
|
+
})).Parameter;
|
|
1585
|
+
if (!parameter) throw new Error(`Failed to retrieve private key from SSM parameter: ${parameterName}`);
|
|
1586
|
+
const parameterValue = parameter.Value;
|
|
1587
|
+
if (!parameterValue) throw new Error(`Failed to retrieve private key from SSM parameter: ${parameterName}`);
|
|
1588
|
+
let keyPair;
|
|
1589
|
+
try {
|
|
1590
|
+
keyPair = JSON.parse(parameterValue);
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
throw new Error(`Invalid JSON format in SSM parameter: ${parameterName}. ${error instanceof Error ? error.message : String(error)}`);
|
|
1593
|
+
}
|
|
1594
|
+
const privateKey = keyPair.privateKey;
|
|
1595
|
+
if (!privateKey || typeof privateKey !== "string") throw new Error(`Invalid private key format in SSM parameter: ${parameterName}`);
|
|
1596
|
+
return privateKey;
|
|
1597
|
+
};
|
|
1598
|
+
const resolvePrivateKey = (config) => {
|
|
1599
|
+
if ("getPrivateKey" in config && typeof config.getPrivateKey === "function") return config.getPrivateKey();
|
|
1600
|
+
const cacheKey = `${config.ssmRegion}:${config.ssmParameterName}`;
|
|
1601
|
+
const cachedPrivateKey = privateKeyCache.get(cacheKey);
|
|
1602
|
+
if (cachedPrivateKey) return cachedPrivateKey;
|
|
1603
|
+
const privateKeyPromise = getPrivateKeyFromSsm(config.ssmRegion, config.ssmParameterName).catch((error) => {
|
|
1604
|
+
privateKeyCache.delete(cacheKey);
|
|
1605
|
+
throw error;
|
|
1606
|
+
});
|
|
1607
|
+
privateKeyCache.set(cacheKey, privateKeyPromise);
|
|
1608
|
+
return privateKeyPromise;
|
|
1609
|
+
};
|
|
1610
|
+
const resolvePublicBaseUrl = async (config, context) => {
|
|
1611
|
+
const publicBaseUrl = typeof config.publicBaseUrl === "function" ? await config.publicBaseUrl(context) : config.publicBaseUrl;
|
|
1612
|
+
if (!publicBaseUrl) throw new Error("CloudFront publicBaseUrl resolver returned an empty URL");
|
|
1613
|
+
return publicBaseUrl;
|
|
1614
|
+
};
|
|
1615
|
+
const withCloudFrontSignedUrl = (storageFactory, config) => {
|
|
1616
|
+
return () => {
|
|
1617
|
+
const baseStorage = storageFactory();
|
|
1618
|
+
return {
|
|
1619
|
+
...baseStorage,
|
|
1620
|
+
name: `${baseStorage.name}WithCloudFrontSignedUrl`,
|
|
1621
|
+
async getDownloadUrl(storageUri, context) {
|
|
1622
|
+
const storageUrl = new URL(storageUri);
|
|
1623
|
+
if (storageUrl.protocol !== "s3:") return baseStorage.getDownloadUrl(storageUri, context);
|
|
1624
|
+
const [privateKey, publicBaseUrl] = await Promise.all([resolvePrivateKey(config), resolvePublicBaseUrl(config, context)]);
|
|
1625
|
+
const url = new URL(publicBaseUrl);
|
|
1626
|
+
url.pathname = storageUrl.pathname;
|
|
1627
|
+
url.search = "";
|
|
1628
|
+
return { fileUrl: getSignedUrl$1({
|
|
1629
|
+
url: url.toString(),
|
|
1630
|
+
keyPairId: config.keyPairId,
|
|
1631
|
+
privateKey,
|
|
1632
|
+
dateLessThan: new Date(Date.now() + (config.expiresSeconds ?? ONE_YEAR_IN_SECONDS) * 1e3).toISOString()
|
|
1633
|
+
}) };
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
};
|
|
1637
|
+
};
|
|
1638
|
+
//#endregion
|
|
1639
|
+
//#region src/s3LambdaEdgeStorage.ts
|
|
1640
|
+
const awsLambdaEdgeStorage = (config, hooks) => {
|
|
1641
|
+
return withCloudFrontSignedUrl(s3Storage(config, hooks), config);
|
|
1642
|
+
};
|
|
1643
|
+
const s3LambdaEdgeStorage = awsLambdaEdgeStorage;
|
|
1644
|
+
//#endregion
|
|
1645
|
+
export { awsLambdaEdgeStorage, s3Database, s3LambdaEdgeStorage, s3Storage, withCloudFrontSignedUrl };
|