@hot-updater/cloudflare 0.36.6 → 1.0.0-rc.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.
Files changed (49) hide show
  1. package/dist/iac/index.cjs +989 -11
  2. package/dist/iac/index.mjs +992 -15
  3. package/dist/index.cjs +919 -6989
  4. package/dist/index.d.cts +11 -47
  5. package/dist/index.d.mts +11 -47
  6. package/dist/index.mjs +917 -7006
  7. package/dist/worker/index.cjs +898 -336
  8. package/dist/worker/index.d.cts +18 -33
  9. package/dist/worker/index.d.mts +18 -33
  10. package/dist/worker/index.mjs +899 -331
  11. package/package.json +7 -10
  12. package/sql/bundles.sql +183 -35
  13. package/src/d1Database.spec.ts +143 -392
  14. package/src/d1Database.ts +55 -650
  15. package/src/d1Implementation.spec.ts +252 -0
  16. package/src/d1Implementation.ts +742 -0
  17. package/src/d1Query.spec.ts +46 -0
  18. package/src/d1Query.ts +89 -0
  19. package/src/d1Rows.spec.ts +111 -0
  20. package/src/d1Rows.ts +326 -0
  21. package/src/d1Sql.spec.ts +30 -0
  22. package/src/d1Sql.ts +175 -0
  23. package/src/r2S3Storage.ts +93 -137
  24. package/src/r2Storage.spec.ts +120 -218
  25. package/src/r2Storage.ts +13 -61
  26. package/src/r2WorkerStorage.spec.ts +161 -50
  27. package/src/r2WorkerStorage.ts +85 -118
  28. package/src/worker/d1Database.spec.ts +33 -0
  29. package/src/worker/d1Database.ts +52 -0
  30. package/src/worker/index.ts +3 -31
  31. package/worker/dist/README.md +1 -1
  32. package/worker/dist/index.js +5968 -4701
  33. package/worker/dist/index.js.map +4 -4
  34. package/worker/migrations/0001_hot-updater_1.0.0.sql +197 -0
  35. package/worker/migrations/0002_hot-updater_artifact_byte_sizes.sql +11 -0
  36. package/worker/src/databaseMigration.integration.spec.ts +114 -0
  37. package/worker/src/databasePlugins.integration.spec.ts +292 -0
  38. package/worker/src/index.integration.spec.ts +102 -294
  39. package/worker/src/index.ts +14 -66
  40. package/worker/src/storagePlugin.integration.spec.ts +59 -0
  41. package/worker/wrangler.json +5 -2
  42. package/src/cloudflareWorkerDatabase.spec.ts +0 -260
  43. package/src/cloudflareWorkerDatabase.ts +0 -651
  44. package/src/r2WranglerStorage.ts +0 -193
  45. package/worker/migrations/0001_hot-updater_init.sql +0 -16
  46. package/worker/migrations/0002_hot-updater_0.13.0.sql +0 -8
  47. package/worker/migrations/0003_hot-updater_0.18.0.sql +0 -38
  48. package/worker/migrations/0004_hot-updater_0.29.0.sql +0 -11
  49. package/worker/migrations/0005_hot-updater_0.31.0.sql +0 -24
@@ -1,73 +1,184 @@
1
- import type { RequestEnvContext } from "@hot-updater/plugin-core";
2
- import { describe, expect, it, vi } from "vitest";
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
2
 
4
- import {
5
- type CloudflareWorkerStorageEnv,
6
- r2WorkerStorage,
7
- } from "./r2WorkerStorage";
3
+ import { r2WorkerStorage } from "./r2WorkerStorage";
8
4
 
9
- type TestContext = RequestEnvContext<CloudflareWorkerStorageEnv>;
5
+ const createBucket = (
6
+ get: ReturnType<typeof vi.fn>,
7
+ overrides: Partial<R2Bucket> = {},
8
+ ) =>
9
+ ({
10
+ get,
11
+ head: vi.fn(),
12
+ put: vi.fn(),
13
+ delete: vi.fn(),
14
+ list: vi.fn(),
15
+ ...overrides,
16
+ }) as unknown as R2Bucket;
10
17
 
11
18
  describe("r2WorkerStorage", () => {
12
- it("reads manifest text directly from the R2 binding", async () => {
19
+ afterEach(() => {
20
+ vi.unstubAllGlobals();
21
+ });
22
+
23
+ it("reads bytes from the R2 binding captured by the implementation", async () => {
13
24
  const get = vi.fn(async (key: string) => ({
14
- text: async () => `text:${key}`,
25
+ body: new Response(`text:${key}`).body,
26
+ httpEtag: '"etag"',
27
+ size: `text:${key}`.length,
28
+ writeHttpMetadata: vi.fn(),
15
29
  }));
16
30
  const storage = r2WorkerStorage({
17
- jwtSecret: "secret",
18
- publicBaseUrl: "https://assets.example.com",
19
- })();
31
+ bucket: createBucket(get),
32
+ bucketName: "bundles",
33
+ downloadUrlSigningKey: "test-signing-key",
34
+ });
20
35
 
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");
36
+ const { response } = await storage.get({
37
+ storageUri: "r2://bundles/app/manifest.json",
38
+ });
39
+
40
+ expect(await response?.text()).toBe("text:app/manifest.json");
30
41
  expect(get).toHaveBeenCalledWith("app/manifest.json");
31
42
  });
32
43
 
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
- }));
44
+ it("rejects URIs owned by a different R2 binding", async () => {
45
+ const get = vi.fn();
37
46
  const storage = r2WorkerStorage({
38
- jwtSecret: "secret",
39
- publicBaseUrl: "https://assets.example.com",
40
- })();
47
+ bucket: createBucket(get),
48
+ bucketName: "bundles",
49
+ downloadUrlSigningKey: "test-signing-key",
50
+ });
41
51
 
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,
52
+ await expect(
53
+ storage.get({ storageUri: "r2://other/app/manifest.json" }),
54
+ ).rejects.toThrow(
55
+ 'Bucket name mismatch: expected "bundles", but found "other".',
51
56
  );
52
-
53
- expect(get).toHaveBeenCalledWith("assets/bootsplash/logo-ios@2x.png");
57
+ expect(get).not.toHaveBeenCalled();
54
58
  });
55
59
 
56
- it("fails fast when the R2 binding is missing", async () => {
60
+ it("writes and deletes the exact object key through the R2 binding", async () => {
61
+ let uploaded = "";
62
+ const put = vi.fn(async (_key: string, body: Uint8Array) => {
63
+ uploaded = new TextDecoder().decode(body);
64
+ return null;
65
+ });
66
+ const bucket = createBucket(vi.fn(), {
67
+ put: put as unknown as R2Bucket["put"],
68
+ });
57
69
  const storage = r2WorkerStorage({
58
- jwtSecret: "secret",
59
- publicBaseUrl: "https://assets.example.com",
60
- })();
70
+ bucket,
71
+ bucketName: "bundles",
72
+ downloadUrlSigningKey: "test-signing-key",
73
+ });
61
74
 
75
+ await storage.put({
76
+ key: "releases/bundle.zip",
77
+ body: new Response("bundle").body!,
78
+ contentType: "application/zip",
79
+ });
62
80
  await expect(
63
- storage.profiles.runtime.readText("r2://bundles/app/manifest.json", {
64
- env: {
65
- JWT_SECRET: "secret",
81
+ storage.delete({
82
+ storageUri: "r2://bundles/releases/bundle.zip",
83
+ }),
84
+ ).resolves.toEqual({
85
+ deleted: true,
86
+ });
87
+
88
+ expect(bucket.put).toHaveBeenCalledWith(
89
+ "releases/bundle.zip",
90
+ expect.any(Uint8Array),
91
+ {
92
+ httpMetadata: {
93
+ cacheControl: "max-age=31536000",
94
+ contentType: "application/zip",
66
95
  },
67
- request: new Request("https://updates.example.com"),
68
- } as TestContext),
69
- ).rejects.toThrow(
70
- "r2WorkerStorage requires env.BUCKET in the hot updater context.",
96
+ },
97
+ );
98
+ expect(uploaded).toBe("bundle");
99
+ expect(bucket.delete).toHaveBeenCalledWith("releases/bundle.zip");
100
+ });
101
+
102
+ it("round-trips reserved characters without changing the R2 binding key", async () => {
103
+ const bucket = createBucket(vi.fn());
104
+ const storage = r2WorkerStorage({
105
+ bucket,
106
+ bucketName: "bundles",
107
+ downloadUrlSigningKey: "test-signing-key",
108
+ });
109
+ const key = "릴리스 folder/logo@2x #100%/bundle.zip";
110
+
111
+ const uploaded = await storage.put({
112
+ key,
113
+ body: new Response("bundle").body!,
114
+ contentType: "application/zip",
115
+ });
116
+ expect(uploaded.storageUri).toContain("logo%402x");
117
+ await storage.delete({ storageUri: uploaded.storageUri });
118
+ await storage.delete({ storageUri: uploaded.storageUri });
119
+
120
+ expect(bucket.put).toHaveBeenCalledWith(
121
+ key,
122
+ expect.any(Uint8Array),
123
+ expect.any(Object),
124
+ );
125
+ expect(bucket.delete).toHaveBeenNthCalledWith(1, key);
126
+ expect(bucket.delete).toHaveBeenNthCalledWith(2, key);
127
+ });
128
+
129
+ it("uses a fixed-length stream when the upload length is known", async () => {
130
+ const expectedLengths: number[] = [];
131
+ class TestFixedLengthStream extends TransformStream<
132
+ Uint8Array,
133
+ Uint8Array
134
+ > {
135
+ constructor(expectedLength: number) {
136
+ super();
137
+ expectedLengths.push(expectedLength);
138
+ }
139
+ }
140
+ vi.stubGlobal("FixedLengthStream", TestFixedLengthStream);
141
+ let uploaded = "";
142
+ const put = vi.fn(
143
+ async (_key: string, body: ReadableStream<Uint8Array>) => {
144
+ uploaded = await new Response(body).text();
145
+ return null;
146
+ },
71
147
  );
148
+ const storage = r2WorkerStorage({
149
+ bucket: createBucket(vi.fn(), {
150
+ put: put as unknown as R2Bucket["put"],
151
+ }),
152
+ bucketName: "bundles",
153
+ downloadUrlSigningKey: "test-signing-key",
154
+ });
155
+
156
+ await storage.put({
157
+ key: "releases/bundle.zip",
158
+ body: new Response("bundle").body!,
159
+ contentLength: 6,
160
+ contentType: "application/zip",
161
+ });
162
+
163
+ expect(expectedLengths).toEqual([6]);
164
+ expect(uploaded).toBe("bundle");
165
+ });
166
+
167
+ it("returns a stable signed download path for private R2 objects", async () => {
168
+ const storage = r2WorkerStorage({
169
+ bucket: createBucket(vi.fn()),
170
+ bucketName: "bundles",
171
+ downloadUrlSigningKey: "test-signing-key",
172
+ });
173
+
174
+ const first = await storage.getDownloadUrl({
175
+ storageUri: "r2://bundles/releases/bundle.zip",
176
+ });
177
+ const second = await storage.getDownloadUrl({
178
+ storageUri: "r2://bundles/releases/bundle.zip",
179
+ });
180
+
181
+ expect(first).toEqual(second);
182
+ expect(first.url).toMatch(/^\/storage\//);
72
183
  });
73
184
  });
@@ -1,127 +1,94 @@
1
- import { signToken } from "@hot-updater/js";
2
1
  import {
3
- createRuntimeStoragePlugin,
4
- decodeStorageObjectKey,
5
- type HotUpdaterContext,
6
- type RequestEnvContext,
2
+ createStorageKeyBuilder,
3
+ createStorageDownloadUrl,
4
+ createStoragePlugin,
5
+ createStorageUri,
6
+ parseStorageUri,
7
+ type StoragePluginWith,
7
8
  } from "@hot-updater/plugin-core";
8
9
 
9
- export interface CloudflareWorkerStorageEnv {
10
- JWT_SECRET: string;
11
- BUCKET: {
12
- get: (key: string) => Promise<{ text: () => Promise<string> } | null>;
13
- };
14
- }
15
-
16
- type ContextResolver<TContext, TValue> = (
17
- context?: HotUpdaterContext<TContext>,
18
- ) => TValue | Promise<TValue>;
19
-
20
- export interface CloudflareWorkerStorageConfig<
21
- TContext extends RequestEnvContext<CloudflareWorkerStorageEnv>,
22
- > {
23
- jwtSecret?: string | ContextResolver<TContext, string>;
24
- publicBaseUrl: string | ContextResolver<TContext, string>;
10
+ export interface CloudflareWorkerStorageConfig {
11
+ readonly bucket: R2Bucket;
12
+ readonly bucketName: string;
13
+ readonly basePath?: string;
14
+ readonly downloadUrlSigningKey: string;
25
15
  }
26
16
 
27
- const resolveContextValue = async <TContext, TValue>(
28
- value: TValue | ContextResolver<TContext, TValue>,
29
- context?: HotUpdaterContext<TContext>,
30
- ) => {
31
- return typeof value === "function"
32
- ? await (value as ContextResolver<TContext, TValue>)(context)
33
- : value;
34
- };
35
-
36
- const resolveJwtSecretFromContext = (
37
- context?: RequestEnvContext<CloudflareWorkerStorageEnv>,
38
- ) => {
39
- const jwtSecret = context?.env?.JWT_SECRET;
40
-
41
- if (!jwtSecret) {
42
- throw new Error(
43
- "r2WorkerStorage requires env.JWT_SECRET in the hot updater context.",
44
- );
45
- }
46
-
47
- return jwtSecret;
48
- };
49
-
50
- const resolveR2BucketFromContext = (
51
- context?: RequestEnvContext<CloudflareWorkerStorageEnv>,
52
- ) => {
53
- const bucket = context?.env?.BUCKET;
54
-
55
- if (!bucket) {
56
- throw new Error(
57
- "r2WorkerStorage requires env.BUCKET in the hot updater context.",
58
- );
59
- }
60
-
61
- return bucket;
62
- };
63
-
64
- const createPublicObjectPath = (storageUrl: URL) =>
65
- `${storageUrl.host}${storageUrl.pathname}`;
66
-
67
- const createR2ObjectKey = (storageUrl: URL) =>
68
- decodeStorageObjectKey(storageUrl.pathname.replace(/^\/+/, ""));
69
-
70
- export const r2WorkerStorage = <
71
- TContext extends RequestEnvContext<CloudflareWorkerStorageEnv> =
72
- RequestEnvContext<CloudflareWorkerStorageEnv>,
73
- >(
74
- config: CloudflareWorkerStorageConfig<TContext>,
75
- ) => {
76
- return createRuntimeStoragePlugin<
77
- CloudflareWorkerStorageConfig<TContext>,
78
- TContext
79
- >({
80
- name: "r2WorkerStorage",
81
- supportedProtocol: "r2",
82
- factory: (config) => ({
83
- async readText(storageUri, context) {
84
- const storageUrl = new URL(storageUri);
85
-
86
- if (storageUrl.protocol !== "r2:") {
87
- throw new Error("Invalid R2 storage URI protocol");
88
- }
89
-
90
- const bucket = resolveR2BucketFromContext(context);
91
- const key = createR2ObjectKey(storageUrl);
92
- const object = await bucket.get(key);
93
- if (!object) {
94
- return null;
95
- }
96
-
97
- return object.text();
98
- },
99
- async getDownloadUrl(storageUri, context) {
100
- const storageUrl = new URL(storageUri);
101
-
102
- if (storageUrl.protocol !== "r2:") {
103
- throw new Error("Invalid R2 storage URI protocol");
104
- }
17
+ export const r2WorkerStorage = (
18
+ config: CloudflareWorkerStorageConfig,
19
+ ): StoragePluginWith<
20
+ "put" | "get" | "getDownloadUrl" | "exists" | "delete"
21
+ > => {
22
+ const getStorageKey = createStorageKeyBuilder(config.basePath);
23
+ const getDownloadUrl = createStorageDownloadUrl(config.downloadUrlSigningKey);
24
+
25
+ const parseAndValidate = (storageUri: string) => {
26
+ const parsed = parseStorageUri(storageUri, "r2");
27
+ if (parsed.bucket !== config.bucketName) {
28
+ throw new Error(
29
+ `Bucket name mismatch: expected "${config.bucketName}", but found "${parsed.bucket}".`,
30
+ );
31
+ }
32
+ return parsed;
33
+ };
105
34
 
106
- const key = createPublicObjectPath(storageUrl);
107
- const [jwtSecret, publicBaseUrl] = await Promise.all([
108
- resolveContextValue(
109
- config.jwtSecret ?? resolveJwtSecretFromContext,
110
- context,
35
+ return createStoragePlugin({
36
+ name: "r2Storage",
37
+ protocol: "r2",
38
+ async put({ key, body, contentLength, contentType }) {
39
+ const storageKey = getStorageKey(key);
40
+ const uploadOptions = {
41
+ httpMetadata: {
42
+ contentType,
43
+ cacheControl: "max-age=31536000",
44
+ },
45
+ };
46
+ if (contentLength === undefined) {
47
+ const bufferedBody = new Uint8Array(
48
+ await new Response(body).arrayBuffer(),
49
+ );
50
+ await config.bucket.put(storageKey, bufferedBody, uploadOptions);
51
+ } else {
52
+ const fixedLengthBody = new FixedLengthStream(contentLength);
53
+ await Promise.all([
54
+ body.pipeTo(fixedLengthBody.writable),
55
+ config.bucket.put(
56
+ storageKey,
57
+ fixedLengthBody.readable,
58
+ uploadOptions,
111
59
  ),
112
- resolveContextValue(config.publicBaseUrl, context),
113
60
  ]);
114
- const token = await signToken(key, jwtSecret);
115
- const url = new URL(publicBaseUrl);
116
-
117
- url.pathname = key;
118
- url.search = "";
119
- url.searchParams.set("token", token);
120
-
121
- return {
122
- fileUrl: url.toString(),
123
- };
124
- },
125
- }),
126
- })(config);
61
+ }
62
+ return {
63
+ storageUri: createStorageUri({
64
+ protocol: "r2",
65
+ bucket: config.bucketName,
66
+ key: storageKey,
67
+ }),
68
+ };
69
+ },
70
+ async get({ storageUri }) {
71
+ const { key } = parseAndValidate(storageUri);
72
+ const object = await config.bucket.get(key);
73
+ if (!object) return { response: null };
74
+ const headers = new Headers();
75
+ object.writeHttpMetadata(headers);
76
+ headers.set("etag", object.httpEtag);
77
+ headers.set("content-length", String(object.size));
78
+ return { response: new Response(object.body, { headers }) };
79
+ },
80
+ async getDownloadUrl({ storageUri }) {
81
+ parseAndValidate(storageUri);
82
+ return getDownloadUrl({ storageUri });
83
+ },
84
+ async exists({ storageUri }) {
85
+ const { key } = parseAndValidate(storageUri);
86
+ return { exists: (await config.bucket.head(key)) !== null };
87
+ },
88
+ async delete({ storageUri }) {
89
+ const { key } = parseAndValidate(storageUri);
90
+ await config.bucket.delete(key);
91
+ return { deleted: true };
92
+ },
93
+ });
127
94
  };
@@ -0,0 +1,33 @@
1
+ import { expect, it } from "vitest";
2
+
3
+ import { d1Database, type D1Like } from "./d1Database";
4
+
5
+ const database: D1Like = {
6
+ prepare: () => ({
7
+ bind: () => ({
8
+ all: async () => ({
9
+ results: [{ id: "channel-production", name: "production" }],
10
+ }),
11
+ }),
12
+ }),
13
+ batch: async (statements) =>
14
+ Promise.all(statements.map((item) => item.all())),
15
+ };
16
+
17
+ it("uses the D1 binding supplied at the Worker composition boundary", async () => {
18
+ const plugin = d1Database(database);
19
+
20
+ expect(plugin.name).toBe("d1Database");
21
+ expect(Object.keys(plugin.models).sort()).toEqual([
22
+ "analytics",
23
+ "apiKeys",
24
+ "bundlePatches",
25
+ "bundles",
26
+ "channels",
27
+ "releaseCatalogs",
28
+ "releases",
29
+ ]);
30
+ await expect(plugin.models.channels.list({})).resolves.toEqual({
31
+ channels: [{ id: "channel-production", name: "production" }],
32
+ });
33
+ });
@@ -0,0 +1,52 @@
1
+ import { createDatabasePlugin } from "@hot-updater/plugin-core";
2
+ import { createDatabasePluginAdapter } from "@hot-updater/plugin-core/internal";
3
+
4
+ import { createD1Implementation } from "../d1Implementation";
5
+
6
+ type D1Result = {
7
+ readonly results?: readonly unknown[];
8
+ };
9
+
10
+ type D1BoundStatement = {
11
+ all: () => Promise<D1Result>;
12
+ };
13
+
14
+ type D1PreparedStatement = {
15
+ bind: (...values: readonly unknown[]) => D1BoundStatement;
16
+ };
17
+
18
+ export type D1Like = {
19
+ prepare(sql: string): D1PreparedStatement;
20
+ batch(statements: D1BoundStatement[]): Promise<readonly D1Result[]>;
21
+ };
22
+
23
+ export interface CloudflareWorkerDatabaseEnv {
24
+ readonly DB: D1Like;
25
+ }
26
+
27
+ export const d1Database = (database: D1Like) => {
28
+ const implementation = createD1Implementation({
29
+ async query(sql, params) {
30
+ const result = await database
31
+ .prepare(sql)
32
+ .bind(...params)
33
+ .all();
34
+ return result.results ?? [];
35
+ },
36
+ async batch(statements) {
37
+ const results = await database.batch(
38
+ statements.map(({ sql, params }) =>
39
+ database.prepare(sql).bind(...params),
40
+ ),
41
+ );
42
+ return results.map(({ results }) => results ?? []);
43
+ },
44
+ });
45
+ const adapter = createDatabasePluginAdapter("d1Database", implementation);
46
+ return createDatabasePlugin({
47
+ name: "d1Database",
48
+ models: adapter.models,
49
+ commit: adapter.commit,
50
+ ...(adapter.dispose ? { dispose: adapter.dispose } : {}),
51
+ });
52
+ };
@@ -1,33 +1,5 @@
1
- export { verifyJwtSignedUrl } from "@hot-updater/js";
2
-
3
- import type { RequestEnvContext as BaseRequestEnvContext } from "@hot-updater/plugin-core";
4
-
5
- import {
6
- type CloudflareWorkerDatabaseEnv,
7
- d1WorkerDatabase,
8
- } from "../cloudflareWorkerDatabase";
9
- import {
1
+ export { d1Database, type D1Like } from "./d1Database";
2
+ export {
3
+ r2WorkerStorage as r2Storage,
10
4
  type CloudflareWorkerStorageConfig,
11
- type CloudflareWorkerStorageEnv,
12
- r2WorkerStorage,
13
5
  } from "../r2WorkerStorage";
14
-
15
- export type { CloudflareWorkerDatabaseEnv, CloudflareWorkerStorageEnv };
16
-
17
- export interface CloudflareWorkerRuntimeEnv
18
- extends CloudflareWorkerDatabaseEnv, CloudflareWorkerStorageEnv {}
19
-
20
- export type RequestEnvContext<TEnv = CloudflareWorkerRuntimeEnv> =
21
- BaseRequestEnvContext<TEnv>;
22
-
23
- export const d1Database = <
24
- TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> =
25
- RequestEnvContext<CloudflareWorkerRuntimeEnv>,
26
- >() => d1WorkerDatabase<TContext>();
27
-
28
- export const r2Storage = <
29
- TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> =
30
- RequestEnvContext<CloudflareWorkerRuntimeEnv>,
31
- >(
32
- config: CloudflareWorkerStorageConfig<TContext>,
33
- ) => r2WorkerStorage<TContext>(config);
@@ -1 +1 @@
1
- This folder contains the built output assets for the worker "hot-updater" generated at 2026-08-30T03:59:50.247Z.
1
+ This folder contains the built output assets for the worker "hot-updater" generated at 2026-08-31T01:29:11.045Z.