@hot-updater/cloudflare 0.30.12 → 0.31.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/d1Database.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import {
2
2
  DEFAULT_ROLLOUT_COHORT_COUNT,
3
- type SnakeCaseBundle,
3
+ getAssetBaseStorageUri,
4
+ getBundlePatches,
5
+ getManifestFileHash,
6
+ getManifestStorageUri,
7
+ stripBundleArtifactMetadata,
4
8
  } from "@hot-updater/core";
5
9
  import type {
6
10
  Bundle,
@@ -30,6 +34,36 @@ interface BuildQueryResult {
30
34
  params: any[];
31
35
  }
32
36
 
37
+ interface D1BundleRow {
38
+ id: string;
39
+ channel: string;
40
+ enabled: number | boolean;
41
+ should_force_update: number | boolean;
42
+ file_hash: string;
43
+ git_commit_hash: string | null;
44
+ message: string | null;
45
+ platform: "ios" | "android";
46
+ target_app_version: string | null;
47
+ storage_uri: string;
48
+ fingerprint_hash: string | null;
49
+ metadata: unknown;
50
+ manifest_storage_uri?: string | null;
51
+ manifest_file_hash?: string | null;
52
+ asset_base_storage_uri?: string | null;
53
+ rollout_cohort_count: number | null;
54
+ target_cohorts: string | null;
55
+ }
56
+
57
+ interface D1BundlePatchRow {
58
+ id: string;
59
+ bundle_id: string;
60
+ base_bundle_id: string;
61
+ base_file_hash: string;
62
+ patch_file_hash: string;
63
+ patch_storage_uri: string;
64
+ order_index: number | null;
65
+ }
66
+
33
67
  async function resolvePage<T>(singlePage: any): Promise<T[]> {
34
68
  const results: T[] = [];
35
69
  for await (const page of singlePage.iterPages()) {
@@ -152,8 +186,54 @@ function parseTargetCohorts(value: unknown): string[] | null {
152
186
  return null;
153
187
  }
154
188
 
155
- // Helper function to transform snake_case row to Bundle
156
- function transformRowToBundle(row: SnakeCaseBundle): Bundle {
189
+ const parseMetadata = (value: unknown): Bundle["metadata"] => {
190
+ if (!value) return undefined;
191
+ if (typeof value === "string") {
192
+ try {
193
+ return parseMetadata(JSON.parse(value) as unknown);
194
+ } catch {
195
+ return undefined;
196
+ }
197
+ }
198
+ return typeof value === "object" && !Array.isArray(value)
199
+ ? (value as Bundle["metadata"])
200
+ : undefined;
201
+ };
202
+
203
+ const buildBundlePatchId = (bundleId: string, baseBundleId: string) =>
204
+ `${bundleId}:${baseBundleId}`;
205
+
206
+ const bundleToPatchRows = (bundle: Bundle): D1BundlePatchRow[] =>
207
+ getBundlePatches(bundle).map((patch, index) => ({
208
+ id: buildBundlePatchId(bundle.id, patch.baseBundleId),
209
+ bundle_id: bundle.id,
210
+ base_bundle_id: patch.baseBundleId,
211
+ base_file_hash: patch.baseFileHash,
212
+ patch_file_hash: patch.patchFileHash,
213
+ patch_storage_uri: patch.patchStorageUri,
214
+ order_index: index,
215
+ }));
216
+
217
+ function transformRowToBundle(
218
+ row: D1BundleRow,
219
+ patchRows: D1BundlePatchRow[] = [],
220
+ ): Bundle {
221
+ const rawMetadata = parseMetadata(row.metadata);
222
+ const patches = patchRows
223
+ .slice()
224
+ .sort(
225
+ (left, right) =>
226
+ (left.order_index ?? 0) - (right.order_index ?? 0) ||
227
+ left.base_bundle_id.localeCompare(right.base_bundle_id),
228
+ )
229
+ .map((patch) => ({
230
+ baseBundleId: patch.base_bundle_id,
231
+ baseFileHash: patch.base_file_hash,
232
+ patchFileHash: patch.patch_file_hash,
233
+ patchStorageUri: patch.patch_storage_uri,
234
+ }));
235
+ const primaryPatch = patches[0] ?? null;
236
+
157
237
  return {
158
238
  id: row.id,
159
239
  channel: row.channel,
@@ -166,7 +246,15 @@ function transformRowToBundle(row: SnakeCaseBundle): Bundle {
166
246
  targetAppVersion: row.target_app_version,
167
247
  storageUri: row.storage_uri,
168
248
  fingerprintHash: row.fingerprint_hash,
169
- metadata: row?.metadata ? JSON.parse(row?.metadata as string) : {},
249
+ metadata: stripBundleArtifactMetadata(rawMetadata),
250
+ manifestStorageUri: row.manifest_storage_uri ?? null,
251
+ manifestFileHash: row.manifest_file_hash ?? null,
252
+ assetBaseStorageUri: row.asset_base_storage_uri ?? null,
253
+ patches,
254
+ patchBaseBundleId: primaryPatch?.baseBundleId ?? null,
255
+ patchBaseFileHash: primaryPatch?.baseFileHash ?? null,
256
+ patchFileHash: primaryPatch?.patchFileHash ?? null,
257
+ patchStorageUri: primaryPatch?.patchStorageUri ?? null,
170
258
  rolloutCohortCount:
171
259
  (row.rollout_cohort_count as number | null) ??
172
260
  DEFAULT_ROLLOUT_COHORT_COUNT,
@@ -180,6 +268,36 @@ export const d1Database = createDatabasePlugin<D1DatabaseConfig>({
180
268
  const cf = new Cloudflare({
181
269
  apiToken: config.cloudflareApiToken,
182
270
  });
271
+ const getPatchMap = async (bundleIds: string[]) => {
272
+ const patchMap = new Map<string, D1BundlePatchRow[]>();
273
+
274
+ if (bundleIds.length === 0) {
275
+ return patchMap;
276
+ }
277
+
278
+ const placeholders = bundleIds.map(() => "?").join(", ");
279
+ const sql = minify(`
280
+ SELECT *
281
+ FROM bundle_patches
282
+ WHERE bundle_id IN (${placeholders})
283
+ ORDER BY order_index ASC, base_bundle_id ASC
284
+ `);
285
+
286
+ const result = await cf.d1.database.query(config.databaseId, {
287
+ account_id: config.accountId,
288
+ sql,
289
+ params: bundleIds,
290
+ });
291
+ const rows = await resolvePage<D1BundlePatchRow>(result);
292
+
293
+ for (const row of rows) {
294
+ const current = patchMap.get(row.bundle_id) ?? [];
295
+ current.push(row);
296
+ patchMap.set(row.bundle_id, current);
297
+ }
298
+
299
+ return patchMap;
300
+ };
183
301
 
184
302
  // Helper function to get total count
185
303
  async function getTotalCount(conditions: QueryConditions): Promise<number> {
@@ -227,8 +345,9 @@ export const d1Database = createDatabasePlugin<D1DatabaseConfig>({
227
345
  params,
228
346
  });
229
347
 
230
- const rows = await resolvePage<SnakeCaseBundle>(result);
231
- return rows.map(transformRowToBundle);
348
+ const rows = await resolvePage<D1BundleRow>(result);
349
+ const patchMap = await getPatchMap(rows.map((row) => row.id));
350
+ return rows.map((row) => transformRowToBundle(row, patchMap.get(row.id)));
232
351
  }
233
352
 
234
353
  async function queryBundlesForUpdateInfo(
@@ -246,8 +365,9 @@ export const d1Database = createDatabasePlugin<D1DatabaseConfig>({
246
365
  params,
247
366
  });
248
367
 
249
- const rows = await resolvePage<SnakeCaseBundle>(result);
250
- return rows.map(transformRowToBundle);
368
+ const rows = await resolvePage<D1BundleRow>(result);
369
+ const patchMap = await getPatchMap(rows.map((row) => row.id));
370
+ return rows.map((row) => transformRowToBundle(row, patchMap.get(row.id)));
251
371
  }
252
372
 
253
373
  async function getTargetAppVersionsForUpdateInfo({
@@ -318,19 +438,22 @@ export const d1Database = createDatabasePlugin<D1DatabaseConfig>({
318
438
  async getBundleById(bundleId) {
319
439
  const sql = minify(/* sql */ `
320
440
  SELECT * FROM bundles WHERE id = ? LIMIT 1`);
321
- const singlePage = await cf.d1.database.query(config.databaseId, {
322
- account_id: config.accountId,
323
- sql,
324
- params: [bundleId],
325
- });
441
+ const [singlePage, patchMap] = await Promise.all([
442
+ cf.d1.database.query(config.databaseId, {
443
+ account_id: config.accountId,
444
+ sql,
445
+ params: [bundleId],
446
+ }),
447
+ getPatchMap([bundleId]),
448
+ ]);
326
449
 
327
- const rows = await resolvePage<SnakeCaseBundle>(singlePage);
450
+ const rows = await resolvePage<D1BundleRow>(singlePage);
328
451
 
329
452
  if (rows.length === 0) {
330
453
  return null;
331
454
  }
332
455
 
333
- return transformRowToBundle(rows[0]);
456
+ return transformRowToBundle(rows[0], patchMap.get(bundleId));
334
457
  },
335
458
 
336
459
  async getBundles(options) {
@@ -388,6 +511,24 @@ export const d1Database = createDatabasePlugin<D1DatabaseConfig>({
388
511
  DELETE FROM bundles WHERE id = ?
389
512
  `);
390
513
 
514
+ const deletePatchSql = minify(/* sql */ `
515
+ DELETE FROM bundle_patches WHERE bundle_id = ?
516
+ `);
517
+ await cf.d1.database.query(config.databaseId, {
518
+ account_id: config.accountId,
519
+ sql: deletePatchSql,
520
+ params: [op.data.id],
521
+ });
522
+
523
+ const deleteBasePatchSql = minify(/* sql */ `
524
+ DELETE FROM bundle_patches WHERE base_bundle_id = ?
525
+ `);
526
+ await cf.d1.database.query(config.databaseId, {
527
+ account_id: config.accountId,
528
+ sql: deleteBasePatchSql,
529
+ params: [op.data.id],
530
+ });
531
+
391
532
  await cf.d1.database.query(config.databaseId, {
392
533
  account_id: config.accountId,
393
534
  sql: deleteSql,
@@ -410,10 +551,13 @@ export const d1Database = createDatabasePlugin<D1DatabaseConfig>({
410
551
  storage_uri,
411
552
  fingerprint_hash,
412
553
  metadata,
554
+ manifest_storage_uri,
555
+ manifest_file_hash,
556
+ asset_base_storage_uri,
413
557
  rollout_cohort_count,
414
558
  target_cohorts
415
559
  )
416
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
560
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
417
561
  `);
418
562
 
419
563
  const params = [
@@ -428,9 +572,12 @@ export const d1Database = createDatabasePlugin<D1DatabaseConfig>({
428
572
  bundle.targetAppVersion,
429
573
  bundle.storageUri,
430
574
  bundle.fingerprintHash,
431
- bundle.metadata
432
- ? JSON.stringify(bundle.metadata)
433
- : JSON.stringify({}),
575
+ JSON.stringify(
576
+ stripBundleArtifactMetadata(bundle.metadata) ?? {},
577
+ ),
578
+ getManifestStorageUri(bundle),
579
+ getManifestFileHash(bundle),
580
+ getAssetBaseStorageUri(bundle),
434
581
  bundle.rolloutCohortCount ?? DEFAULT_ROLLOUT_COHORT_COUNT,
435
582
  bundle.targetCohorts
436
583
  ? JSON.stringify(bundle.targetCohorts)
@@ -442,6 +589,46 @@ export const d1Database = createDatabasePlugin<D1DatabaseConfig>({
442
589
  sql: upsertSql,
443
590
  params: params as string[],
444
591
  });
592
+
593
+ await cf.d1.database.query(config.databaseId, {
594
+ account_id: config.accountId,
595
+ sql: minify(`
596
+ DELETE FROM bundle_patches WHERE bundle_id = ?
597
+ `),
598
+ params: [bundle.id],
599
+ });
600
+
601
+ const patchRows = bundleToPatchRows(bundle);
602
+ if (patchRows.length > 0) {
603
+ const patchInsertSql = minify(`
604
+ INSERT OR REPLACE INTO bundle_patches (
605
+ id,
606
+ bundle_id,
607
+ base_bundle_id,
608
+ base_file_hash,
609
+ patch_file_hash,
610
+ patch_storage_uri,
611
+ order_index
612
+ )
613
+ VALUES (?, ?, ?, ?, ?, ?, ?)
614
+ `);
615
+
616
+ for (const patchRow of patchRows) {
617
+ await cf.d1.database.query(config.databaseId, {
618
+ account_id: config.accountId,
619
+ sql: patchInsertSql,
620
+ params: [
621
+ patchRow.id,
622
+ patchRow.bundle_id,
623
+ patchRow.base_bundle_id,
624
+ patchRow.base_file_hash,
625
+ patchRow.patch_file_hash,
626
+ patchRow.patch_storage_uri,
627
+ String(patchRow.order_index ?? 0),
628
+ ],
629
+ });
630
+ }
631
+ }
445
632
  }
446
633
  }
447
634
  },
@@ -0,0 +1,85 @@
1
+ import fs from "fs/promises";
2
+
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { r2Storage } from "./r2Storage";
6
+
7
+ const { wrangler } = vi.hoisted(() => ({
8
+ wrangler: vi.fn(),
9
+ }));
10
+
11
+ vi.mock("./utils/createWrangler", () => ({
12
+ createWrangler: vi.fn(() => wrangler),
13
+ }));
14
+
15
+ describe("r2Storage", () => {
16
+ beforeEach(() => {
17
+ wrangler.mockReset();
18
+ });
19
+
20
+ it("downloads R2 objects with wrangler to the given file path", async () => {
21
+ wrangler.mockImplementation(async (...args: string[]) => {
22
+ const fileIndex = args.indexOf("--file");
23
+ const downloadPath = args[fileIndex + 1];
24
+
25
+ await fs.writeFile(
26
+ downloadPath,
27
+ JSON.stringify({
28
+ bundleId: "bundle-1",
29
+ assets: {},
30
+ }),
31
+ );
32
+
33
+ return {
34
+ exitCode: 0,
35
+ stderr: "",
36
+ };
37
+ });
38
+
39
+ const storage = r2Storage({
40
+ accountId: "account-id",
41
+ bucketName: "test-bucket",
42
+ cloudflareApiToken: "api-token",
43
+ })();
44
+
45
+ const downloadPath = "/tmp/hot-updater-test-manifest.json";
46
+ await fs.rm(downloadPath, { force: true });
47
+
48
+ await storage.profiles.node.downloadFile(
49
+ "r2://test-bucket/releases/bundle-1/manifest.json",
50
+ downloadPath,
51
+ );
52
+
53
+ expect(JSON.parse(await fs.readFile(downloadPath, "utf8"))).toEqual({
54
+ bundleId: "bundle-1",
55
+ assets: {},
56
+ });
57
+ expect(wrangler).toHaveBeenCalledWith(
58
+ "r2",
59
+ "object",
60
+ "get",
61
+ "test-bucket/releases/bundle-1/manifest.json",
62
+ "--file",
63
+ downloadPath,
64
+ "--remote",
65
+ );
66
+ });
67
+
68
+ it("rejects downloads from a different bucket", async () => {
69
+ const storage = r2Storage({
70
+ accountId: "account-id",
71
+ bucketName: "test-bucket",
72
+ cloudflareApiToken: "api-token",
73
+ })();
74
+
75
+ await expect(
76
+ storage.profiles.node.downloadFile(
77
+ "r2://other-bucket/releases/bundle-1/manifest.json",
78
+ "/tmp/hot-updater-test-manifest.json",
79
+ ),
80
+ ).rejects.toThrow(
81
+ 'Bucket name mismatch: expected "test-bucket", but found "other-bucket".',
82
+ );
83
+ expect(wrangler).not.toHaveBeenCalled();
84
+ });
85
+ });
package/src/r2Storage.ts CHANGED
@@ -1,8 +1,9 @@
1
- import path from "path";
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
2
3
 
3
4
  import {
4
5
  createStorageKeyBuilder,
5
- createStoragePlugin,
6
+ createNodeStoragePlugin,
6
7
  getContentType,
7
8
  parseStorageUri,
8
9
  } from "@hot-updater/plugin-core";
@@ -22,28 +23,8 @@ export interface R2StorageConfig {
22
23
 
23
24
  /**
24
25
  * Cloudflare R2 storage plugin for Hot Updater.
25
- *
26
- * Note: This plugin does not support `getDownloadUrl()`.
27
- * If you need download URL generation, use `s3Storage` from `@hot-updater/aws` instead,
28
- * which is fully compatible with Cloudflare R2.
29
- *
30
- * @example
31
- * ```typescript
32
- * // Using s3Storage with Cloudflare R2 for download URL support
33
- * import { s3Storage } from "@hot-updater/aws";
34
- *
35
- * s3Storage({
36
- * region: "auto",
37
- * endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
38
- * credentials: {
39
- * accessKeyId: "YOUR_ACCESS_KEY_ID",
40
- * secretAccessKey: "YOUR_SECRET_ACCESS_KEY",
41
- * },
42
- * bucketName: "YOUR_BUCKET_NAME",
43
- * })
44
- * ```
45
26
  */
46
- export const r2Storage = createStoragePlugin<R2StorageConfig>({
27
+ export const r2Storage = createNodeStoragePlugin<R2StorageConfig>({
47
28
  name: "r2Storage",
48
29
  supportedProtocol: "r2",
49
30
  factory: (config) => {
@@ -110,20 +91,35 @@ export const r2Storage = createStoragePlugin<R2StorageConfig>({
110
91
  storageUri: `r2://${bucketName}/${Key}`,
111
92
  };
112
93
  },
113
- async getDownloadUrl() {
114
- throw new Error(
115
- "`r2Storage` does not support `getDownloadUrl()`. Use `s3Storage` from `@hot-updater/aws` instead (compatible with Cloudflare R2).\n\n" +
116
- "Example:\n" +
117
- "s3Storage({\n" +
118
- " region: 'auto',\n" +
119
- " endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',\n" +
120
- " credentials: {\n" +
121
- " accessKeyId: 'YOUR_ACCESS_KEY_ID',\n" +
122
- " secretAccessKey: 'YOUR_SECRET_ACCESS_KEY',\n" +
123
- " },\n" +
124
- " bucketName: 'YOUR_BUCKET_NAME',\n" +
125
- "})",
126
- );
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
+ }
127
123
  },
128
124
  };
129
125
  },
@@ -0,0 +1,50 @@
1
+ import type { RequestEnvContext } from "@hot-updater/plugin-core";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import {
5
+ type CloudflareWorkerStorageEnv,
6
+ r2WorkerStorage,
7
+ } from "./r2WorkerStorage";
8
+
9
+ type TestContext = RequestEnvContext<CloudflareWorkerStorageEnv>;
10
+
11
+ describe("r2WorkerStorage", () => {
12
+ it("reads manifest text directly from the R2 binding", async () => {
13
+ const get = vi.fn(async (key: string) => ({
14
+ text: async () => `text:${key}`,
15
+ }));
16
+ const storage = r2WorkerStorage({
17
+ jwtSecret: "secret",
18
+ publicBaseUrl: "https://assets.example.com",
19
+ })();
20
+
21
+ await expect(
22
+ storage.profiles.runtime.readText("r2://bundles/app/manifest.json", {
23
+ env: {
24
+ BUCKET: { get },
25
+ JWT_SECRET: "secret",
26
+ },
27
+ request: new Request("https://updates.example.com"),
28
+ } satisfies TestContext),
29
+ ).resolves.toBe("text:app/manifest.json");
30
+ expect(get).toHaveBeenCalledWith("app/manifest.json");
31
+ });
32
+
33
+ it("fails fast when the R2 binding is missing", async () => {
34
+ const storage = r2WorkerStorage({
35
+ jwtSecret: "secret",
36
+ publicBaseUrl: "https://assets.example.com",
37
+ })();
38
+
39
+ await expect(
40
+ storage.profiles.runtime.readText("r2://bundles/app/manifest.json", {
41
+ env: {
42
+ JWT_SECRET: "secret",
43
+ },
44
+ request: new Request("https://updates.example.com"),
45
+ } as TestContext),
46
+ ).rejects.toThrow(
47
+ "r2WorkerStorage requires env.BUCKET in the hot updater context.",
48
+ );
49
+ });
50
+ });
@@ -1,12 +1,15 @@
1
1
  import { signToken } from "@hot-updater/js";
2
- import type {
3
- HotUpdaterContext,
4
- RequestEnvContext,
5
- StoragePlugin,
2
+ import {
3
+ createRuntimeStoragePlugin,
4
+ type HotUpdaterContext,
5
+ type RequestEnvContext,
6
6
  } from "@hot-updater/plugin-core";
7
7
 
8
8
  export interface CloudflareWorkerStorageEnv {
9
9
  JWT_SECRET: string;
10
+ BUCKET: {
11
+ get: (key: string) => Promise<{ text: () => Promise<string> } | null>;
12
+ };
10
13
  }
11
14
 
12
15
  type ContextResolver<TContext, TValue> = (
@@ -43,25 +46,54 @@ const resolveJwtSecretFromContext = (
43
46
  return jwtSecret;
44
47
  };
45
48
 
49
+ const resolveR2BucketFromContext = (
50
+ context?: RequestEnvContext<CloudflareWorkerStorageEnv>,
51
+ ) => {
52
+ const bucket = context?.env?.BUCKET;
53
+
54
+ if (!bucket) {
55
+ throw new Error(
56
+ "r2WorkerStorage requires env.BUCKET in the hot updater context.",
57
+ );
58
+ }
59
+
60
+ return bucket;
61
+ };
62
+
63
+ const createPublicObjectPath = (storageUrl: URL) =>
64
+ `${storageUrl.host}${storageUrl.pathname}`;
65
+
66
+ const createR2ObjectKey = (storageUrl: URL) =>
67
+ storageUrl.pathname.replace(/^\/+/, "");
68
+
46
69
  export const r2WorkerStorage = <
47
70
  TContext extends RequestEnvContext<CloudflareWorkerStorageEnv> =
48
71
  RequestEnvContext<CloudflareWorkerStorageEnv>,
49
72
  >(
50
73
  config: CloudflareWorkerStorageConfig<TContext>,
51
74
  ) => {
52
- return (): StoragePlugin<TContext> => {
53
- return {
54
- name: "r2WorkerStorage",
55
- supportedProtocol: "r2",
56
- async upload() {
57
- throw new Error(
58
- "r2WorkerStorage does not support upload() in the worker runtime.",
59
- );
60
- },
61
- async delete() {
62
- throw new Error(
63
- "r2WorkerStorage does not support delete() in the worker runtime.",
64
- );
75
+ return createRuntimeStoragePlugin<
76
+ CloudflareWorkerStorageConfig<TContext>,
77
+ TContext
78
+ >({
79
+ name: "r2WorkerStorage",
80
+ supportedProtocol: "r2",
81
+ factory: (config) => ({
82
+ async readText(storageUri, context) {
83
+ const storageUrl = new URL(storageUri);
84
+
85
+ if (storageUrl.protocol !== "r2:") {
86
+ throw new Error("Invalid R2 storage URI protocol");
87
+ }
88
+
89
+ const bucket = resolveR2BucketFromContext(context);
90
+ const key = createR2ObjectKey(storageUrl);
91
+ const object = await bucket.get(key);
92
+ if (!object) {
93
+ return null;
94
+ }
95
+
96
+ return object.text();
65
97
  },
66
98
  async getDownloadUrl(storageUri, context) {
67
99
  const storageUrl = new URL(storageUri);
@@ -70,7 +102,7 @@ export const r2WorkerStorage = <
70
102
  throw new Error("Invalid R2 storage URI protocol");
71
103
  }
72
104
 
73
- const key = `${storageUrl.host}${storageUrl.pathname}`;
105
+ const key = createPublicObjectPath(storageUrl);
74
106
  const [jwtSecret, publicBaseUrl] = await Promise.all([
75
107
  resolveContextValue(
76
108
  config.jwtSecret ?? resolveJwtSecretFromContext,
@@ -89,6 +121,6 @@ export const r2WorkerStorage = <
89
121
  fileUrl: url.toString(),
90
122
  };
91
123
  },
92
- };
93
- };
124
+ }),
125
+ })(config);
94
126
  };
@@ -1 +1 @@
1
- This folder contains the built output assets for the worker "hot-updater" generated at 2026-05-15T15:32:08.967Z.
1
+ This folder contains the built output assets for the worker "hot-updater" generated at 2026-05-15T19:15:31.472Z.