@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 +16 -12
- package/dist/index.d.cts +7 -9
- package/dist/index.d.mts +7 -9
- package/dist/index.mjs +14 -10
- package/dist/init/index.cjs +11 -9
- package/dist/init/index.mjs +8 -6
- package/dist/lambda/index.cjs +81 -135
- package/package.json +8 -7
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
|
|
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,
|
|
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
|
-
|
|
558
|
-
parseAndValidate(
|
|
559
|
-
return getDownloadUrl(
|
|
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,
|
|
592
|
-
(0,
|
|
593
|
-
(0,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
557
|
-
parseAndValidate(
|
|
558
|
-
return getDownloadUrl(
|
|
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 {
|
package/dist/init/index.cjs
CHANGED
|
@@ -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
|
|
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,
|
|
6637
|
-
(0,
|
|
6638
|
-
(0,
|
|
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: "
|
|
8461
|
-
named: [
|
|
8458
|
+
pkg: "hot-updater/plugins",
|
|
8459
|
+
named: [
|
|
8460
|
+
"apiKeys",
|
|
8461
|
+
"insights",
|
|
8462
|
+
"remoteConfig"
|
|
8463
|
+
]
|
|
8462
8464
|
}],
|
|
8463
|
-
configString: "
|
|
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":
|
package/dist/init/index.mjs
CHANGED
|
@@ -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
|
|
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: "
|
|
8456
|
-
named: [
|
|
8453
|
+
pkg: "hot-updater/plugins",
|
|
8454
|
+
named: [
|
|
8455
|
+
"apiKeys",
|
|
8456
|
+
"insights",
|
|
8457
|
+
"remoteConfig"
|
|
8458
|
+
]
|
|
8457
8459
|
}],
|
|
8458
|
-
configString: "
|
|
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":
|
package/dist/lambda/index.cjs
CHANGED
|
@@ -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.
|
|
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
|
|
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`
|
|
6987
|
-
const PUBLIC_CLIENT_ROUTES = /* @__PURE__ */ new Set(["version"
|
|
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
|
-
|
|
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
|
-
|
|
7104
|
-
|
|
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
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
const
|
|
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
|
-
|
|
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
|
-
/**
|
|
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(
|
|
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 =
|
|
7215
|
-
const {
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
13863
|
-
parseAndValidate(
|
|
13864
|
-
return getDownloadUrl(
|
|
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:
|
|
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.
|
|
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/
|
|
49
|
-
"@hot-updater/
|
|
50
|
-
"@hot-updater/
|
|
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.
|
|
66
|
-
"@hot-updater/
|
|
67
|
-
"@hot-updater/
|
|
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",
|