@hot-updater/cloudflare 0.31.3 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/r2Storage.ts CHANGED
@@ -1,126 +1,66 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
1
  import {
5
- createStorageKeyBuilder,
6
- createNodeStoragePlugin,
7
- getContentType,
8
- parseStorageUri,
2
+ createUniversalStoragePlugin,
3
+ type StoragePluginHooks,
4
+ type UniversalStoragePlugin,
9
5
  } from "@hot-updater/plugin-core";
10
- import { ExecaError } from "execa";
11
6
 
12
- import { createWrangler } from "./utils/createWrangler";
7
+ import {
8
+ createS3RuntimeStorageProfile,
9
+ createS3StorageProfile,
10
+ type R2S3StorageConfig,
11
+ } from "./r2S3Storage";
12
+ import {
13
+ createWranglerRuntimeStorageProfile,
14
+ createWranglerStorageProfile,
15
+ type R2WranglerStorageConfig,
16
+ } from "./r2WranglerStorage";
13
17
 
14
- export interface R2StorageConfig {
15
- cloudflareApiToken: string;
16
- accountId: string;
17
- bucketName: string;
18
- /**
19
- * Base path where bundles will be stored in the bucket
20
- */
21
- basePath?: string;
22
- }
18
+ export type R2StorageConfig = R2S3StorageConfig | R2WranglerStorageConfig;
19
+
20
+ export type { R2S3StorageConfig, R2WranglerStorageConfig };
21
+
22
+ const hasS3Credentials = (
23
+ config: R2StorageConfig,
24
+ ): config is R2S3StorageConfig => {
25
+ return Boolean(config.credentials);
26
+ };
23
27
 
24
28
  /**
25
29
  * Cloudflare R2 storage plugin for Hot Updater.
26
30
  */
27
- export const r2Storage = createNodeStoragePlugin<R2StorageConfig>({
31
+ interface R2Storage {
32
+ (
33
+ config: R2S3StorageConfig,
34
+ hooks?: StoragePluginHooks,
35
+ ): () => UniversalStoragePlugin;
36
+ /**
37
+ * @deprecated `cloudflareApiToken` uses the Wrangler CLI for R2 operations,
38
+ * which is slower than direct S3-compatible API access. Create R2
39
+ * S3-compatible credentials in the Cloudflare dashboard and pass them with
40
+ * `r2Storage({ credentials })` instead.
41
+ */
42
+ (
43
+ config: R2WranglerStorageConfig,
44
+ hooks?: StoragePluginHooks,
45
+ ): () => UniversalStoragePlugin;
46
+ }
47
+
48
+ const createR2StoragePlugin = createUniversalStoragePlugin<R2StorageConfig>({
28
49
  name: "r2Storage",
29
50
  supportedProtocol: "r2",
30
51
  factory: (config) => {
31
- const { bucketName, cloudflareApiToken, accountId } = config;
32
- const wrangler = createWrangler({
33
- accountId,
34
- cloudflareApiToken: cloudflareApiToken,
35
- cwd: process.cwd(),
36
- });
37
-
38
- const getStorageKey = createStorageKeyBuilder(config.basePath);
52
+ if (hasS3Credentials(config)) {
53
+ return {
54
+ node: createS3StorageProfile(config),
55
+ runtime: createS3RuntimeStorageProfile(config),
56
+ };
57
+ }
39
58
 
40
59
  return {
41
- async delete(storageUri) {
42
- const { bucket, key } = parseStorageUri(storageUri, "r2");
43
- if (bucket !== bucketName) {
44
- throw new Error(
45
- `Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`,
46
- );
47
- }
48
-
49
- try {
50
- await wrangler(
51
- "r2",
52
- "object",
53
- "delete",
54
- [bucketName, key].join("/"),
55
- "--remote",
56
- );
57
- } catch {
58
- throw new Error("Can not delete bundle");
59
- }
60
- },
61
- async upload(key, filePath) {
62
- const contentType = getContentType(filePath);
63
-
64
- const filename = path.basename(filePath);
65
-
66
- const Key = getStorageKey(key, filename);
67
- try {
68
- const { stderr, exitCode } = await wrangler(
69
- "r2",
70
- "object",
71
- "put",
72
- [bucketName, Key].join("/"),
73
- "--file",
74
- filePath,
75
- "--content-type",
76
- contentType,
77
- "--remote",
78
- );
79
- if (exitCode !== 0 && stderr) {
80
- throw new Error(stderr);
81
- }
82
- } catch (error) {
83
- if (error instanceof ExecaError) {
84
- throw new Error(error.stderr || error.stdout);
85
- }
86
-
87
- throw error;
88
- }
89
-
90
- return {
91
- storageUri: `r2://${bucketName}/${Key}`,
92
- };
93
- },
94
- async downloadFile(storageUri, filePath) {
95
- const { bucket, key } = parseStorageUri(storageUri, "r2");
96
- if (bucket !== bucketName) {
97
- throw new Error(
98
- `Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`,
99
- );
100
- }
101
-
102
- try {
103
- await fs.mkdir(path.dirname(filePath), { recursive: true });
104
- const { stderr, exitCode } = await wrangler(
105
- "r2",
106
- "object",
107
- "get",
108
- [bucketName, key].join("/"),
109
- "--file",
110
- filePath,
111
- "--remote",
112
- );
113
- if (exitCode !== 0 && stderr) {
114
- throw new Error(stderr);
115
- }
116
- } catch (error) {
117
- if (error instanceof ExecaError) {
118
- throw new Error(error.stderr || error.stdout);
119
- }
120
-
121
- throw error;
122
- }
123
- },
60
+ node: createWranglerStorageProfile(config),
61
+ runtime: createWranglerRuntimeStorageProfile(),
124
62
  };
125
63
  },
126
64
  });
65
+
66
+ export const r2Storage: R2Storage = createR2StoragePlugin;
@@ -0,0 +1,193 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ createStorageKeyBuilder,
7
+ getContentType,
8
+ type NodeStorageProfile,
9
+ parseStorageUri,
10
+ type RuntimeStorageProfile,
11
+ } from "@hot-updater/plugin-core";
12
+ import { ExecaError } from "execa";
13
+
14
+ import { createWrangler } from "./utils/createWrangler";
15
+
16
+ /**
17
+ * @deprecated `cloudflareApiToken` uses the Wrangler CLI for R2 operations,
18
+ * which is slower than direct S3-compatible API access. Create R2
19
+ * S3-compatible credentials in the Cloudflare dashboard and pass them with
20
+ * `r2Storage({ credentials })` instead.
21
+ */
22
+ export interface R2WranglerStorageConfig {
23
+ accountId: string;
24
+ bucketName: string;
25
+ /**
26
+ * @deprecated This token keeps R2 access on the slower Wrangler CLI path.
27
+ * Create R2 S3-compatible credentials in the Cloudflare dashboard and use
28
+ * `credentials` instead.
29
+ */
30
+ cloudflareApiToken: string;
31
+ /**
32
+ * Base path where bundles will be stored in the bucket
33
+ */
34
+ basePath?: string;
35
+ credentials?: never;
36
+ }
37
+
38
+ const ensureExpectedR2Bucket = (bucket: string, bucketName: string) => {
39
+ if (bucket !== bucketName) {
40
+ throw new Error(
41
+ `Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`,
42
+ );
43
+ }
44
+ };
45
+
46
+ const isR2ObjectNotFoundError = (error: ExecaError) => {
47
+ const output = [error.stderr, error.stdout, error.shortMessage, error.message]
48
+ .filter(Boolean)
49
+ .join("\n")
50
+ .toLowerCase();
51
+
52
+ return (
53
+ output.includes("not found") ||
54
+ output.includes("no such object") ||
55
+ output.includes("does not exist")
56
+ );
57
+ };
58
+
59
+ export const createWranglerStorageProfile = (
60
+ config: R2WranglerStorageConfig,
61
+ ): NodeStorageProfile => {
62
+ const { bucketName, cloudflareApiToken, accountId } = config;
63
+ const wrangler = createWrangler({
64
+ accountId,
65
+ cloudflareApiToken,
66
+ cwd: process.cwd(),
67
+ });
68
+ const getStorageKey = createStorageKeyBuilder(config.basePath);
69
+
70
+ return {
71
+ async delete(storageUri) {
72
+ const { bucket, key } = parseStorageUri(storageUri, "r2");
73
+ ensureExpectedR2Bucket(bucket, bucketName);
74
+
75
+ try {
76
+ await wrangler(
77
+ "r2",
78
+ "object",
79
+ "delete",
80
+ [bucketName, key].join("/"),
81
+ "--remote",
82
+ );
83
+ } catch {
84
+ throw new Error("Can not delete bundle");
85
+ }
86
+ },
87
+ async upload(key, filePath) {
88
+ const contentType = getContentType(filePath);
89
+
90
+ const filename = path.basename(filePath);
91
+
92
+ const Key = getStorageKey(key, filename);
93
+ try {
94
+ const { stderr, exitCode } = await wrangler(
95
+ "r2",
96
+ "object",
97
+ "put",
98
+ [bucketName, Key].join("/"),
99
+ "--file",
100
+ filePath,
101
+ "--content-type",
102
+ contentType,
103
+ "--remote",
104
+ );
105
+ if (exitCode !== 0 && stderr) {
106
+ throw new Error(stderr);
107
+ }
108
+ } catch (error) {
109
+ if (error instanceof ExecaError) {
110
+ throw new Error(error.stderr || error.stdout);
111
+ }
112
+
113
+ throw error;
114
+ }
115
+
116
+ return {
117
+ storageUri: `r2://${bucketName}/${Key}`,
118
+ };
119
+ },
120
+ async exists(storageUri: string) {
121
+ const { bucket, key } = parseStorageUri(storageUri, "r2");
122
+ ensureExpectedR2Bucket(bucket, bucketName);
123
+
124
+ const tempDir = await fs.mkdtemp(
125
+ path.join(os.tmpdir(), "hot-updater-r2-exists-"),
126
+ );
127
+ const tempFilePath = path.join(tempDir, "object");
128
+
129
+ try {
130
+ await wrangler(
131
+ "r2",
132
+ "object",
133
+ "get",
134
+ [bucketName, key].join("/"),
135
+ "--file",
136
+ tempFilePath,
137
+ "--remote",
138
+ );
139
+ return true;
140
+ } catch (error) {
141
+ if (error instanceof ExecaError && isR2ObjectNotFoundError(error)) {
142
+ return false;
143
+ }
144
+
145
+ throw error;
146
+ } finally {
147
+ await fs.rm(tempDir, { force: true, recursive: true });
148
+ }
149
+ },
150
+ async downloadFile(storageUri, filePath) {
151
+ const { bucket, key } = parseStorageUri(storageUri, "r2");
152
+ ensureExpectedR2Bucket(bucket, bucketName);
153
+
154
+ try {
155
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
156
+ const { stderr, exitCode } = await wrangler(
157
+ "r2",
158
+ "object",
159
+ "get",
160
+ [bucketName, key].join("/"),
161
+ "--file",
162
+ filePath,
163
+ "--remote",
164
+ );
165
+ if (exitCode !== 0 && stderr) {
166
+ throw new Error(stderr);
167
+ }
168
+ } catch (error) {
169
+ if (error instanceof ExecaError) {
170
+ throw new Error(error.stderr || error.stdout);
171
+ }
172
+
173
+ throw error;
174
+ }
175
+ },
176
+ };
177
+ };
178
+
179
+ export const createWranglerRuntimeStorageProfile =
180
+ (): RuntimeStorageProfile => {
181
+ const error = new Error(
182
+ "r2Storage runtime profile requires R2 S3 credentials. Wrangler-based R2 access is only supported by the node profile.",
183
+ );
184
+
185
+ return {
186
+ async readText() {
187
+ throw error;
188
+ },
189
+ async getDownloadUrl() {
190
+ throw error;
191
+ },
192
+ };
193
+ };
@@ -1 +1 @@
1
- This folder contains the built output assets for the worker "hot-updater" generated at 2026-05-18T08:09:40.025Z.
1
+ This folder contains the built output assets for the worker "hot-updater" generated at 2026-05-21T13:59:33.325Z.
@@ -5236,6 +5236,52 @@ function calculatePagination(total, options) {
5236
5236
  }
5237
5237
  __name(calculatePagination, "calculatePagination");
5238
5238
 
5239
+ // ../plugin-core/dist/contentAddressedAssets.mjs
5240
+ init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_process();
5241
+ init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_console();
5242
+ init_performance2();
5243
+ var getContentAddressedAssetStoragePath = /* @__PURE__ */ __name(({ assetPath, fileHash }) => {
5244
+ const extension = assetPath.endsWith(".br") ? ".br" : assetPath.includes(".") ? `.${assetPath.split(".").pop()}` : "";
5245
+ return `sha256/${fileHash.slice(0, 2)}/${fileHash}${extension}`;
5246
+ }, "getContentAddressedAssetStoragePath");
5247
+
5248
+ // ../plugin-core/dist/assetStorageLayout.mjs
5249
+ init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_process();
5250
+ init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_console();
5251
+ init_performance2();
5252
+
5253
+ // ../plugin-core/dist/legacyAssetStorageLayout.mjs
5254
+ init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_process();
5255
+ init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_console();
5256
+ init_performance2();
5257
+ var getLegacyManifestAssetStoragePath = /* @__PURE__ */ __name(({ assetPath }) => assetPath, "getLegacyManifestAssetStoragePath");
5258
+
5259
+ // ../plugin-core/dist/assetStorageLayout.mjs
5260
+ var createStorageUriWithRelativePath = /* @__PURE__ */ __name(({ baseStorageUri, relativePath }) => {
5261
+ const storageUrl = new URL(baseStorageUri);
5262
+ storageUrl.pathname = `${storageUrl.pathname.replace(/\/+$/, "")}/${relativePath.replace(/\\/g, "/").split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
5263
+ return storageUrl.toString();
5264
+ }, "createStorageUriWithRelativePath");
5265
+ var getAssetStorageLayout = /* @__PURE__ */ __name((assetBaseStorageUri) => {
5266
+ const pathname = new URL(assetBaseStorageUri).pathname.replace(/\/+$/, "");
5267
+ return pathname.endsWith("/assets") || pathname === "/assets" ? "content-addressed" : "legacy-files";
5268
+ }, "getAssetStorageLayout");
5269
+ var getManifestAssetStoragePath = /* @__PURE__ */ __name(({ assetBaseStorageUri, assetPath, fileHash }) => {
5270
+ if (getAssetStorageLayout(assetBaseStorageUri) === "content-addressed") return getContentAddressedAssetStoragePath({
5271
+ assetPath,
5272
+ fileHash
5273
+ });
5274
+ return getLegacyManifestAssetStoragePath({ assetPath });
5275
+ }, "getManifestAssetStoragePath");
5276
+ var resolveManifestAssetStorageUri = /* @__PURE__ */ __name(({ assetBaseStorageUri, assetPath, fileHash }) => createStorageUriWithRelativePath({
5277
+ baseStorageUri: assetBaseStorageUri,
5278
+ relativePath: getManifestAssetStoragePath({
5279
+ assetBaseStorageUri,
5280
+ assetPath,
5281
+ fileHash
5282
+ })
5283
+ }), "resolveManifestAssetStorageUri");
5284
+
5239
5285
  // ../plugin-core/dist/createDatabasePlugin.mjs
5240
5286
  init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_process();
5241
5287
  init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_console();
@@ -5707,7 +5753,7 @@ var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
5707
5753
  var __getOwnPropNames2 = Object.getOwnPropertyNames;
5708
5754
  var __getProtoOf2 = Object.getPrototypeOf;
5709
5755
  var __hasOwnProp2 = Object.prototype.hasOwnProperty;
5710
- var __commonJSMin = /* @__PURE__ */ __name((cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports), "__commonJSMin");
5756
+ var __commonJSMin = /* @__PURE__ */ __name((cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), "__commonJSMin");
5711
5757
  var __copyProps2 = /* @__PURE__ */ __name((to, from, except, desc) => {
5712
5758
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames2(from), i = 0, n = keys.length, key; i < n; i++) {
5713
5759
  key = keys[i];
@@ -6718,7 +6764,6 @@ var require_min_version2 = /* @__PURE__ */ __commonJSMin((exports, module) => {
6718
6764
  case "<":
6719
6765
  case "<=":
6720
6766
  break;
6721
- /* istanbul ignore next */
6722
6767
  default:
6723
6768
  throw new Error(`Unexpected operation: ${comparator.operator}`);
6724
6769
  }
@@ -8310,6 +8355,9 @@ var createProfiledStoragePlugin = /* @__PURE__ */ __name(({ createProfiles, name
8310
8355
  async downloadFile(storageUri, filePath) {
8311
8356
  return requireNodeProfile().downloadFile(storageUri, filePath);
8312
8357
  },
8358
+ async exists(storageUri) {
8359
+ return requireNodeProfile().exists(storageUri);
8360
+ },
8313
8361
  async upload(key, filePath) {
8314
8362
  return requireNodeProfile().upload(key, filePath);
8315
8363
  }
@@ -8434,13 +8482,6 @@ var isBundleManifest = /* @__PURE__ */ __name((value) => {
8434
8482
  }
8435
8483
  );
8436
8484
  }, "isBundleManifest");
8437
- var createChildStorageUri = /* @__PURE__ */ __name((baseStorageUri, relativePath) => {
8438
- const baseUrl = new URL(baseStorageUri);
8439
- const normalizedBasePath = baseUrl.pathname.replace(/\/+$/, "");
8440
- const relativeSegments = relativePath.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment));
8441
- baseUrl.pathname = `${normalizedBasePath}/${relativeSegments.join("/")}`;
8442
- return baseUrl.toString();
8443
- }, "createChildStorageUri");
8444
8485
  async function fetchBundleManifest(storageUri, readStorageText, resolveFileUrl, context2) {
8445
8486
  const [storageText, fileUrl] = await Promise.all([
8446
8487
  readStorageText(storageUri, context2),
@@ -8491,10 +8532,11 @@ async function resolveChangedAssets({
8491
8532
  }
8492
8533
  const usesBrotliAsset = BR_COMPRESSED_ASSET_PATH_RE.test(assetPath);
8493
8534
  const downloadPath = usesBrotliAsset ? `${assetPath}.br` : assetPath;
8494
- const storageUri = createChildStorageUri(
8535
+ const storageUri = resolveManifestAssetStorageUri({
8495
8536
  assetBaseStorageUri,
8496
- downloadPath
8497
- );
8537
+ assetPath: downloadPath,
8538
+ fileHash: asset.fileHash
8539
+ });
8498
8540
  const patch = patchDescriptor?.assetPath === assetPath ? patchDescriptor.patch : null;
8499
8541
  let fileUrl = null;
8500
8542
  try {
@@ -9022,7 +9064,7 @@ init_performance2();
9022
9064
  // ../../packages/server/package.json
9023
9065
  var package_default = {
9024
9066
  name: "@hot-updater/server",
9025
- version: "0.31.3",
9067
+ version: "0.32.0",
9026
9068
  type: "module",
9027
9069
  description: "React Native OTA solution for self-hosted",
9028
9070
  sideEffects: false,
@@ -9120,6 +9162,8 @@ var HandlerBadRequestError = class extends Error {
9120
9162
  };
9121
9163
  var SDK_VERSION_HEADER = "Hot-Updater-SDK-Version";
9122
9164
  var EXPLICIT_NO_UPDATE_MIN_SDK_VERSION = "0.31.0";
9165
+ var DEFAULT_BUNDLE_LIST_LIMIT = 50;
9166
+ var MAX_BUNDLE_LIST_LIMIT = 100;
9123
9167
  var supportsExplicitNoUpdateResponse = /* @__PURE__ */ __name((request) => {
9124
9168
  const sdkVersion = request.headers.get(SDK_VERSION_HEADER)?.trim();
9125
9169
  if (!sdkVersion) {
@@ -9187,6 +9231,19 @@ var parseStringArraySearchParam = /* @__PURE__ */ __name((url, key) => {
9187
9231
  const values = url.searchParams.getAll(key);
9188
9232
  return values.length > 0 ? values : void 0;
9189
9233
  }, "parseStringArraySearchParam");
9234
+ var parsePositiveIntegerSearchParam = /* @__PURE__ */ __name((url, key, defaultValue, maxValue) => {
9235
+ const value = url.searchParams.get(key);
9236
+ if (value === null) {
9237
+ return defaultValue;
9238
+ }
9239
+ const parsed = Number(value);
9240
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > maxValue) {
9241
+ throw new HandlerBadRequestError(
9242
+ `The '${key}' query parameter must be a positive integer between 1 and ${maxValue}.`
9243
+ );
9244
+ }
9245
+ return parsed;
9246
+ }, "parsePositiveIntegerSearchParam");
9190
9247
  var requirePlatformParam = /* @__PURE__ */ __name((params) => {
9191
9248
  const platform2 = requireRouteParam(params, "platform");
9192
9249
  if (!isPlatform(platform2)) {
@@ -9271,7 +9328,12 @@ var handleGetBundles = /* @__PURE__ */ __name(async (_params, request, api, cont
9271
9328
  const url = new URL(request.url);
9272
9329
  const channel2 = url.searchParams.get("channel") ?? void 0;
9273
9330
  const platform2 = url.searchParams.get("platform");
9274
- const limit = Number(url.searchParams.get("limit")) || 50;
9331
+ const limit = parsePositiveIntegerSearchParam(
9332
+ url,
9333
+ "limit",
9334
+ DEFAULT_BUNDLE_LIST_LIMIT,
9335
+ MAX_BUNDLE_LIST_LIMIT
9336
+ );
9275
9337
  const pageParam = url.searchParams.get("page");
9276
9338
  const offset = url.searchParams.get("offset");
9277
9339
  const after = url.searchParams.get("after") ?? void 0;
@@ -9407,14 +9469,13 @@ var routes = {
9407
9469
  };
9408
9470
  function createHandler(api, options = {}) {
9409
9471
  const basePath = options.basePath ?? "/api";
9410
- const updateCheckEnabled = options.routes?.updateCheck ?? true;
9411
- const versionEnabled = options.routes?.version ?? true;
9412
- const bundlesEnabled = options.routes?.bundles ?? true;
9472
+ const routeOptions = {
9473
+ updateCheck: options.routes?.updateCheck ?? true,
9474
+ bundles: options.routes?.bundles ?? false
9475
+ };
9413
9476
  const router = createRouter();
9414
- if (versionEnabled) {
9415
- addRoute(router, "GET", "/version", "version");
9416
- }
9417
- if (updateCheckEnabled) {
9477
+ addRoute(router, "GET", "/version", "version");
9478
+ if (routeOptions.updateCheck) {
9418
9479
  addRoute(
9419
9480
  router,
9420
9481
  "GET",
@@ -9440,7 +9501,7 @@ function createHandler(api, options = {}) {
9440
9501
  "appVersionUpdateWithCohort"
9441
9502
  );
9442
9503
  }
9443
- if (bundlesEnabled) {
9504
+ if (routeOptions.bundles) {
9444
9505
  addRoute(router, "GET", "/api/bundles/channels", "getChannels");
9445
9506
  addRoute(router, "GET", "/api/bundles/:id", "getBundle");
9446
9507
  addRoute(router, "GET", "/api/bundles", "getBundles");
@@ -9461,7 +9522,8 @@ function createHandler(api, options = {}) {
9461
9522
  headers: { "Content-Type": "application/json" }
9462
9523
  });
9463
9524
  }
9464
- const handler = routes[match2.data];
9525
+ const routeName = match2.data;
9526
+ const handler = routes[routeName];
9465
9527
  if (!handler) {
9466
9528
  return new Response(JSON.stringify({ error: "Handler not found" }), {
9467
9529
  status: 500,
@@ -12318,7 +12380,6 @@ var hotUpdater = createHotUpdater({
12318
12380
  basePath: HOT_UPDATER_BASE_PATH,
12319
12381
  routes: {
12320
12382
  updateCheck: true,
12321
- version: true,
12322
12383
  bundles: false
12323
12384
  }
12324
12385
  });