@hot-updater/cloudflare 1.2.0-rc.7 → 1.2.0-rc.9

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/d1Rest.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SqlResult, SqlStatement } from "@hot-updater/plugin-core";
1
+ import type { Lazy, SqlResult, SqlStatement } from "@hot-updater/plugin-core";
2
2
  import Cloudflare from "cloudflare";
3
3
 
4
4
  import { D1ExecutionError, type D1ResultLike, toSqlResult } from "./d1Executor";
@@ -8,10 +8,10 @@ export interface D1RestConfig {
8
8
  readonly accountId: string;
9
9
  /**
10
10
  * An API token with D1 edit permission, or a function that returns it,
11
- * such as one that reads it with `secretEnv`: called on the first query,
12
- * so a config loads without the token.
11
+ * such as `secret("<name>")` in `hot-updater.config.ts`: called on the
12
+ * first query, so a config loads without the token.
13
13
  */
14
- readonly cloudflareApiToken: string | (() => string | Promise<string>);
14
+ readonly cloudflareApiToken: Lazy<string>;
15
15
  }
16
16
 
17
17
  /** The REST API binds text: each value goes as JSON and is read back with `json_extract`. */
@@ -1,154 +1,31 @@
1
+ import type { StorageAdapterWith } from "@hot-updater/plugin-core";
1
2
  import {
2
- DeleteObjectCommand,
3
- GetObjectCommand,
4
- HeadObjectCommand,
5
- S3Client,
3
+ createS3Storage,
6
4
  type S3ClientConfig,
7
- } from "@aws-sdk/client-s3";
8
- import { Upload } from "@aws-sdk/lib-storage";
9
- import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
10
- import {
11
- createStorageKeyBuilder,
12
- createStorageAdapter,
13
- createStorageUri,
14
- parseStorageUri,
15
- type StorageAdapterWith,
16
- } from "@hot-updater/plugin-core";
5
+ type S3CredentialsOption,
6
+ type S3StorageOperations,
7
+ } from "@hot-updater/storage-s3";
17
8
 
18
- export interface R2S3StorageConfig extends S3ClientConfig {
9
+ export interface R2S3StorageConfig extends Omit<S3ClientConfig, "credentials"> {
19
10
  accountId: string;
20
11
  bucketName: string;
21
- credentials: NonNullable<S3ClientConfig["credentials"]>;
12
+ /** The SDK's credentials, or an R2 access key whose fields are lazy. */
13
+ credentials: NonNullable<S3CredentialsOption>;
22
14
  /** Base path where bundles will be stored in the bucket. */
23
15
  basePath?: string;
24
16
  }
25
17
 
26
- /** Devices download right after the update check that returns the URL. */
27
- const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
28
-
29
- const isObjectNotFoundError = (error: unknown) => {
30
- if (
31
- error instanceof Error &&
32
- (error.name === "NotFound" || error.name === "NoSuchKey")
33
- ) {
34
- return true;
35
- }
36
- return (
37
- typeof error === "object" &&
38
- error !== null &&
39
- "$metadata" in error &&
40
- (error as { $metadata?: { httpStatusCode?: number } }).$metadata
41
- ?.httpStatusCode === 404
42
- );
43
- };
44
-
45
18
  export const createR2S3Storage = (
46
19
  config: R2S3StorageConfig,
47
- ): StorageAdapterWith<
48
- "put" | "get" | "getDownloadUrl" | "exists" | "delete"
49
- > => {
50
- const {
51
- accountId,
52
- basePath,
53
- bucketName,
54
- endpoint,
55
- forcePathStyle,
56
- region,
57
- ...s3Config
58
- } = config;
59
- const client = new S3Client({
60
- ...s3Config,
61
- endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
62
- forcePathStyle: forcePathStyle ?? true,
63
- region: region ?? "auto",
64
- });
65
- const getStorageKey = createStorageKeyBuilder(basePath);
66
-
67
- const parseAndValidate = (storageUri: string) => {
68
- const parsed = parseStorageUri(storageUri, "r2");
69
- if (parsed.bucket !== bucketName) {
70
- throw new Error(
71
- `Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`,
72
- );
73
- }
74
- return parsed;
75
- };
76
-
77
- return createStorageAdapter({
78
- name: "r2Storage",
79
- protocol: "r2",
80
- async put({ key, body, contentLength, contentType }) {
81
- const storageKey = getStorageKey(key);
82
- await new Upload({
83
- client,
84
- params: {
85
- Body: body,
86
- Bucket: bucketName,
87
- CacheControl: "max-age=31536000",
88
- ContentLength: contentLength,
89
- ContentType: contentType,
90
- Key: storageKey,
91
- },
92
- }).done();
93
- return {
94
- storageUri: createStorageUri({
95
- protocol: "r2",
96
- bucket: bucketName,
97
- key: storageKey,
98
- }),
99
- };
100
- },
101
- async get({ storageUri }) {
102
- const { key } = parseAndValidate(storageUri);
103
- try {
104
- const response = await client.send(
105
- new GetObjectCommand({ Bucket: bucketName, Key: key }),
106
- );
107
- if (!response.Body) return { response: null };
108
- const headers = new Headers();
109
- if (response.ContentType)
110
- headers.set("content-type", response.ContentType);
111
- if (response.ContentLength !== undefined) {
112
- headers.set("content-length", String(response.ContentLength));
113
- }
114
- return {
115
- response: new Response(response.Body.transformToWebStream(), {
116
- headers,
117
- }),
118
- };
119
- } catch (error) {
120
- if (isObjectNotFoundError(error)) return { response: null };
121
- throw error;
122
- }
123
- },
124
- async getDownloadUrl({ storageUri }) {
125
- const { key } = parseAndValidate(storageUri);
126
- return {
127
- url: await getSignedUrl(
128
- client,
129
- new GetObjectCommand({ Bucket: bucketName, Key: key }),
130
- { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS },
131
- ),
132
- };
20
+ ): StorageAdapterWith<S3StorageOperations> => {
21
+ const { accountId, endpoint, forcePathStyle, region, ...s3Config } = config;
22
+ return createS3Storage(
23
+ { name: "r2Storage", protocol: "r2" },
24
+ {
25
+ ...s3Config,
26
+ endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
27
+ forcePathStyle: forcePathStyle ?? true,
28
+ region: region ?? "auto",
133
29
  },
134
- async exists({ storageUri }) {
135
- const { key } = parseAndValidate(storageUri);
136
- try {
137
- await client.send(
138
- new HeadObjectCommand({ Bucket: bucketName, Key: key }),
139
- );
140
- return { exists: true };
141
- } catch (error) {
142
- if (isObjectNotFoundError(error)) return { exists: false };
143
- throw error;
144
- }
145
- },
146
- async delete({ storageUri }) {
147
- const { key } = parseAndValidate(storageUri);
148
- await client.send(
149
- new DeleteObjectCommand({ Bucket: bucketName, Key: key }),
150
- );
151
- return { deleted: true };
152
- },
153
- });
30
+ );
154
31
  };
package/src/r2Storage.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { StorageAdapterWith } from "@hot-updater/plugin-core";
2
+ import type { S3StorageOperations } from "@hot-updater/storage-s3";
2
3
 
3
4
  import { createR2S3Storage, type R2S3StorageConfig } from "./r2S3Storage";
4
5
 
@@ -7,9 +8,7 @@ export type { R2S3StorageConfig };
7
8
 
8
9
  export const r2Storage = (
9
10
  config: R2S3StorageConfig,
10
- ): StorageAdapterWith<
11
- "put" | "get" | "getDownloadUrl" | "exists" | "delete"
12
- > => {
11
+ ): StorageAdapterWith<S3StorageOperations> => {
13
12
  if (!config.credentials) {
14
13
  throw new Error(
15
14
  "r2Storage requires S3-compatible credentials: pass credentials with an R2 access key ID and secret access key.",
@@ -4,6 +4,7 @@ import {
4
4
  createStorageUri,
5
5
  parseStorageUri,
6
6
  type StorageAdapterWith,
7
+ type StorageObject,
7
8
  } from "@hot-updater/plugin-core";
8
9
 
9
10
  import { presignR2GetUrl, type R2Credentials } from "./presignR2Url";
@@ -28,6 +29,16 @@ export type CloudflareWorkerStorageConfig = {
28
29
 
29
30
  /** Devices download right after the update check that returns the URL. */
30
31
  const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
32
+ /** Keys the binding deletes in one call. */
33
+ const DELETE_BATCH_SIZE = 1000;
34
+
35
+ type R2WorkerOperations =
36
+ | "put"
37
+ | "get"
38
+ | "exists"
39
+ | "delete"
40
+ | "listObjects"
41
+ | "deleteObjects";
31
42
 
32
43
  /**
33
44
  * Storage on the Worker's R2 binding. With R2's S3-compatible credentials,
@@ -39,14 +50,15 @@ export function r2WorkerStorage(
39
50
  config: CloudflareWorkerStorageConfig & {
40
51
  readonly credentials: R2Credentials;
41
52
  },
42
- ): StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete">;
53
+ ): StorageAdapterWith<R2WorkerOperations | "getDownloadUrl">;
43
54
  export function r2WorkerStorage(
44
55
  config: CloudflareWorkerStorageConfig,
45
- ): StorageAdapterWith<"put" | "get" | "exists" | "delete">;
56
+ ): StorageAdapterWith<R2WorkerOperations>;
46
57
  export function r2WorkerStorage(
47
58
  config: CloudflareWorkerStorageConfig,
48
- ): StorageAdapterWith<"put" | "get" | "exists" | "delete"> {
59
+ ): StorageAdapterWith<R2WorkerOperations> {
49
60
  const getStorageKey = createStorageKeyBuilder(config.basePath);
61
+ const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
50
62
 
51
63
  const parseAndValidate = (storageUri: string) => {
52
64
  const parsed = parseStorageUri(storageUri, "r2");
@@ -128,5 +140,46 @@ export function r2WorkerStorage(
128
140
  await config.bucket.delete(key);
129
141
  return { deleted: true };
130
142
  },
143
+ async listObjects(prefix) {
144
+ const folder = getStorageKey(prefix ?? "");
145
+ const objects: StorageObject[] = [];
146
+ let cursor: string | undefined;
147
+ do {
148
+ const listed = await config.bucket.list({
149
+ prefix: folder ? `${folder}/` : "",
150
+ ...(cursor === undefined ? {} : { cursor }),
151
+ });
152
+ for (const object of listed.objects) {
153
+ if (!object.key.startsWith(basePrefix)) continue;
154
+ // A key no `put` writes, such as a folder marker, has no URI.
155
+ let storageUri: string;
156
+ try {
157
+ storageUri = createStorageUri({
158
+ protocol: "r2",
159
+ bucket: config.bucketName,
160
+ key: object.key,
161
+ });
162
+ } catch {
163
+ continue;
164
+ }
165
+ objects.push({
166
+ key: object.key.slice(basePrefix.length),
167
+ lastModifiedAt: object.uploaded,
168
+ size: object.size,
169
+ storageUri,
170
+ });
171
+ }
172
+ cursor = listed.truncated ? listed.cursor : undefined;
173
+ } while (cursor !== undefined);
174
+ return objects;
175
+ },
176
+ async deleteObjects(relativeKeys) {
177
+ const keys = relativeKeys.map((key) => getStorageKey(key));
178
+ for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) {
179
+ await config.bucket.delete(
180
+ keys.slice(offset, offset + DELETE_BATCH_SIZE),
181
+ );
182
+ }
183
+ },
131
184
  });
132
185
  }
@@ -1 +1 @@
1
- This folder contains the built output assets for the worker "hot-updater" generated at 2026-10-29T02:19:11.482Z.
1
+ This folder contains the built output assets for the worker "hot-updater" generated at 2026-10-29T15:00:44.535Z.
@@ -4632,7 +4632,7 @@ var createSqlAdapter = /* @__PURE__ */ __name(({ executor, tablePrefix = "", max
4632
4632
  }, "createSqlAdapter");
4633
4633
 
4634
4634
  // ../../packages/server/dist/package.mjs
4635
- var version = "1.2.0-rc.7";
4635
+ var version = "1.2.0-rc.9";
4636
4636
 
4637
4637
  // ../../packages/server/dist/version.mjs
4638
4638
  var HOT_UPDATER_SERVER_VERSION = version;
@@ -13683,8 +13683,10 @@ var presignR2GetUrl = /* @__PURE__ */ __name(async ({
13683
13683
 
13684
13684
  // src/r2WorkerStorage.ts
13685
13685
  var PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
13686
+ var DELETE_BATCH_SIZE = 1e3;
13686
13687
  function r2WorkerStorage(config) {
13687
13688
  const getStorageKey = createStorageKeyBuilder(config.basePath);
13689
+ const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
13688
13690
  const parseAndValidate = /* @__PURE__ */ __name((storageUri) => {
13689
13691
  const parsed = parseStorageUri(storageUri, "r2");
13690
13692
  if (parsed.bucket !== config.bucketName) {
@@ -13761,6 +13763,46 @@ function r2WorkerStorage(config) {
13761
13763
  const { key } = parseAndValidate(storageUri);
13762
13764
  await config.bucket.delete(key);
13763
13765
  return { deleted: true };
13766
+ },
13767
+ async listObjects(prefix) {
13768
+ const folder = getStorageKey(prefix ?? "");
13769
+ const objects = [];
13770
+ let cursor;
13771
+ do {
13772
+ const listed2 = await config.bucket.list({
13773
+ prefix: folder ? `${folder}/` : "",
13774
+ ...cursor === void 0 ? {} : { cursor }
13775
+ });
13776
+ for (const object of listed2.objects) {
13777
+ if (!object.key.startsWith(basePrefix)) continue;
13778
+ let storageUri;
13779
+ try {
13780
+ storageUri = createStorageUri({
13781
+ protocol: "r2",
13782
+ bucket: config.bucketName,
13783
+ key: object.key
13784
+ });
13785
+ } catch {
13786
+ continue;
13787
+ }
13788
+ objects.push({
13789
+ key: object.key.slice(basePrefix.length),
13790
+ lastModifiedAt: object.uploaded,
13791
+ size: object.size,
13792
+ storageUri
13793
+ });
13794
+ }
13795
+ cursor = listed2.truncated ? listed2.cursor : void 0;
13796
+ } while (cursor !== void 0);
13797
+ return objects;
13798
+ },
13799
+ async deleteObjects(relativeKeys) {
13800
+ const keys = relativeKeys.map((key) => getStorageKey(key));
13801
+ for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) {
13802
+ await config.bucket.delete(
13803
+ keys.slice(offset, offset + DELETE_BATCH_SIZE)
13804
+ );
13805
+ }
13764
13806
  }
13765
13807
  });
13766
13808
  }