@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.
@@ -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
- /** `"public"`, or nothing; a `clientAccess` object says what replaced it. */
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(isRecord(value) ? "clientAccess objects were removed in 1.0. Set clientAccess: \"public\", or add a plugin that provides clientAuth to plugins." : "clientAccess must be \"public\"; to protect client routes, add a plugin that provides clientAuth to plugins.");
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 = Object.freeze([...options.storage ?? []]);
32
- const { downloadStorageObject, readStorageText, resolveFileUrl } = require_storageAccess.createStorageAccess(storage);
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
- for (const adapter of storage) (0, _hot_updater_plugin_core.assertStorageOperations)(adapter, ["get", "getDownloadUrl"]);
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 every storage adapter must
28
- * serve downloads (`get` and `getDownloadUrl`), so a server fails where it
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 list storage that only uploads.
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, in order, as a frozen copy. */
58
- readonly storage: readonly StorageAdapter[];
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" | RemovedClientAccess;
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: the server reads and signs the URIs of each
117
- * adapter's protocol.
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?: readonly StorageAdapter[];
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, RemovedClientAccess, RuntimeHotUpdaterAPI, createHotUpdater };
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 every storage adapter must
28
- * serve downloads (`get` and `getDownloadUrl`), so a server fails where it
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 list storage that only uploads.
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, in order, as a frozen copy. */
58
- readonly storage: readonly StorageAdapter[];
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" | RemovedClientAccess;
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: the server reads and signs the URIs of each
117
- * adapter's protocol.
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?: readonly StorageAdapter[];
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, RemovedClientAccess, RuntimeHotUpdaterAPI, createHotUpdater };
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
- /** `"public"`, or nothing; a `clientAccess` object says what replaced it. */
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(isRecord(value) ? "clientAccess objects were removed in 1.0. Set clientAccess: \"public\", or add a plugin that provides clientAuth to plugins." : "clientAccess must be \"public\"; to protect client routes, add a plugin that provides clientAuth to plugins.");
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 = Object.freeze([...options.storage ?? []]);
33
- const { downloadStorageObject, readStorageText, resolveFileUrl } = createStorageAccess(storage);
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
- for (const adapter of storage) assertStorageOperations(adapter, ["get", "getDownloadUrl"]);
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` and storage downloads never pass through the client policy. */
22
- const PUBLIC_CLIENT_ROUTES = /* @__PURE__ */ new Set(["version", "downloadStorageObject"]);
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
- const createDownloadStorageRouteHandler = (downloadStorageObject) => async (params) => {
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` and storage downloads never pass through the client policy. */
23
- const PUBLIC_CLIENT_ROUTES = /* @__PURE__ */ new Set(["version", "downloadStorageObject"]);
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
- const createDownloadStorageRouteHandler = (downloadStorageObject) => async (params) => {
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 Response.json({ error: "Invalid app version" }, {
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 Response.json({ error: "Invalid app version" }, {
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
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "1.0.0-rc.43";
2
+ var version = "1.0.0-rc.44";
3
3
  //#endregion
4
4
  Object.defineProperty(exports, "version", {
5
5
  enumerable: true,
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.0.0-rc.43";
2
+ var version = "1.0.0-rc.44";
3
3
  //#endregion
4
4
  export { version };
@@ -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";
@@ -0,0 +1,4 @@
1
+ export * from "@hot-updater/plugin-api-keys/server";
2
+ export * from "@hot-updater/plugin-insights/server";
3
+ export * from "@hot-updater/plugin-remote-config/server";
4
+ export {};
@@ -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
- const protocol = new URL(value).protocol;
5
- if (protocol !== "http:" && protocol !== "https:") throw new Error("Storage getDownloadUrl must resolve to an HTTP(S) URL.");
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
- const resolveDownloadPath = (value, storageUri) => {
11
- const parsed = (0, _hot_updater_plugin_core.parseStorageDownloadPath)(value);
12
- if (!parsed || parsed.storageUri !== storageUri) throw new Error("Storage getDownloadUrl must return an HTTP(S) URL or a valid storage download path.");
13
- return value;
14
- };
15
- const tokensEqual = (left, right) => {
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
- try {
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
  };
@@ -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
- const protocol = new URL(value).protocol;
5
- if (protocol !== "http:" && protocol !== "https:") throw new Error("Storage getDownloadUrl must resolve to an HTTP(S) URL.");
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
- const resolveDownloadPath = (value, storageUri) => {
11
- const parsed = parseStorageDownloadPath(value);
12
- if (!parsed || parsed.storageUri !== storageUri) throw new Error("Storage getDownloadUrl must return an HTTP(S) URL or a valid storage download path.");
13
- return value;
14
- };
15
- const tokensEqual = (left, right) => {
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
- try {
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.43",
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/api-keys": {
32
- "import": "./dist/plugins/api-keys/index.mjs",
33
- "require": "./dist/plugins/api-keys/index.cjs"
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-core": "1.0.0-rc.43",
62
- "@hot-updater/plugin-api-keys": "1.0.0-rc.43",
63
- "@hot-updater/protocol": "1.0.0-rc.43",
64
- "@hot-updater/plugin-remote-config": "1.0.0-rc.43",
65
- "@hot-updater/plugin-insights": "1.0.0-rc.43"
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.43",
107
- "@hot-updater/test-utils": "1.0.0-rc.43"
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": [