@hot-updater/server 0.31.3 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/_rolldown/runtime.cjs +1 -1
- package/dist/_virtual/_rolldown/runtime.mjs +1 -1
- package/dist/db/createBundleDiff.cjs +19 -13
- package/dist/db/createBundleDiff.mjs +15 -9
- package/dist/db/schemaEnhancements.cjs +1 -1
- package/dist/db/updateArtifacts.cjs +6 -6
- package/dist/db/updateArtifacts.mjs +6 -6
- package/dist/handler.cjs +18 -8
- package/dist/handler.d.cts +9 -10
- package/dist/handler.d.mts +9 -10
- package/dist/handler.mjs +17 -7
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/node_modules/.pnpm/fumadb@0.2.2_drizzle-orm@0.44.7_@cloudflare_workers-types@4.20260313.1_@electric-sql_pg_c72c8c754becd21f6d6662e8fbd28e7f/node_modules/fumadb/dist/index.d.cts +1 -1
- package/dist/node_modules/.pnpm/fumadb@0.2.2_drizzle-orm@0.44.7_@cloudflare_workers-types@4.20260313.1_@electric-sql_pg_c72c8c754becd21f6d6662e8fbd28e7f/node_modules/fumadb/dist/index.d.mts +1 -1
- package/dist/node_modules/.pnpm/fumadb@0.2.2_drizzle-orm@0.44.7_@cloudflare_workers-types@4.20260313.1_@electric-sql_pg_c72c8c754becd21f6d6662e8fbd28e7f/node_modules/fumadb/dist/query/index.d.cts +1 -1
- package/dist/node_modules/.pnpm/fumadb@0.2.2_drizzle-orm@0.44.7_@cloudflare_workers-types@4.20260313.1_@electric-sql_pg_c72c8c754becd21f6d6662e8fbd28e7f/node_modules/fumadb/dist/query/index.d.mts +1 -1
- package/dist/packages/server/package.cjs +1 -1
- package/dist/packages/server/package.mjs +1 -1
- package/package.json +7 -7
- package/src/db/createBundleDiff.spec.ts +3 -0
- package/src/db/createBundleDiff.ts +27 -21
- package/src/db/pluginCore.spec.ts +109 -0
- package/src/db/updateArtifacts.ts +8 -19
- package/src/handler-standalone.integration.spec.ts +12 -0
- package/src/handler.spec.ts +117 -19
- package/src/handler.ts +47 -21
- package/src/runtime.spec.ts +7 -4
|
@@ -5,7 +5,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __commonJSMin = (cb, mod) => () => (mod ||
|
|
8
|
+
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
9
9
|
var __copyProps = (to, from, except, desc) => {
|
|
10
10
|
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
11
|
key = keys[i];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
//#region \0rolldown/runtime.js
|
|
3
|
-
var __commonJSMin = (cb, mod) => () => (mod ||
|
|
3
|
+
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
4
4
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
5
|
//#endregion
|
|
6
6
|
export { __commonJSMin, __require };
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
2
3
|
let node_crypto = require("node:crypto");
|
|
3
|
-
node_crypto = require_runtime.__toESM(node_crypto
|
|
4
|
+
node_crypto = require_runtime.__toESM(node_crypto);
|
|
4
5
|
let node_fs_promises = require("node:fs/promises");
|
|
5
|
-
node_fs_promises = require_runtime.__toESM(node_fs_promises
|
|
6
|
+
node_fs_promises = require_runtime.__toESM(node_fs_promises);
|
|
6
7
|
let node_os = require("node:os");
|
|
7
|
-
node_os = require_runtime.__toESM(node_os
|
|
8
|
+
node_os = require_runtime.__toESM(node_os);
|
|
8
9
|
let node_path = require("node:path");
|
|
9
|
-
node_path = require_runtime.__toESM(node_path
|
|
10
|
+
node_path = require_runtime.__toESM(node_path);
|
|
10
11
|
let node_util = require("node:util");
|
|
11
12
|
let node_zlib = require("node:zlib");
|
|
12
13
|
let _hot_updater_bsdiff = require("@hot-updater/bsdiff");
|
|
@@ -27,11 +28,6 @@ const isBundleManifest = (value) => {
|
|
|
27
28
|
return typeof manifestAsset.fileHash === "string" && (manifestAsset.signature === void 0 || typeof manifestAsset.signature === "string");
|
|
28
29
|
});
|
|
29
30
|
};
|
|
30
|
-
const createChildStorageUri = (baseStorageUri, relativePath) => {
|
|
31
|
-
const baseUrl = new URL(baseStorageUri);
|
|
32
|
-
baseUrl.pathname = `${baseUrl.pathname.replace(/\/+$/, "")}/${relativePath.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
|
|
33
|
-
return baseUrl.toString();
|
|
34
|
-
};
|
|
35
31
|
const getRelativeStorageDir = (relativePath) => {
|
|
36
32
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
37
33
|
const dirname = node_path.default.posix.dirname(normalized);
|
|
@@ -76,18 +72,28 @@ function resolveHbcAssetPath(manifest) {
|
|
|
76
72
|
if (candidates.length > 1) throw new Error(`Expected exactly one Hermes bundle asset in manifest, found ${candidates.length}: ${candidates.join(", ")}`);
|
|
77
73
|
return candidates[0];
|
|
78
74
|
}
|
|
79
|
-
async function fetchAssetBytes(bundle, assetPath, storagePlugin) {
|
|
75
|
+
async function fetchAssetBytes(bundle, assetPath, manifest, storagePlugin) {
|
|
80
76
|
const assetBaseStorageUri = (0, _hot_updater_core.getAssetBaseStorageUri)(bundle);
|
|
81
77
|
if (!assetBaseStorageUri) throw new Error(`Bundle ${bundle.id} does not have asset storage metadata`);
|
|
78
|
+
const asset = manifest.assets[assetPath];
|
|
79
|
+
if (!asset) throw new Error(`Asset ${assetPath} is missing from manifest`);
|
|
82
80
|
if (BR_COMPRESSED_ASSET_PATH_RE.test(assetPath)) {
|
|
83
|
-
const compressedAssetStorageUri =
|
|
81
|
+
const compressedAssetStorageUri = (0, _hot_updater_plugin_core.resolveManifestAssetStorageUri)({
|
|
82
|
+
assetBaseStorageUri,
|
|
83
|
+
assetPath: `${assetPath}.br`,
|
|
84
|
+
fileHash: asset.fileHash
|
|
85
|
+
});
|
|
84
86
|
let compressedBytes = null;
|
|
85
87
|
try {
|
|
86
88
|
compressedBytes = await downloadStorageBytes(compressedAssetStorageUri, storagePlugin);
|
|
87
89
|
} catch {}
|
|
88
90
|
if (compressedBytes) return new Uint8Array(await decompressBrotli(compressedBytes));
|
|
89
91
|
}
|
|
90
|
-
return downloadStorageBytes(
|
|
92
|
+
return downloadStorageBytes((0, _hot_updater_plugin_core.resolveManifestAssetStorageUri)({
|
|
93
|
+
assetBaseStorageUri,
|
|
94
|
+
assetPath,
|
|
95
|
+
fileHash: asset.fileHash
|
|
96
|
+
}), storagePlugin);
|
|
91
97
|
}
|
|
92
98
|
async function getFileHash(filePath) {
|
|
93
99
|
const file = await node_fs_promises.default.readFile(filePath);
|
|
@@ -117,7 +123,7 @@ async function createBundleDiff({ baseBundleId, bundleId }, deps, options = {})
|
|
|
117
123
|
const targetAssetHash = targetManifest.assets[targetAssetPath]?.fileHash;
|
|
118
124
|
if (!baseAssetHash || !targetAssetHash) throw new Error("Hermes asset hash is missing from manifest");
|
|
119
125
|
if (baseAssetHash === targetAssetHash) throw new Error("Hermes bundle is unchanged; no diff patch is required");
|
|
120
|
-
const [baseBytes, targetBytes] = await Promise.all([fetchAssetBytes(baseBundle, baseAssetPath, deps.storagePlugin), fetchAssetBytes(targetBundle, targetAssetPath, deps.storagePlugin)]);
|
|
126
|
+
const [baseBytes, targetBytes] = await Promise.all([fetchAssetBytes(baseBundle, baseAssetPath, baseManifest, deps.storagePlugin), fetchAssetBytes(targetBundle, targetAssetPath, targetManifest, deps.storagePlugin)]);
|
|
121
127
|
const patchBytes = await (0, _hot_updater_bsdiff.hdiff)(baseBytes, targetBytes);
|
|
122
128
|
const workDir = await node_fs_promises.default.mkdtemp(node_path.default.join(node_os.default.tmpdir(), "hot-updater-console-bsdiff-"));
|
|
123
129
|
const patchFilename = `${node_path.default.posix.basename(targetAssetPath)}.bsdiff`;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveManifestAssetStorageUri } from "@hot-updater/plugin-core";
|
|
1
2
|
import crypto, { randomUUID } from "node:crypto";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import os from "node:os";
|
|
@@ -22,11 +23,6 @@ const isBundleManifest = (value) => {
|
|
|
22
23
|
return typeof manifestAsset.fileHash === "string" && (manifestAsset.signature === void 0 || typeof manifestAsset.signature === "string");
|
|
23
24
|
});
|
|
24
25
|
};
|
|
25
|
-
const createChildStorageUri = (baseStorageUri, relativePath) => {
|
|
26
|
-
const baseUrl = new URL(baseStorageUri);
|
|
27
|
-
baseUrl.pathname = `${baseUrl.pathname.replace(/\/+$/, "")}/${relativePath.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
|
|
28
|
-
return baseUrl.toString();
|
|
29
|
-
};
|
|
30
26
|
const getRelativeStorageDir = (relativePath) => {
|
|
31
27
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
32
28
|
const dirname = path.posix.dirname(normalized);
|
|
@@ -71,18 +67,28 @@ function resolveHbcAssetPath(manifest) {
|
|
|
71
67
|
if (candidates.length > 1) throw new Error(`Expected exactly one Hermes bundle asset in manifest, found ${candidates.length}: ${candidates.join(", ")}`);
|
|
72
68
|
return candidates[0];
|
|
73
69
|
}
|
|
74
|
-
async function fetchAssetBytes(bundle, assetPath, storagePlugin) {
|
|
70
|
+
async function fetchAssetBytes(bundle, assetPath, manifest, storagePlugin) {
|
|
75
71
|
const assetBaseStorageUri = getAssetBaseStorageUri(bundle);
|
|
76
72
|
if (!assetBaseStorageUri) throw new Error(`Bundle ${bundle.id} does not have asset storage metadata`);
|
|
73
|
+
const asset = manifest.assets[assetPath];
|
|
74
|
+
if (!asset) throw new Error(`Asset ${assetPath} is missing from manifest`);
|
|
77
75
|
if (BR_COMPRESSED_ASSET_PATH_RE.test(assetPath)) {
|
|
78
|
-
const compressedAssetStorageUri =
|
|
76
|
+
const compressedAssetStorageUri = resolveManifestAssetStorageUri({
|
|
77
|
+
assetBaseStorageUri,
|
|
78
|
+
assetPath: `${assetPath}.br`,
|
|
79
|
+
fileHash: asset.fileHash
|
|
80
|
+
});
|
|
79
81
|
let compressedBytes = null;
|
|
80
82
|
try {
|
|
81
83
|
compressedBytes = await downloadStorageBytes(compressedAssetStorageUri, storagePlugin);
|
|
82
84
|
} catch {}
|
|
83
85
|
if (compressedBytes) return new Uint8Array(await decompressBrotli(compressedBytes));
|
|
84
86
|
}
|
|
85
|
-
return downloadStorageBytes(
|
|
87
|
+
return downloadStorageBytes(resolveManifestAssetStorageUri({
|
|
88
|
+
assetBaseStorageUri,
|
|
89
|
+
assetPath,
|
|
90
|
+
fileHash: asset.fileHash
|
|
91
|
+
}), storagePlugin);
|
|
86
92
|
}
|
|
87
93
|
async function getFileHash(filePath) {
|
|
88
94
|
const file = await fs.readFile(filePath);
|
|
@@ -112,7 +118,7 @@ async function createBundleDiff({ baseBundleId, bundleId }, deps, options = {})
|
|
|
112
118
|
const targetAssetHash = targetManifest.assets[targetAssetPath]?.fileHash;
|
|
113
119
|
if (!baseAssetHash || !targetAssetHash) throw new Error("Hermes asset hash is missing from manifest");
|
|
114
120
|
if (baseAssetHash === targetAssetHash) throw new Error("Hermes bundle is unchanged; no diff patch is required");
|
|
115
|
-
const [baseBytes, targetBytes] = await Promise.all([fetchAssetBytes(baseBundle, baseAssetPath, deps.storagePlugin), fetchAssetBytes(targetBundle, targetAssetPath, deps.storagePlugin)]);
|
|
121
|
+
const [baseBytes, targetBytes] = await Promise.all([fetchAssetBytes(baseBundle, baseAssetPath, baseManifest, deps.storagePlugin), fetchAssetBytes(targetBundle, targetAssetPath, targetManifest, deps.storagePlugin)]);
|
|
116
122
|
const patchBytes = await hdiff(baseBytes, targetBytes);
|
|
117
123
|
const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "hot-updater-console-bsdiff-"));
|
|
118
124
|
const patchFilename = `${path.posix.basename(targetAssetPath)}.bsdiff`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
2
2
|
let _hot_updater_core = require("@hot-updater/core");
|
|
3
3
|
let semver = require("semver");
|
|
4
|
-
semver = require_runtime.__toESM(semver
|
|
4
|
+
semver = require_runtime.__toESM(semver);
|
|
5
5
|
//#region src/db/schemaEnhancements.ts
|
|
6
6
|
const normalizeNullableString = (value) => {
|
|
7
7
|
if (value === null || value === void 0) return null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
2
3
|
let _hot_updater_core = require("@hot-updater/core");
|
|
3
4
|
//#region src/db/updateArtifacts.ts
|
|
4
5
|
const HBC_ASSET_PATH_RE = /\.bundle$/;
|
|
@@ -18,11 +19,6 @@ const isBundleManifest = (value) => {
|
|
|
18
19
|
return typeof manifestAsset.fileHash === "string" && (manifestAsset.signature === void 0 || typeof manifestAsset.signature === "string");
|
|
19
20
|
});
|
|
20
21
|
};
|
|
21
|
-
const createChildStorageUri = (baseStorageUri, relativePath) => {
|
|
22
|
-
const baseUrl = new URL(baseStorageUri);
|
|
23
|
-
baseUrl.pathname = `${baseUrl.pathname.replace(/\/+$/, "")}/${relativePath.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
|
|
24
|
-
return baseUrl.toString();
|
|
25
|
-
};
|
|
26
22
|
const parseBundleMetadata = (value) => {
|
|
27
23
|
if (!value) return;
|
|
28
24
|
let parsedValue = value;
|
|
@@ -61,7 +57,11 @@ async function resolveChangedAssets({ assetBaseStorageUri, currentManifest, curr
|
|
|
61
57
|
const changedEntries = await Promise.all(Object.entries(targetManifest.assets).map(async ([assetPath, asset]) => {
|
|
62
58
|
if ((currentManifest?.assets[assetPath])?.fileHash === asset.fileHash) return null;
|
|
63
59
|
const usesBrotliAsset = BR_COMPRESSED_ASSET_PATH_RE.test(assetPath);
|
|
64
|
-
const storageUri =
|
|
60
|
+
const storageUri = (0, _hot_updater_plugin_core.resolveManifestAssetStorageUri)({
|
|
61
|
+
assetBaseStorageUri,
|
|
62
|
+
assetPath: usesBrotliAsset ? `${assetPath}.br` : assetPath,
|
|
63
|
+
fileHash: asset.fileHash
|
|
64
|
+
});
|
|
65
65
|
const patch = patchDescriptor?.assetPath === assetPath ? patchDescriptor.patch : null;
|
|
66
66
|
let fileUrl = null;
|
|
67
67
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveManifestAssetStorageUri } from "@hot-updater/plugin-core";
|
|
1
2
|
import { getAssetBaseStorageUri, getBundlePatch, getManifestFileHash, getManifestStorageUri, stripBundleArtifactMetadata } from "@hot-updater/core";
|
|
2
3
|
//#region src/db/updateArtifacts.ts
|
|
3
4
|
const HBC_ASSET_PATH_RE = /\.bundle$/;
|
|
@@ -17,11 +18,6 @@ const isBundleManifest = (value) => {
|
|
|
17
18
|
return typeof manifestAsset.fileHash === "string" && (manifestAsset.signature === void 0 || typeof manifestAsset.signature === "string");
|
|
18
19
|
});
|
|
19
20
|
};
|
|
20
|
-
const createChildStorageUri = (baseStorageUri, relativePath) => {
|
|
21
|
-
const baseUrl = new URL(baseStorageUri);
|
|
22
|
-
baseUrl.pathname = `${baseUrl.pathname.replace(/\/+$/, "")}/${relativePath.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
|
|
23
|
-
return baseUrl.toString();
|
|
24
|
-
};
|
|
25
21
|
const parseBundleMetadata = (value) => {
|
|
26
22
|
if (!value) return;
|
|
27
23
|
let parsedValue = value;
|
|
@@ -60,7 +56,11 @@ async function resolveChangedAssets({ assetBaseStorageUri, currentManifest, curr
|
|
|
60
56
|
const changedEntries = await Promise.all(Object.entries(targetManifest.assets).map(async ([assetPath, asset]) => {
|
|
61
57
|
if ((currentManifest?.assets[assetPath])?.fileHash === asset.fileHash) return null;
|
|
62
58
|
const usesBrotliAsset = BR_COMPRESSED_ASSET_PATH_RE.test(assetPath);
|
|
63
|
-
const storageUri =
|
|
59
|
+
const storageUri = resolveManifestAssetStorageUri({
|
|
60
|
+
assetBaseStorageUri,
|
|
61
|
+
assetPath: usesBrotliAsset ? `${assetPath}.br` : assetPath,
|
|
62
|
+
fileHash: asset.fileHash
|
|
63
|
+
});
|
|
64
64
|
const patch = patchDescriptor?.assetPath === assetPath ? patchDescriptor.patch : null;
|
|
65
65
|
let fileUrl = null;
|
|
66
66
|
try {
|
package/dist/handler.cjs
CHANGED
|
@@ -2,7 +2,7 @@ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
|
|
|
2
2
|
const require_internalRouter = require("./internalRouter.cjs");
|
|
3
3
|
const require_version = require("./version.cjs");
|
|
4
4
|
let semver = require("semver");
|
|
5
|
-
semver = require_runtime.__toESM(semver
|
|
5
|
+
semver = require_runtime.__toESM(semver);
|
|
6
6
|
//#region src/handler.ts
|
|
7
7
|
var HandlerBadRequestError = class extends Error {
|
|
8
8
|
constructor(message) {
|
|
@@ -12,6 +12,8 @@ var HandlerBadRequestError = class extends Error {
|
|
|
12
12
|
};
|
|
13
13
|
const SDK_VERSION_HEADER = "Hot-Updater-SDK-Version";
|
|
14
14
|
const EXPLICIT_NO_UPDATE_MIN_SDK_VERSION = "0.31.0";
|
|
15
|
+
const DEFAULT_BUNDLE_LIST_LIMIT = 50;
|
|
16
|
+
const MAX_BUNDLE_LIST_LIMIT = 100;
|
|
15
17
|
const supportsExplicitNoUpdateResponse = (request) => {
|
|
16
18
|
const sdkVersion = request.headers.get(SDK_VERSION_HEADER)?.trim();
|
|
17
19
|
if (!sdkVersion) return false;
|
|
@@ -61,6 +63,13 @@ const parseStringArraySearchParam = (url, key) => {
|
|
|
61
63
|
const values = url.searchParams.getAll(key);
|
|
62
64
|
return values.length > 0 ? values : void 0;
|
|
63
65
|
};
|
|
66
|
+
const parsePositiveIntegerSearchParam = (url, key, defaultValue, maxValue) => {
|
|
67
|
+
const value = url.searchParams.get(key);
|
|
68
|
+
if (value === null) return defaultValue;
|
|
69
|
+
const parsed = Number(value);
|
|
70
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > maxValue) throw new HandlerBadRequestError(`The '${key}' query parameter must be a positive integer between 1 and ${maxValue}.`);
|
|
71
|
+
return parsed;
|
|
72
|
+
};
|
|
64
73
|
const requirePlatformParam = (params) => {
|
|
65
74
|
const platform = requireRouteParam(params, "platform");
|
|
66
75
|
if (!isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
|
|
@@ -129,7 +138,7 @@ const handleGetBundles = async (_params, request, api, context) => {
|
|
|
129
138
|
const url = new URL(request.url);
|
|
130
139
|
const channel = url.searchParams.get("channel") ?? void 0;
|
|
131
140
|
const platform = url.searchParams.get("platform");
|
|
132
|
-
const limit =
|
|
141
|
+
const limit = parsePositiveIntegerSearchParam(url, "limit", DEFAULT_BUNDLE_LIST_LIMIT, MAX_BUNDLE_LIST_LIMIT);
|
|
133
142
|
const pageParam = url.searchParams.get("page");
|
|
134
143
|
const offset = url.searchParams.get("offset");
|
|
135
144
|
const after = url.searchParams.get("after") ?? void 0;
|
|
@@ -231,18 +240,19 @@ const routes = {
|
|
|
231
240
|
*/
|
|
232
241
|
function createHandler(api, options = {}) {
|
|
233
242
|
const basePath = options.basePath ?? "/api";
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
243
|
+
const routeOptions = {
|
|
244
|
+
updateCheck: options.routes?.updateCheck ?? true,
|
|
245
|
+
bundles: options.routes?.bundles ?? false
|
|
246
|
+
};
|
|
237
247
|
const router = require_internalRouter.createRouter();
|
|
238
|
-
|
|
239
|
-
if (
|
|
248
|
+
require_internalRouter.addRoute(router, "GET", "/version", "version");
|
|
249
|
+
if (routeOptions.updateCheck) {
|
|
240
250
|
require_internalRouter.addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId", "fingerprintUpdateWithCohort");
|
|
241
251
|
require_internalRouter.addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId/:cohort", "fingerprintUpdateWithCohort");
|
|
242
252
|
require_internalRouter.addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId", "appVersionUpdateWithCohort");
|
|
243
253
|
require_internalRouter.addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId/:cohort", "appVersionUpdateWithCohort");
|
|
244
254
|
}
|
|
245
|
-
if (
|
|
255
|
+
if (routeOptions.bundles) {
|
|
246
256
|
require_internalRouter.addRoute(router, "GET", "/api/bundles/channels", "getChannels");
|
|
247
257
|
require_internalRouter.addRoute(router, "GET", "/api/bundles/:id", "getBundle");
|
|
248
258
|
require_internalRouter.addRoute(router, "GET", "/api/bundles", "getBundles");
|
package/dist/handler.d.cts
CHANGED
|
@@ -18,27 +18,26 @@ interface HandlerOptions {
|
|
|
18
18
|
* @default "/api"
|
|
19
19
|
*/
|
|
20
20
|
basePath?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Route groups to mount. Omit this option to use the default route groups.
|
|
23
|
+
* When provided, both route groups must be specified explicitly.
|
|
24
|
+
* The `/version` endpoint is always mounted for diagnostics.
|
|
25
|
+
*/
|
|
21
26
|
routes?: HandlerRoutes;
|
|
22
27
|
}
|
|
23
28
|
interface HandlerRoutes {
|
|
24
29
|
/**
|
|
25
30
|
* Controls whether update-check routes are mounted.
|
|
26
|
-
*
|
|
27
|
-
*/
|
|
28
|
-
updateCheck?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Controls whether the `/version` endpoint is mounted.
|
|
31
|
-
* Useful for diagnostics and lightweight health/version checks.
|
|
32
|
-
* @default true
|
|
31
|
+
* Defaults to `true` only when `routes` is omitted.
|
|
33
32
|
*/
|
|
34
|
-
|
|
33
|
+
updateCheck: boolean;
|
|
35
34
|
/**
|
|
36
35
|
* Controls whether bundle management routes are mounted.
|
|
37
36
|
* This includes `/api/bundles*`, which are used by the
|
|
38
37
|
* CLI `standaloneRepository` plugin.
|
|
39
|
-
*
|
|
38
|
+
* Defaults to `false` only when `routes` is omitted.
|
|
40
39
|
*/
|
|
41
|
-
bundles
|
|
40
|
+
bundles: boolean;
|
|
42
41
|
}
|
|
43
42
|
/**
|
|
44
43
|
* Creates a Web Standard Request handler for Hot Updater API
|
package/dist/handler.d.mts
CHANGED
|
@@ -18,27 +18,26 @@ interface HandlerOptions {
|
|
|
18
18
|
* @default "/api"
|
|
19
19
|
*/
|
|
20
20
|
basePath?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Route groups to mount. Omit this option to use the default route groups.
|
|
23
|
+
* When provided, both route groups must be specified explicitly.
|
|
24
|
+
* The `/version` endpoint is always mounted for diagnostics.
|
|
25
|
+
*/
|
|
21
26
|
routes?: HandlerRoutes;
|
|
22
27
|
}
|
|
23
28
|
interface HandlerRoutes {
|
|
24
29
|
/**
|
|
25
30
|
* Controls whether update-check routes are mounted.
|
|
26
|
-
*
|
|
27
|
-
*/
|
|
28
|
-
updateCheck?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Controls whether the `/version` endpoint is mounted.
|
|
31
|
-
* Useful for diagnostics and lightweight health/version checks.
|
|
32
|
-
* @default true
|
|
31
|
+
* Defaults to `true` only when `routes` is omitted.
|
|
33
32
|
*/
|
|
34
|
-
|
|
33
|
+
updateCheck: boolean;
|
|
35
34
|
/**
|
|
36
35
|
* Controls whether bundle management routes are mounted.
|
|
37
36
|
* This includes `/api/bundles*`, which are used by the
|
|
38
37
|
* CLI `standaloneRepository` plugin.
|
|
39
|
-
*
|
|
38
|
+
* Defaults to `false` only when `routes` is omitted.
|
|
40
39
|
*/
|
|
41
|
-
bundles
|
|
40
|
+
bundles: boolean;
|
|
42
41
|
}
|
|
43
42
|
/**
|
|
44
43
|
* Creates a Web Standard Request handler for Hot Updater API
|
package/dist/handler.mjs
CHANGED
|
@@ -10,6 +10,8 @@ var HandlerBadRequestError = class extends Error {
|
|
|
10
10
|
};
|
|
11
11
|
const SDK_VERSION_HEADER = "Hot-Updater-SDK-Version";
|
|
12
12
|
const EXPLICIT_NO_UPDATE_MIN_SDK_VERSION = "0.31.0";
|
|
13
|
+
const DEFAULT_BUNDLE_LIST_LIMIT = 50;
|
|
14
|
+
const MAX_BUNDLE_LIST_LIMIT = 100;
|
|
13
15
|
const supportsExplicitNoUpdateResponse = (request) => {
|
|
14
16
|
const sdkVersion = request.headers.get(SDK_VERSION_HEADER)?.trim();
|
|
15
17
|
if (!sdkVersion) return false;
|
|
@@ -59,6 +61,13 @@ const parseStringArraySearchParam = (url, key) => {
|
|
|
59
61
|
const values = url.searchParams.getAll(key);
|
|
60
62
|
return values.length > 0 ? values : void 0;
|
|
61
63
|
};
|
|
64
|
+
const parsePositiveIntegerSearchParam = (url, key, defaultValue, maxValue) => {
|
|
65
|
+
const value = url.searchParams.get(key);
|
|
66
|
+
if (value === null) return defaultValue;
|
|
67
|
+
const parsed = Number(value);
|
|
68
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > maxValue) throw new HandlerBadRequestError(`The '${key}' query parameter must be a positive integer between 1 and ${maxValue}.`);
|
|
69
|
+
return parsed;
|
|
70
|
+
};
|
|
62
71
|
const requirePlatformParam = (params) => {
|
|
63
72
|
const platform = requireRouteParam(params, "platform");
|
|
64
73
|
if (!isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
|
|
@@ -127,7 +136,7 @@ const handleGetBundles = async (_params, request, api, context) => {
|
|
|
127
136
|
const url = new URL(request.url);
|
|
128
137
|
const channel = url.searchParams.get("channel") ?? void 0;
|
|
129
138
|
const platform = url.searchParams.get("platform");
|
|
130
|
-
const limit =
|
|
139
|
+
const limit = parsePositiveIntegerSearchParam(url, "limit", DEFAULT_BUNDLE_LIST_LIMIT, MAX_BUNDLE_LIST_LIMIT);
|
|
131
140
|
const pageParam = url.searchParams.get("page");
|
|
132
141
|
const offset = url.searchParams.get("offset");
|
|
133
142
|
const after = url.searchParams.get("after") ?? void 0;
|
|
@@ -229,18 +238,19 @@ const routes = {
|
|
|
229
238
|
*/
|
|
230
239
|
function createHandler(api, options = {}) {
|
|
231
240
|
const basePath = options.basePath ?? "/api";
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
241
|
+
const routeOptions = {
|
|
242
|
+
updateCheck: options.routes?.updateCheck ?? true,
|
|
243
|
+
bundles: options.routes?.bundles ?? false
|
|
244
|
+
};
|
|
235
245
|
const router = createRouter();
|
|
236
|
-
|
|
237
|
-
if (
|
|
246
|
+
addRoute(router, "GET", "/version", "version");
|
|
247
|
+
if (routeOptions.updateCheck) {
|
|
238
248
|
addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId", "fingerprintUpdateWithCohort");
|
|
239
249
|
addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId/:cohort", "fingerprintUpdateWithCohort");
|
|
240
250
|
addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId", "appVersionUpdateWithCohort");
|
|
241
251
|
addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId/:cohort", "appVersionUpdateWithCohort");
|
|
242
252
|
}
|
|
243
|
-
if (
|
|
253
|
+
if (routeOptions.bundles) {
|
|
244
254
|
addRoute(router, "GET", "/api/bundles/channels", "getChannels");
|
|
245
255
|
addRoute(router, "GET", "/api/bundles/:id", "getBundle");
|
|
246
256
|
addRoute(router, "GET", "/api/bundles", "getBundles");
|
package/dist/index.d.cts
CHANGED
|
@@ -4,4 +4,4 @@ import { HotUpdaterClient, HotUpdaterDB, Migrator } from "./db/ormCore.cjs";
|
|
|
4
4
|
import { HOT_UPDATER_SERVER_VERSION } from "./version.cjs";
|
|
5
5
|
import { CreateHotUpdaterOptions, HotUpdaterAPI, createHotUpdater } from "./db/index.cjs";
|
|
6
6
|
import { Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.cjs";
|
|
7
|
-
export {
|
|
7
|
+
export { Bundle, ChannelsResponse, CreateBundleDiffDependencies, CreateBundleDiffInput, CreateBundleDiffOptions, CreateHotUpdaterOptions, DataResponse, HOT_UPDATER_SERVER_VERSION, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, Paginated, PaginatedResult, PaginationInfo, PaginationOptions, createBundleDiff, createHandler, createHotUpdater };
|
package/dist/index.d.mts
CHANGED
|
@@ -4,4 +4,4 @@ import { HotUpdaterClient, HotUpdaterDB, Migrator } from "./db/ormCore.mjs";
|
|
|
4
4
|
import { HOT_UPDATER_SERVER_VERSION } from "./version.mjs";
|
|
5
5
|
import { CreateHotUpdaterOptions, HotUpdaterAPI, createHotUpdater } from "./db/index.mjs";
|
|
6
6
|
import { Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.mjs";
|
|
7
|
-
export {
|
|
7
|
+
export { Bundle, ChannelsResponse, CreateBundleDiffDependencies, CreateBundleDiffInput, CreateBundleDiffOptions, CreateHotUpdaterOptions, DataResponse, HOT_UPDATER_SERVER_VERSION, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, Paginated, PaginatedResult, PaginationInfo, PaginationOptions, createBundleDiff, createHandler, createHotUpdater };
|
|
@@ -46,4 +46,4 @@ interface FumaDBFactory<Schemas extends AnySchema[]> {
|
|
|
46
46
|
}
|
|
47
47
|
type InferFumaDB<Factory extends FumaDBFactory<any>> = Factory extends FumaDBFactory<infer Schemas> ? FumaDB<Schemas> : never;
|
|
48
48
|
//#endregion
|
|
49
|
-
export type
|
|
49
|
+
export { type FumaDBFactory, type InferFumaDB };
|
|
@@ -46,4 +46,4 @@ interface FumaDBFactory<Schemas extends AnySchema[]> {
|
|
|
46
46
|
}
|
|
47
47
|
type InferFumaDB<Factory extends FumaDBFactory<any>> = Factory extends FumaDBFactory<infer Schemas> ? FumaDB<Schemas> : never;
|
|
48
48
|
//#endregion
|
|
49
|
-
export type
|
|
49
|
+
export { type FumaDBFactory, type InferFumaDB };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"mongodb": "6.20.0",
|
|
56
56
|
"rou3": "0.7.9",
|
|
57
57
|
"semver": "^7.7.2",
|
|
58
|
-
"@hot-updater/bsdiff": "0.
|
|
59
|
-
"@hot-updater/core": "0.
|
|
60
|
-
"@hot-updater/
|
|
61
|
-
"@hot-updater/
|
|
58
|
+
"@hot-updater/bsdiff": "0.32.0",
|
|
59
|
+
"@hot-updater/core": "0.32.0",
|
|
60
|
+
"@hot-updater/plugin-core": "0.32.0",
|
|
61
|
+
"@hot-updater/js": "0.32.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@electric-sql/pglite": "0.2.17",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"kysely-pglite-dialect": "1.2.0",
|
|
69
69
|
"msw": "^2.7.0",
|
|
70
70
|
"uuidv7": "^1.0.2",
|
|
71
|
-
"@hot-updater/standalone": "0.
|
|
72
|
-
"@hot-updater/test-utils": "0.
|
|
71
|
+
"@hot-updater/standalone": "0.32.0",
|
|
72
|
+
"@hot-updater/test-utils": "0.32.0"
|
|
73
73
|
},
|
|
74
74
|
"inlinedDependencies": {
|
|
75
75
|
"@noble/hashes": "1.8.0",
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
DatabasePlugin,
|
|
18
18
|
NodeStoragePlugin,
|
|
19
19
|
} from "@hot-updater/plugin-core";
|
|
20
|
+
import { resolveManifestAssetStorageUri } from "@hot-updater/plugin-core";
|
|
20
21
|
|
|
21
22
|
type BundleManifest = {
|
|
22
23
|
bundleId: string;
|
|
@@ -80,21 +81,6 @@ const isBundleManifest = (value: unknown): value is BundleManifest => {
|
|
|
80
81
|
);
|
|
81
82
|
};
|
|
82
83
|
|
|
83
|
-
const createChildStorageUri = (
|
|
84
|
-
baseStorageUri: string,
|
|
85
|
-
relativePath: string,
|
|
86
|
-
) => {
|
|
87
|
-
const baseUrl = new URL(baseStorageUri);
|
|
88
|
-
const normalizedBasePath = baseUrl.pathname.replace(/\/+$/, "");
|
|
89
|
-
const relativeSegments = relativePath
|
|
90
|
-
.split("/")
|
|
91
|
-
.filter(Boolean)
|
|
92
|
-
.map((segment) => encodeURIComponent(segment));
|
|
93
|
-
|
|
94
|
-
baseUrl.pathname = `${normalizedBasePath}/${relativeSegments.join("/")}`;
|
|
95
|
-
return baseUrl.toString();
|
|
96
|
-
};
|
|
97
|
-
|
|
98
84
|
const getRelativeStorageDir = (relativePath: string) => {
|
|
99
85
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
100
86
|
const dirname = path.posix.dirname(normalized);
|
|
@@ -188,18 +174,24 @@ function resolveHbcAssetPath(manifest: BundleManifest) {
|
|
|
188
174
|
async function fetchAssetBytes(
|
|
189
175
|
bundle: Bundle,
|
|
190
176
|
assetPath: string,
|
|
177
|
+
manifest: BundleManifest,
|
|
191
178
|
storagePlugin: NodeStoragePlugin | null,
|
|
192
179
|
) {
|
|
193
180
|
const assetBaseStorageUri = getAssetBaseStorageUri(bundle);
|
|
194
181
|
if (!assetBaseStorageUri) {
|
|
195
182
|
throw new Error(`Bundle ${bundle.id} does not have asset storage metadata`);
|
|
196
183
|
}
|
|
184
|
+
const asset = manifest.assets[assetPath];
|
|
185
|
+
if (!asset) {
|
|
186
|
+
throw new Error(`Asset ${assetPath} is missing from manifest`);
|
|
187
|
+
}
|
|
197
188
|
|
|
198
189
|
if (BR_COMPRESSED_ASSET_PATH_RE.test(assetPath)) {
|
|
199
|
-
const compressedAssetStorageUri =
|
|
190
|
+
const compressedAssetStorageUri = resolveManifestAssetStorageUri({
|
|
200
191
|
assetBaseStorageUri,
|
|
201
|
-
`${assetPath}.br`,
|
|
202
|
-
|
|
192
|
+
assetPath: `${assetPath}.br`,
|
|
193
|
+
fileHash: asset.fileHash,
|
|
194
|
+
});
|
|
203
195
|
|
|
204
196
|
let compressedBytes: Uint8Array | null = null;
|
|
205
197
|
try {
|
|
@@ -216,7 +208,11 @@ async function fetchAssetBytes(
|
|
|
216
208
|
}
|
|
217
209
|
}
|
|
218
210
|
|
|
219
|
-
const assetStorageUri =
|
|
211
|
+
const assetStorageUri = resolveManifestAssetStorageUri({
|
|
212
|
+
assetBaseStorageUri,
|
|
213
|
+
assetPath,
|
|
214
|
+
fileHash: asset.fileHash,
|
|
215
|
+
});
|
|
220
216
|
return downloadStorageBytes(assetStorageUri, storagePlugin);
|
|
221
217
|
}
|
|
222
218
|
|
|
@@ -300,8 +296,18 @@ export async function createBundleDiff(
|
|
|
300
296
|
}
|
|
301
297
|
|
|
302
298
|
const [baseBytes, targetBytes] = await Promise.all([
|
|
303
|
-
fetchAssetBytes(
|
|
304
|
-
|
|
299
|
+
fetchAssetBytes(
|
|
300
|
+
baseBundle,
|
|
301
|
+
baseAssetPath,
|
|
302
|
+
baseManifest,
|
|
303
|
+
deps.storagePlugin,
|
|
304
|
+
),
|
|
305
|
+
fetchAssetBytes(
|
|
306
|
+
targetBundle,
|
|
307
|
+
targetAssetPath,
|
|
308
|
+
targetManifest,
|
|
309
|
+
deps.storagePlugin,
|
|
310
|
+
),
|
|
305
311
|
]);
|
|
306
312
|
|
|
307
313
|
const patchBytes = await hdiff(baseBytes, targetBytes);
|
|
@@ -195,6 +195,115 @@ describe("createPluginDatabaseCore", () => {
|
|
|
195
195
|
});
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
it("resolves manifest changed assets from deterministic content-addressed storage", async () => {
|
|
199
|
+
const currentBundle = {
|
|
200
|
+
...baseBundle,
|
|
201
|
+
id: "00000000-0000-0000-0000-000000000001",
|
|
202
|
+
manifestStorageUri: "r2://bucket/current/manifest.json",
|
|
203
|
+
manifestFileHash: "sig:current-manifest",
|
|
204
|
+
assetBaseStorageUri: "r2://bucket/assets",
|
|
205
|
+
};
|
|
206
|
+
const targetBundle = {
|
|
207
|
+
...baseBundle,
|
|
208
|
+
id: "00000000-0000-0000-0000-000000000002",
|
|
209
|
+
fileHash: "hash-2",
|
|
210
|
+
manifestStorageUri: "r2://bucket/target/manifest.json",
|
|
211
|
+
manifestFileHash: "sig:target-manifest",
|
|
212
|
+
assetBaseStorageUri: "r2://bucket/assets",
|
|
213
|
+
};
|
|
214
|
+
const manifests = new Map([
|
|
215
|
+
[
|
|
216
|
+
currentBundle.manifestStorageUri,
|
|
217
|
+
JSON.stringify({
|
|
218
|
+
bundleId: currentBundle.id,
|
|
219
|
+
assets: {
|
|
220
|
+
"index.ios.bundle": {
|
|
221
|
+
fileHash: "old-bundle-hash",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
targetBundle.manifestStorageUri,
|
|
228
|
+
JSON.stringify({
|
|
229
|
+
bundleId: targetBundle.id,
|
|
230
|
+
assets: {
|
|
231
|
+
"index.ios.bundle": {
|
|
232
|
+
fileHash: "new-bundle-hash",
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
],
|
|
237
|
+
]);
|
|
238
|
+
const plugin: DatabasePlugin<TestContext> = {
|
|
239
|
+
name: "content-addressed-manifest-plugin",
|
|
240
|
+
async appendBundle() {},
|
|
241
|
+
async commitBundle() {},
|
|
242
|
+
async deleteBundle() {},
|
|
243
|
+
async getBundleById(bundleId) {
|
|
244
|
+
if (bundleId === currentBundle.id) return currentBundle;
|
|
245
|
+
if (bundleId === targetBundle.id) return targetBundle;
|
|
246
|
+
return null;
|
|
247
|
+
},
|
|
248
|
+
async getUpdateInfo() {
|
|
249
|
+
return {
|
|
250
|
+
fileHash: targetBundle.fileHash,
|
|
251
|
+
id: targetBundle.id,
|
|
252
|
+
message: targetBundle.message,
|
|
253
|
+
shouldForceUpdate: targetBundle.shouldForceUpdate,
|
|
254
|
+
status: "UPDATE",
|
|
255
|
+
storageUri: targetBundle.storageUri,
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
async getBundles() {
|
|
259
|
+
return {
|
|
260
|
+
data: [targetBundle],
|
|
261
|
+
pagination: {
|
|
262
|
+
currentPage: 1,
|
|
263
|
+
hasNextPage: false,
|
|
264
|
+
hasPreviousPage: false,
|
|
265
|
+
total: 1,
|
|
266
|
+
totalPages: 1,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
async getChannels() {
|
|
271
|
+
return ["production"];
|
|
272
|
+
},
|
|
273
|
+
async updateBundle() {},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const core = createPluginDatabaseCore(
|
|
277
|
+
() => plugin,
|
|
278
|
+
async (storageUri) => {
|
|
279
|
+
if (!storageUri) return null;
|
|
280
|
+
const url = new URL(storageUri);
|
|
281
|
+
return `https://assets.example.com/${url.host}${url.pathname}`;
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
readStorageText: async (storageUri) =>
|
|
285
|
+
manifests.get(storageUri) ?? null,
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
await expect(
|
|
290
|
+
core.api.getAppUpdateInfo({
|
|
291
|
+
...updateArgs,
|
|
292
|
+
bundleId: currentBundle.id,
|
|
293
|
+
}),
|
|
294
|
+
).resolves.toMatchObject({
|
|
295
|
+
changedAssets: {
|
|
296
|
+
"index.ios.bundle": {
|
|
297
|
+
file: {
|
|
298
|
+
compression: "br",
|
|
299
|
+
url: "https://assets.example.com/bucket/assets/sha256/ne/new-bundle-hash.br",
|
|
300
|
+
},
|
|
301
|
+
fileHash: "new-bundle-hash",
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
198
307
|
it("falls back to archive metadata when manifest changed assets cannot be resolved", async () => {
|
|
199
308
|
const currentBundle = {
|
|
200
309
|
...baseBundle,
|
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
type Bundle,
|
|
9
9
|
type ChangedAsset,
|
|
10
10
|
} from "@hot-updater/core";
|
|
11
|
-
import
|
|
11
|
+
import {
|
|
12
|
+
resolveManifestAssetStorageUri,
|
|
13
|
+
type HotUpdaterContext,
|
|
14
|
+
} from "@hot-updater/plugin-core";
|
|
12
15
|
|
|
13
16
|
type BundleManifest = {
|
|
14
17
|
bundleId: string;
|
|
@@ -74,21 +77,6 @@ const isBundleManifest = (value: unknown): value is BundleManifest => {
|
|
|
74
77
|
);
|
|
75
78
|
};
|
|
76
79
|
|
|
77
|
-
const createChildStorageUri = (
|
|
78
|
-
baseStorageUri: string,
|
|
79
|
-
relativePath: string,
|
|
80
|
-
) => {
|
|
81
|
-
const baseUrl = new URL(baseStorageUri);
|
|
82
|
-
const normalizedBasePath = baseUrl.pathname.replace(/\/+$/, "");
|
|
83
|
-
const relativeSegments = relativePath
|
|
84
|
-
.split("/")
|
|
85
|
-
.filter(Boolean)
|
|
86
|
-
.map((segment) => encodeURIComponent(segment));
|
|
87
|
-
|
|
88
|
-
baseUrl.pathname = `${normalizedBasePath}/${relativeSegments.join("/")}`;
|
|
89
|
-
return baseUrl.toString();
|
|
90
|
-
};
|
|
91
|
-
|
|
92
80
|
export const parseBundleMetadata = (
|
|
93
81
|
value: unknown,
|
|
94
82
|
): Bundle["metadata"] | undefined => {
|
|
@@ -213,10 +201,11 @@ async function resolveChangedAssets<TContext>({
|
|
|
213
201
|
|
|
214
202
|
const usesBrotliAsset = BR_COMPRESSED_ASSET_PATH_RE.test(assetPath);
|
|
215
203
|
const downloadPath = usesBrotliAsset ? `${assetPath}.br` : assetPath;
|
|
216
|
-
const storageUri =
|
|
204
|
+
const storageUri = resolveManifestAssetStorageUri({
|
|
217
205
|
assetBaseStorageUri,
|
|
218
|
-
downloadPath,
|
|
219
|
-
|
|
206
|
+
assetPath: downloadPath,
|
|
207
|
+
fileHash: asset.fileHash,
|
|
208
|
+
});
|
|
220
209
|
const patch =
|
|
221
210
|
patchDescriptor?.assetPath === assetPath ? patchDescriptor.patch : null;
|
|
222
211
|
|
|
@@ -34,6 +34,10 @@ const api = createHotUpdater({
|
|
|
34
34
|
provider: "postgresql",
|
|
35
35
|
}),
|
|
36
36
|
basePath: "/hot-updater",
|
|
37
|
+
routes: {
|
|
38
|
+
updateCheck: true,
|
|
39
|
+
bundles: true,
|
|
40
|
+
},
|
|
37
41
|
});
|
|
38
42
|
|
|
39
43
|
// Setup MSW server to intercept HTTP requests
|
|
@@ -341,6 +345,10 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
341
345
|
provider: "postgresql",
|
|
342
346
|
}),
|
|
343
347
|
basePath: "/api/v2",
|
|
348
|
+
routes: {
|
|
349
|
+
updateCheck: true,
|
|
350
|
+
bundles: true,
|
|
351
|
+
},
|
|
344
352
|
});
|
|
345
353
|
|
|
346
354
|
// Setup MSW for custom basePath
|
|
@@ -411,6 +419,10 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
411
419
|
const blobApi = createHotUpdater({
|
|
412
420
|
database: createInMemoryBlobDatabase(store),
|
|
413
421
|
basePath: "/blob-hot-updater",
|
|
422
|
+
routes: {
|
|
423
|
+
updateCheck: true,
|
|
424
|
+
bundles: true,
|
|
425
|
+
},
|
|
414
426
|
});
|
|
415
427
|
const handleBlobRequest = async (request: Request) => {
|
|
416
428
|
const response = await blobApi.handler(request);
|
package/src/handler.spec.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Bundle, NIL_UUID } from "@hot-updater/core";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
|
-
import { createHandler, type HandlerAPI } from "./handler";
|
|
4
|
+
import { createHandler, type HandlerAPI, type HandlerRoutes } from "./handler";
|
|
5
5
|
import { HOT_UPDATER_SERVER_VERSION } from "./version";
|
|
6
6
|
|
|
7
7
|
const NEXT_SDK_VERSION_FOR_TEST = "0.31.0";
|
|
@@ -51,6 +51,19 @@ const createApi = () =>
|
|
|
51
51
|
deleteBundleById: vi.fn<HandlerAPI<TestContext>["deleteBundleById"]>(),
|
|
52
52
|
}) satisfies HandlerAPI<TestContext>;
|
|
53
53
|
|
|
54
|
+
const createManagementHandler = (
|
|
55
|
+
api: HandlerAPI<TestContext>,
|
|
56
|
+
routes: Partial<HandlerRoutes> = {},
|
|
57
|
+
) =>
|
|
58
|
+
createHandler(api, {
|
|
59
|
+
basePath: "/hot-updater",
|
|
60
|
+
routes: {
|
|
61
|
+
updateCheck: true,
|
|
62
|
+
bundles: true,
|
|
63
|
+
...routes,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
54
67
|
describe("createHandler", () => {
|
|
55
68
|
it("supports the app-version route without a cohort segment", async () => {
|
|
56
69
|
const api = createApi();
|
|
@@ -218,44 +231,113 @@ describe("createHandler", () => {
|
|
|
218
231
|
expect(updateResponse.status).toBe(200);
|
|
219
232
|
});
|
|
220
233
|
|
|
221
|
-
it("
|
|
234
|
+
it("does not mount bundle routes by default", async () => {
|
|
235
|
+
const api = createApi();
|
|
236
|
+
const handler = createHandler(api, { basePath: "/hot-updater" });
|
|
237
|
+
|
|
238
|
+
const versionResponse = await handler(
|
|
239
|
+
new Request("http://localhost/hot-updater/version"),
|
|
240
|
+
);
|
|
241
|
+
const bundlesResponse = await handler(
|
|
242
|
+
new Request("http://localhost/hot-updater/api/bundles"),
|
|
243
|
+
);
|
|
244
|
+
const updateResponse = await handler(
|
|
245
|
+
new Request(
|
|
246
|
+
"http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
expect(versionResponse.status).toBe(200);
|
|
251
|
+
expect(bundlesResponse.status).toBe(404);
|
|
252
|
+
expect(updateResponse.status).toBe(200);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("mounts bundle routes when explicitly enabled", async () => {
|
|
222
256
|
const api = createApi();
|
|
257
|
+
api.getBundles.mockResolvedValueOnce({
|
|
258
|
+
data: [],
|
|
259
|
+
pagination: {
|
|
260
|
+
total: 0,
|
|
261
|
+
hasNextPage: false,
|
|
262
|
+
hasPreviousPage: false,
|
|
263
|
+
currentPage: 1,
|
|
264
|
+
totalPages: 0,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
223
267
|
const handler = createHandler(api, {
|
|
224
268
|
basePath: "/hot-updater",
|
|
225
269
|
routes: {
|
|
226
270
|
updateCheck: true,
|
|
227
|
-
|
|
228
|
-
bundles: false,
|
|
271
|
+
bundles: true,
|
|
229
272
|
},
|
|
230
273
|
});
|
|
231
274
|
|
|
232
|
-
const
|
|
233
|
-
new Request("http://localhost/hot-updater/version"),
|
|
234
|
-
);
|
|
235
|
-
const bundlesResponse = await handler(
|
|
275
|
+
const response = await handler(
|
|
236
276
|
new Request("http://localhost/hot-updater/api/bundles"),
|
|
237
277
|
);
|
|
278
|
+
|
|
279
|
+
expect(response.status).toBe(200);
|
|
280
|
+
expect(api.getBundles).toHaveBeenCalledWith(
|
|
281
|
+
{
|
|
282
|
+
cursor: undefined,
|
|
283
|
+
limit: 50,
|
|
284
|
+
page: undefined,
|
|
285
|
+
where: {},
|
|
286
|
+
},
|
|
287
|
+
undefined,
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("keeps update-check routes mounted for partial runtime route config", async () => {
|
|
292
|
+
const api = createApi();
|
|
293
|
+
const handler = createHandler(api, {
|
|
294
|
+
basePath: "/hot-updater",
|
|
295
|
+
routes: JSON.parse('{"bundles":true}') as HandlerRoutes,
|
|
296
|
+
});
|
|
297
|
+
|
|
238
298
|
const updateResponse = await handler(
|
|
239
299
|
new Request(
|
|
240
300
|
"http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
|
|
241
301
|
),
|
|
242
302
|
);
|
|
243
303
|
|
|
244
|
-
expect(versionResponse.status).toBe(404);
|
|
245
|
-
expect(bundlesResponse.status).toBe(404);
|
|
246
304
|
expect(updateResponse.status).toBe(200);
|
|
247
305
|
});
|
|
248
306
|
|
|
249
|
-
it("
|
|
307
|
+
it("keeps the version route mounted when update-check routes are disabled", async () => {
|
|
250
308
|
const api = createApi();
|
|
251
309
|
const handler = createHandler(api, {
|
|
252
310
|
basePath: "/hot-updater",
|
|
253
311
|
routes: {
|
|
254
312
|
updateCheck: false,
|
|
255
|
-
bundles:
|
|
313
|
+
bundles: false,
|
|
256
314
|
},
|
|
257
315
|
});
|
|
258
316
|
|
|
317
|
+
const versionResponse = await handler(
|
|
318
|
+
new Request("http://localhost/hot-updater/version"),
|
|
319
|
+
);
|
|
320
|
+
const bundlesResponse = await handler(
|
|
321
|
+
new Request("http://localhost/hot-updater/api/bundles"),
|
|
322
|
+
);
|
|
323
|
+
const updateResponse = await handler(
|
|
324
|
+
new Request(
|
|
325
|
+
"http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
expect(versionResponse.status).toBe(200);
|
|
330
|
+
await expect(versionResponse.json()).resolves.toEqual({
|
|
331
|
+
version: HOT_UPDATER_SERVER_VERSION,
|
|
332
|
+
});
|
|
333
|
+
expect(bundlesResponse.status).toBe(404);
|
|
334
|
+
expect(updateResponse.status).toBe(404);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("can mount bundle routes without update-check routes", async () => {
|
|
338
|
+
const api = createApi();
|
|
339
|
+
const handler = createManagementHandler(api, { updateCheck: false });
|
|
340
|
+
|
|
259
341
|
const versionResponse = await handler(
|
|
260
342
|
new Request("http://localhost/hot-updater/version"),
|
|
261
343
|
);
|
|
@@ -294,7 +376,7 @@ describe("createHandler", () => {
|
|
|
294
376
|
totalPages: 26,
|
|
295
377
|
},
|
|
296
378
|
});
|
|
297
|
-
const handler =
|
|
379
|
+
const handler = createManagementHandler(api);
|
|
298
380
|
|
|
299
381
|
const response = await handler(
|
|
300
382
|
new Request(
|
|
@@ -339,7 +421,7 @@ describe("createHandler", () => {
|
|
|
339
421
|
totalPages: 1,
|
|
340
422
|
},
|
|
341
423
|
});
|
|
342
|
-
const handler =
|
|
424
|
+
const handler = createManagementHandler(api);
|
|
343
425
|
|
|
344
426
|
const response = await handler(
|
|
345
427
|
new Request(
|
|
@@ -382,7 +464,7 @@ describe("createHandler", () => {
|
|
|
382
464
|
previousCursor: "bundle-9",
|
|
383
465
|
},
|
|
384
466
|
});
|
|
385
|
-
const handler =
|
|
467
|
+
const handler = createManagementHandler(api);
|
|
386
468
|
|
|
387
469
|
const response = await handler(
|
|
388
470
|
new Request(
|
|
@@ -421,7 +503,7 @@ describe("createHandler", () => {
|
|
|
421
503
|
previousCursor: null,
|
|
422
504
|
},
|
|
423
505
|
});
|
|
424
|
-
const handler =
|
|
506
|
+
const handler = createManagementHandler(api);
|
|
425
507
|
|
|
426
508
|
const response = await handler(
|
|
427
509
|
new Request(
|
|
@@ -448,7 +530,7 @@ describe("createHandler", () => {
|
|
|
448
530
|
|
|
449
531
|
it("returns 400 when bundle list requests still send offset pagination", async () => {
|
|
450
532
|
const api = createApi();
|
|
451
|
-
const handler =
|
|
533
|
+
const handler = createManagementHandler(api);
|
|
452
534
|
|
|
453
535
|
const response = await handler(
|
|
454
536
|
new Request(
|
|
@@ -478,7 +560,7 @@ describe("createHandler", () => {
|
|
|
478
560
|
previousCursor: "bundle-9",
|
|
479
561
|
},
|
|
480
562
|
});
|
|
481
|
-
const handler =
|
|
563
|
+
const handler = createManagementHandler(api);
|
|
482
564
|
|
|
483
565
|
const response = await handler(
|
|
484
566
|
new Request(
|
|
@@ -505,7 +587,7 @@ describe("createHandler", () => {
|
|
|
505
587
|
|
|
506
588
|
it("returns 400 when bundle list requests send an invalid page", async () => {
|
|
507
589
|
const api = createApi();
|
|
508
|
-
const handler =
|
|
590
|
+
const handler = createManagementHandler(api);
|
|
509
591
|
|
|
510
592
|
const response = await handler(
|
|
511
593
|
new Request("http://localhost/hot-updater/api/bundles?limit=20&page=0"),
|
|
@@ -518,6 +600,22 @@ describe("createHandler", () => {
|
|
|
518
600
|
expect(api.getBundles).not.toHaveBeenCalled();
|
|
519
601
|
});
|
|
520
602
|
|
|
603
|
+
it("returns 400 when bundle list limit exceeds the maximum", async () => {
|
|
604
|
+
const api = createApi();
|
|
605
|
+
const handler = createManagementHandler(api);
|
|
606
|
+
|
|
607
|
+
const response = await handler(
|
|
608
|
+
new Request("http://localhost/hot-updater/api/bundles?limit=101"),
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
expect(response.status).toBe(400);
|
|
612
|
+
await expect(response.json()).resolves.toEqual({
|
|
613
|
+
error:
|
|
614
|
+
"The 'limit' query parameter must be a positive integer between 1 and 100.",
|
|
615
|
+
});
|
|
616
|
+
expect(api.getBundles).not.toHaveBeenCalled();
|
|
617
|
+
});
|
|
618
|
+
|
|
521
619
|
it("returns 400 when the platform route parameter is invalid", async () => {
|
|
522
620
|
const api = createApi();
|
|
523
621
|
const handler = createHandler(api, { basePath: "/hot-updater" });
|
package/src/handler.ts
CHANGED
|
@@ -52,28 +52,27 @@ export interface HandlerOptions {
|
|
|
52
52
|
* @default "/api"
|
|
53
53
|
*/
|
|
54
54
|
basePath?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Route groups to mount. Omit this option to use the default route groups.
|
|
57
|
+
* When provided, both route groups must be specified explicitly.
|
|
58
|
+
* The `/version` endpoint is always mounted for diagnostics.
|
|
59
|
+
*/
|
|
55
60
|
routes?: HandlerRoutes;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
export interface HandlerRoutes {
|
|
59
64
|
/**
|
|
60
65
|
* Controls whether update-check routes are mounted.
|
|
61
|
-
*
|
|
66
|
+
* Defaults to `true` only when `routes` is omitted.
|
|
62
67
|
*/
|
|
63
|
-
updateCheck
|
|
64
|
-
/**
|
|
65
|
-
* Controls whether the `/version` endpoint is mounted.
|
|
66
|
-
* Useful for diagnostics and lightweight health/version checks.
|
|
67
|
-
* @default true
|
|
68
|
-
*/
|
|
69
|
-
version?: boolean;
|
|
68
|
+
updateCheck: boolean;
|
|
70
69
|
/**
|
|
71
70
|
* Controls whether bundle management routes are mounted.
|
|
72
71
|
* This includes `/api/bundles*`, which are used by the
|
|
73
72
|
* CLI `standaloneRepository` plugin.
|
|
74
|
-
*
|
|
73
|
+
* Defaults to `false` only when `routes` is omitted.
|
|
75
74
|
*/
|
|
76
|
-
bundles
|
|
75
|
+
bundles: boolean;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
type RouteHandler<TContext = unknown> = (
|
|
@@ -92,6 +91,8 @@ class HandlerBadRequestError extends Error {
|
|
|
92
91
|
|
|
93
92
|
const SDK_VERSION_HEADER = "Hot-Updater-SDK-Version";
|
|
94
93
|
const EXPLICIT_NO_UPDATE_MIN_SDK_VERSION = "0.31.0";
|
|
94
|
+
const DEFAULT_BUNDLE_LIST_LIMIT = 50;
|
|
95
|
+
const MAX_BUNDLE_LIST_LIMIT = 100;
|
|
95
96
|
|
|
96
97
|
const supportsExplicitNoUpdateResponse = (request: Request) => {
|
|
97
98
|
const sdkVersion = request.headers.get(SDK_VERSION_HEADER)?.trim();
|
|
@@ -191,6 +192,27 @@ const parseStringArraySearchParam = (url: URL, key: string) => {
|
|
|
191
192
|
return values.length > 0 ? values : undefined;
|
|
192
193
|
};
|
|
193
194
|
|
|
195
|
+
const parsePositiveIntegerSearchParam = (
|
|
196
|
+
url: URL,
|
|
197
|
+
key: string,
|
|
198
|
+
defaultValue: number,
|
|
199
|
+
maxValue: number,
|
|
200
|
+
): number => {
|
|
201
|
+
const value = url.searchParams.get(key);
|
|
202
|
+
if (value === null) {
|
|
203
|
+
return defaultValue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const parsed = Number(value);
|
|
207
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > maxValue) {
|
|
208
|
+
throw new HandlerBadRequestError(
|
|
209
|
+
`The '${key}' query parameter must be a positive integer between 1 and ${maxValue}.`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return parsed;
|
|
214
|
+
};
|
|
215
|
+
|
|
194
216
|
const requirePlatformParam = (params: Record<string, string>): Platform => {
|
|
195
217
|
const platform = requireRouteParam(params, "platform");
|
|
196
218
|
|
|
@@ -317,7 +339,12 @@ const handleGetBundles: RouteHandler = async (
|
|
|
317
339
|
const url = new URL(request.url);
|
|
318
340
|
const channel = url.searchParams.get("channel") ?? undefined;
|
|
319
341
|
const platform = url.searchParams.get("platform");
|
|
320
|
-
const limit =
|
|
342
|
+
const limit = parsePositiveIntegerSearchParam(
|
|
343
|
+
url,
|
|
344
|
+
"limit",
|
|
345
|
+
DEFAULT_BUNDLE_LIST_LIMIT,
|
|
346
|
+
MAX_BUNDLE_LIST_LIMIT,
|
|
347
|
+
);
|
|
321
348
|
const pageParam = url.searchParams.get("page");
|
|
322
349
|
const offset = url.searchParams.get("offset");
|
|
323
350
|
const after = url.searchParams.get("after") ?? undefined;
|
|
@@ -511,19 +538,18 @@ export function createHandler<TContext = unknown>(
|
|
|
511
538
|
context?: HotUpdaterContext<TContext>,
|
|
512
539
|
) => Promise<Response> {
|
|
513
540
|
const basePath = options.basePath ?? "/api";
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
541
|
+
const routeOptions = {
|
|
542
|
+
updateCheck: options.routes?.updateCheck ?? true,
|
|
543
|
+
bundles: options.routes?.bundles ?? false,
|
|
544
|
+
};
|
|
517
545
|
|
|
518
546
|
// Create and configure router
|
|
519
547
|
const router = createRouter();
|
|
520
548
|
|
|
521
549
|
// Register routes
|
|
522
|
-
|
|
523
|
-
addRoute(router, "GET", "/version", "version");
|
|
524
|
-
}
|
|
550
|
+
addRoute(router, "GET", "/version", "version");
|
|
525
551
|
|
|
526
|
-
if (
|
|
552
|
+
if (routeOptions.updateCheck) {
|
|
527
553
|
addRoute(
|
|
528
554
|
router,
|
|
529
555
|
"GET",
|
|
@@ -550,7 +576,7 @@ export function createHandler<TContext = unknown>(
|
|
|
550
576
|
);
|
|
551
577
|
}
|
|
552
578
|
|
|
553
|
-
if (
|
|
579
|
+
if (routeOptions.bundles) {
|
|
554
580
|
addRoute(router, "GET", "/api/bundles/channels", "getChannels");
|
|
555
581
|
addRoute(router, "GET", "/api/bundles/:id", "getBundle");
|
|
556
582
|
addRoute(router, "GET", "/api/bundles", "getBundles");
|
|
@@ -583,8 +609,8 @@ export function createHandler<TContext = unknown>(
|
|
|
583
609
|
});
|
|
584
610
|
}
|
|
585
611
|
|
|
586
|
-
|
|
587
|
-
const handler = routes[
|
|
612
|
+
const routeName = match.data as string;
|
|
613
|
+
const handler = routes[routeName] as RouteHandler<TContext>;
|
|
588
614
|
if (!handler) {
|
|
589
615
|
return new Response(JSON.stringify({ error: "Handler not found" }), {
|
|
590
616
|
status: 500,
|
package/src/runtime.spec.ts
CHANGED
|
@@ -80,6 +80,7 @@ describe("runtime createHotUpdater", () => {
|
|
|
80
80
|
node: {
|
|
81
81
|
delete: vi.fn(),
|
|
82
82
|
downloadFile: vi.fn(),
|
|
83
|
+
exists: vi.fn(async () => false),
|
|
83
84
|
upload: vi.fn(),
|
|
84
85
|
},
|
|
85
86
|
},
|
|
@@ -660,7 +661,7 @@ describe("runtime createHotUpdater", () => {
|
|
|
660
661
|
});
|
|
661
662
|
});
|
|
662
663
|
|
|
663
|
-
it("
|
|
664
|
+
it("keeps the version route mounted when update-check routes are disabled", async () => {
|
|
664
665
|
const database = createDatabasePlugin({
|
|
665
666
|
name: "version-disabled-plugin",
|
|
666
667
|
factory: () => ({
|
|
@@ -690,8 +691,7 @@ describe("runtime createHotUpdater", () => {
|
|
|
690
691
|
database,
|
|
691
692
|
basePath: "/api/check-update",
|
|
692
693
|
routes: {
|
|
693
|
-
updateCheck:
|
|
694
|
-
version: false,
|
|
694
|
+
updateCheck: false,
|
|
695
695
|
bundles: false,
|
|
696
696
|
},
|
|
697
697
|
});
|
|
@@ -700,7 +700,10 @@ describe("runtime createHotUpdater", () => {
|
|
|
700
700
|
new Request("https://updates.example.com/api/check-update/version"),
|
|
701
701
|
);
|
|
702
702
|
|
|
703
|
-
expect(response.status).toBe(
|
|
703
|
+
expect(response.status).toBe(200);
|
|
704
|
+
await expect(response.json()).resolves.toEqual({
|
|
705
|
+
version: HOT_UPDATER_SERVER_VERSION,
|
|
706
|
+
});
|
|
704
707
|
});
|
|
705
708
|
|
|
706
709
|
it("clears pending plugin changes after a failed mutation commit", async () => {
|