@hot-updater/server 1.0.0-rc.42 → 1.0.0-rc.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +40 -4
- package/dist/handlerReleaseCatalogRoutes.mjs +40 -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";
|
|
@@ -9,10 +10,42 @@ const ORIGIN_CACHE_MAX_ENTRIES = 128;
|
|
|
9
10
|
const NO_CATALOG_HEADER = "x-hot-updater-catalog";
|
|
10
11
|
/** A shared cache keeps a catalog, or a scope's lack of one, five seconds. */
|
|
11
12
|
const CATALOG_CACHE_CONTROL = "public, max-age=0, s-maxage=5";
|
|
13
|
+
/**
|
|
14
|
+
* The same five seconds for a CDN that reads `CDN-Cache-Control` (RFC 9213),
|
|
15
|
+
* such as Cloudflare's Workers Cache, which then ignores `Cache-Control`.
|
|
16
|
+
* `s-maxage` forbids serving stale, so there an expired catalog held requests
|
|
17
|
+
* at that location until the origin answered. Here, for five more seconds,
|
|
18
|
+
* the CDN answers with the expired copy while it revalidates in the
|
|
19
|
+
* background. `stale-if-error=0` keeps origin errors visible: without it,
|
|
20
|
+
* Cloudflare serves a stale copy on error indefinitely.
|
|
21
|
+
*
|
|
22
|
+
* A device can see a catalog up to 15 seconds after a deploy or a Roll back
|
|
23
|
+
* replaced it: the origin's 5-second catalog memo, then 5 seconds fresh and
|
|
24
|
+
* 5 stale at the CDN. A revoked API key can read a cached catalog up to
|
|
25
|
+
* 10 seconds. Devices and caches that don't read the header keep
|
|
26
|
+
* `Cache-Control`.
|
|
27
|
+
*/
|
|
28
|
+
const CATALOG_CDN_CACHE_CONTROL = "public, max-age=5, stale-while-revalidate=5, stale-if-error=0";
|
|
12
29
|
const privateNotFound = () => Response.json({ error: "Not found" }, {
|
|
13
30
|
status: 404,
|
|
14
31
|
headers: { "cache-control": "private, no-store" }
|
|
15
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
|
+
};
|
|
16
49
|
/**
|
|
17
50
|
* A scope with no catalog yet, such as a store version before its first
|
|
18
51
|
* OTA release: cached like a catalog, so its update checks don't all reach
|
|
@@ -24,6 +57,7 @@ const missingCatalog = () => Response.json({ error: "Not found" }, {
|
|
|
24
57
|
status: 404,
|
|
25
58
|
headers: {
|
|
26
59
|
"cache-control": CATALOG_CACHE_CONTROL,
|
|
60
|
+
"cdn-cache-control": CATALOG_CDN_CACHE_CONTROL,
|
|
27
61
|
vary: "Accept-Encoding",
|
|
28
62
|
[NO_CATALOG_HEADER]: "none"
|
|
29
63
|
}
|
|
@@ -38,6 +72,7 @@ const catalogResponse = async (catalog, request) => {
|
|
|
38
72
|
const etag = `"sha256:${await responseHash(body)}"`;
|
|
39
73
|
const headers = {
|
|
40
74
|
"cache-control": CATALOG_CACHE_CONTROL,
|
|
75
|
+
"cdn-cache-control": CATALOG_CDN_CACHE_CONTROL,
|
|
41
76
|
"content-type": CATALOG_CONTENT_TYPE,
|
|
42
77
|
etag,
|
|
43
78
|
vary: "Accept-Encoding"
|
|
@@ -89,16 +124,15 @@ const createReleaseCatalogRouteHandlers = () => {
|
|
|
89
124
|
appVersionReleaseCatalog: async (params, request, { core }) => {
|
|
90
125
|
const rawAppVersion = require_handlerParameters.requireRouteParam(params, "appVersion");
|
|
91
126
|
const appVersion = (0, _hot_updater_plugin_core.canonicalizeAppVersion)(rawAppVersion);
|
|
92
|
-
if (appVersion === null || appVersion !== rawAppVersion) return
|
|
93
|
-
status: 400,
|
|
94
|
-
headers: { "cache-control": "private, no-store" }
|
|
95
|
-
});
|
|
127
|
+
if (appVersion === null || appVersion !== rawAppVersion) return badRequest("Invalid app version");
|
|
96
128
|
const input = {
|
|
97
129
|
appVersion,
|
|
98
130
|
channelKey: require_handlerParameters.requireRouteParam(params, "channelKey"),
|
|
99
131
|
platform: require_handlerParameters.requirePlatformParam(params),
|
|
100
132
|
strategy: "APP_VERSION"
|
|
101
133
|
};
|
|
134
|
+
const invalid = invalidScope(input);
|
|
135
|
+
if (invalid) return invalid;
|
|
102
136
|
return catalogResponse(await loadCatalog(`app-version:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
|
|
103
137
|
},
|
|
104
138
|
fingerprintReleaseCatalog: async (params, request, { core }) => {
|
|
@@ -108,6 +142,8 @@ const createReleaseCatalogRouteHandlers = () => {
|
|
|
108
142
|
platform: require_handlerParameters.requirePlatformParam(params),
|
|
109
143
|
strategy: "FINGERPRINT"
|
|
110
144
|
};
|
|
145
|
+
const invalid = invalidScope(input);
|
|
146
|
+
if (invalid) return invalid;
|
|
111
147
|
return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
|
|
112
148
|
},
|
|
113
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";
|
|
@@ -9,10 +10,42 @@ const ORIGIN_CACHE_MAX_ENTRIES = 128;
|
|
|
9
10
|
const NO_CATALOG_HEADER = "x-hot-updater-catalog";
|
|
10
11
|
/** A shared cache keeps a catalog, or a scope's lack of one, five seconds. */
|
|
11
12
|
const CATALOG_CACHE_CONTROL = "public, max-age=0, s-maxage=5";
|
|
13
|
+
/**
|
|
14
|
+
* The same five seconds for a CDN that reads `CDN-Cache-Control` (RFC 9213),
|
|
15
|
+
* such as Cloudflare's Workers Cache, which then ignores `Cache-Control`.
|
|
16
|
+
* `s-maxage` forbids serving stale, so there an expired catalog held requests
|
|
17
|
+
* at that location until the origin answered. Here, for five more seconds,
|
|
18
|
+
* the CDN answers with the expired copy while it revalidates in the
|
|
19
|
+
* background. `stale-if-error=0` keeps origin errors visible: without it,
|
|
20
|
+
* Cloudflare serves a stale copy on error indefinitely.
|
|
21
|
+
*
|
|
22
|
+
* A device can see a catalog up to 15 seconds after a deploy or a Roll back
|
|
23
|
+
* replaced it: the origin's 5-second catalog memo, then 5 seconds fresh and
|
|
24
|
+
* 5 stale at the CDN. A revoked API key can read a cached catalog up to
|
|
25
|
+
* 10 seconds. Devices and caches that don't read the header keep
|
|
26
|
+
* `Cache-Control`.
|
|
27
|
+
*/
|
|
28
|
+
const CATALOG_CDN_CACHE_CONTROL = "public, max-age=5, stale-while-revalidate=5, stale-if-error=0";
|
|
12
29
|
const privateNotFound = () => Response.json({ error: "Not found" }, {
|
|
13
30
|
status: 404,
|
|
14
31
|
headers: { "cache-control": "private, no-store" }
|
|
15
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
|
+
};
|
|
16
49
|
/**
|
|
17
50
|
* A scope with no catalog yet, such as a store version before its first
|
|
18
51
|
* OTA release: cached like a catalog, so its update checks don't all reach
|
|
@@ -24,6 +57,7 @@ const missingCatalog = () => Response.json({ error: "Not found" }, {
|
|
|
24
57
|
status: 404,
|
|
25
58
|
headers: {
|
|
26
59
|
"cache-control": CATALOG_CACHE_CONTROL,
|
|
60
|
+
"cdn-cache-control": CATALOG_CDN_CACHE_CONTROL,
|
|
27
61
|
vary: "Accept-Encoding",
|
|
28
62
|
[NO_CATALOG_HEADER]: "none"
|
|
29
63
|
}
|
|
@@ -38,6 +72,7 @@ const catalogResponse = async (catalog, request) => {
|
|
|
38
72
|
const etag = `"sha256:${await responseHash(body)}"`;
|
|
39
73
|
const headers = {
|
|
40
74
|
"cache-control": CATALOG_CACHE_CONTROL,
|
|
75
|
+
"cdn-cache-control": CATALOG_CDN_CACHE_CONTROL,
|
|
41
76
|
"content-type": CATALOG_CONTENT_TYPE,
|
|
42
77
|
etag,
|
|
43
78
|
vary: "Accept-Encoding"
|
|
@@ -89,16 +124,15 @@ const createReleaseCatalogRouteHandlers = () => {
|
|
|
89
124
|
appVersionReleaseCatalog: async (params, request, { core }) => {
|
|
90
125
|
const rawAppVersion = requireRouteParam(params, "appVersion");
|
|
91
126
|
const appVersion = canonicalizeAppVersion(rawAppVersion);
|
|
92
|
-
if (appVersion === null || appVersion !== rawAppVersion) return
|
|
93
|
-
status: 400,
|
|
94
|
-
headers: { "cache-control": "private, no-store" }
|
|
95
|
-
});
|
|
127
|
+
if (appVersion === null || appVersion !== rawAppVersion) return badRequest("Invalid app version");
|
|
96
128
|
const input = {
|
|
97
129
|
appVersion,
|
|
98
130
|
channelKey: requireRouteParam(params, "channelKey"),
|
|
99
131
|
platform: requirePlatformParam(params),
|
|
100
132
|
strategy: "APP_VERSION"
|
|
101
133
|
};
|
|
134
|
+
const invalid = invalidScope(input);
|
|
135
|
+
if (invalid) return invalid;
|
|
102
136
|
return catalogResponse(await loadCatalog(`app-version:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
|
|
103
137
|
},
|
|
104
138
|
fingerprintReleaseCatalog: async (params, request, { core }) => {
|
|
@@ -108,6 +142,8 @@ const createReleaseCatalogRouteHandlers = () => {
|
|
|
108
142
|
platform: requirePlatformParam(params),
|
|
109
143
|
strategy: "FINGERPRINT"
|
|
110
144
|
};
|
|
145
|
+
const invalid = invalidScope(input);
|
|
146
|
+
if (invalid) return invalid;
|
|
111
147
|
return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
|
|
112
148
|
},
|
|
113
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/plugin-
|
|
64
|
-
"@hot-updater/
|
|
65
|
-
"@hot-updater/plugin-core": "1.0.0-rc.
|
|
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": [
|