@hot-updater/aws 1.0.0-rc.43 → 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.43";
4594
+ const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.44";
4641
4595
  //#endregion
4642
4596
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4643
4597
  /**
@@ -6823,6 +6777,22 @@ const privateNotFound = () => Response.json({ error: "Not found" }, {
6823
6777
  status: 404,
6824
6778
  headers: { "cache-control": "private, no-store" }
6825
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
+ };
6826
6796
  /**
6827
6797
  * A scope with no catalog yet, such as a store version before its first
6828
6798
  * OTA release: cached like a catalog, so its update checks don't all reach
@@ -6901,16 +6871,15 @@ const createReleaseCatalogRouteHandlers = () => {
6901
6871
  appVersionReleaseCatalog: async (params, request, { core }) => {
6902
6872
  const rawAppVersion = requireRouteParam(params, "appVersion");
6903
6873
  const appVersion = canonicalizeAppVersion(rawAppVersion);
6904
- if (appVersion === null || appVersion !== rawAppVersion) return Response.json({ error: "Invalid app version" }, {
6905
- status: 400,
6906
- headers: { "cache-control": "private, no-store" }
6907
- });
6874
+ if (appVersion === null || appVersion !== rawAppVersion) return badRequest("Invalid app version");
6908
6875
  const input = {
6909
6876
  appVersion,
6910
6877
  channelKey: requireRouteParam(params, "channelKey"),
6911
6878
  platform: requirePlatformParam(params),
6912
6879
  strategy: "APP_VERSION"
6913
6880
  };
6881
+ const invalid = invalidScope(input);
6882
+ if (invalid) return invalid;
6914
6883
  return catalogResponse(await loadCatalog(`app-version:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
6915
6884
  },
6916
6885
  fingerprintReleaseCatalog: async (params, request, { core }) => {
@@ -6920,6 +6889,8 @@ const createReleaseCatalogRouteHandlers = () => {
6920
6889
  platform: requirePlatformParam(params),
6921
6890
  strategy: "FINGERPRINT"
6922
6891
  };
6892
+ const invalid = invalidScope(input);
6893
+ if (invalid) return invalid;
6923
6894
  return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
6924
6895
  },
6925
6896
  artifactV1: async (params, _request, { core }) => {
@@ -7001,8 +6972,8 @@ const errorResponse = (error, status) => Response.json({ error }, {
7001
6972
  headers: { "cache-control": "private, no-store" },
7002
6973
  status
7003
6974
  });
7004
- /** `/version` and storage downloads never pass through the client policy. */
7005
- 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"]);
7006
6977
  const routesOf = Symbol.for("@hot-updater/server/routes");
7007
6978
  const withVary = (response, varyHeaders) => {
7008
6979
  const cacheControl = response.headers.get("cache-control") ?? "";
@@ -7047,26 +7018,11 @@ const createRequestHandler = ({ api, clientPolicy, privateResponses = false, rou
7047
7018
  });
7048
7019
  }
7049
7020
  };
7050
- const createDownloadStorageRouteHandler = (downloadStorageObject) => async (params) => {
7051
- const token = params.token;
7052
- const signature = params.signature;
7053
- if (!token || !signature) return errorResponse("Not found", 404);
7054
- const response = await downloadStorageObject(token, signature);
7055
- if (!response) return errorResponse("Not found", 404);
7056
- const headers = new Headers(response.headers);
7057
- if (!headers.has("cache-control")) headers.set("cache-control", "public, max-age=31536000, immutable");
7058
- return new Response(response.body, {
7059
- headers,
7060
- status: response.status,
7061
- statusText: response.statusText
7062
- });
7063
- };
7064
- function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, endpoints = [], plugins = [] }) {
7021
+ function createHotUpdaterHandlers({ api, clientPolicy, endpoints = [], plugins = [] }) {
7065
7022
  const routeHandlers = {
7066
7023
  ...createVersionRouteHandlers(plugins),
7067
7024
  ...createReleaseCatalogRouteHandlers(),
7068
- ...createAdminRouteHandlers(),
7069
- ...downloadStorageObject === void 0 ? {} : { downloadStorageObject: createDownloadStorageRouteHandler(downloadStorageObject) }
7025
+ ...createAdminRouteHandlers()
7070
7026
  };
7071
7027
  const routes = [];
7072
7028
  const shapes = /* @__PURE__ */ new Map();
@@ -7085,7 +7041,6 @@ function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, en
7085
7041
  const clientRouter = createRouter();
7086
7042
  const addClientRoute = mount(clientRouter, false);
7087
7043
  addClientRoute("GET", "/version", "version");
7088
- if (downloadStorageObject !== void 0) addClientRoute("GET", "/storage/:token/:signature", "downloadStorageObject");
7089
7044
  addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
7090
7045
  addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
7091
7046
  addClientRoute("GET", "/artifacts/v1/:targetBundleId/from/:currentBundleId", "artifactV1");
@@ -7117,33 +7072,23 @@ function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, en
7117
7072
  }
7118
7073
  //#endregion
7119
7074
  //#region ../../packages/server/dist/storageAccess.mjs
7075
+ /** A URL devices download from, which only an absolute HTTP(S) URL can be. */
7120
7076
  const assertRemoteUrl = (value) => {
7121
- const protocol = new URL(value).protocol;
7122
- 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.");
7123
7082
  return value;
7124
7083
  };
7125
7084
  const getStorageProtocol = (storageUri) => new URL(storageUri).protocol.replace(":", "");
7126
7085
  const isRemoteUrlProtocol = (protocol) => protocol === "http" || protocol === "https";
7127
- const resolveDownloadPath = (value, storageUri) => {
7128
- const parsed = parseStorageDownloadPath(value);
7129
- if (!parsed || parsed.storageUri !== storageUri) throw new Error("Storage getDownloadUrl must return an HTTP(S) URL or a valid storage download path.");
7130
- return value;
7131
- };
7132
- const tokensEqual = (left, right) => {
7133
- const leftBytes = new TextEncoder().encode(left);
7134
- const rightBytes = new TextEncoder().encode(right);
7135
- let difference = leftBytes.length ^ rightBytes.length;
7136
- const length = Math.max(leftBytes.length, rightBytes.length);
7137
- for (let index = 0; index < length; index += 1) difference |= (leftBytes[index] ?? 0) ^ (rightBytes[index] ?? 0);
7138
- return difference === 0;
7139
- };
7140
- const createStorageAccess = (storageAdapters) => {
7141
- const protocols = /* @__PURE__ */ new Set();
7142
- for (const storage of storageAdapters) {
7143
- if (protocols.has(storage.protocol)) throw new Error(`Multiple storage adapters handle protocol: ${storage.protocol}`);
7144
- protocols.add(storage.protocol);
7145
- }
7146
- 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;
7147
7092
  const readStorageResponse = async (storageUri) => {
7148
7093
  const protocol = getStorageProtocol(storageUri);
7149
7094
  const storage = findStorage(protocol);
@@ -7167,38 +7112,12 @@ const createStorageAccess = (storageAdapters) => {
7167
7112
  }
7168
7113
  if (!storage.getDownloadUrl) return null;
7169
7114
  const { url: downloadUrl } = await storage.getDownloadUrl({ storageUri });
7170
- try {
7171
- return assertRemoteUrl(downloadUrl);
7172
- } catch (error) {
7173
- if (/^[a-z][a-z\d+.-]*:/i.test(downloadUrl)) throw error;
7174
- }
7175
- return resolveDownloadPath(downloadUrl, storageUri);
7115
+ return assertRemoteUrl(downloadUrl);
7176
7116
  };
7177
7117
  const readStorageText = async (storageUri) => {
7178
7118
  return (await readStorageResponse(storageUri))?.text() ?? null;
7179
7119
  };
7180
7120
  return {
7181
- downloadStorageObject: storageAdapters.some((storage) => storage.getDownloadUrl !== void 0) ? async (storageUriToken, encodedSignature) => {
7182
- const requestedPath = `/storage/${storageUriToken}/${encodedSignature}`;
7183
- const requested = parseStorageDownloadPath(requestedPath);
7184
- if (!requested) return null;
7185
- let storage;
7186
- try {
7187
- storage = findStorage(getStorageProtocol(requested.storageUri));
7188
- } catch {
7189
- return null;
7190
- }
7191
- if (!storage?.getDownloadUrl || !storage.get) return null;
7192
- const { url: downloadUrl } = await storage.getDownloadUrl({ storageUri: requested.storageUri });
7193
- try {
7194
- new URL(downloadUrl);
7195
- return null;
7196
- } catch {
7197
- if (/^[a-z][a-z\d+.-]*:/i.test(downloadUrl)) return null;
7198
- }
7199
- if (!tokensEqual(downloadUrl, requestedPath)) return null;
7200
- return (await storage.get({ storageUri: requested.storageUri })).response;
7201
- } : void 0,
7202
7121
  readStorageText,
7203
7122
  resolveFileUrl
7204
7123
  };
@@ -7220,17 +7139,21 @@ const databaseOf = (value) => {
7220
7139
  }
7221
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.");
7222
7141
  };
7223
- /** `"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. */
7224
7148
  const isPublic = (value) => {
7225
7149
  if (value === void 0) return false;
7226
7150
  if (value === "public") return true;
7227
- 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.");
7228
7152
  };
7229
7153
  function createHotUpdater(options) {
7230
- for (const key of ["authorityId", "catalogId"]) if (Object.hasOwn(options, key)) throw new TypeError(`Remove ${key} from createHotUpdater options. Catalog identity is managed internally.`);
7231
7154
  const database = databaseOf(options.database);
7232
- const storage = Object.freeze([...options.storage ?? []]);
7233
- const { downloadStorageObject, readStorageText, resolveFileUrl } = createStorageAccess(storage);
7155
+ const storage = storageOf(options.storage);
7156
+ const { readStorageText, resolveFileUrl } = createStorageAccess(storage);
7234
7157
  const publicClients = isPublic(options.clientAccess);
7235
7158
  const configured = Object.freeze([...options.plugins ?? []]);
7236
7159
  const plugins = assemblePlugins(configured, database, { storage: {
@@ -7249,7 +7172,6 @@ function createHotUpdater(options) {
7249
7172
  const handlers = createHotUpdaterHandlers({
7250
7173
  api: { core: plugins.core },
7251
7174
  ...clientPolicy === void 0 ? {} : { clientPolicy },
7252
- downloadStorageObject,
7253
7175
  endpoints: plugins.endpoints,
7254
7176
  plugins: Object.keys(plugins.api)
7255
7177
  });
@@ -7258,7 +7180,7 @@ function createHotUpdater(options) {
7258
7180
  adapterName: database.name,
7259
7181
  get handlers() {
7260
7182
  if (!serving) {
7261
- for (const adapter of storage) assertStorageOperations(adapter, ["get", "getDownloadUrl"]);
7183
+ assertStorageOperations(storage, ["get", "getDownloadUrl"]);
7262
7184
  serving = true;
7263
7185
  }
7264
7186
  return handlers;
@@ -13761,10 +13683,11 @@ const plugins = [
13761
13683
  ];
13762
13684
  //#endregion
13763
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;
13764
13688
  const isObjectNotFoundError = (error) => error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey");
13765
13689
  function s3Storage(config) {
13766
- const { bucketName, basePath, downloadUrlSigningKey, getDownloadUrl: configuredGetDownloadUrl, ...s3Config } = config;
13767
- const getDownloadUrl = configuredGetDownloadUrl ?? (downloadUrlSigningKey ? createStorageDownloadUrl(downloadUrlSigningKey) : void 0);
13690
+ const { bucketName, basePath, getDownloadUrl, ...s3Config } = config;
13768
13691
  const client = new _aws_sdk_client_s3.S3Client(applyS3RuntimeAwsConfig(s3Config));
13769
13692
  const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
13770
13693
  const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
@@ -13877,10 +13800,15 @@ function s3Storage(config) {
13877
13800
  throw error;
13878
13801
  }
13879
13802
  },
13880
- ...getDownloadUrl ? { async getDownloadUrl(input) {
13881
- parseAndValidate(input.storageUri);
13882
- return getDownloadUrl(input);
13883
- } } : {},
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
+ },
13884
13812
  async exists({ storageUri }) {
13885
13813
  const { key } = parseAndValidate(storageUri);
13886
13814
  try {
@@ -13924,7 +13852,7 @@ const getHotUpdater = (distributionDomainName) => {
13924
13852
  const hotUpdater = createHotUpdater({
13925
13853
  database,
13926
13854
  plugins,
13927
- storage: [s3Storage({
13855
+ storage: s3Storage({
13928
13856
  bucketName: S3_BUCKET_NAME,
13929
13857
  region: SSM_REGION,
13930
13858
  getDownloadUrl: cloudFrontDownloadUrl({
@@ -13933,7 +13861,7 @@ const getHotUpdater = (distributionDomainName) => {
13933
13861
  ssmParameterName: SSM_PARAMETER_NAME,
13934
13862
  publicBaseUrl: `https://${distributionDomainName}`
13935
13863
  })
13936
- })]
13864
+ })
13937
13865
  });
13938
13866
  hotUpdaterByDistribution.set(distributionDomainName, hotUpdater);
13939
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.43",
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.43",
49
- "@hot-updater/test-utils": "1.0.0-rc.43",
50
- "@hot-updater/protocol": "1.0.0-rc.43"
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/plugin-core": "1.0.0-rc.43",
66
- "@hot-updater/cli-tools": "1.0.0-rc.43",
67
- "@hot-updater/server": "1.0.0-rc.43"
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",