@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.
@@ -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 = void 0 && (void 0).__classPrivateFieldGet || function(receiver, state, kind, f) {
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$1) {
1308
- if (typeof path$1 !== "string") return null;
1309
- const last = path$1.replace(/^.*[/\\]/, "").toLowerCase();
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$1.length;
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 Mime_default(standard_default, other_default)._freeze();
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
- await client.send(new CreateInvalidationCommand({
1420
- DistributionId: distributionId,
1421
- InvalidationBatch: {
1422
- CallerReference: `invalidation-${timestamp}`,
1423
- Paths: {
1424
- Quantity: paths.length,
1425
- Items: paths
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",...s3Config } = config;
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,...s3Config } = config;
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
- export { s3Database, s3Storage };
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 };