@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/dist/index.cjs +6 -85
- package/dist/index.d.cts +9 -8
- package/dist/index.d.mts +9 -8
- package/dist/index.mjs +7 -86
- package/dist/init/index.cjs +16 -17
- package/dist/init/index.mjs +17 -18
- package/dist/worker/index.cjs +39 -0
- package/dist/worker/index.d.cts +3 -2
- package/dist/worker/index.d.mts +3 -2
- package/dist/worker/index.mjs +39 -0
- package/package.json +10 -9
- package/src/d1Rest.ts +4 -4
- package/src/r2S3Storage.ts +18 -141
- package/src/r2Storage.ts +2 -3
- package/src/r2WorkerStorage.ts +56 -3
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +43 -1
- package/worker/dist/index.js.map +3 -3
package/dist/index.cjs
CHANGED
|
@@ -24,9 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
25
25
|
let cloudflare = require("cloudflare");
|
|
26
26
|
cloudflare = __toESM(cloudflare, 1);
|
|
27
|
-
let
|
|
28
|
-
let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
|
|
29
|
-
let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
27
|
+
let _hot_updater_storage_s3 = require("@hot-updater/storage-s3");
|
|
30
28
|
//#region src/d1Executor.ts
|
|
31
29
|
var D1ExecutionError = class extends Error {
|
|
32
30
|
name = "D1ExecutionError";
|
|
@@ -157,94 +155,17 @@ const d1Database = (config) => ({
|
|
|
157
155
|
});
|
|
158
156
|
//#endregion
|
|
159
157
|
//#region src/r2S3Storage.ts
|
|
160
|
-
/** Devices download right after the update check that returns the URL. */
|
|
161
|
-
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
162
|
-
const isObjectNotFoundError = (error) => {
|
|
163
|
-
if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
|
|
164
|
-
return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
|
|
165
|
-
};
|
|
166
158
|
const createR2S3Storage = (config) => {
|
|
167
|
-
const { accountId,
|
|
168
|
-
|
|
159
|
+
const { accountId, endpoint, forcePathStyle, region, ...s3Config } = config;
|
|
160
|
+
return (0, _hot_updater_storage_s3.createS3Storage)({
|
|
161
|
+
name: "r2Storage",
|
|
162
|
+
protocol: "r2"
|
|
163
|
+
}, {
|
|
169
164
|
...s3Config,
|
|
170
165
|
endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
|
|
171
166
|
forcePathStyle: forcePathStyle ?? true,
|
|
172
167
|
region: region ?? "auto"
|
|
173
168
|
});
|
|
174
|
-
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(basePath);
|
|
175
|
-
const parseAndValidate = (storageUri) => {
|
|
176
|
-
const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
|
|
177
|
-
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
178
|
-
return parsed;
|
|
179
|
-
};
|
|
180
|
-
return (0, _hot_updater_plugin_core.createStorageAdapter)({
|
|
181
|
-
name: "r2Storage",
|
|
182
|
-
protocol: "r2",
|
|
183
|
-
async put({ key, body, contentLength, contentType }) {
|
|
184
|
-
const storageKey = getStorageKey(key);
|
|
185
|
-
await new _aws_sdk_lib_storage.Upload({
|
|
186
|
-
client,
|
|
187
|
-
params: {
|
|
188
|
-
Body: body,
|
|
189
|
-
Bucket: bucketName,
|
|
190
|
-
CacheControl: "max-age=31536000",
|
|
191
|
-
ContentLength: contentLength,
|
|
192
|
-
ContentType: contentType,
|
|
193
|
-
Key: storageKey
|
|
194
|
-
}
|
|
195
|
-
}).done();
|
|
196
|
-
return { storageUri: (0, _hot_updater_plugin_core.createStorageUri)({
|
|
197
|
-
protocol: "r2",
|
|
198
|
-
bucket: bucketName,
|
|
199
|
-
key: storageKey
|
|
200
|
-
}) };
|
|
201
|
-
},
|
|
202
|
-
async get({ storageUri }) {
|
|
203
|
-
const { key } = parseAndValidate(storageUri);
|
|
204
|
-
try {
|
|
205
|
-
const response = await client.send(new _aws_sdk_client_s3.GetObjectCommand({
|
|
206
|
-
Bucket: bucketName,
|
|
207
|
-
Key: key
|
|
208
|
-
}));
|
|
209
|
-
if (!response.Body) return { response: null };
|
|
210
|
-
const headers = new Headers();
|
|
211
|
-
if (response.ContentType) headers.set("content-type", response.ContentType);
|
|
212
|
-
if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
|
|
213
|
-
return { response: new Response(response.Body.transformToWebStream(), { headers }) };
|
|
214
|
-
} catch (error) {
|
|
215
|
-
if (isObjectNotFoundError(error)) return { response: null };
|
|
216
|
-
throw error;
|
|
217
|
-
}
|
|
218
|
-
},
|
|
219
|
-
async getDownloadUrl({ storageUri }) {
|
|
220
|
-
const { key } = parseAndValidate(storageUri);
|
|
221
|
-
return { url: await (0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.GetObjectCommand({
|
|
222
|
-
Bucket: bucketName,
|
|
223
|
-
Key: key
|
|
224
|
-
}), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
|
|
225
|
-
},
|
|
226
|
-
async exists({ storageUri }) {
|
|
227
|
-
const { key } = parseAndValidate(storageUri);
|
|
228
|
-
try {
|
|
229
|
-
await client.send(new _aws_sdk_client_s3.HeadObjectCommand({
|
|
230
|
-
Bucket: bucketName,
|
|
231
|
-
Key: key
|
|
232
|
-
}));
|
|
233
|
-
return { exists: true };
|
|
234
|
-
} catch (error) {
|
|
235
|
-
if (isObjectNotFoundError(error)) return { exists: false };
|
|
236
|
-
throw error;
|
|
237
|
-
}
|
|
238
|
-
},
|
|
239
|
-
async delete({ storageUri }) {
|
|
240
|
-
const { key } = parseAndValidate(storageUri);
|
|
241
|
-
await client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
|
|
242
|
-
Bucket: bucketName,
|
|
243
|
-
Key: key
|
|
244
|
-
}));
|
|
245
|
-
return { deleted: true };
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
169
|
};
|
|
249
170
|
//#endregion
|
|
250
171
|
//#region src/r2Storage.ts
|
package/dist/index.d.cts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { StorageAdapterWith, ToolingDatabase } from "@hot-updater/plugin-core";
|
|
2
|
-
import { S3ClientConfig } from "@
|
|
1
|
+
import { Lazy, StorageAdapterWith, ToolingDatabase } from "@hot-updater/plugin-core";
|
|
2
|
+
import { S3ClientConfig, S3CredentialsOption, S3StorageOperations } from "@hot-updater/storage-s3";
|
|
3
3
|
//#region src/d1Rest.d.ts
|
|
4
4
|
interface D1RestConfig {
|
|
5
5
|
readonly databaseId: string;
|
|
6
6
|
readonly accountId: string;
|
|
7
7
|
/**
|
|
8
8
|
* An API token with D1 edit permission, or a function that returns it,
|
|
9
|
-
* such as
|
|
10
|
-
* so a config loads without the token.
|
|
9
|
+
* such as `secret("<name>")` in `hot-updater.config.ts`: called on the
|
|
10
|
+
* first query, so a config loads without the token.
|
|
11
11
|
*/
|
|
12
|
-
readonly cloudflareApiToken:
|
|
12
|
+
readonly cloudflareApiToken: Lazy<string>;
|
|
13
13
|
}
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/d1Executor.d.ts
|
|
@@ -31,16 +31,17 @@ type D1DatabaseConfig = D1RestConfig;
|
|
|
31
31
|
declare const d1Database: (config: D1DatabaseConfig) => ToolingDatabase;
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region src/r2S3Storage.d.ts
|
|
34
|
-
interface R2S3StorageConfig extends S3ClientConfig {
|
|
34
|
+
interface R2S3StorageConfig extends Omit<S3ClientConfig, "credentials"> {
|
|
35
35
|
accountId: string;
|
|
36
36
|
bucketName: string;
|
|
37
|
-
credentials
|
|
37
|
+
/** The SDK's credentials, or an R2 access key whose fields are lazy. */
|
|
38
|
+
credentials: NonNullable<S3CredentialsOption>;
|
|
38
39
|
/** Base path where bundles will be stored in the bucket. */
|
|
39
40
|
basePath?: string;
|
|
40
41
|
}
|
|
41
42
|
//#endregion
|
|
42
43
|
//#region src/r2Storage.d.ts
|
|
43
44
|
type R2StorageConfig = R2S3StorageConfig;
|
|
44
|
-
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<
|
|
45
|
+
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<S3StorageOperations>;
|
|
45
46
|
//#endregion
|
|
46
47
|
export { D1DatabaseConfig, D1ExecutionError, type R2S3StorageConfig, R2StorageConfig, d1Database, r2Storage };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { StorageAdapterWith, ToolingDatabase } from "@hot-updater/plugin-core";
|
|
2
|
-
import { S3ClientConfig } from "@
|
|
1
|
+
import { Lazy, StorageAdapterWith, ToolingDatabase } from "@hot-updater/plugin-core";
|
|
2
|
+
import { S3ClientConfig, S3CredentialsOption, S3StorageOperations } from "@hot-updater/storage-s3";
|
|
3
3
|
//#region src/d1Rest.d.ts
|
|
4
4
|
interface D1RestConfig {
|
|
5
5
|
readonly databaseId: string;
|
|
6
6
|
readonly accountId: string;
|
|
7
7
|
/**
|
|
8
8
|
* An API token with D1 edit permission, or a function that returns it,
|
|
9
|
-
* such as
|
|
10
|
-
* so a config loads without the token.
|
|
9
|
+
* such as `secret("<name>")` in `hot-updater.config.ts`: called on the
|
|
10
|
+
* first query, so a config loads without the token.
|
|
11
11
|
*/
|
|
12
|
-
readonly cloudflareApiToken:
|
|
12
|
+
readonly cloudflareApiToken: Lazy<string>;
|
|
13
13
|
}
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/d1Executor.d.ts
|
|
@@ -31,16 +31,17 @@ type D1DatabaseConfig = D1RestConfig;
|
|
|
31
31
|
declare const d1Database: (config: D1DatabaseConfig) => ToolingDatabase;
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region src/r2S3Storage.d.ts
|
|
34
|
-
interface R2S3StorageConfig extends S3ClientConfig {
|
|
34
|
+
interface R2S3StorageConfig extends Omit<S3ClientConfig, "credentials"> {
|
|
35
35
|
accountId: string;
|
|
36
36
|
bucketName: string;
|
|
37
|
-
credentials
|
|
37
|
+
/** The SDK's credentials, or an R2 access key whose fields are lazy. */
|
|
38
|
+
credentials: NonNullable<S3CredentialsOption>;
|
|
38
39
|
/** Base path where bundles will be stored in the bucket. */
|
|
39
40
|
basePath?: string;
|
|
40
41
|
}
|
|
41
42
|
//#endregion
|
|
42
43
|
//#region src/r2Storage.d.ts
|
|
43
44
|
type R2StorageConfig = R2S3StorageConfig;
|
|
44
|
-
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<
|
|
45
|
+
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<S3StorageOperations>;
|
|
45
46
|
//#endregion
|
|
46
47
|
export { D1DatabaseConfig, D1ExecutionError, type R2S3StorageConfig, R2StorageConfig, d1Database, r2Storage };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { WRITE_GUARD_TABLE, coreSettings, coreTarget, createEngineDatabase, createSqlAdapter,
|
|
1
|
+
import { WRITE_GUARD_TABLE, coreSettings, coreTarget, createEngineDatabase, createSqlAdapter, createTableStatements, generateEngineSql } from "@hot-updater/plugin-core";
|
|
2
2
|
import Cloudflare from "cloudflare";
|
|
3
|
-
import {
|
|
4
|
-
import { Upload } from "@aws-sdk/lib-storage";
|
|
5
|
-
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
3
|
+
import { createS3Storage } from "@hot-updater/storage-s3";
|
|
6
4
|
//#region src/d1Executor.ts
|
|
7
5
|
var D1ExecutionError = class extends Error {
|
|
8
6
|
name = "D1ExecutionError";
|
|
@@ -133,94 +131,17 @@ const d1Database = (config) => ({
|
|
|
133
131
|
});
|
|
134
132
|
//#endregion
|
|
135
133
|
//#region src/r2S3Storage.ts
|
|
136
|
-
/** Devices download right after the update check that returns the URL. */
|
|
137
|
-
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
138
|
-
const isObjectNotFoundError = (error) => {
|
|
139
|
-
if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
|
|
140
|
-
return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
|
|
141
|
-
};
|
|
142
134
|
const createR2S3Storage = (config) => {
|
|
143
|
-
const { accountId,
|
|
144
|
-
|
|
135
|
+
const { accountId, endpoint, forcePathStyle, region, ...s3Config } = config;
|
|
136
|
+
return createS3Storage({
|
|
137
|
+
name: "r2Storage",
|
|
138
|
+
protocol: "r2"
|
|
139
|
+
}, {
|
|
145
140
|
...s3Config,
|
|
146
141
|
endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
|
|
147
142
|
forcePathStyle: forcePathStyle ?? true,
|
|
148
143
|
region: region ?? "auto"
|
|
149
144
|
});
|
|
150
|
-
const getStorageKey = createStorageKeyBuilder(basePath);
|
|
151
|
-
const parseAndValidate = (storageUri) => {
|
|
152
|
-
const parsed = parseStorageUri(storageUri, "r2");
|
|
153
|
-
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
154
|
-
return parsed;
|
|
155
|
-
};
|
|
156
|
-
return createStorageAdapter({
|
|
157
|
-
name: "r2Storage",
|
|
158
|
-
protocol: "r2",
|
|
159
|
-
async put({ key, body, contentLength, contentType }) {
|
|
160
|
-
const storageKey = getStorageKey(key);
|
|
161
|
-
await new Upload({
|
|
162
|
-
client,
|
|
163
|
-
params: {
|
|
164
|
-
Body: body,
|
|
165
|
-
Bucket: bucketName,
|
|
166
|
-
CacheControl: "max-age=31536000",
|
|
167
|
-
ContentLength: contentLength,
|
|
168
|
-
ContentType: contentType,
|
|
169
|
-
Key: storageKey
|
|
170
|
-
}
|
|
171
|
-
}).done();
|
|
172
|
-
return { storageUri: createStorageUri({
|
|
173
|
-
protocol: "r2",
|
|
174
|
-
bucket: bucketName,
|
|
175
|
-
key: storageKey
|
|
176
|
-
}) };
|
|
177
|
-
},
|
|
178
|
-
async get({ storageUri }) {
|
|
179
|
-
const { key } = parseAndValidate(storageUri);
|
|
180
|
-
try {
|
|
181
|
-
const response = await client.send(new GetObjectCommand({
|
|
182
|
-
Bucket: bucketName,
|
|
183
|
-
Key: key
|
|
184
|
-
}));
|
|
185
|
-
if (!response.Body) return { response: null };
|
|
186
|
-
const headers = new Headers();
|
|
187
|
-
if (response.ContentType) headers.set("content-type", response.ContentType);
|
|
188
|
-
if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
|
|
189
|
-
return { response: new Response(response.Body.transformToWebStream(), { headers }) };
|
|
190
|
-
} catch (error) {
|
|
191
|
-
if (isObjectNotFoundError(error)) return { response: null };
|
|
192
|
-
throw error;
|
|
193
|
-
}
|
|
194
|
-
},
|
|
195
|
-
async getDownloadUrl({ storageUri }) {
|
|
196
|
-
const { key } = parseAndValidate(storageUri);
|
|
197
|
-
return { url: await getSignedUrl(client, new GetObjectCommand({
|
|
198
|
-
Bucket: bucketName,
|
|
199
|
-
Key: key
|
|
200
|
-
}), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
|
|
201
|
-
},
|
|
202
|
-
async exists({ storageUri }) {
|
|
203
|
-
const { key } = parseAndValidate(storageUri);
|
|
204
|
-
try {
|
|
205
|
-
await client.send(new HeadObjectCommand({
|
|
206
|
-
Bucket: bucketName,
|
|
207
|
-
Key: key
|
|
208
|
-
}));
|
|
209
|
-
return { exists: true };
|
|
210
|
-
} catch (error) {
|
|
211
|
-
if (isObjectNotFoundError(error)) return { exists: false };
|
|
212
|
-
throw error;
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
async delete({ storageUri }) {
|
|
216
|
-
const { key } = parseAndValidate(storageUri);
|
|
217
|
-
await client.send(new DeleteObjectCommand({
|
|
218
|
-
Bucket: bucketName,
|
|
219
|
-
Key: key
|
|
220
|
-
}));
|
|
221
|
-
return { deleted: true };
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
145
|
};
|
|
225
146
|
//#endregion
|
|
226
147
|
//#region src/r2Storage.ts
|
package/dist/init/index.cjs
CHANGED
|
@@ -11420,10 +11420,11 @@ const inputCloudflareInitSecrets = async ({ accountId, bucketName, apiToken, acc
|
|
|
11420
11420
|
//#region iac/configTemplate.ts
|
|
11421
11421
|
/**
|
|
11422
11422
|
* The config init writes. With the `resources` init set up, it names them,
|
|
11423
|
-
* and reads the API token and R2 keys with `
|
|
11423
|
+
* and reads the API token and R2 keys with `secret()`, from the
|
|
11424
11424
|
* environment and `.env.hotupdater`, then the config's `secrets` adapter.
|
|
11425
|
-
* Without them, as the agent scaffold writes it
|
|
11426
|
-
* the environment
|
|
11425
|
+
* Without them, as the agent scaffold writes it before the agent creates the
|
|
11426
|
+
* resources, it reads their IDs from the environment, where the agent
|
|
11427
|
+
* records them, and the credentials with `secret()` alike.
|
|
11427
11428
|
*/
|
|
11428
11429
|
const getConfigScaffold = (build, resources) => {
|
|
11429
11430
|
const builder = new _hot_updater_cli_tools.ConfigBuilder().setBuildType(build).setPlugins({
|
|
@@ -11446,8 +11447,8 @@ const getConfigScaffold = (build, resources) => {
|
|
|
11446
11447
|
bucketName: process.env.HOT_UPDATER_CLOUDFLARE_R2_BUCKET_NAME!,
|
|
11447
11448
|
accountId: process.env.HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID!,
|
|
11448
11449
|
credentials: {
|
|
11449
|
-
accessKeyId:
|
|
11450
|
-
secretAccessKey:
|
|
11450
|
+
accessKeyId: secret("HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID"),
|
|
11451
|
+
secretAccessKey: secret("HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY"),
|
|
11451
11452
|
},
|
|
11452
11453
|
})`
|
|
11453
11454
|
}).setDatabase({
|
|
@@ -11458,8 +11459,11 @@ const getConfigScaffold = (build, resources) => {
|
|
|
11458
11459
|
configString: `d1Database({
|
|
11459
11460
|
databaseId: process.env.HOT_UPDATER_CLOUDFLARE_D1_DATABASE_ID!,
|
|
11460
11461
|
accountId: process.env.HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID!,
|
|
11461
|
-
cloudflareApiToken:
|
|
11462
|
+
cloudflareApiToken: secret("HOT_UPDATER_CLOUDFLARE_API_TOKEN"),
|
|
11462
11463
|
})`
|
|
11464
|
+
}).addImport({
|
|
11465
|
+
pkg: "hot-updater",
|
|
11466
|
+
named: ["secret"]
|
|
11463
11467
|
}));
|
|
11464
11468
|
const accountId = JSON.stringify(resources.accountId);
|
|
11465
11469
|
const storageConfig = {
|
|
@@ -11470,12 +11474,9 @@ const getConfigScaffold = (build, resources) => {
|
|
|
11470
11474
|
configString: `r2Storage({
|
|
11471
11475
|
bucketName: ${JSON.stringify(resources.bucketName)},
|
|
11472
11476
|
accountId: ${accountId},
|
|
11473
|
-
credentials:
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
accessKeyId: env.HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID,
|
|
11477
|
-
secretAccessKey: env.HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY,
|
|
11478
|
-
};
|
|
11477
|
+
credentials: {
|
|
11478
|
+
accessKeyId: secret("HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID"),
|
|
11479
|
+
secretAccessKey: secret("HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY"),
|
|
11479
11480
|
},
|
|
11480
11481
|
})`
|
|
11481
11482
|
};
|
|
@@ -11487,15 +11488,13 @@ const getConfigScaffold = (build, resources) => {
|
|
|
11487
11488
|
configString: `d1Database({
|
|
11488
11489
|
databaseId: ${JSON.stringify(resources.d1DatabaseId)},
|
|
11489
11490
|
accountId: ${accountId},
|
|
11490
|
-
cloudflareApiToken:
|
|
11491
|
-
(await databaseSecrets()).HOT_UPDATER_CLOUDFLARE_API_TOKEN,
|
|
11491
|
+
cloudflareApiToken: secret("HOT_UPDATER_CLOUDFLARE_API_TOKEN"),
|
|
11492
11492
|
})`
|
|
11493
11493
|
};
|
|
11494
|
-
const helperStatements = [(0, _hot_updater_cli_tools.secretEnvHelper)("storageSecrets", ["HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID", "HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY"]), (0, _hot_updater_cli_tools.secretEnvHelper)("databaseSecrets", ["HOT_UPDATER_CLOUDFLARE_API_TOKEN"])];
|
|
11495
11494
|
return (0, _hot_updater_cli_tools.createHotUpdaterConfigScaffoldFromBuilder)(builder.setStorage(storageConfig).setDatabase(databaseConfig).addImport({
|
|
11496
11495
|
pkg: "hot-updater",
|
|
11497
|
-
named: ["
|
|
11498
|
-
})
|
|
11496
|
+
named: ["secret"]
|
|
11497
|
+
}));
|
|
11499
11498
|
};
|
|
11500
11499
|
//#endregion
|
|
11501
11500
|
//#region iac/managedWorker.ts
|
package/dist/init/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { ConfigBuilder, InitError, LegacyInfrastructureError, assembleServer, assertInfrastructureGenerationAtUrl, assertInitInputs, assertInitProviderInputs, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, printAppSetup, provisionClientCredential, readHotUpdaterInitEnv, resolveInitProviderInput,
|
|
3
|
+
import { ConfigBuilder, InitError, LegacyInfrastructureError, assembleServer, assertInfrastructureGenerationAtUrl, assertInitInputs, assertInitProviderInputs, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, printAppSetup, provisionClientCredential, readHotUpdaterInitEnv, resolveInitProviderInput, writeHotUpdaterFiles } from "@hot-updater/cli-tools";
|
|
4
4
|
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
|
|
5
5
|
import Cloudflare$1, { Cloudflare } from "cloudflare";
|
|
6
6
|
import { WRITE_GUARD_TABLE, coreSettings, coreTarget, createEngineDatabase, createSqlAdapter, createTableStatements, generateEngineSql } from "@hot-updater/plugin-core";
|
|
@@ -11413,10 +11413,11 @@ const inputCloudflareInitSecrets = async ({ accountId, bucketName, apiToken, acc
|
|
|
11413
11413
|
//#region iac/configTemplate.ts
|
|
11414
11414
|
/**
|
|
11415
11415
|
* The config init writes. With the `resources` init set up, it names them,
|
|
11416
|
-
* and reads the API token and R2 keys with `
|
|
11416
|
+
* and reads the API token and R2 keys with `secret()`, from the
|
|
11417
11417
|
* environment and `.env.hotupdater`, then the config's `secrets` adapter.
|
|
11418
|
-
* Without them, as the agent scaffold writes it
|
|
11419
|
-
* the environment
|
|
11418
|
+
* Without them, as the agent scaffold writes it before the agent creates the
|
|
11419
|
+
* resources, it reads their IDs from the environment, where the agent
|
|
11420
|
+
* records them, and the credentials with `secret()` alike.
|
|
11420
11421
|
*/
|
|
11421
11422
|
const getConfigScaffold = (build, resources) => {
|
|
11422
11423
|
const builder = new ConfigBuilder().setBuildType(build).setPlugins({
|
|
@@ -11439,8 +11440,8 @@ const getConfigScaffold = (build, resources) => {
|
|
|
11439
11440
|
bucketName: process.env.HOT_UPDATER_CLOUDFLARE_R2_BUCKET_NAME!,
|
|
11440
11441
|
accountId: process.env.HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID!,
|
|
11441
11442
|
credentials: {
|
|
11442
|
-
accessKeyId:
|
|
11443
|
-
secretAccessKey:
|
|
11443
|
+
accessKeyId: secret("HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID"),
|
|
11444
|
+
secretAccessKey: secret("HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY"),
|
|
11444
11445
|
},
|
|
11445
11446
|
})`
|
|
11446
11447
|
}).setDatabase({
|
|
@@ -11451,8 +11452,11 @@ const getConfigScaffold = (build, resources) => {
|
|
|
11451
11452
|
configString: `d1Database({
|
|
11452
11453
|
databaseId: process.env.HOT_UPDATER_CLOUDFLARE_D1_DATABASE_ID!,
|
|
11453
11454
|
accountId: process.env.HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID!,
|
|
11454
|
-
cloudflareApiToken:
|
|
11455
|
+
cloudflareApiToken: secret("HOT_UPDATER_CLOUDFLARE_API_TOKEN"),
|
|
11455
11456
|
})`
|
|
11457
|
+
}).addImport({
|
|
11458
|
+
pkg: "hot-updater",
|
|
11459
|
+
named: ["secret"]
|
|
11456
11460
|
}));
|
|
11457
11461
|
const accountId = JSON.stringify(resources.accountId);
|
|
11458
11462
|
const storageConfig = {
|
|
@@ -11463,12 +11467,9 @@ const getConfigScaffold = (build, resources) => {
|
|
|
11463
11467
|
configString: `r2Storage({
|
|
11464
11468
|
bucketName: ${JSON.stringify(resources.bucketName)},
|
|
11465
11469
|
accountId: ${accountId},
|
|
11466
|
-
credentials:
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
accessKeyId: env.HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID,
|
|
11470
|
-
secretAccessKey: env.HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY,
|
|
11471
|
-
};
|
|
11470
|
+
credentials: {
|
|
11471
|
+
accessKeyId: secret("HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID"),
|
|
11472
|
+
secretAccessKey: secret("HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY"),
|
|
11472
11473
|
},
|
|
11473
11474
|
})`
|
|
11474
11475
|
};
|
|
@@ -11480,15 +11481,13 @@ const getConfigScaffold = (build, resources) => {
|
|
|
11480
11481
|
configString: `d1Database({
|
|
11481
11482
|
databaseId: ${JSON.stringify(resources.d1DatabaseId)},
|
|
11482
11483
|
accountId: ${accountId},
|
|
11483
|
-
cloudflareApiToken:
|
|
11484
|
-
(await databaseSecrets()).HOT_UPDATER_CLOUDFLARE_API_TOKEN,
|
|
11484
|
+
cloudflareApiToken: secret("HOT_UPDATER_CLOUDFLARE_API_TOKEN"),
|
|
11485
11485
|
})`
|
|
11486
11486
|
};
|
|
11487
|
-
const helperStatements = [secretEnvHelper("storageSecrets", ["HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID", "HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY"]), secretEnvHelper("databaseSecrets", ["HOT_UPDATER_CLOUDFLARE_API_TOKEN"])];
|
|
11488
11487
|
return createHotUpdaterConfigScaffoldFromBuilder(builder.setStorage(storageConfig).setDatabase(databaseConfig).addImport({
|
|
11489
11488
|
pkg: "hot-updater",
|
|
11490
|
-
named: ["
|
|
11491
|
-
})
|
|
11489
|
+
named: ["secret"]
|
|
11490
|
+
}));
|
|
11492
11491
|
};
|
|
11493
11492
|
//#endregion
|
|
11494
11493
|
//#region iac/managedWorker.ts
|
package/dist/worker/index.cjs
CHANGED
|
@@ -121,8 +121,11 @@ const presignR2GetUrl = async ({ accountId, bucketName, credentials, expiresInSe
|
|
|
121
121
|
//#region src/r2WorkerStorage.ts
|
|
122
122
|
/** Devices download right after the update check that returns the URL. */
|
|
123
123
|
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
124
|
+
/** Keys the binding deletes in one call. */
|
|
125
|
+
const DELETE_BATCH_SIZE = 1e3;
|
|
124
126
|
function r2WorkerStorage(config) {
|
|
125
127
|
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
|
|
128
|
+
const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
|
|
126
129
|
const parseAndValidate = (storageUri) => {
|
|
127
130
|
const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
|
|
128
131
|
if (parsed.bucket !== config.bucketName) throw new Error(`Bucket name mismatch: expected "${config.bucketName}", but found "${parsed.bucket}".`);
|
|
@@ -178,6 +181,42 @@ function r2WorkerStorage(config) {
|
|
|
178
181
|
const { key } = parseAndValidate(storageUri);
|
|
179
182
|
await config.bucket.delete(key);
|
|
180
183
|
return { deleted: true };
|
|
184
|
+
},
|
|
185
|
+
async listObjects(prefix) {
|
|
186
|
+
const folder = getStorageKey(prefix ?? "");
|
|
187
|
+
const objects = [];
|
|
188
|
+
let cursor;
|
|
189
|
+
do {
|
|
190
|
+
const listed = await config.bucket.list({
|
|
191
|
+
prefix: folder ? `${folder}/` : "",
|
|
192
|
+
...cursor === void 0 ? {} : { cursor }
|
|
193
|
+
});
|
|
194
|
+
for (const object of listed.objects) {
|
|
195
|
+
if (!object.key.startsWith(basePrefix)) continue;
|
|
196
|
+
let storageUri;
|
|
197
|
+
try {
|
|
198
|
+
storageUri = (0, _hot_updater_plugin_core.createStorageUri)({
|
|
199
|
+
protocol: "r2",
|
|
200
|
+
bucket: config.bucketName,
|
|
201
|
+
key: object.key
|
|
202
|
+
});
|
|
203
|
+
} catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
objects.push({
|
|
207
|
+
key: object.key.slice(basePrefix.length),
|
|
208
|
+
lastModifiedAt: object.uploaded,
|
|
209
|
+
size: object.size,
|
|
210
|
+
storageUri
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
cursor = listed.truncated ? listed.cursor : void 0;
|
|
214
|
+
} while (cursor !== void 0);
|
|
215
|
+
return objects;
|
|
216
|
+
},
|
|
217
|
+
async deleteObjects(relativeKeys) {
|
|
218
|
+
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
219
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) await config.bucket.delete(keys.slice(offset, offset + DELETE_BATCH_SIZE));
|
|
181
220
|
}
|
|
182
221
|
});
|
|
183
222
|
}
|
package/dist/worker/index.d.cts
CHANGED
|
@@ -48,6 +48,7 @@ type CloudflareWorkerStorageConfig = {
|
|
|
48
48
|
readonly accountId?: undefined;
|
|
49
49
|
readonly credentials?: undefined;
|
|
50
50
|
});
|
|
51
|
+
type R2WorkerOperations = "put" | "get" | "exists" | "delete" | "listObjects" | "deleteObjects";
|
|
51
52
|
/**
|
|
52
53
|
* Storage on the Worker's R2 binding. With R2's S3-compatible credentials,
|
|
53
54
|
* its download URLs are presigned and devices download from the bucket;
|
|
@@ -56,7 +57,7 @@ type CloudflareWorkerStorageConfig = {
|
|
|
56
57
|
*/
|
|
57
58
|
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig & {
|
|
58
59
|
readonly credentials: R2Credentials;
|
|
59
|
-
}): StorageAdapterWith<
|
|
60
|
-
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig): StorageAdapterWith<
|
|
60
|
+
}): StorageAdapterWith<R2WorkerOperations | "getDownloadUrl">;
|
|
61
|
+
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig): StorageAdapterWith<R2WorkerOperations>;
|
|
61
62
|
//#endregion
|
|
62
63
|
export { type CloudflareWorkerStorageConfig, type D1Like, d1Database, r2WorkerStorage as r2Storage };
|
package/dist/worker/index.d.mts
CHANGED
|
@@ -48,6 +48,7 @@ type CloudflareWorkerStorageConfig = {
|
|
|
48
48
|
readonly accountId?: undefined;
|
|
49
49
|
readonly credentials?: undefined;
|
|
50
50
|
});
|
|
51
|
+
type R2WorkerOperations = "put" | "get" | "exists" | "delete" | "listObjects" | "deleteObjects";
|
|
51
52
|
/**
|
|
52
53
|
* Storage on the Worker's R2 binding. With R2's S3-compatible credentials,
|
|
53
54
|
* its download URLs are presigned and devices download from the bucket;
|
|
@@ -56,7 +57,7 @@ type CloudflareWorkerStorageConfig = {
|
|
|
56
57
|
*/
|
|
57
58
|
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig & {
|
|
58
59
|
readonly credentials: R2Credentials;
|
|
59
|
-
}): StorageAdapterWith<
|
|
60
|
-
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig): StorageAdapterWith<
|
|
60
|
+
}): StorageAdapterWith<R2WorkerOperations | "getDownloadUrl">;
|
|
61
|
+
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig): StorageAdapterWith<R2WorkerOperations>;
|
|
61
62
|
//#endregion
|
|
62
63
|
export { type CloudflareWorkerStorageConfig, type D1Like, d1Database, r2WorkerStorage as r2Storage };
|
package/dist/worker/index.mjs
CHANGED
|
@@ -120,8 +120,11 @@ const presignR2GetUrl = async ({ accountId, bucketName, credentials, expiresInSe
|
|
|
120
120
|
//#region src/r2WorkerStorage.ts
|
|
121
121
|
/** Devices download right after the update check that returns the URL. */
|
|
122
122
|
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
123
|
+
/** Keys the binding deletes in one call. */
|
|
124
|
+
const DELETE_BATCH_SIZE = 1e3;
|
|
123
125
|
function r2WorkerStorage(config) {
|
|
124
126
|
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
127
|
+
const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
|
|
125
128
|
const parseAndValidate = (storageUri) => {
|
|
126
129
|
const parsed = parseStorageUri(storageUri, "r2");
|
|
127
130
|
if (parsed.bucket !== config.bucketName) throw new Error(`Bucket name mismatch: expected "${config.bucketName}", but found "${parsed.bucket}".`);
|
|
@@ -177,6 +180,42 @@ function r2WorkerStorage(config) {
|
|
|
177
180
|
const { key } = parseAndValidate(storageUri);
|
|
178
181
|
await config.bucket.delete(key);
|
|
179
182
|
return { deleted: true };
|
|
183
|
+
},
|
|
184
|
+
async listObjects(prefix) {
|
|
185
|
+
const folder = getStorageKey(prefix ?? "");
|
|
186
|
+
const objects = [];
|
|
187
|
+
let cursor;
|
|
188
|
+
do {
|
|
189
|
+
const listed = await config.bucket.list({
|
|
190
|
+
prefix: folder ? `${folder}/` : "",
|
|
191
|
+
...cursor === void 0 ? {} : { cursor }
|
|
192
|
+
});
|
|
193
|
+
for (const object of listed.objects) {
|
|
194
|
+
if (!object.key.startsWith(basePrefix)) continue;
|
|
195
|
+
let storageUri;
|
|
196
|
+
try {
|
|
197
|
+
storageUri = createStorageUri({
|
|
198
|
+
protocol: "r2",
|
|
199
|
+
bucket: config.bucketName,
|
|
200
|
+
key: object.key
|
|
201
|
+
});
|
|
202
|
+
} catch {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
objects.push({
|
|
206
|
+
key: object.key.slice(basePrefix.length),
|
|
207
|
+
lastModifiedAt: object.uploaded,
|
|
208
|
+
size: object.size,
|
|
209
|
+
storageUri
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
cursor = listed.truncated ? listed.cursor : void 0;
|
|
213
|
+
} while (cursor !== void 0);
|
|
214
|
+
return objects;
|
|
215
|
+
},
|
|
216
|
+
async deleteObjects(relativeKeys) {
|
|
217
|
+
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
218
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) await config.bucket.delete(keys.slice(offset, offset + DELETE_BATCH_SIZE));
|
|
180
219
|
}
|
|
181
220
|
});
|
|
182
221
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/cloudflare",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.2.0-rc.
|
|
4
|
+
"version": "1.2.0-rc.9",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -49,18 +49,19 @@
|
|
|
49
49
|
"package.json"
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@aws-sdk/client-s3": "3.1066.0",
|
|
53
|
-
"@aws-sdk/lib-storage": "3.1008.0",
|
|
54
|
-
"@aws-sdk/s3-request-presigner": "3.1066.0",
|
|
55
52
|
"cloudflare": "4.2.0",
|
|
56
53
|
"hono": "4.12.34",
|
|
57
54
|
"uuidv7": "^1.0.2",
|
|
58
|
-
"@hot-updater/cli-tools": "1.2.0-rc.
|
|
59
|
-
"@hot-updater/protocol": "1.2.0-rc.
|
|
60
|
-
"@hot-updater/plugin-core": "1.2.0-rc.
|
|
61
|
-
"@hot-updater/server": "1.2.0-rc.
|
|
55
|
+
"@hot-updater/cli-tools": "1.2.0-rc.9",
|
|
56
|
+
"@hot-updater/protocol": "1.2.0-rc.9",
|
|
57
|
+
"@hot-updater/plugin-core": "1.2.0-rc.9",
|
|
58
|
+
"@hot-updater/server": "1.2.0-rc.9",
|
|
59
|
+
"@hot-updater/storage-s3": "1.2.0-rc.9"
|
|
62
60
|
},
|
|
63
61
|
"devDependencies": {
|
|
62
|
+
"@aws-sdk/client-s3": "3.1066.0",
|
|
63
|
+
"@aws-sdk/lib-storage": "3.1066.0",
|
|
64
|
+
"@aws-sdk/s3-request-presigner": "3.1066.0",
|
|
64
65
|
"@cloudflare/vitest-pool-workers": "0.13.0",
|
|
65
66
|
"@cloudflare/workers-types": "^4.20260312.1",
|
|
66
67
|
"@types/node": "^20",
|
|
@@ -74,7 +75,7 @@
|
|
|
74
75
|
"vitest": "4.1.4",
|
|
75
76
|
"wrangler": "^4.69.0",
|
|
76
77
|
"xdg-app-paths": "^8.3.0",
|
|
77
|
-
"@hot-updater/test-utils": "1.2.0-rc.
|
|
78
|
+
"@hot-updater/test-utils": "1.2.0-rc.9"
|
|
78
79
|
},
|
|
79
80
|
"scripts": {
|
|
80
81
|
"build": "tsdown && pnpm build:worker",
|