@hot-updater/aws 1.0.0-rc.42 → 1.0.0-rc.44

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/index.cjs CHANGED
@@ -7,9 +7,7 @@ let _aws_sdk_client_ssm = require("@aws-sdk/client-ssm");
7
7
  let _aws_sdk_cloudfront_signer = require("@aws-sdk/cloudfront-signer");
8
8
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
9
9
  let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
10
- let _hot_updater_server_plugins_api_keys = require("@hot-updater/server/plugins/api-keys");
11
- let _hot_updater_server_plugins_insights = require("@hot-updater/server/plugins/insights");
12
- let _hot_updater_server_plugins_remote_config = require("@hot-updater/server/plugins/remote-config");
10
+ let _hot_updater_server_plugins = require("@hot-updater/server/plugins");
13
11
  //#region src/cloudFrontInvalidation.ts
14
12
  const DEFAULT_POLL_INTERVAL_MS = 2e3;
15
13
  const DEFAULT_TIMEOUT_MS = 3e5;
@@ -438,10 +436,11 @@ const cloudFrontDownloadUrl = (config) => async ({ storageUri }) => {
438
436
  };
439
437
  //#endregion
440
438
  //#region src/s3Storage.ts
439
+ /** Devices download right after the update check that returns the URL. */
440
+ const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
441
441
  const isObjectNotFoundError = (error) => error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey");
442
442
  function s3Storage(config) {
443
- const { bucketName, basePath, downloadUrlSigningKey, getDownloadUrl: configuredGetDownloadUrl, ...s3Config } = config;
444
- const getDownloadUrl = configuredGetDownloadUrl ?? (downloadUrlSigningKey ? (0, _hot_updater_plugin_core.createStorageDownloadUrl)(downloadUrlSigningKey) : void 0);
443
+ const { bucketName, basePath, getDownloadUrl, ...s3Config } = config;
445
444
  const client = new _aws_sdk_client_s3.S3Client(applyS3RuntimeAwsConfig(s3Config));
446
445
  const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
447
446
  const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(normalizedBasePath);
@@ -554,10 +553,15 @@ function s3Storage(config) {
554
553
  throw error;
555
554
  }
556
555
  },
557
- ...getDownloadUrl ? { async getDownloadUrl(input) {
558
- parseAndValidate(input.storageUri);
559
- return getDownloadUrl(input);
560
- } } : {},
556
+ async getDownloadUrl({ storageUri }) {
557
+ const { key } = parseAndValidate(storageUri);
558
+ if (getDownloadUrl) return getDownloadUrl({ storageUri });
559
+ const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
560
+ return { url: await getSignedUrl(client, new _aws_sdk_client_s3.GetObjectCommand({
561
+ Bucket: bucketName,
562
+ Key: key
563
+ }), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
564
+ },
561
565
  async exists({ storageUri }) {
562
566
  const { key } = parseAndValidate(storageUri);
563
567
  try {
@@ -588,9 +592,9 @@ function s3Storage(config) {
588
592
  * client route, and Remote Config. Its Lambda@Edge function imports this list.
589
593
  */
590
594
  const plugins = [
591
- (0, _hot_updater_server_plugins_insights.insights)(),
592
- (0, _hot_updater_server_plugins_api_keys.apiKeys)(),
593
- (0, _hot_updater_server_plugins_remote_config.remoteConfig)()
595
+ (0, _hot_updater_server_plugins.insights)(),
596
+ (0, _hot_updater_server_plugins.apiKeys)(),
597
+ (0, _hot_updater_server_plugins.remoteConfig)()
594
598
  ];
595
599
  //#endregion
596
600
  exports.cloudFrontDownloadUrl = cloudFrontDownloadUrl;
package/dist/index.d.cts CHANGED
@@ -56,16 +56,14 @@ interface S3StorageConfig extends S3ClientConfig {
56
56
  bucketName: string;
57
57
  /** Base path where bundles will be stored in the bucket. */
58
58
  basePath?: string;
59
- downloadUrlSigningKey?: string;
59
+ /**
60
+ * The URL devices download an object from, such as
61
+ * `cloudFrontDownloadUrl(...)`. Without it, a presigned S3 URL that
62
+ * expires in an hour.
63
+ */
60
64
  getDownloadUrl?: StorageAdapter["getDownloadUrl"];
61
65
  }
62
- type S3StorageConfigWithDownloadUrl = S3StorageConfig & ({
63
- downloadUrlSigningKey: string;
64
- } | {
65
- getDownloadUrl: NonNullable<StorageAdapter["getDownloadUrl"]>;
66
- });
67
- type S3StorageOperations = "put" | "get" | "exists" | "delete" | "listObjects" | "deleteObjects";
68
- declare function s3Storage(config: S3StorageConfigWithDownloadUrl): StorageAdapterWith<S3StorageOperations | "getDownloadUrl">;
66
+ type S3StorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
69
67
  declare function s3Storage(config: S3StorageConfig): StorageAdapterWith<S3StorageOperations>;
70
68
  //#endregion
71
69
  //#region ../insights/dist/server/eventRow.d.mts
@@ -1081,4 +1079,4 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
1081
1079
  endpoints: readonly import("@hot-updater/plugin-core").PluginEndpoint[];
1082
1080
  }, {}>];
1083
1081
  //#endregion
1084
- export { CloudFrontDownloadUrlOptions, type DynamoDBConfig, S3StorageConfig, S3StorageConfigWithDownloadUrl, cloudFrontDownloadUrl, dynamoDB, migrateDynamoDB, plugins, s3Storage };
1082
+ export { CloudFrontDownloadUrlOptions, type DynamoDBConfig, S3StorageConfig, cloudFrontDownloadUrl, dynamoDB, migrateDynamoDB, plugins, s3Storage };
package/dist/index.d.mts CHANGED
@@ -56,16 +56,14 @@ interface S3StorageConfig extends S3ClientConfig {
56
56
  bucketName: string;
57
57
  /** Base path where bundles will be stored in the bucket. */
58
58
  basePath?: string;
59
- downloadUrlSigningKey?: string;
59
+ /**
60
+ * The URL devices download an object from, such as
61
+ * `cloudFrontDownloadUrl(...)`. Without it, a presigned S3 URL that
62
+ * expires in an hour.
63
+ */
60
64
  getDownloadUrl?: StorageAdapter["getDownloadUrl"];
61
65
  }
62
- type S3StorageConfigWithDownloadUrl = S3StorageConfig & ({
63
- downloadUrlSigningKey: string;
64
- } | {
65
- getDownloadUrl: NonNullable<StorageAdapter["getDownloadUrl"]>;
66
- });
67
- type S3StorageOperations = "put" | "get" | "exists" | "delete" | "listObjects" | "deleteObjects";
68
- declare function s3Storage(config: S3StorageConfigWithDownloadUrl): StorageAdapterWith<S3StorageOperations | "getDownloadUrl">;
66
+ type S3StorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
69
67
  declare function s3Storage(config: S3StorageConfig): StorageAdapterWith<S3StorageOperations>;
70
68
  //#endregion
71
69
  //#region ../insights/dist/server/eventRow.d.mts
@@ -1081,4 +1079,4 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
1081
1079
  endpoints: readonly import("@hot-updater/plugin-core").PluginEndpoint[];
1082
1080
  }, {}>];
1083
1081
  //#endregion
1084
- export { CloudFrontDownloadUrlOptions, type DynamoDBConfig, S3StorageConfig, S3StorageConfigWithDownloadUrl, cloudFrontDownloadUrl, dynamoDB, migrateDynamoDB, plugins, s3Storage };
1082
+ export { CloudFrontDownloadUrlOptions, type DynamoDBConfig, S3StorageConfig, cloudFrontDownloadUrl, dynamoDB, migrateDynamoDB, plugins, s3Storage };
package/dist/index.mjs CHANGED
@@ -1,14 +1,12 @@
1
1
  import { CreateTableCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DynamoDBClient, UpdateTimeToLiveCommand, waitUntilTableExists } from "@aws-sdk/client-dynamodb";
2
- import { createEngineDatabase, createKvAdapter, createStorageAdapter, createStorageDownloadUrl, createStorageKeyBuilder, createStorageUri, migrateCoreSchema, parseStorageUri } from "@hot-updater/plugin-core";
2
+ import { createEngineDatabase, createKvAdapter, createStorageAdapter, createStorageKeyBuilder, createStorageUri, migrateCoreSchema, parseStorageUri } from "@hot-updater/plugin-core";
3
3
  import { CloudFrontClient, CreateInvalidationCommand, GetInvalidationCommand } from "@aws-sdk/client-cloudfront";
4
4
  import { BatchGetCommand, BatchWriteCommand, DynamoDBDocumentClient, QueryCommand, TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
5
5
  import { SSM } from "@aws-sdk/client-ssm";
6
6
  import { getSignedUrl } from "@aws-sdk/cloudfront-signer";
7
7
  import { DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, S3Client } from "@aws-sdk/client-s3";
8
8
  import { Upload } from "@aws-sdk/lib-storage";
9
- import { apiKeys } from "@hot-updater/server/plugins/api-keys";
10
- import { insights } from "@hot-updater/server/plugins/insights";
11
- import { remoteConfig } from "@hot-updater/server/plugins/remote-config";
9
+ import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
12
10
  //#region src/cloudFrontInvalidation.ts
13
11
  const DEFAULT_POLL_INTERVAL_MS = 2e3;
14
12
  const DEFAULT_TIMEOUT_MS = 3e5;
@@ -437,10 +435,11 @@ const cloudFrontDownloadUrl = (config) => async ({ storageUri }) => {
437
435
  };
438
436
  //#endregion
439
437
  //#region src/s3Storage.ts
438
+ /** Devices download right after the update check that returns the URL. */
439
+ const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
440
440
  const isObjectNotFoundError = (error) => error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey");
441
441
  function s3Storage(config) {
442
- const { bucketName, basePath, downloadUrlSigningKey, getDownloadUrl: configuredGetDownloadUrl, ...s3Config } = config;
443
- const getDownloadUrl = configuredGetDownloadUrl ?? (downloadUrlSigningKey ? createStorageDownloadUrl(downloadUrlSigningKey) : void 0);
442
+ const { bucketName, basePath, getDownloadUrl, ...s3Config } = config;
444
443
  const client = new S3Client(applyS3RuntimeAwsConfig(s3Config));
445
444
  const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
446
445
  const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
@@ -553,10 +552,15 @@ function s3Storage(config) {
553
552
  throw error;
554
553
  }
555
554
  },
556
- ...getDownloadUrl ? { async getDownloadUrl(input) {
557
- parseAndValidate(input.storageUri);
558
- return getDownloadUrl(input);
559
- } } : {},
555
+ async getDownloadUrl({ storageUri }) {
556
+ const { key } = parseAndValidate(storageUri);
557
+ if (getDownloadUrl) return getDownloadUrl({ storageUri });
558
+ const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
559
+ return { url: await getSignedUrl(client, new GetObjectCommand({
560
+ Bucket: bucketName,
561
+ Key: key
562
+ }), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
563
+ },
560
564
  async exists({ storageUri }) {
561
565
  const { key } = parseAndValidate(storageUri);
562
566
  try {
@@ -47,9 +47,7 @@ let _aws_sdk_client_dynamodb = require("@aws-sdk/client-dynamodb");
47
47
  let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
48
48
  let _aws_sdk_client_cloudfront = require("@aws-sdk/client-cloudfront");
49
49
  let _aws_sdk_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
50
- let _hot_updater_server_plugins_api_keys = require("@hot-updater/server/plugins/api-keys");
51
- let _hot_updater_server_plugins_insights = require("@hot-updater/server/plugins/insights");
52
- let _hot_updater_server_plugins_remote_config = require("@hot-updater/server/plugins/remote-config");
50
+ let _hot_updater_server_plugins = require("@hot-updater/server/plugins");
53
51
  let _aws_sdk_credential_providers = require("@aws-sdk/credential-providers");
54
52
  let _aws_sdk_client_lambda = require("@aws-sdk/client-lambda");
55
53
  let crypto$1 = require("crypto");
@@ -6633,9 +6631,9 @@ const dynamoDB = (config) => {
6633
6631
  * client route, and Remote Config. Its Lambda@Edge function imports this list.
6634
6632
  */
6635
6633
  const plugins = [
6636
- (0, _hot_updater_server_plugins_insights.insights)(),
6637
- (0, _hot_updater_server_plugins_api_keys.apiKeys)(),
6638
- (0, _hot_updater_server_plugins_remote_config.remoteConfig)()
6634
+ (0, _hot_updater_server_plugins.insights)(),
6635
+ (0, _hot_updater_server_plugins.apiKeys)(),
6636
+ (0, _hot_updater_server_plugins.remoteConfig)()
6639
6637
  ];
6640
6638
  //#endregion
6641
6639
  //#region iac/init/index.ts
@@ -8457,10 +8455,14 @@ const awsOptions = {
8457
8455
  }
8458
8456
  const builder = new _hot_updater_cli_tools.ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setPlugins({
8459
8457
  imports: [{
8460
- pkg: "@hot-updater/aws",
8461
- named: ["plugins"]
8458
+ pkg: "hot-updater/plugins",
8459
+ named: [
8460
+ "apiKeys",
8461
+ "insights",
8462
+ "remoteConfig"
8463
+ ]
8462
8464
  }],
8463
- configString: "plugins"
8465
+ configString: "[apiKeys(), insights(), remoteConfig()]"
8464
8466
  }).setIntermediateCode(helperStatements.map((statement) => statement.code.trim()).join("\n\n"));
8465
8467
  switch (authMode.mode) {
8466
8468
  case "sso":
@@ -20,9 +20,7 @@ import { CreateTableCommand, DescribeTableCommand, DescribeTimeToLiveCommand, Dy
20
20
  import { SETTINGS_TABLE, aggregateBatchingTables, createEngineDatabase, createKvAdapter, encodeKvKey, migrateCoreSchema, toolingTargetOf } from "@hot-updater/plugin-core";
21
21
  import { CloudFront, CloudFrontClient, CreateInvalidationCommand, GetInvalidationCommand } from "@aws-sdk/client-cloudfront";
22
22
  import { BatchGetCommand, BatchWriteCommand, DynamoDBDocumentClient, QueryCommand, TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
23
- import { apiKeys } from "@hot-updater/server/plugins/api-keys";
24
- import { insights } from "@hot-updater/server/plugins/insights";
25
- import { remoteConfig } from "@hot-updater/server/plugins/remote-config";
23
+ import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
26
24
  import { fromIni, fromNodeProviderChain, fromSSO } from "@aws-sdk/credential-providers";
27
25
  import { Lambda } from "@aws-sdk/client-lambda";
28
26
  import crypto$1 from "crypto";
@@ -8452,10 +8450,14 @@ const awsOptions = {
8452
8450
  }
8453
8451
  const builder = new ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setPlugins({
8454
8452
  imports: [{
8455
- pkg: "@hot-updater/aws",
8456
- named: ["plugins"]
8453
+ pkg: "hot-updater/plugins",
8454
+ named: [
8455
+ "apiKeys",
8456
+ "insights",
8457
+ "remoteConfig"
8458
+ ]
8457
8459
  }],
8458
- configString: "plugins"
8460
+ configString: "[apiKeys(), insights(), remoteConfig()]"
8459
8461
  }).setIntermediateCode(helperStatements.map((statement) => statement.code.trim()).join("\n\n"));
8460
8462
  switch (authMode.mode) {
8461
8463
  case "sso":
@@ -1451,52 +1451,6 @@ var ReleaseCatalogMutationError = class extends Error {
1451
1451
  }
1452
1452
  };
1453
1453
  //#endregion
1454
- //#region ../plugin-core/dist/storageDownloadPath.mjs
1455
- const encodeBase64Url$1 = (value) => {
1456
- let binary = "";
1457
- for (const byte of new TextEncoder().encode(value)) binary += String.fromCharCode(byte);
1458
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
1459
- };
1460
- const decodeBase64Url$1 = (value) => {
1461
- try {
1462
- const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
1463
- const binary = atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "="));
1464
- return new TextDecoder(void 0, { fatal: true }).decode(Uint8Array.from(binary, (character) => character.charCodeAt(0)));
1465
- } catch {
1466
- return null;
1467
- }
1468
- };
1469
- const encodeBytes = (bytes) => {
1470
- let binary = "";
1471
- for (const byte of bytes) binary += String.fromCharCode(byte);
1472
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
1473
- };
1474
- const createStorageDownloadPath = (storageUri, signature) => `/storage/${encodeBase64Url$1(storageUri)}/${encodeURIComponent(signature)}`;
1475
- const parseStorageDownloadPath = (path) => {
1476
- const match = /^\/storage\/([^/]+)\/([^/]+)$/.exec(path);
1477
- if (!match) return null;
1478
- const storageUri = decodeBase64Url$1(match[1]);
1479
- if (storageUri === null) return null;
1480
- try {
1481
- return {
1482
- signature: decodeURIComponent(match[2]),
1483
- storageUri
1484
- };
1485
- } catch {
1486
- return null;
1487
- }
1488
- };
1489
- const createStorageDownloadUrl = (signingKey) => {
1490
- const key = crypto.subtle.importKey("raw", new TextEncoder().encode(signingKey), {
1491
- hash: "SHA-256",
1492
- name: "HMAC"
1493
- }, false, ["sign"]);
1494
- return async ({ storageUri }) => {
1495
- const signature = encodeBytes(new Uint8Array(await crypto.subtle.sign("HMAC", await key, new TextEncoder().encode(storageUri))));
1496
- return { url: createStorageDownloadPath(storageUri, signature) };
1497
- };
1498
- };
1499
- //#endregion
1500
1454
  //#region ../plugin-core/dist/serverPlugin/definePlugin.mjs
1501
1455
  /** Declares a plugin; Hot Updater's own plugins and third-party ones use the same contract. */
1502
1456
  const definePlugin = (plugin) => plugin;
@@ -4637,7 +4591,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
4637
4591
  };
4638
4592
  //#endregion
4639
4593
  //#region ../../packages/server/dist/version.mjs
4640
- const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.42";
4594
+ const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.44";
4641
4595
  //#endregion
4642
4596
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4643
4597
  /**
@@ -6803,10 +6757,42 @@ const ORIGIN_CACHE_MAX_ENTRIES = 128;
6803
6757
  const NO_CATALOG_HEADER = "x-hot-updater-catalog";
6804
6758
  /** A shared cache keeps a catalog, or a scope's lack of one, five seconds. */
6805
6759
  const CATALOG_CACHE_CONTROL = "public, max-age=0, s-maxage=5";
6760
+ /**
6761
+ * The same five seconds for a CDN that reads `CDN-Cache-Control` (RFC 9213),
6762
+ * such as Cloudflare's Workers Cache, which then ignores `Cache-Control`.
6763
+ * `s-maxage` forbids serving stale, so there an expired catalog held requests
6764
+ * at that location until the origin answered. Here, for five more seconds,
6765
+ * the CDN answers with the expired copy while it revalidates in the
6766
+ * background. `stale-if-error=0` keeps origin errors visible: without it,
6767
+ * Cloudflare serves a stale copy on error indefinitely.
6768
+ *
6769
+ * A device can see a catalog up to 15 seconds after a deploy or a Roll back
6770
+ * replaced it: the origin's 5-second catalog memo, then 5 seconds fresh and
6771
+ * 5 stale at the CDN. A revoked API key can read a cached catalog up to
6772
+ * 10 seconds. Devices and caches that don't read the header keep
6773
+ * `Cache-Control`.
6774
+ */
6775
+ const CATALOG_CDN_CACHE_CONTROL = "public, max-age=5, stale-while-revalidate=5, stale-if-error=0";
6806
6776
  const privateNotFound = () => Response.json({ error: "Not found" }, {
6807
6777
  status: 404,
6808
6778
  headers: { "cache-control": "private, no-store" }
6809
6779
  });
6780
+ const badRequest = (error) => Response.json({ error }, {
6781
+ status: 400,
6782
+ headers: { "cache-control": "private, no-store" }
6783
+ });
6784
+ /**
6785
+ * A 400 for a catalog path whose scope is malformed, such as a channel name
6786
+ * where its base64url key belongs; null for a well-formed scope.
6787
+ */
6788
+ const invalidScope = (input) => {
6789
+ try {
6790
+ createReleaseCatalogScopeKey(input);
6791
+ return null;
6792
+ } catch (error) {
6793
+ return badRequest(error instanceof Error ? error.message : "Invalid Release Catalog scope");
6794
+ }
6795
+ };
6810
6796
  /**
6811
6797
  * A scope with no catalog yet, such as a store version before its first
6812
6798
  * OTA release: cached like a catalog, so its update checks don't all reach
@@ -6818,6 +6804,7 @@ const missingCatalog = () => Response.json({ error: "Not found" }, {
6818
6804
  status: 404,
6819
6805
  headers: {
6820
6806
  "cache-control": CATALOG_CACHE_CONTROL,
6807
+ "cdn-cache-control": CATALOG_CDN_CACHE_CONTROL,
6821
6808
  vary: "Accept-Encoding",
6822
6809
  [NO_CATALOG_HEADER]: "none"
6823
6810
  }
@@ -6832,6 +6819,7 @@ const catalogResponse = async (catalog, request) => {
6832
6819
  const etag = `"sha256:${await responseHash(body)}"`;
6833
6820
  const headers = {
6834
6821
  "cache-control": CATALOG_CACHE_CONTROL,
6822
+ "cdn-cache-control": CATALOG_CDN_CACHE_CONTROL,
6835
6823
  "content-type": CATALOG_CONTENT_TYPE,
6836
6824
  etag,
6837
6825
  vary: "Accept-Encoding"
@@ -6883,16 +6871,15 @@ const createReleaseCatalogRouteHandlers = () => {
6883
6871
  appVersionReleaseCatalog: async (params, request, { core }) => {
6884
6872
  const rawAppVersion = requireRouteParam(params, "appVersion");
6885
6873
  const appVersion = canonicalizeAppVersion(rawAppVersion);
6886
- if (appVersion === null || appVersion !== rawAppVersion) return Response.json({ error: "Invalid app version" }, {
6887
- status: 400,
6888
- headers: { "cache-control": "private, no-store" }
6889
- });
6874
+ if (appVersion === null || appVersion !== rawAppVersion) return badRequest("Invalid app version");
6890
6875
  const input = {
6891
6876
  appVersion,
6892
6877
  channelKey: requireRouteParam(params, "channelKey"),
6893
6878
  platform: requirePlatformParam(params),
6894
6879
  strategy: "APP_VERSION"
6895
6880
  };
6881
+ const invalid = invalidScope(input);
6882
+ if (invalid) return invalid;
6896
6883
  return catalogResponse(await loadCatalog(`app-version:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
6897
6884
  },
6898
6885
  fingerprintReleaseCatalog: async (params, request, { core }) => {
@@ -6902,6 +6889,8 @@ const createReleaseCatalogRouteHandlers = () => {
6902
6889
  platform: requirePlatformParam(params),
6903
6890
  strategy: "FINGERPRINT"
6904
6891
  };
6892
+ const invalid = invalidScope(input);
6893
+ if (invalid) return invalid;
6905
6894
  return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
6906
6895
  },
6907
6896
  artifactV1: async (params, _request, { core }) => {
@@ -6983,8 +6972,8 @@ const errorResponse = (error, status) => Response.json({ error }, {
6983
6972
  headers: { "cache-control": "private, no-store" },
6984
6973
  status
6985
6974
  });
6986
- /** `/version` and storage downloads never pass through the client policy. */
6987
- const PUBLIC_CLIENT_ROUTES = /* @__PURE__ */ new Set(["version", "downloadStorageObject"]);
6975
+ /** `/version` never passes through the client policy. */
6976
+ const PUBLIC_CLIENT_ROUTES = /* @__PURE__ */ new Set(["version"]);
6988
6977
  const routesOf = Symbol.for("@hot-updater/server/routes");
6989
6978
  const withVary = (response, varyHeaders) => {
6990
6979
  const cacheControl = response.headers.get("cache-control") ?? "";
@@ -7029,26 +7018,11 @@ const createRequestHandler = ({ api, clientPolicy, privateResponses = false, rou
7029
7018
  });
7030
7019
  }
7031
7020
  };
7032
- const createDownloadStorageRouteHandler = (downloadStorageObject) => async (params) => {
7033
- const token = params.token;
7034
- const signature = params.signature;
7035
- if (!token || !signature) return errorResponse("Not found", 404);
7036
- const response = await downloadStorageObject(token, signature);
7037
- if (!response) return errorResponse("Not found", 404);
7038
- const headers = new Headers(response.headers);
7039
- if (!headers.has("cache-control")) headers.set("cache-control", "public, max-age=31536000, immutable");
7040
- return new Response(response.body, {
7041
- headers,
7042
- status: response.status,
7043
- statusText: response.statusText
7044
- });
7045
- };
7046
- function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, endpoints = [], plugins = [] }) {
7021
+ function createHotUpdaterHandlers({ api, clientPolicy, endpoints = [], plugins = [] }) {
7047
7022
  const routeHandlers = {
7048
7023
  ...createVersionRouteHandlers(plugins),
7049
7024
  ...createReleaseCatalogRouteHandlers(),
7050
- ...createAdminRouteHandlers(),
7051
- ...downloadStorageObject === void 0 ? {} : { downloadStorageObject: createDownloadStorageRouteHandler(downloadStorageObject) }
7025
+ ...createAdminRouteHandlers()
7052
7026
  };
7053
7027
  const routes = [];
7054
7028
  const shapes = /* @__PURE__ */ new Map();
@@ -7067,7 +7041,6 @@ function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, en
7067
7041
  const clientRouter = createRouter();
7068
7042
  const addClientRoute = mount(clientRouter, false);
7069
7043
  addClientRoute("GET", "/version", "version");
7070
- if (downloadStorageObject !== void 0) addClientRoute("GET", "/storage/:token/:signature", "downloadStorageObject");
7071
7044
  addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
7072
7045
  addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
7073
7046
  addClientRoute("GET", "/artifacts/v1/:targetBundleId/from/:currentBundleId", "artifactV1");
@@ -7099,33 +7072,23 @@ function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, en
7099
7072
  }
7100
7073
  //#endregion
7101
7074
  //#region ../../packages/server/dist/storageAccess.mjs
7075
+ /** A URL devices download from, which only an absolute HTTP(S) URL can be. */
7102
7076
  const assertRemoteUrl = (value) => {
7103
- const protocol = new URL(value).protocol;
7104
- if (protocol !== "http:" && protocol !== "https:") throw new Error("Storage getDownloadUrl must resolve to an HTTP(S) URL.");
7077
+ let protocol;
7078
+ try {
7079
+ protocol = new URL(value).protocol;
7080
+ } catch {}
7081
+ if (protocol !== "http:" && protocol !== "https:") throw new Error("Storage getDownloadUrl must resolve to an absolute HTTP(S) URL.");
7105
7082
  return value;
7106
7083
  };
7107
7084
  const getStorageProtocol = (storageUri) => new URL(storageUri).protocol.replace(":", "");
7108
7085
  const isRemoteUrlProtocol = (protocol) => protocol === "http" || protocol === "https";
7109
- const resolveDownloadPath = (value, storageUri) => {
7110
- const parsed = parseStorageDownloadPath(value);
7111
- if (!parsed || parsed.storageUri !== storageUri) throw new Error("Storage getDownloadUrl must return an HTTP(S) URL or a valid storage download path.");
7112
- return value;
7113
- };
7114
- const tokensEqual = (left, right) => {
7115
- const leftBytes = new TextEncoder().encode(left);
7116
- const rightBytes = new TextEncoder().encode(right);
7117
- let difference = leftBytes.length ^ rightBytes.length;
7118
- const length = Math.max(leftBytes.length, rightBytes.length);
7119
- for (let index = 0; index < length; index += 1) difference |= (leftBytes[index] ?? 0) ^ (rightBytes[index] ?? 0);
7120
- return difference === 0;
7121
- };
7122
- const createStorageAccess = (storageAdapters) => {
7123
- const protocols = /* @__PURE__ */ new Set();
7124
- for (const storage of storageAdapters) {
7125
- if (protocols.has(storage.protocol)) throw new Error(`Multiple storage adapters handle protocol: ${storage.protocol}`);
7126
- protocols.add(storage.protocol);
7127
- }
7128
- const findStorage = (protocol) => storageAdapters.find((item) => item.protocol === protocol);
7086
+ /**
7087
+ * Reads and download URLs over the server's storage, for the URIs of its
7088
+ * protocol; an HTTP(S) URI is used as it is.
7089
+ */
7090
+ const createStorageAccess = (storageAdapter) => {
7091
+ const findStorage = (protocol) => storageAdapter.protocol === protocol ? storageAdapter : void 0;
7129
7092
  const readStorageResponse = async (storageUri) => {
7130
7093
  const protocol = getStorageProtocol(storageUri);
7131
7094
  const storage = findStorage(protocol);
@@ -7149,38 +7112,12 @@ const createStorageAccess = (storageAdapters) => {
7149
7112
  }
7150
7113
  if (!storage.getDownloadUrl) return null;
7151
7114
  const { url: downloadUrl } = await storage.getDownloadUrl({ storageUri });
7152
- try {
7153
- return assertRemoteUrl(downloadUrl);
7154
- } catch (error) {
7155
- if (/^[a-z][a-z\d+.-]*:/i.test(downloadUrl)) throw error;
7156
- }
7157
- return resolveDownloadPath(downloadUrl, storageUri);
7115
+ return assertRemoteUrl(downloadUrl);
7158
7116
  };
7159
7117
  const readStorageText = async (storageUri) => {
7160
7118
  return (await readStorageResponse(storageUri))?.text() ?? null;
7161
7119
  };
7162
7120
  return {
7163
- downloadStorageObject: storageAdapters.some((storage) => storage.getDownloadUrl !== void 0) ? async (storageUriToken, encodedSignature) => {
7164
- const requestedPath = `/storage/${storageUriToken}/${encodedSignature}`;
7165
- const requested = parseStorageDownloadPath(requestedPath);
7166
- if (!requested) return null;
7167
- let storage;
7168
- try {
7169
- storage = findStorage(getStorageProtocol(requested.storageUri));
7170
- } catch {
7171
- return null;
7172
- }
7173
- if (!storage?.getDownloadUrl || !storage.get) return null;
7174
- const { url: downloadUrl } = await storage.getDownloadUrl({ storageUri: requested.storageUri });
7175
- try {
7176
- new URL(downloadUrl);
7177
- return null;
7178
- } catch {
7179
- if (/^[a-z][a-z\d+.-]*:/i.test(downloadUrl)) return null;
7180
- }
7181
- if (!tokensEqual(downloadUrl, requestedPath)) return null;
7182
- return (await storage.get({ storageUri: requested.storageUri })).response;
7183
- } : void 0,
7184
7121
  readStorageText,
7185
7122
  resolveFileUrl
7186
7123
  };
@@ -7202,17 +7139,21 @@ const databaseOf = (value) => {
7202
7139
  }
7203
7140
  throw new HotUpdaterConfigError("database must be a Hot Updater 1.0 database, such as kyselyAdapter(...) or postgres(...). Upgrade the provider package that created it.");
7204
7141
  };
7205
- /** `"public"`, or nothing; a `clientAccess` object says what replaced it. */
7142
+ /** The configured storage, when it is a storage adapter. */
7143
+ const storageOf = (value) => {
7144
+ if (isRecord$4(value) && typeof value.name === "string" && typeof value.protocol === "string") return value;
7145
+ throw new HotUpdaterConfigError("storage must be a storage adapter, such as s3Storage(...): the one hot-updater.config.ts uploads with.");
7146
+ };
7147
+ /** `"public"`, or nothing. */
7206
7148
  const isPublic = (value) => {
7207
7149
  if (value === void 0) return false;
7208
7150
  if (value === "public") return true;
7209
- throw new HotUpdaterConfigError(isRecord$4(value) ? "clientAccess objects were removed in 1.0. Set clientAccess: \"public\", or add a plugin that provides clientAuth to plugins." : "clientAccess must be \"public\"; to protect client routes, add a plugin that provides clientAuth to plugins.");
7151
+ throw new HotUpdaterConfigError("clientAccess must be \"public\"; to protect client routes, add a plugin that provides clientAuth to plugins.");
7210
7152
  };
7211
7153
  function createHotUpdater(options) {
7212
- for (const key of ["authorityId", "catalogId"]) if (Object.hasOwn(options, key)) throw new TypeError(`Remove ${key} from createHotUpdater options. Catalog identity is managed internally.`);
7213
7154
  const database = databaseOf(options.database);
7214
- const storage = Object.freeze([...options.storage ?? []]);
7215
- const { downloadStorageObject, readStorageText, resolveFileUrl } = createStorageAccess(storage);
7155
+ const storage = storageOf(options.storage);
7156
+ const { readStorageText, resolveFileUrl } = createStorageAccess(storage);
7216
7157
  const publicClients = isPublic(options.clientAccess);
7217
7158
  const configured = Object.freeze([...options.plugins ?? []]);
7218
7159
  const plugins = assemblePlugins(configured, database, { storage: {
@@ -7231,7 +7172,6 @@ function createHotUpdater(options) {
7231
7172
  const handlers = createHotUpdaterHandlers({
7232
7173
  api: { core: plugins.core },
7233
7174
  ...clientPolicy === void 0 ? {} : { clientPolicy },
7234
- downloadStorageObject,
7235
7175
  endpoints: plugins.endpoints,
7236
7176
  plugins: Object.keys(plugins.api)
7237
7177
  });
@@ -7240,7 +7180,7 @@ function createHotUpdater(options) {
7240
7180
  adapterName: database.name,
7241
7181
  get handlers() {
7242
7182
  if (!serving) {
7243
- for (const adapter of storage) assertStorageOperations(adapter, ["get", "getDownloadUrl"]);
7183
+ assertStorageOperations(storage, ["get", "getDownloadUrl"]);
7244
7184
  serving = true;
7245
7185
  }
7246
7186
  return handlers;
@@ -13743,10 +13683,11 @@ const plugins = [
13743
13683
  ];
13744
13684
  //#endregion
13745
13685
  //#region src/s3Storage.ts
13686
+ /** Devices download right after the update check that returns the URL. */
13687
+ const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
13746
13688
  const isObjectNotFoundError = (error) => error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey");
13747
13689
  function s3Storage(config) {
13748
- const { bucketName, basePath, downloadUrlSigningKey, getDownloadUrl: configuredGetDownloadUrl, ...s3Config } = config;
13749
- const getDownloadUrl = configuredGetDownloadUrl ?? (downloadUrlSigningKey ? createStorageDownloadUrl(downloadUrlSigningKey) : void 0);
13690
+ const { bucketName, basePath, getDownloadUrl, ...s3Config } = config;
13750
13691
  const client = new _aws_sdk_client_s3.S3Client(applyS3RuntimeAwsConfig(s3Config));
13751
13692
  const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
13752
13693
  const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
@@ -13859,10 +13800,15 @@ function s3Storage(config) {
13859
13800
  throw error;
13860
13801
  }
13861
13802
  },
13862
- ...getDownloadUrl ? { async getDownloadUrl(input) {
13863
- parseAndValidate(input.storageUri);
13864
- return getDownloadUrl(input);
13865
- } } : {},
13803
+ async getDownloadUrl({ storageUri }) {
13804
+ const { key } = parseAndValidate(storageUri);
13805
+ if (getDownloadUrl) return getDownloadUrl({ storageUri });
13806
+ const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
13807
+ return { url: await getSignedUrl(client, new _aws_sdk_client_s3.GetObjectCommand({
13808
+ Bucket: bucketName,
13809
+ Key: key
13810
+ }), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
13811
+ },
13866
13812
  async exists({ storageUri }) {
13867
13813
  const { key } = parseAndValidate(storageUri);
13868
13814
  try {
@@ -13906,7 +13852,7 @@ const getHotUpdater = (distributionDomainName) => {
13906
13852
  const hotUpdater = createHotUpdater({
13907
13853
  database,
13908
13854
  plugins,
13909
- storage: [s3Storage({
13855
+ storage: s3Storage({
13910
13856
  bucketName: S3_BUCKET_NAME,
13911
13857
  region: SSM_REGION,
13912
13858
  getDownloadUrl: cloudFrontDownloadUrl({
@@ -13915,7 +13861,7 @@ const getHotUpdater = (distributionDomainName) => {
13915
13861
  ssmParameterName: SSM_PARAMETER_NAME,
13916
13862
  publicBaseUrl: `https://${distributionDomainName}`
13917
13863
  })
13918
- })]
13864
+ })
13919
13865
  });
13920
13866
  hotUpdaterByDistribution.set(distributionDomainName, hotUpdater);
13921
13867
  return hotUpdater;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/aws",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.42",
4
+ "version": "1.0.0-rc.44",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -45,9 +45,9 @@
45
45
  "es-toolkit": "^1.32.0",
46
46
  "execa": "9.5.2",
47
47
  "mime": "^4.0.4",
48
- "@hot-updater/mock": "1.0.0-rc.42",
49
- "@hot-updater/test-utils": "1.0.0-rc.42",
50
- "@hot-updater/protocol": "1.0.0-rc.42"
48
+ "@hot-updater/protocol": "1.0.0-rc.44",
49
+ "@hot-updater/mock": "1.0.0-rc.44",
50
+ "@hot-updater/test-utils": "1.0.0-rc.44"
51
51
  },
52
52
  "dependencies": {
53
53
  "@aws-sdk/client-cloudfront": "3.1066.0",
@@ -61,10 +61,11 @@
61
61
  "@aws-sdk/credential-providers": "3.1066.0",
62
62
  "@aws-sdk/lib-storage": "3.1066.0",
63
63
  "@aws-sdk/lib-dynamodb": "3.1066.0",
64
+ "@aws-sdk/s3-request-presigner": "3.1066.0",
64
65
  "hono": "4.12.34",
65
- "@hot-updater/cli-tools": "1.0.0-rc.42",
66
- "@hot-updater/server": "1.0.0-rc.42",
67
- "@hot-updater/plugin-core": "1.0.0-rc.42"
66
+ "@hot-updater/cli-tools": "1.0.0-rc.44",
67
+ "@hot-updater/plugin-core": "1.0.0-rc.44",
68
+ "@hot-updater/server": "1.0.0-rc.44"
68
69
  },
69
70
  "scripts": {
70
71
  "build": "tsdown",