@hot-updater/server 1.0.0-rc.43 → 1.0.0-rc.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/createHotUpdaterCore.cjs +10 -7
- package/dist/createHotUpdaterCore.d.cts +11 -20
- package/dist/createHotUpdaterCore.d.mts +11 -20
- package/dist/createHotUpdaterCore.mjs +10 -7
- package/dist/handler.cjs +4 -20
- package/dist/handler.mjs +4 -20
- package/dist/handlerReleaseCatalogRoutes.cjs +22 -4
- package/dist/handlerReleaseCatalogRoutes.mjs +22 -4
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/index.cjs +27 -0
- package/dist/plugins/index.d.cts +6 -0
- package/dist/plugins/index.d.mts +6 -0
- package/dist/plugins/index.mjs +4 -0
- package/dist/storageAccess.cjs +13 -50
- package/dist/storageAccess.mjs +13 -50
- package/package.json +11 -19
|
@@ -19,17 +19,21 @@ const databaseOf = (value) => {
|
|
|
19
19
|
}
|
|
20
20
|
throw new _hot_updater_plugin_core.HotUpdaterConfigError("database must be a Hot Updater 1.0 database, such as kyselyAdapter(...) or postgres(...). Upgrade the provider package that created it.");
|
|
21
21
|
};
|
|
22
|
-
/**
|
|
22
|
+
/** The configured storage, when it is a storage adapter. */
|
|
23
|
+
const storageOf = (value) => {
|
|
24
|
+
if (isRecord(value) && typeof value.name === "string" && typeof value.protocol === "string") return value;
|
|
25
|
+
throw new _hot_updater_plugin_core.HotUpdaterConfigError("storage must be a storage adapter, such as s3Storage(...): the one hot-updater.config.ts uploads with.");
|
|
26
|
+
};
|
|
27
|
+
/** `"public"`, or nothing. */
|
|
23
28
|
const isPublic = (value) => {
|
|
24
29
|
if (value === void 0) return false;
|
|
25
30
|
if (value === "public") return true;
|
|
26
|
-
throw new _hot_updater_plugin_core.HotUpdaterConfigError(
|
|
31
|
+
throw new _hot_updater_plugin_core.HotUpdaterConfigError("clientAccess must be \"public\"; to protect client routes, add a plugin that provides clientAuth to plugins.");
|
|
27
32
|
};
|
|
28
33
|
function createHotUpdater(options) {
|
|
29
|
-
for (const key of ["authorityId", "catalogId"]) if (Object.hasOwn(options, key)) throw new TypeError(`Remove ${key} from createHotUpdater options. Catalog identity is managed internally.`);
|
|
30
34
|
const database = databaseOf(options.database);
|
|
31
|
-
const storage =
|
|
32
|
-
const {
|
|
35
|
+
const storage = storageOf(options.storage);
|
|
36
|
+
const { readStorageText, resolveFileUrl } = require_storageAccess.createStorageAccess(storage);
|
|
33
37
|
const publicClients = isPublic(options.clientAccess);
|
|
34
38
|
const configured = Object.freeze([...options.plugins ?? []]);
|
|
35
39
|
const plugins = require_assemblePlugins.assemblePlugins(configured, database, { storage: {
|
|
@@ -48,7 +52,6 @@ function createHotUpdater(options) {
|
|
|
48
52
|
const handlers = require_handler.createHotUpdaterHandlers({
|
|
49
53
|
api: { core: plugins.core },
|
|
50
54
|
...clientPolicy === void 0 ? {} : { clientPolicy },
|
|
51
|
-
downloadStorageObject,
|
|
52
55
|
endpoints: plugins.endpoints,
|
|
53
56
|
plugins: Object.keys(plugins.api)
|
|
54
57
|
});
|
|
@@ -57,7 +60,7 @@ function createHotUpdater(options) {
|
|
|
57
60
|
adapterName: database.name,
|
|
58
61
|
get handlers() {
|
|
59
62
|
if (!serving) {
|
|
60
|
-
|
|
63
|
+
(0, _hot_updater_plugin_core.assertStorageOperations)(storage, ["get", "getDownloadUrl"]);
|
|
61
64
|
serving = true;
|
|
62
65
|
}
|
|
63
66
|
return handlers;
|
|
@@ -24,10 +24,10 @@ interface ClientAuthProvider {
|
|
|
24
24
|
}
|
|
25
25
|
type RuntimeHotUpdaterAPI<TPlugins extends readonly AnyHotUpdaterPlugin[] = readonly AnyHotUpdaterPlugin[]> = {
|
|
26
26
|
/**
|
|
27
|
-
* The routes a host mounts. On first access
|
|
28
|
-
*
|
|
27
|
+
* The routes a host mounts. On first access the storage must serve
|
|
28
|
+
* downloads (`get` and `getDownloadUrl`), so a server fails where it
|
|
29
29
|
* mounts them, while tooling that only reads the definition, such as the
|
|
30
|
-
* CLI, can
|
|
30
|
+
* CLI, can configure storage that only uploads.
|
|
31
31
|
*/
|
|
32
32
|
readonly handlers: HotUpdaterHandlers;
|
|
33
33
|
/**
|
|
@@ -54,8 +54,8 @@ type RuntimeHotUpdaterAPI<TPlugins extends readonly AnyHotUpdaterPlugin[] = read
|
|
|
54
54
|
* closes it (`dispose`).
|
|
55
55
|
*/
|
|
56
56
|
readonly database: ToolingDatabase;
|
|
57
|
-
/** The storage as configured
|
|
58
|
-
readonly storage:
|
|
57
|
+
/** The storage as configured. */
|
|
58
|
+
readonly storage: StorageAdapter;
|
|
59
59
|
/** The plugins as configured, as a frozen copy, whose tables tooling creates. */
|
|
60
60
|
readonly plugins: TPlugins;
|
|
61
61
|
/**
|
|
@@ -72,18 +72,8 @@ type RuntimeHotUpdaterAPI<TPlugins extends readonly AnyHotUpdaterPlugin[] = read
|
|
|
72
72
|
readonly clientAuth?: ClientAuthProvider;
|
|
73
73
|
};
|
|
74
74
|
type HotUpdaterAPI = RuntimeHotUpdaterAPI;
|
|
75
|
-
declare const REMOVED_CLIENT_ACCESS = "clientAccess objects were removed in 1.0: set clientAccess: \"public\", or add a plugin that provides clientAuth";
|
|
76
|
-
/**
|
|
77
|
-
* A `clientAccess` object from before 1.0. Its `type` names the fix, so
|
|
78
|
-
* TypeScript reports it where the object is written.
|
|
79
|
-
*/
|
|
80
|
-
interface RemovedClientAccess {
|
|
81
|
-
readonly type: typeof REMOVED_CLIENT_ACCESS;
|
|
82
|
-
readonly headerName?: string;
|
|
83
|
-
}
|
|
84
|
-
type ClientAccessPolicy =
|
|
85
75
|
/** Leaves client routes public; required when no plugin provides clientAuth. */
|
|
86
|
-
"public"
|
|
76
|
+
type ClientAccessPolicy = "public";
|
|
87
77
|
type ProvidesClientAuth<TPlugin> = TPlugin extends {
|
|
88
78
|
readonly provides?: infer TProvides;
|
|
89
79
|
} ? TProvides extends {
|
|
@@ -113,13 +103,14 @@ type CreateHotUpdaterOptions<TPlugins extends readonly AnyHotUpdaterPlugin[] = r
|
|
|
113
103
|
/** A provider's database on the storage engine, such as `kyselyAdapter(...)` or `postgres(...)`. */
|
|
114
104
|
readonly database: ToolingDatabase;
|
|
115
105
|
/**
|
|
116
|
-
* Where bundles are stored
|
|
117
|
-
*
|
|
106
|
+
* Where bundles are stored, the same adapter `hot-updater.config.ts`
|
|
107
|
+
* uploads with: the server reads the URIs of its protocol and returns
|
|
108
|
+
* their download URLs.
|
|
118
109
|
*/
|
|
119
|
-
readonly storage
|
|
110
|
+
readonly storage: StorageAdapter;
|
|
120
111
|
/** The plugins the server runs; at most one provides clientAuth. Defaults to none. */
|
|
121
112
|
readonly plugins?: TPlugins;
|
|
122
113
|
} & ClientAccessRule<TPlugins>;
|
|
123
114
|
declare function createHotUpdater<const TPlugins extends readonly AnyHotUpdaterPlugin[] = readonly []>(options: CreateHotUpdaterOptions<TPlugins>): RuntimeHotUpdaterAPI<NoInfer<TPlugins>>;
|
|
124
115
|
//#endregion
|
|
125
|
-
export { ClientAccessPolicy, ClientAccessRule, ClientAuthProvider, ClientEndpoint, CreateHotUpdaterOptions, HotUpdaterAPI,
|
|
116
|
+
export { ClientAccessPolicy, ClientAccessRule, ClientAuthProvider, ClientEndpoint, CreateHotUpdaterOptions, HotUpdaterAPI, RuntimeHotUpdaterAPI, createHotUpdater };
|
|
@@ -24,10 +24,10 @@ interface ClientAuthProvider {
|
|
|
24
24
|
}
|
|
25
25
|
type RuntimeHotUpdaterAPI<TPlugins extends readonly AnyHotUpdaterPlugin[] = readonly AnyHotUpdaterPlugin[]> = {
|
|
26
26
|
/**
|
|
27
|
-
* The routes a host mounts. On first access
|
|
28
|
-
*
|
|
27
|
+
* The routes a host mounts. On first access the storage must serve
|
|
28
|
+
* downloads (`get` and `getDownloadUrl`), so a server fails where it
|
|
29
29
|
* mounts them, while tooling that only reads the definition, such as the
|
|
30
|
-
* CLI, can
|
|
30
|
+
* CLI, can configure storage that only uploads.
|
|
31
31
|
*/
|
|
32
32
|
readonly handlers: HotUpdaterHandlers;
|
|
33
33
|
/**
|
|
@@ -54,8 +54,8 @@ type RuntimeHotUpdaterAPI<TPlugins extends readonly AnyHotUpdaterPlugin[] = read
|
|
|
54
54
|
* closes it (`dispose`).
|
|
55
55
|
*/
|
|
56
56
|
readonly database: ToolingDatabase;
|
|
57
|
-
/** The storage as configured
|
|
58
|
-
readonly storage:
|
|
57
|
+
/** The storage as configured. */
|
|
58
|
+
readonly storage: StorageAdapter;
|
|
59
59
|
/** The plugins as configured, as a frozen copy, whose tables tooling creates. */
|
|
60
60
|
readonly plugins: TPlugins;
|
|
61
61
|
/**
|
|
@@ -72,18 +72,8 @@ type RuntimeHotUpdaterAPI<TPlugins extends readonly AnyHotUpdaterPlugin[] = read
|
|
|
72
72
|
readonly clientAuth?: ClientAuthProvider;
|
|
73
73
|
};
|
|
74
74
|
type HotUpdaterAPI = RuntimeHotUpdaterAPI;
|
|
75
|
-
declare const REMOVED_CLIENT_ACCESS = "clientAccess objects were removed in 1.0: set clientAccess: \"public\", or add a plugin that provides clientAuth";
|
|
76
|
-
/**
|
|
77
|
-
* A `clientAccess` object from before 1.0. Its `type` names the fix, so
|
|
78
|
-
* TypeScript reports it where the object is written.
|
|
79
|
-
*/
|
|
80
|
-
interface RemovedClientAccess {
|
|
81
|
-
readonly type: typeof REMOVED_CLIENT_ACCESS;
|
|
82
|
-
readonly headerName?: string;
|
|
83
|
-
}
|
|
84
|
-
type ClientAccessPolicy =
|
|
85
75
|
/** Leaves client routes public; required when no plugin provides clientAuth. */
|
|
86
|
-
"public"
|
|
76
|
+
type ClientAccessPolicy = "public";
|
|
87
77
|
type ProvidesClientAuth<TPlugin> = TPlugin extends {
|
|
88
78
|
readonly provides?: infer TProvides;
|
|
89
79
|
} ? TProvides extends {
|
|
@@ -113,13 +103,14 @@ type CreateHotUpdaterOptions<TPlugins extends readonly AnyHotUpdaterPlugin[] = r
|
|
|
113
103
|
/** A provider's database on the storage engine, such as `kyselyAdapter(...)` or `postgres(...)`. */
|
|
114
104
|
readonly database: ToolingDatabase;
|
|
115
105
|
/**
|
|
116
|
-
* Where bundles are stored
|
|
117
|
-
*
|
|
106
|
+
* Where bundles are stored, the same adapter `hot-updater.config.ts`
|
|
107
|
+
* uploads with: the server reads the URIs of its protocol and returns
|
|
108
|
+
* their download URLs.
|
|
118
109
|
*/
|
|
119
|
-
readonly storage
|
|
110
|
+
readonly storage: StorageAdapter;
|
|
120
111
|
/** The plugins the server runs; at most one provides clientAuth. Defaults to none. */
|
|
121
112
|
readonly plugins?: TPlugins;
|
|
122
113
|
} & ClientAccessRule<TPlugins>;
|
|
123
114
|
declare function createHotUpdater<const TPlugins extends readonly AnyHotUpdaterPlugin[] = readonly []>(options: CreateHotUpdaterOptions<TPlugins>): RuntimeHotUpdaterAPI<NoInfer<TPlugins>>;
|
|
124
115
|
//#endregion
|
|
125
|
-
export { ClientAccessPolicy, ClientAccessRule, ClientAuthProvider, ClientEndpoint, CreateHotUpdaterOptions, HotUpdaterAPI,
|
|
116
|
+
export { ClientAccessPolicy, ClientAccessRule, ClientAuthProvider, ClientEndpoint, CreateHotUpdaterOptions, HotUpdaterAPI, RuntimeHotUpdaterAPI, createHotUpdater };
|
|
@@ -20,17 +20,21 @@ const databaseOf = (value) => {
|
|
|
20
20
|
}
|
|
21
21
|
throw new HotUpdaterConfigError("database must be a Hot Updater 1.0 database, such as kyselyAdapter(...) or postgres(...). Upgrade the provider package that created it.");
|
|
22
22
|
};
|
|
23
|
-
/**
|
|
23
|
+
/** The configured storage, when it is a storage adapter. */
|
|
24
|
+
const storageOf = (value) => {
|
|
25
|
+
if (isRecord(value) && typeof value.name === "string" && typeof value.protocol === "string") return value;
|
|
26
|
+
throw new HotUpdaterConfigError("storage must be a storage adapter, such as s3Storage(...): the one hot-updater.config.ts uploads with.");
|
|
27
|
+
};
|
|
28
|
+
/** `"public"`, or nothing. */
|
|
24
29
|
const isPublic = (value) => {
|
|
25
30
|
if (value === void 0) return false;
|
|
26
31
|
if (value === "public") return true;
|
|
27
|
-
throw new HotUpdaterConfigError(
|
|
32
|
+
throw new HotUpdaterConfigError("clientAccess must be \"public\"; to protect client routes, add a plugin that provides clientAuth to plugins.");
|
|
28
33
|
};
|
|
29
34
|
function createHotUpdater(options) {
|
|
30
|
-
for (const key of ["authorityId", "catalogId"]) if (Object.hasOwn(options, key)) throw new TypeError(`Remove ${key} from createHotUpdater options. Catalog identity is managed internally.`);
|
|
31
35
|
const database = databaseOf(options.database);
|
|
32
|
-
const storage =
|
|
33
|
-
const {
|
|
36
|
+
const storage = storageOf(options.storage);
|
|
37
|
+
const { readStorageText, resolveFileUrl } = createStorageAccess(storage);
|
|
34
38
|
const publicClients = isPublic(options.clientAccess);
|
|
35
39
|
const configured = Object.freeze([...options.plugins ?? []]);
|
|
36
40
|
const plugins = assemblePlugins(configured, database, { storage: {
|
|
@@ -49,7 +53,6 @@ function createHotUpdater(options) {
|
|
|
49
53
|
const handlers = createHotUpdaterHandlers({
|
|
50
54
|
api: { core: plugins.core },
|
|
51
55
|
...clientPolicy === void 0 ? {} : { clientPolicy },
|
|
52
|
-
downloadStorageObject,
|
|
53
56
|
endpoints: plugins.endpoints,
|
|
54
57
|
plugins: Object.keys(plugins.api)
|
|
55
58
|
});
|
|
@@ -58,7 +61,7 @@ function createHotUpdater(options) {
|
|
|
58
61
|
adapterName: database.name,
|
|
59
62
|
get handlers() {
|
|
60
63
|
if (!serving) {
|
|
61
|
-
|
|
64
|
+
assertStorageOperations(storage, ["get", "getDownloadUrl"]);
|
|
62
65
|
serving = true;
|
|
63
66
|
}
|
|
64
67
|
return handlers;
|
package/dist/handler.cjs
CHANGED
|
@@ -18,8 +18,8 @@ const errorResponse = (error, status) => Response.json({ error }, {
|
|
|
18
18
|
headers: { "cache-control": "private, no-store" },
|
|
19
19
|
status
|
|
20
20
|
});
|
|
21
|
-
/** `/version`
|
|
22
|
-
const PUBLIC_CLIENT_ROUTES = /* @__PURE__ */ new Set(["version"
|
|
21
|
+
/** `/version` never passes through the client policy. */
|
|
22
|
+
const PUBLIC_CLIENT_ROUTES = /* @__PURE__ */ new Set(["version"]);
|
|
23
23
|
const routesOf = Symbol.for("@hot-updater/server/routes");
|
|
24
24
|
const withVary = (response, varyHeaders) => {
|
|
25
25
|
const cacheControl = response.headers.get("cache-control") ?? "";
|
|
@@ -64,26 +64,11 @@ const createRequestHandler = ({ api, clientPolicy, privateResponses = false, rou
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
|
-
|
|
68
|
-
const token = params.token;
|
|
69
|
-
const signature = params.signature;
|
|
70
|
-
if (!token || !signature) return errorResponse("Not found", 404);
|
|
71
|
-
const response = await downloadStorageObject(token, signature);
|
|
72
|
-
if (!response) return errorResponse("Not found", 404);
|
|
73
|
-
const headers = new Headers(response.headers);
|
|
74
|
-
if (!headers.has("cache-control")) headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
75
|
-
return new Response(response.body, {
|
|
76
|
-
headers,
|
|
77
|
-
status: response.status,
|
|
78
|
-
statusText: response.statusText
|
|
79
|
-
});
|
|
80
|
-
};
|
|
81
|
-
function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, endpoints = [], plugins = [] }) {
|
|
67
|
+
function createHotUpdaterHandlers({ api, clientPolicy, endpoints = [], plugins = [] }) {
|
|
82
68
|
const routeHandlers = {
|
|
83
69
|
...require_handlerVersionRoutes.createVersionRouteHandlers(plugins),
|
|
84
70
|
...require_handlerReleaseCatalogRoutes.createReleaseCatalogRouteHandlers(),
|
|
85
|
-
...require_handlerAdminRoutes.createAdminRouteHandlers()
|
|
86
|
-
...downloadStorageObject === void 0 ? {} : { downloadStorageObject: createDownloadStorageRouteHandler(downloadStorageObject) }
|
|
71
|
+
...require_handlerAdminRoutes.createAdminRouteHandlers()
|
|
87
72
|
};
|
|
88
73
|
const routes = [];
|
|
89
74
|
const shapes = /* @__PURE__ */ new Map();
|
|
@@ -102,7 +87,6 @@ function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, en
|
|
|
102
87
|
const clientRouter = require_internalRouter.createRouter();
|
|
103
88
|
const addClientRoute = mount(clientRouter, false);
|
|
104
89
|
addClientRoute("GET", "/version", "version");
|
|
105
|
-
if (downloadStorageObject !== void 0) addClientRoute("GET", "/storage/:token/:signature", "downloadStorageObject");
|
|
106
90
|
addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
|
|
107
91
|
addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
|
|
108
92
|
addClientRoute("GET", "/artifacts/v1/:targetBundleId/from/:currentBundleId", "artifactV1");
|
package/dist/handler.mjs
CHANGED
|
@@ -19,8 +19,8 @@ const errorResponse = (error, status) => Response.json({ error }, {
|
|
|
19
19
|
headers: { "cache-control": "private, no-store" },
|
|
20
20
|
status
|
|
21
21
|
});
|
|
22
|
-
/** `/version`
|
|
23
|
-
const PUBLIC_CLIENT_ROUTES = /* @__PURE__ */ new Set(["version"
|
|
22
|
+
/** `/version` never passes through the client policy. */
|
|
23
|
+
const PUBLIC_CLIENT_ROUTES = /* @__PURE__ */ new Set(["version"]);
|
|
24
24
|
const routesOf = Symbol.for("@hot-updater/server/routes");
|
|
25
25
|
const withVary = (response, varyHeaders) => {
|
|
26
26
|
const cacheControl = response.headers.get("cache-control") ?? "";
|
|
@@ -65,26 +65,11 @@ const createRequestHandler = ({ api, clientPolicy, privateResponses = false, rou
|
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
|
-
|
|
69
|
-
const token = params.token;
|
|
70
|
-
const signature = params.signature;
|
|
71
|
-
if (!token || !signature) return errorResponse("Not found", 404);
|
|
72
|
-
const response = await downloadStorageObject(token, signature);
|
|
73
|
-
if (!response) return errorResponse("Not found", 404);
|
|
74
|
-
const headers = new Headers(response.headers);
|
|
75
|
-
if (!headers.has("cache-control")) headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
76
|
-
return new Response(response.body, {
|
|
77
|
-
headers,
|
|
78
|
-
status: response.status,
|
|
79
|
-
statusText: response.statusText
|
|
80
|
-
});
|
|
81
|
-
};
|
|
82
|
-
function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, endpoints = [], plugins = [] }) {
|
|
68
|
+
function createHotUpdaterHandlers({ api, clientPolicy, endpoints = [], plugins = [] }) {
|
|
83
69
|
const routeHandlers = {
|
|
84
70
|
...createVersionRouteHandlers(plugins),
|
|
85
71
|
...createReleaseCatalogRouteHandlers(),
|
|
86
|
-
...createAdminRouteHandlers()
|
|
87
|
-
...downloadStorageObject === void 0 ? {} : { downloadStorageObject: createDownloadStorageRouteHandler(downloadStorageObject) }
|
|
72
|
+
...createAdminRouteHandlers()
|
|
88
73
|
};
|
|
89
74
|
const routes = [];
|
|
90
75
|
const shapes = /* @__PURE__ */ new Map();
|
|
@@ -103,7 +88,6 @@ function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, en
|
|
|
103
88
|
const clientRouter = createRouter();
|
|
104
89
|
const addClientRoute = mount(clientRouter, false);
|
|
105
90
|
addClientRoute("GET", "/version", "version");
|
|
106
|
-
if (downloadStorageObject !== void 0) addClientRoute("GET", "/storage/:token/:signature", "downloadStorageObject");
|
|
107
91
|
addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
|
|
108
92
|
addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
|
|
109
93
|
addClientRoute("GET", "/artifacts/v1/:targetBundleId/from/:currentBundleId", "artifactV1");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_handlerParameters = require("./handlerParameters.cjs");
|
|
2
2
|
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
3
|
+
let _hot_updater_protocol = require("@hot-updater/protocol");
|
|
3
4
|
//#region src/handlerReleaseCatalogRoutes.ts
|
|
4
5
|
const CATALOG_CONTENT_TYPE = "application/vnd.hot-updater.release-catalog+json; version=1";
|
|
5
6
|
const ARTIFACT_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=1";
|
|
@@ -29,6 +30,22 @@ const privateNotFound = () => Response.json({ error: "Not found" }, {
|
|
|
29
30
|
status: 404,
|
|
30
31
|
headers: { "cache-control": "private, no-store" }
|
|
31
32
|
});
|
|
33
|
+
const badRequest = (error) => Response.json({ error }, {
|
|
34
|
+
status: 400,
|
|
35
|
+
headers: { "cache-control": "private, no-store" }
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* A 400 for a catalog path whose scope is malformed, such as a channel name
|
|
39
|
+
* where its base64url key belongs; null for a well-formed scope.
|
|
40
|
+
*/
|
|
41
|
+
const invalidScope = (input) => {
|
|
42
|
+
try {
|
|
43
|
+
(0, _hot_updater_protocol.createReleaseCatalogScopeKey)(input);
|
|
44
|
+
return null;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return badRequest(error instanceof Error ? error.message : "Invalid Release Catalog scope");
|
|
47
|
+
}
|
|
48
|
+
};
|
|
32
49
|
/**
|
|
33
50
|
* A scope with no catalog yet, such as a store version before its first
|
|
34
51
|
* OTA release: cached like a catalog, so its update checks don't all reach
|
|
@@ -107,16 +124,15 @@ const createReleaseCatalogRouteHandlers = () => {
|
|
|
107
124
|
appVersionReleaseCatalog: async (params, request, { core }) => {
|
|
108
125
|
const rawAppVersion = require_handlerParameters.requireRouteParam(params, "appVersion");
|
|
109
126
|
const appVersion = (0, _hot_updater_plugin_core.canonicalizeAppVersion)(rawAppVersion);
|
|
110
|
-
if (appVersion === null || appVersion !== rawAppVersion) return
|
|
111
|
-
status: 400,
|
|
112
|
-
headers: { "cache-control": "private, no-store" }
|
|
113
|
-
});
|
|
127
|
+
if (appVersion === null || appVersion !== rawAppVersion) return badRequest("Invalid app version");
|
|
114
128
|
const input = {
|
|
115
129
|
appVersion,
|
|
116
130
|
channelKey: require_handlerParameters.requireRouteParam(params, "channelKey"),
|
|
117
131
|
platform: require_handlerParameters.requirePlatformParam(params),
|
|
118
132
|
strategy: "APP_VERSION"
|
|
119
133
|
};
|
|
134
|
+
const invalid = invalidScope(input);
|
|
135
|
+
if (invalid) return invalid;
|
|
120
136
|
return catalogResponse(await loadCatalog(`app-version:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
|
|
121
137
|
},
|
|
122
138
|
fingerprintReleaseCatalog: async (params, request, { core }) => {
|
|
@@ -126,6 +142,8 @@ const createReleaseCatalogRouteHandlers = () => {
|
|
|
126
142
|
platform: require_handlerParameters.requirePlatformParam(params),
|
|
127
143
|
strategy: "FINGERPRINT"
|
|
128
144
|
};
|
|
145
|
+
const invalid = invalidScope(input);
|
|
146
|
+
if (invalid) return invalid;
|
|
129
147
|
return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
|
|
130
148
|
},
|
|
131
149
|
artifactV1: async (params, _request, { core }) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { requirePlatformParam, requireRouteParam } from "./handlerParameters.mjs";
|
|
2
2
|
import { canonicalizeAppVersion } from "@hot-updater/plugin-core";
|
|
3
|
+
import { createReleaseCatalogScopeKey } from "@hot-updater/protocol";
|
|
3
4
|
//#region src/handlerReleaseCatalogRoutes.ts
|
|
4
5
|
const CATALOG_CONTENT_TYPE = "application/vnd.hot-updater.release-catalog+json; version=1";
|
|
5
6
|
const ARTIFACT_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=1";
|
|
@@ -29,6 +30,22 @@ const privateNotFound = () => Response.json({ error: "Not found" }, {
|
|
|
29
30
|
status: 404,
|
|
30
31
|
headers: { "cache-control": "private, no-store" }
|
|
31
32
|
});
|
|
33
|
+
const badRequest = (error) => Response.json({ error }, {
|
|
34
|
+
status: 400,
|
|
35
|
+
headers: { "cache-control": "private, no-store" }
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* A 400 for a catalog path whose scope is malformed, such as a channel name
|
|
39
|
+
* where its base64url key belongs; null for a well-formed scope.
|
|
40
|
+
*/
|
|
41
|
+
const invalidScope = (input) => {
|
|
42
|
+
try {
|
|
43
|
+
createReleaseCatalogScopeKey(input);
|
|
44
|
+
return null;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return badRequest(error instanceof Error ? error.message : "Invalid Release Catalog scope");
|
|
47
|
+
}
|
|
48
|
+
};
|
|
32
49
|
/**
|
|
33
50
|
* A scope with no catalog yet, such as a store version before its first
|
|
34
51
|
* OTA release: cached like a catalog, so its update checks don't all reach
|
|
@@ -107,16 +124,15 @@ const createReleaseCatalogRouteHandlers = () => {
|
|
|
107
124
|
appVersionReleaseCatalog: async (params, request, { core }) => {
|
|
108
125
|
const rawAppVersion = requireRouteParam(params, "appVersion");
|
|
109
126
|
const appVersion = canonicalizeAppVersion(rawAppVersion);
|
|
110
|
-
if (appVersion === null || appVersion !== rawAppVersion) return
|
|
111
|
-
status: 400,
|
|
112
|
-
headers: { "cache-control": "private, no-store" }
|
|
113
|
-
});
|
|
127
|
+
if (appVersion === null || appVersion !== rawAppVersion) return badRequest("Invalid app version");
|
|
114
128
|
const input = {
|
|
115
129
|
appVersion,
|
|
116
130
|
channelKey: requireRouteParam(params, "channelKey"),
|
|
117
131
|
platform: requirePlatformParam(params),
|
|
118
132
|
strategy: "APP_VERSION"
|
|
119
133
|
};
|
|
134
|
+
const invalid = invalidScope(input);
|
|
135
|
+
if (invalid) return invalid;
|
|
120
136
|
return catalogResponse(await loadCatalog(`app-version:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
|
|
121
137
|
},
|
|
122
138
|
fingerprintReleaseCatalog: async (params, request, { core }) => {
|
|
@@ -126,6 +142,8 @@ const createReleaseCatalogRouteHandlers = () => {
|
|
|
126
142
|
platform: requirePlatformParam(params),
|
|
127
143
|
strategy: "FINGERPRINT"
|
|
128
144
|
};
|
|
145
|
+
const invalid = invalidScope(input);
|
|
146
|
+
if (invalid) return invalid;
|
|
129
147
|
return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
|
|
130
148
|
},
|
|
131
149
|
artifactV1: async (params, _request, { core }) => {
|
package/dist/package.cjs
CHANGED
package/dist/package.mjs
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
var _hot_updater_plugin_api_keys_server = require("@hot-updater/plugin-api-keys/server");
|
|
2
|
+
Object.keys(_hot_updater_plugin_api_keys_server).forEach(function(k) {
|
|
3
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
4
|
+
enumerable: true,
|
|
5
|
+
get: function() {
|
|
6
|
+
return _hot_updater_plugin_api_keys_server[k];
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
var _hot_updater_plugin_insights_server = require("@hot-updater/plugin-insights/server");
|
|
11
|
+
Object.keys(_hot_updater_plugin_insights_server).forEach(function(k) {
|
|
12
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function() {
|
|
15
|
+
return _hot_updater_plugin_insights_server[k];
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
var _hot_updater_plugin_remote_config_server = require("@hot-updater/plugin-remote-config/server");
|
|
20
|
+
Object.keys(_hot_updater_plugin_remote_config_server).forEach(function(k) {
|
|
21
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
get: function() {
|
|
24
|
+
return _hot_updater_plugin_remote_config_server[k];
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import "./api-keys/index.cjs";
|
|
2
|
+
import "./insights/index.cjs";
|
|
3
|
+
import "./remote-config/index.cjs";
|
|
4
|
+
export * from "@hot-updater/plugin-api-keys/server";
|
|
5
|
+
export * from "@hot-updater/plugin-insights/server";
|
|
6
|
+
export * from "@hot-updater/plugin-remote-config/server";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import "./api-keys/index.mjs";
|
|
2
|
+
import "./insights/index.mjs";
|
|
3
|
+
import "./remote-config/index.mjs";
|
|
4
|
+
export * from "@hot-updater/plugin-api-keys/server";
|
|
5
|
+
export * from "@hot-updater/plugin-insights/server";
|
|
6
|
+
export * from "@hot-updater/plugin-remote-config/server";
|
package/dist/storageAccess.cjs
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
|
-
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
2
1
|
//#region src/storageAccess.ts
|
|
2
|
+
/** A URL devices download from, which only an absolute HTTP(S) URL can be. */
|
|
3
3
|
const assertRemoteUrl = (value) => {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
let protocol;
|
|
5
|
+
try {
|
|
6
|
+
protocol = new URL(value).protocol;
|
|
7
|
+
} catch {}
|
|
8
|
+
if (protocol !== "http:" && protocol !== "https:") throw new Error("Storage getDownloadUrl must resolve to an absolute HTTP(S) URL.");
|
|
6
9
|
return value;
|
|
7
10
|
};
|
|
8
11
|
const getStorageProtocol = (storageUri) => new URL(storageUri).protocol.replace(":", "");
|
|
9
12
|
const isRemoteUrlProtocol = (protocol) => protocol === "http" || protocol === "https";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const leftBytes = new TextEncoder().encode(left);
|
|
17
|
-
const rightBytes = new TextEncoder().encode(right);
|
|
18
|
-
let difference = leftBytes.length ^ rightBytes.length;
|
|
19
|
-
const length = Math.max(leftBytes.length, rightBytes.length);
|
|
20
|
-
for (let index = 0; index < length; index += 1) difference |= (leftBytes[index] ?? 0) ^ (rightBytes[index] ?? 0);
|
|
21
|
-
return difference === 0;
|
|
22
|
-
};
|
|
23
|
-
const createStorageAccess = (storageAdapters) => {
|
|
24
|
-
const protocols = /* @__PURE__ */ new Set();
|
|
25
|
-
for (const storage of storageAdapters) {
|
|
26
|
-
if (protocols.has(storage.protocol)) throw new Error(`Multiple storage adapters handle protocol: ${storage.protocol}`);
|
|
27
|
-
protocols.add(storage.protocol);
|
|
28
|
-
}
|
|
29
|
-
const findStorage = (protocol) => storageAdapters.find((item) => item.protocol === protocol);
|
|
13
|
+
/**
|
|
14
|
+
* Reads and download URLs over the server's storage, for the URIs of its
|
|
15
|
+
* protocol; an HTTP(S) URI is used as it is.
|
|
16
|
+
*/
|
|
17
|
+
const createStorageAccess = (storageAdapter) => {
|
|
18
|
+
const findStorage = (protocol) => storageAdapter.protocol === protocol ? storageAdapter : void 0;
|
|
30
19
|
const readStorageResponse = async (storageUri) => {
|
|
31
20
|
const protocol = getStorageProtocol(storageUri);
|
|
32
21
|
const storage = findStorage(protocol);
|
|
@@ -50,38 +39,12 @@ const createStorageAccess = (storageAdapters) => {
|
|
|
50
39
|
}
|
|
51
40
|
if (!storage.getDownloadUrl) return null;
|
|
52
41
|
const { url: downloadUrl } = await storage.getDownloadUrl({ storageUri });
|
|
53
|
-
|
|
54
|
-
return assertRemoteUrl(downloadUrl);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
if (/^[a-z][a-z\d+.-]*:/i.test(downloadUrl)) throw error;
|
|
57
|
-
}
|
|
58
|
-
return resolveDownloadPath(downloadUrl, storageUri);
|
|
42
|
+
return assertRemoteUrl(downloadUrl);
|
|
59
43
|
};
|
|
60
44
|
const readStorageText = async (storageUri) => {
|
|
61
45
|
return (await readStorageResponse(storageUri))?.text() ?? null;
|
|
62
46
|
};
|
|
63
47
|
return {
|
|
64
|
-
downloadStorageObject: storageAdapters.some((storage) => storage.getDownloadUrl !== void 0) ? async (storageUriToken, encodedSignature) => {
|
|
65
|
-
const requestedPath = `/storage/${storageUriToken}/${encodedSignature}`;
|
|
66
|
-
const requested = (0, _hot_updater_plugin_core.parseStorageDownloadPath)(requestedPath);
|
|
67
|
-
if (!requested) return null;
|
|
68
|
-
let storage;
|
|
69
|
-
try {
|
|
70
|
-
storage = findStorage(getStorageProtocol(requested.storageUri));
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
if (!storage?.getDownloadUrl || !storage.get) return null;
|
|
75
|
-
const { url: downloadUrl } = await storage.getDownloadUrl({ storageUri: requested.storageUri });
|
|
76
|
-
try {
|
|
77
|
-
new URL(downloadUrl);
|
|
78
|
-
return null;
|
|
79
|
-
} catch {
|
|
80
|
-
if (/^[a-z][a-z\d+.-]*:/i.test(downloadUrl)) return null;
|
|
81
|
-
}
|
|
82
|
-
if (!tokensEqual(downloadUrl, requestedPath)) return null;
|
|
83
|
-
return (await storage.get({ storageUri: requested.storageUri })).response;
|
|
84
|
-
} : void 0,
|
|
85
48
|
readStorageText,
|
|
86
49
|
resolveFileUrl
|
|
87
50
|
};
|
package/dist/storageAccess.mjs
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
|
-
import { parseStorageDownloadPath } from "@hot-updater/plugin-core";
|
|
2
1
|
//#region src/storageAccess.ts
|
|
2
|
+
/** A URL devices download from, which only an absolute HTTP(S) URL can be. */
|
|
3
3
|
const assertRemoteUrl = (value) => {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
let protocol;
|
|
5
|
+
try {
|
|
6
|
+
protocol = new URL(value).protocol;
|
|
7
|
+
} catch {}
|
|
8
|
+
if (protocol !== "http:" && protocol !== "https:") throw new Error("Storage getDownloadUrl must resolve to an absolute HTTP(S) URL.");
|
|
6
9
|
return value;
|
|
7
10
|
};
|
|
8
11
|
const getStorageProtocol = (storageUri) => new URL(storageUri).protocol.replace(":", "");
|
|
9
12
|
const isRemoteUrlProtocol = (protocol) => protocol === "http" || protocol === "https";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const leftBytes = new TextEncoder().encode(left);
|
|
17
|
-
const rightBytes = new TextEncoder().encode(right);
|
|
18
|
-
let difference = leftBytes.length ^ rightBytes.length;
|
|
19
|
-
const length = Math.max(leftBytes.length, rightBytes.length);
|
|
20
|
-
for (let index = 0; index < length; index += 1) difference |= (leftBytes[index] ?? 0) ^ (rightBytes[index] ?? 0);
|
|
21
|
-
return difference === 0;
|
|
22
|
-
};
|
|
23
|
-
const createStorageAccess = (storageAdapters) => {
|
|
24
|
-
const protocols = /* @__PURE__ */ new Set();
|
|
25
|
-
for (const storage of storageAdapters) {
|
|
26
|
-
if (protocols.has(storage.protocol)) throw new Error(`Multiple storage adapters handle protocol: ${storage.protocol}`);
|
|
27
|
-
protocols.add(storage.protocol);
|
|
28
|
-
}
|
|
29
|
-
const findStorage = (protocol) => storageAdapters.find((item) => item.protocol === protocol);
|
|
13
|
+
/**
|
|
14
|
+
* Reads and download URLs over the server's storage, for the URIs of its
|
|
15
|
+
* protocol; an HTTP(S) URI is used as it is.
|
|
16
|
+
*/
|
|
17
|
+
const createStorageAccess = (storageAdapter) => {
|
|
18
|
+
const findStorage = (protocol) => storageAdapter.protocol === protocol ? storageAdapter : void 0;
|
|
30
19
|
const readStorageResponse = async (storageUri) => {
|
|
31
20
|
const protocol = getStorageProtocol(storageUri);
|
|
32
21
|
const storage = findStorage(protocol);
|
|
@@ -50,38 +39,12 @@ const createStorageAccess = (storageAdapters) => {
|
|
|
50
39
|
}
|
|
51
40
|
if (!storage.getDownloadUrl) return null;
|
|
52
41
|
const { url: downloadUrl } = await storage.getDownloadUrl({ storageUri });
|
|
53
|
-
|
|
54
|
-
return assertRemoteUrl(downloadUrl);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
if (/^[a-z][a-z\d+.-]*:/i.test(downloadUrl)) throw error;
|
|
57
|
-
}
|
|
58
|
-
return resolveDownloadPath(downloadUrl, storageUri);
|
|
42
|
+
return assertRemoteUrl(downloadUrl);
|
|
59
43
|
};
|
|
60
44
|
const readStorageText = async (storageUri) => {
|
|
61
45
|
return (await readStorageResponse(storageUri))?.text() ?? null;
|
|
62
46
|
};
|
|
63
47
|
return {
|
|
64
|
-
downloadStorageObject: storageAdapters.some((storage) => storage.getDownloadUrl !== void 0) ? async (storageUriToken, encodedSignature) => {
|
|
65
|
-
const requestedPath = `/storage/${storageUriToken}/${encodedSignature}`;
|
|
66
|
-
const requested = parseStorageDownloadPath(requestedPath);
|
|
67
|
-
if (!requested) return null;
|
|
68
|
-
let storage;
|
|
69
|
-
try {
|
|
70
|
-
storage = findStorage(getStorageProtocol(requested.storageUri));
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
if (!storage?.getDownloadUrl || !storage.get) return null;
|
|
75
|
-
const { url: downloadUrl } = await storage.getDownloadUrl({ storageUri: requested.storageUri });
|
|
76
|
-
try {
|
|
77
|
-
new URL(downloadUrl);
|
|
78
|
-
return null;
|
|
79
|
-
} catch {
|
|
80
|
-
if (/^[a-z][a-z\d+.-]*:/i.test(downloadUrl)) return null;
|
|
81
|
-
}
|
|
82
|
-
if (!tokensEqual(downloadUrl, requestedPath)) return null;
|
|
83
|
-
return (await storage.get({ storageUri: requested.storageUri })).response;
|
|
84
|
-
} : void 0,
|
|
85
48
|
readStorageText,
|
|
86
49
|
resolveFileUrl
|
|
87
50
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/server",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.44",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -28,17 +28,9 @@
|
|
|
28
28
|
"import": "./dist/adapters/prisma.mjs",
|
|
29
29
|
"require": "./dist/adapters/prisma.cjs"
|
|
30
30
|
},
|
|
31
|
-
"./plugins
|
|
32
|
-
"import": "./dist/plugins/
|
|
33
|
-
"require": "./dist/plugins/
|
|
34
|
-
},
|
|
35
|
-
"./plugins/insights": {
|
|
36
|
-
"import": "./dist/plugins/insights/index.mjs",
|
|
37
|
-
"require": "./dist/plugins/insights/index.cjs"
|
|
38
|
-
},
|
|
39
|
-
"./plugins/remote-config": {
|
|
40
|
-
"import": "./dist/plugins/remote-config/index.mjs",
|
|
41
|
-
"require": "./dist/plugins/remote-config/index.cjs"
|
|
31
|
+
"./plugins": {
|
|
32
|
+
"import": "./dist/plugins/index.mjs",
|
|
33
|
+
"require": "./dist/plugins/index.cjs"
|
|
42
34
|
},
|
|
43
35
|
"./package.json": "./package.json"
|
|
44
36
|
},
|
|
@@ -58,11 +50,11 @@
|
|
|
58
50
|
"dependencies": {
|
|
59
51
|
"rou3": "0.7.9",
|
|
60
52
|
"verkit": "0.4.0",
|
|
61
|
-
"@hot-updater/plugin-
|
|
62
|
-
"@hot-updater/
|
|
63
|
-
"@hot-updater/
|
|
64
|
-
"@hot-updater/plugin-remote-config": "1.0.0-rc.
|
|
65
|
-
"@hot-updater/plugin-
|
|
53
|
+
"@hot-updater/plugin-insights": "1.0.0-rc.44",
|
|
54
|
+
"@hot-updater/protocol": "1.0.0-rc.44",
|
|
55
|
+
"@hot-updater/plugin-api-keys": "1.0.0-rc.44",
|
|
56
|
+
"@hot-updater/plugin-remote-config": "1.0.0-rc.44",
|
|
57
|
+
"@hot-updater/plugin-core": "1.0.0-rc.44"
|
|
66
58
|
},
|
|
67
59
|
"peerDependencies": {
|
|
68
60
|
"@prisma/client": "*",
|
|
@@ -103,8 +95,8 @@
|
|
|
103
95
|
"prisma": "6.19.3",
|
|
104
96
|
"uuidv7": "^1.0.2",
|
|
105
97
|
"wrangler": "^4.69.0",
|
|
106
|
-
"@hot-updater/standalone": "1.0.0-rc.
|
|
107
|
-
"@hot-updater/test-utils": "1.0.0-rc.
|
|
98
|
+
"@hot-updater/standalone": "1.0.0-rc.44",
|
|
99
|
+
"@hot-updater/test-utils": "1.0.0-rc.44"
|
|
108
100
|
},
|
|
109
101
|
"nx": {
|
|
110
102
|
"implicitDependencies": [
|