@hot-updater/cloudflare 0.36.0 → 0.36.2

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.
@@ -11233,7 +11233,7 @@ const SOURCE_TEMPLATE = `// add this to your App.tsx
11233
11233
  import { HotUpdater } from "@hot-updater/react-native";
11234
11234
 
11235
11235
  function App() {
11236
- return ...
11236
+ return null; // Replace with your app root
11237
11237
  }
11238
11238
 
11239
11239
  export default HotUpdater.wrap({
@@ -11226,7 +11226,7 @@ const SOURCE_TEMPLATE = `// add this to your App.tsx
11226
11226
  import { HotUpdater } from "@hot-updater/react-native";
11227
11227
 
11228
11228
  function App() {
11229
- return ...
11229
+ return null; // Replace with your app root
11230
11230
  }
11231
11231
 
11232
11232
  export default HotUpdater.wrap({
@@ -323,7 +323,7 @@ const resolveR2BucketFromContext = (context) => {
323
323
  return bucket;
324
324
  };
325
325
  const createPublicObjectPath = (storageUrl) => `${storageUrl.host}${storageUrl.pathname}`;
326
- const createR2ObjectKey = (storageUrl) => storageUrl.pathname.replace(/^\/+/, "");
326
+ const createR2ObjectKey = (storageUrl) => (0, _hot_updater_plugin_core.decodeStorageObjectKey)(storageUrl.pathname.replace(/^\/+/, ""));
327
327
  const r2WorkerStorage = (config) => {
328
328
  return (0, _hot_updater_plugin_core.createRuntimeStoragePlugin)({
329
329
  name: "r2WorkerStorage",
@@ -1,6 +1,6 @@
1
1
  import { signToken, verifyJwtSignedUrl } from "@hot-updater/js";
2
2
  import { DEFAULT_ROLLOUT_COHORT_COUNT, NIL_UUID, getAssetBaseStorageUri, getBundlePatches, getManifestFileHash, getManifestStorageUri, stripBundleArtifactMetadata } from "@hot-updater/core";
3
- import { calculatePagination, createDatabasePlugin, createRuntimeStoragePlugin, filterCompatibleAppVersions, resolveUpdateInfoFromBundles } from "@hot-updater/plugin-core";
3
+ import { calculatePagination, createDatabasePlugin, createRuntimeStoragePlugin, decodeStorageObjectKey, filterCompatibleAppVersions, resolveUpdateInfoFromBundles } from "@hot-updater/plugin-core";
4
4
  //#region src/cloudflareWorkerDatabase.ts
5
5
  const buildJsonEachInClause = (columnName, values, params) => {
6
6
  if (values.length === 0) return "1 = 0";
@@ -322,7 +322,7 @@ const resolveR2BucketFromContext = (context) => {
322
322
  return bucket;
323
323
  };
324
324
  const createPublicObjectPath = (storageUrl) => `${storageUrl.host}${storageUrl.pathname}`;
325
- const createR2ObjectKey = (storageUrl) => storageUrl.pathname.replace(/^\/+/, "");
325
+ const createR2ObjectKey = (storageUrl) => decodeStorageObjectKey(storageUrl.pathname.replace(/^\/+/, ""));
326
326
  const r2WorkerStorage = (config) => {
327
327
  return createRuntimeStoragePlugin({
328
328
  name: "r2WorkerStorage",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/cloudflare",
3
3
  "type": "module",
4
- "version": "0.36.0",
4
+ "version": "0.36.2",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -61,11 +61,11 @@
61
61
  "cloudflare": "4.2.0",
62
62
  "hono": "4.12.9",
63
63
  "uuidv7": "^1.0.2",
64
- "@hot-updater/cli-tools": "0.36.0",
65
- "@hot-updater/core": "0.36.0",
66
- "@hot-updater/js": "0.36.0",
67
- "@hot-updater/plugin-core": "0.36.0",
68
- "@hot-updater/server": "0.36.0"
64
+ "@hot-updater/core": "0.36.2",
65
+ "@hot-updater/cli-tools": "0.36.2",
66
+ "@hot-updater/js": "0.36.2",
67
+ "@hot-updater/plugin-core": "0.36.2",
68
+ "@hot-updater/server": "0.36.2"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@cloudflare/vitest-pool-workers": "0.13.0",
@@ -82,7 +82,7 @@
82
82
  "vitest": "4.1.4",
83
83
  "wrangler": "^4.5.0",
84
84
  "xdg-app-paths": "^8.3.0",
85
- "@hot-updater/test-utils": "0.36.0"
85
+ "@hot-updater/test-utils": "0.36.2"
86
86
  },
87
87
  "scripts": {
88
88
  "build": "tsdown && pnpm build:worker",
@@ -279,6 +279,28 @@ describe("r2Storage", () => {
279
279
  );
280
280
  });
281
281
 
282
+ it("decodes percent-encoded keys before signing through the S3 API", async () => {
283
+ mockS3Client();
284
+ const storage = r2Storage({
285
+ accountId: "account-id",
286
+ bucketName: "test-bucket",
287
+ credentials: {
288
+ accessKeyId: "access-key-id",
289
+ secretAccessKey: "secret-access-key",
290
+ },
291
+ })();
292
+
293
+ await storage.profiles.runtime.getDownloadUrl(
294
+ "r2://test-bucket/releases/logo%402x.png",
295
+ );
296
+
297
+ const command = vi.mocked(getSignedUrl).mock.calls.at(-1)?.[1];
298
+ expect(command).toBeInstanceOf(GetObjectCommand);
299
+ expect((command as GetObjectCommand).input.Key).toBe(
300
+ "releases/logo@2x.png",
301
+ );
302
+ });
303
+
282
304
  it("falls back to wrangler without S3 credentials", async () => {
283
305
  wrangler.mockImplementation(async (...args: string[]) => {
284
306
  const fileIndex = args.indexOf("--file");
@@ -382,6 +404,29 @@ describe("r2Storage", () => {
382
404
  );
383
405
  });
384
406
 
407
+ it("decodes percent-encoded keys before using Wrangler", async () => {
408
+ wrangler.mockResolvedValueOnce({ exitCode: 0, stderr: "" });
409
+ const storage = r2Storage({
410
+ accountId: "account-id",
411
+ bucketName: "test-bucket",
412
+ cloudflareApiToken: "api-token",
413
+ })();
414
+
415
+ await storage.profiles.node.exists(
416
+ "r2://test-bucket/releases/logo%402x.png",
417
+ );
418
+
419
+ expect(wrangler).toHaveBeenCalledWith(
420
+ "r2",
421
+ "object",
422
+ "get",
423
+ "test-bucket/releases/logo@2x.png",
424
+ "--file",
425
+ expect.any(String),
426
+ "--remote",
427
+ );
428
+ });
429
+
385
430
  it("rethrows non-missing R2 existence errors", async () => {
386
431
  const error = createExecaError("Authentication failed");
387
432
  wrangler.mockRejectedValueOnce(error);
@@ -30,6 +30,29 @@ describe("r2WorkerStorage", () => {
30
30
  expect(get).toHaveBeenCalledWith("app/manifest.json");
31
31
  });
32
32
 
33
+ it("decodes percent-encoded object keys before reading from R2", async () => {
34
+ const get = vi.fn(async (key: string) => ({
35
+ text: async () => `text:${key}`,
36
+ }));
37
+ const storage = r2WorkerStorage({
38
+ jwtSecret: "secret",
39
+ publicBaseUrl: "https://assets.example.com",
40
+ })();
41
+
42
+ await storage.profiles.runtime.readText(
43
+ "r2://bundles/assets/bootsplash/logo-ios%402x.png",
44
+ {
45
+ env: {
46
+ BUCKET: { get },
47
+ JWT_SECRET: "secret",
48
+ },
49
+ request: new Request("https://updates.example.com"),
50
+ } satisfies TestContext,
51
+ );
52
+
53
+ expect(get).toHaveBeenCalledWith("assets/bootsplash/logo-ios@2x.png");
54
+ });
55
+
33
56
  it("fails fast when the R2 binding is missing", async () => {
34
57
  const storage = r2WorkerStorage({
35
58
  jwtSecret: "secret",
@@ -1,6 +1,7 @@
1
1
  import { signToken } from "@hot-updater/js";
2
2
  import {
3
3
  createRuntimeStoragePlugin,
4
+ decodeStorageObjectKey,
4
5
  type HotUpdaterContext,
5
6
  type RequestEnvContext,
6
7
  } from "@hot-updater/plugin-core";
@@ -64,7 +65,7 @@ const createPublicObjectPath = (storageUrl: URL) =>
64
65
  `${storageUrl.host}${storageUrl.pathname}`;
65
66
 
66
67
  const createR2ObjectKey = (storageUrl: URL) =>
67
- storageUrl.pathname.replace(/^\/+/, "");
68
+ decodeStorageObjectKey(storageUrl.pathname.replace(/^\/+/, ""));
68
69
 
69
70
  export const r2WorkerStorage = <
70
71
  TContext extends RequestEnvContext<CloudflareWorkerStorageEnv> =
@@ -1 +1 @@
1
- This folder contains the built output assets for the worker "hot-updater" generated at 2026-08-13T07:56:54.999Z.
1
+ This folder contains the built output assets for the worker "hot-updater" generated at 2026-08-20T12:36:25.676Z.