@hot-updater/cloudflare 1.0.0-rc.42 → 1.0.0-rc.44
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 +16 -13
- package/dist/index.d.cts +1 -3
- package/dist/index.d.mts +1 -3
- package/dist/index.mjs +14 -11
- package/dist/init/index.cjs +32 -21
- package/dist/init/index.mjs +29 -17
- package/dist/worker/index.cjs +77 -13
- package/dist/worker/index.d.cts +31 -4
- package/dist/worker/index.d.mts +31 -4
- package/dist/worker/index.mjs +75 -11
- package/package.json +7 -6
- package/src/plugins.ts +1 -3
- package/src/presignR2Url.ts +103 -0
- package/src/r2S3Storage.ts +17 -16
- package/src/r2Storage.ts +4 -2
- package/src/r2WorkerStorage.ts +52 -14
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +147 -142
- package/worker/dist/index.js.map +4 -4
- package/worker/src/index.ts +13 -8
- package/worker/wrangler.json +1 -0
package/dist/index.cjs
CHANGED
|
@@ -26,9 +26,8 @@ let cloudflare = require("cloudflare");
|
|
|
26
26
|
cloudflare = __toESM(cloudflare, 1);
|
|
27
27
|
let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
28
28
|
let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
|
|
29
|
-
let
|
|
30
|
-
let
|
|
31
|
-
let _hot_updater_server_plugins_remote_config = require("@hot-updater/server/plugins/remote-config");
|
|
29
|
+
let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
30
|
+
let _hot_updater_server_plugins = require("@hot-updater/server/plugins");
|
|
32
31
|
//#region src/d1Executor.ts
|
|
33
32
|
var D1ExecutionError = class extends Error {
|
|
34
33
|
name = "D1ExecutionError";
|
|
@@ -134,12 +133,14 @@ const d1Database = (config) => {
|
|
|
134
133
|
};
|
|
135
134
|
//#endregion
|
|
136
135
|
//#region src/r2S3Storage.ts
|
|
136
|
+
/** Devices download right after the update check that returns the URL. */
|
|
137
|
+
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
137
138
|
const isObjectNotFoundError = (error) => {
|
|
138
139
|
if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
|
|
139
140
|
return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
|
|
140
141
|
};
|
|
141
142
|
const createR2S3Storage = (config) => {
|
|
142
|
-
const { accountId, basePath, bucketName,
|
|
143
|
+
const { accountId, basePath, bucketName, endpoint, forcePathStyle, region, ...s3Config } = config;
|
|
143
144
|
const client = new _aws_sdk_client_s3.S3Client({
|
|
144
145
|
...s3Config,
|
|
145
146
|
endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
|
|
@@ -147,7 +148,6 @@ const createR2S3Storage = (config) => {
|
|
|
147
148
|
region: region ?? "auto"
|
|
148
149
|
});
|
|
149
150
|
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(basePath);
|
|
150
|
-
const getDownloadUrl = downloadUrlSigningKey ? (0, _hot_updater_plugin_core.createStorageDownloadUrl)(downloadUrlSigningKey) : void 0;
|
|
151
151
|
const parseAndValidate = (storageUri) => {
|
|
152
152
|
const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
|
|
153
153
|
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
@@ -192,10 +192,13 @@ const createR2S3Storage = (config) => {
|
|
|
192
192
|
throw error;
|
|
193
193
|
}
|
|
194
194
|
},
|
|
195
|
-
|
|
196
|
-
parseAndValidate(storageUri);
|
|
197
|
-
return
|
|
198
|
-
|
|
195
|
+
async getDownloadUrl({ storageUri }) {
|
|
196
|
+
const { key } = parseAndValidate(storageUri);
|
|
197
|
+
return { url: await (0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.GetObjectCommand({
|
|
198
|
+
Bucket: bucketName,
|
|
199
|
+
Key: key
|
|
200
|
+
}), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
|
|
201
|
+
},
|
|
199
202
|
async exists({ storageUri }) {
|
|
200
203
|
const { key } = parseAndValidate(storageUri);
|
|
201
204
|
try {
|
|
@@ -222,7 +225,7 @@ const createR2S3Storage = (config) => {
|
|
|
222
225
|
//#endregion
|
|
223
226
|
//#region src/r2Storage.ts
|
|
224
227
|
const r2Storage = (config) => {
|
|
225
|
-
if (!config.credentials) throw new Error("r2Storage requires S3-compatible credentials
|
|
228
|
+
if (!config.credentials) throw new Error("r2Storage requires S3-compatible credentials: pass credentials with an R2 access key ID and secret access key.");
|
|
226
229
|
return createR2S3Storage(config);
|
|
227
230
|
};
|
|
228
231
|
//#endregion
|
|
@@ -232,9 +235,9 @@ const r2Storage = (config) => {
|
|
|
232
235
|
* client route, and Remote Config. Its Worker imports this list.
|
|
233
236
|
*/
|
|
234
237
|
const plugins = [
|
|
235
|
-
(0,
|
|
236
|
-
(0,
|
|
237
|
-
(0,
|
|
238
|
+
(0, _hot_updater_server_plugins.insights)(),
|
|
239
|
+
(0, _hot_updater_server_plugins.apiKeys)(),
|
|
240
|
+
(0, _hot_updater_server_plugins.remoteConfig)()
|
|
238
241
|
];
|
|
239
242
|
//#endregion
|
|
240
243
|
exports.D1ExecutionError = D1ExecutionError;
|
package/dist/index.d.cts
CHANGED
|
@@ -28,13 +28,11 @@ interface R2S3StorageConfig extends S3ClientConfig {
|
|
|
28
28
|
credentials: NonNullable<S3ClientConfig["credentials"]>;
|
|
29
29
|
/** Base path where bundles will be stored in the bucket. */
|
|
30
30
|
basePath?: string;
|
|
31
|
-
/** Required when this storage serves downloads through createHotUpdater. */
|
|
32
|
-
downloadUrlSigningKey?: string;
|
|
33
31
|
}
|
|
34
32
|
//#endregion
|
|
35
33
|
//#region src/r2Storage.d.ts
|
|
36
34
|
type R2StorageConfig = R2S3StorageConfig;
|
|
37
|
-
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<"put" | "get" | "exists" | "delete">;
|
|
35
|
+
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete">;
|
|
38
36
|
//#endregion
|
|
39
37
|
//#region ../insights/dist/server/eventRow.d.mts
|
|
40
38
|
//#region src/server/eventRow.d.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -28,13 +28,11 @@ interface R2S3StorageConfig extends S3ClientConfig {
|
|
|
28
28
|
credentials: NonNullable<S3ClientConfig["credentials"]>;
|
|
29
29
|
/** Base path where bundles will be stored in the bucket. */
|
|
30
30
|
basePath?: string;
|
|
31
|
-
/** Required when this storage serves downloads through createHotUpdater. */
|
|
32
|
-
downloadUrlSigningKey?: string;
|
|
33
31
|
}
|
|
34
32
|
//#endregion
|
|
35
33
|
//#region src/r2Storage.d.ts
|
|
36
34
|
type R2StorageConfig = R2S3StorageConfig;
|
|
37
|
-
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<"put" | "get" | "exists" | "delete">;
|
|
35
|
+
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete">;
|
|
38
36
|
//#endregion
|
|
39
37
|
//#region ../insights/dist/server/eventRow.d.mts
|
|
40
38
|
//#region src/server/eventRow.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { WRITE_GUARD_TABLE, coreSettings, coreTarget, createEngineDatabase, createSqlAdapter, createStorageAdapter,
|
|
1
|
+
import { WRITE_GUARD_TABLE, coreSettings, coreTarget, createEngineDatabase, createSqlAdapter, createStorageAdapter, createStorageKeyBuilder, createStorageUri, createTableStatements, generateEngineSql, parseStorageUri } from "@hot-updater/plugin-core";
|
|
2
2
|
import Cloudflare from "cloudflare";
|
|
3
3
|
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
4
4
|
import { Upload } from "@aws-sdk/lib-storage";
|
|
5
|
-
import {
|
|
6
|
-
import { insights } from "@hot-updater/server/plugins
|
|
7
|
-
import { remoteConfig } from "@hot-updater/server/plugins/remote-config";
|
|
5
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
6
|
+
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
|
|
8
7
|
//#region src/d1Executor.ts
|
|
9
8
|
var D1ExecutionError = class extends Error {
|
|
10
9
|
name = "D1ExecutionError";
|
|
@@ -110,12 +109,14 @@ const d1Database = (config) => {
|
|
|
110
109
|
};
|
|
111
110
|
//#endregion
|
|
112
111
|
//#region src/r2S3Storage.ts
|
|
112
|
+
/** Devices download right after the update check that returns the URL. */
|
|
113
|
+
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
113
114
|
const isObjectNotFoundError = (error) => {
|
|
114
115
|
if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
|
|
115
116
|
return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
|
|
116
117
|
};
|
|
117
118
|
const createR2S3Storage = (config) => {
|
|
118
|
-
const { accountId, basePath, bucketName,
|
|
119
|
+
const { accountId, basePath, bucketName, endpoint, forcePathStyle, region, ...s3Config } = config;
|
|
119
120
|
const client = new S3Client({
|
|
120
121
|
...s3Config,
|
|
121
122
|
endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
|
|
@@ -123,7 +124,6 @@ const createR2S3Storage = (config) => {
|
|
|
123
124
|
region: region ?? "auto"
|
|
124
125
|
});
|
|
125
126
|
const getStorageKey = createStorageKeyBuilder(basePath);
|
|
126
|
-
const getDownloadUrl = downloadUrlSigningKey ? createStorageDownloadUrl(downloadUrlSigningKey) : void 0;
|
|
127
127
|
const parseAndValidate = (storageUri) => {
|
|
128
128
|
const parsed = parseStorageUri(storageUri, "r2");
|
|
129
129
|
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
@@ -168,10 +168,13 @@ const createR2S3Storage = (config) => {
|
|
|
168
168
|
throw error;
|
|
169
169
|
}
|
|
170
170
|
},
|
|
171
|
-
|
|
172
|
-
parseAndValidate(storageUri);
|
|
173
|
-
return
|
|
174
|
-
|
|
171
|
+
async getDownloadUrl({ storageUri }) {
|
|
172
|
+
const { key } = parseAndValidate(storageUri);
|
|
173
|
+
return { url: await getSignedUrl(client, new GetObjectCommand({
|
|
174
|
+
Bucket: bucketName,
|
|
175
|
+
Key: key
|
|
176
|
+
}), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
|
|
177
|
+
},
|
|
175
178
|
async exists({ storageUri }) {
|
|
176
179
|
const { key } = parseAndValidate(storageUri);
|
|
177
180
|
try {
|
|
@@ -198,7 +201,7 @@ const createR2S3Storage = (config) => {
|
|
|
198
201
|
//#endregion
|
|
199
202
|
//#region src/r2Storage.ts
|
|
200
203
|
const r2Storage = (config) => {
|
|
201
|
-
if (!config.credentials) throw new Error("r2Storage requires S3-compatible credentials
|
|
204
|
+
if (!config.credentials) throw new Error("r2Storage requires S3-compatible credentials: pass credentials with an R2 access key ID and secret access key.");
|
|
202
205
|
return createR2S3Storage(config);
|
|
203
206
|
};
|
|
204
207
|
//#endregion
|
package/dist/init/index.cjs
CHANGED
|
@@ -22,17 +22,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
enumerable: true
|
|
23
23
|
}) : target, mod));
|
|
24
24
|
//#endregion
|
|
25
|
-
let crypto = require("crypto");
|
|
26
|
-
crypto = __toESM(crypto, 1);
|
|
27
25
|
let path = require("path");
|
|
28
26
|
path = __toESM(path, 1);
|
|
29
27
|
let _hot_updater_cli_tools = require("@hot-updater/cli-tools");
|
|
30
28
|
let cloudflare = require("cloudflare");
|
|
31
29
|
cloudflare = __toESM(cloudflare, 1);
|
|
32
30
|
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
33
|
-
let
|
|
34
|
-
let _hot_updater_server_plugins_insights = require("@hot-updater/server/plugins/insights");
|
|
35
|
-
let _hot_updater_server_plugins_remote_config = require("@hot-updater/server/plugins/remote-config");
|
|
31
|
+
let _hot_updater_server_plugins = require("@hot-updater/server/plugins");
|
|
36
32
|
let node_url = require("node:url");
|
|
37
33
|
let node_child_process = require("node:child_process");
|
|
38
34
|
let node_string_decoder = require("node:string_decoder");
|
|
@@ -168,9 +164,9 @@ const d1Database = (config) => {
|
|
|
168
164
|
* client route, and Remote Config. Its Worker imports this list.
|
|
169
165
|
*/
|
|
170
166
|
const plugins = [
|
|
171
|
-
(0,
|
|
172
|
-
(0,
|
|
173
|
-
(0,
|
|
167
|
+
(0, _hot_updater_server_plugins.insights)(),
|
|
168
|
+
(0, _hot_updater_server_plugins.apiKeys)(),
|
|
169
|
+
(0, _hot_updater_server_plugins.remoteConfig)()
|
|
174
170
|
];
|
|
175
171
|
//#endregion
|
|
176
172
|
//#region ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
|
|
@@ -11429,20 +11425,24 @@ const getConfigScaffold = (build) => {
|
|
|
11429
11425
|
})`
|
|
11430
11426
|
}).setPlugins({
|
|
11431
11427
|
imports: [{
|
|
11432
|
-
pkg: "
|
|
11433
|
-
named: [
|
|
11428
|
+
pkg: "hot-updater/plugins",
|
|
11429
|
+
named: [
|
|
11430
|
+
"apiKeys",
|
|
11431
|
+
"insights",
|
|
11432
|
+
"remoteConfig"
|
|
11433
|
+
]
|
|
11434
11434
|
}],
|
|
11435
|
-
configString: "
|
|
11435
|
+
configString: "[apiKeys(), insights(), remoteConfig()]"
|
|
11436
11436
|
}));
|
|
11437
11437
|
};
|
|
11438
11438
|
//#endregion
|
|
11439
11439
|
//#region iac/managedWorker.ts
|
|
11440
11440
|
/**
|
|
11441
11441
|
* Readies the staged prebuilt Worker for Wrangler as init deploys it: its
|
|
11442
|
-
* bindings to the D1 database and R2 bucket, and the bucket's name
|
|
11443
|
-
* the Worker reads from its vars.
|
|
11442
|
+
* bindings to the D1 database and R2 bucket, and the bucket's name and the
|
|
11443
|
+
* account's ID, which the Worker reads from its vars to presign R2 URLs.
|
|
11444
11444
|
*/
|
|
11445
|
-
const prepareWorkerDeployment = async (workerRoot, { d1DatabaseId, d1DatabaseName, r2BucketName }) => {
|
|
11445
|
+
const prepareWorkerDeployment = async (workerRoot, { accountId, d1DatabaseId, d1DatabaseName, r2BucketName }) => {
|
|
11446
11446
|
const configPath = path.default.join(workerRoot, "wrangler.json");
|
|
11447
11447
|
const config = JSON.parse(await fs_promises.default.readFile(configPath, "utf-8"));
|
|
11448
11448
|
config.d1_databases = [{
|
|
@@ -11454,12 +11454,15 @@ const prepareWorkerDeployment = async (workerRoot, { d1DatabaseId, d1DatabaseNam
|
|
|
11454
11454
|
binding: "BUCKET",
|
|
11455
11455
|
bucket_name: r2BucketName
|
|
11456
11456
|
}];
|
|
11457
|
-
config.vars = {
|
|
11457
|
+
config.vars = {
|
|
11458
|
+
ACCOUNT_ID: accountId,
|
|
11459
|
+
BUCKET_NAME: r2BucketName
|
|
11460
|
+
};
|
|
11458
11461
|
await fs_promises.default.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
11459
11462
|
};
|
|
11460
11463
|
//#endregion
|
|
11461
11464
|
//#region iac/index.ts
|
|
11462
|
-
const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseId, d1DatabaseName, nonInteractive, r2BucketName, workerName }) => {
|
|
11465
|
+
const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseId, d1DatabaseName, nonInteractive, r2BucketName, r2Credentials, workerName }) => {
|
|
11463
11466
|
const cwd = (0, _hot_updater_cli_tools.getCwd)();
|
|
11464
11467
|
const cloudflarePackagePath = require.resolve("@hot-updater/cloudflare/package.json", { paths: [cwd] });
|
|
11465
11468
|
const cloudflarePackageRoot = path.default.dirname(cloudflarePackagePath);
|
|
@@ -11467,6 +11470,7 @@ const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseI
|
|
|
11467
11470
|
const workerRoot = path.default.join(tmpDir, "worker");
|
|
11468
11471
|
try {
|
|
11469
11472
|
await prepareWorkerDeployment(workerRoot, {
|
|
11473
|
+
accountId,
|
|
11470
11474
|
d1DatabaseId,
|
|
11471
11475
|
d1DatabaseName,
|
|
11472
11476
|
r2BucketName
|
|
@@ -11480,16 +11484,19 @@ const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseI
|
|
|
11480
11484
|
});
|
|
11481
11485
|
await wrangler("d1", "migrations", "apply", d1DatabaseName, "--remote");
|
|
11482
11486
|
await wrangler("deploy", "--name", workerName);
|
|
11483
|
-
const
|
|
11487
|
+
const secretWrangler = createWrangler({
|
|
11484
11488
|
stdio: "pipe",
|
|
11485
11489
|
cloudflareApiToken: apiToken,
|
|
11486
11490
|
cwd: workerRoot,
|
|
11487
11491
|
accountId,
|
|
11488
11492
|
nonInteractive
|
|
11489
|
-
})
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
11493
|
+
});
|
|
11494
|
+
for (const [name, value] of [["R2_ACCESS_KEY_ID", r2Credentials.accessKeyId], ["R2_SECRET_ACCESS_KEY", r2Credentials.secretAccessKey]]) {
|
|
11495
|
+
const secretCommand = secretWrangler("secret", "put", name, "--name", workerName);
|
|
11496
|
+
if (!secretCommand.stdin) throw new Error("Failed to open Wrangler secret input.");
|
|
11497
|
+
secretCommand.stdin.end(value);
|
|
11498
|
+
await secretCommand;
|
|
11499
|
+
}
|
|
11493
11500
|
return workerName;
|
|
11494
11501
|
} catch (error) {
|
|
11495
11502
|
if (error instanceof Error) throw toCloudflareDeploymentError(error, credentialSource);
|
|
@@ -11821,6 +11828,10 @@ const runInit = async ({ build, envFile }) => {
|
|
|
11821
11828
|
d1DatabaseName,
|
|
11822
11829
|
nonInteractive,
|
|
11823
11830
|
r2BucketName: selectedBucketName,
|
|
11831
|
+
r2Credentials: {
|
|
11832
|
+
accessKeyId,
|
|
11833
|
+
secretAccessKey
|
|
11834
|
+
},
|
|
11824
11835
|
workerName
|
|
11825
11836
|
});
|
|
11826
11837
|
const database = d1Database({
|
package/dist/init/index.mjs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import crypto from "crypto";
|
|
3
2
|
import path from "path";
|
|
4
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";
|
|
5
4
|
import Cloudflare$1, { Cloudflare } from "cloudflare";
|
|
6
5
|
import { WRITE_GUARD_TABLE, coreSettings, coreTarget, createEngineDatabase, createSqlAdapter, createTableStatements, generateEngineSql } from "@hot-updater/plugin-core";
|
|
7
|
-
import { apiKeys } from "@hot-updater/server/plugins
|
|
8
|
-
import { insights } from "@hot-updater/server/plugins/insights";
|
|
9
|
-
import { remoteConfig } from "@hot-updater/server/plugins/remote-config";
|
|
6
|
+
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
|
|
10
7
|
import { fileURLToPath } from "node:url";
|
|
11
8
|
import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
|
|
12
9
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -11421,20 +11418,24 @@ const getConfigScaffold = (build) => {
|
|
|
11421
11418
|
})`
|
|
11422
11419
|
}).setPlugins({
|
|
11423
11420
|
imports: [{
|
|
11424
|
-
pkg: "
|
|
11425
|
-
named: [
|
|
11421
|
+
pkg: "hot-updater/plugins",
|
|
11422
|
+
named: [
|
|
11423
|
+
"apiKeys",
|
|
11424
|
+
"insights",
|
|
11425
|
+
"remoteConfig"
|
|
11426
|
+
]
|
|
11426
11427
|
}],
|
|
11427
|
-
configString: "
|
|
11428
|
+
configString: "[apiKeys(), insights(), remoteConfig()]"
|
|
11428
11429
|
}));
|
|
11429
11430
|
};
|
|
11430
11431
|
//#endregion
|
|
11431
11432
|
//#region iac/managedWorker.ts
|
|
11432
11433
|
/**
|
|
11433
11434
|
* Readies the staged prebuilt Worker for Wrangler as init deploys it: its
|
|
11434
|
-
* bindings to the D1 database and R2 bucket, and the bucket's name
|
|
11435
|
-
* the Worker reads from its vars.
|
|
11435
|
+
* bindings to the D1 database and R2 bucket, and the bucket's name and the
|
|
11436
|
+
* account's ID, which the Worker reads from its vars to presign R2 URLs.
|
|
11436
11437
|
*/
|
|
11437
|
-
const prepareWorkerDeployment = async (workerRoot, { d1DatabaseId, d1DatabaseName, r2BucketName }) => {
|
|
11438
|
+
const prepareWorkerDeployment = async (workerRoot, { accountId, d1DatabaseId, d1DatabaseName, r2BucketName }) => {
|
|
11438
11439
|
const configPath = path.join(workerRoot, "wrangler.json");
|
|
11439
11440
|
const config = JSON.parse(await fs$1.readFile(configPath, "utf-8"));
|
|
11440
11441
|
config.d1_databases = [{
|
|
@@ -11446,12 +11447,15 @@ const prepareWorkerDeployment = async (workerRoot, { d1DatabaseId, d1DatabaseNam
|
|
|
11446
11447
|
binding: "BUCKET",
|
|
11447
11448
|
bucket_name: r2BucketName
|
|
11448
11449
|
}];
|
|
11449
|
-
config.vars = {
|
|
11450
|
+
config.vars = {
|
|
11451
|
+
ACCOUNT_ID: accountId,
|
|
11452
|
+
BUCKET_NAME: r2BucketName
|
|
11453
|
+
};
|
|
11450
11454
|
await fs$1.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
11451
11455
|
};
|
|
11452
11456
|
//#endregion
|
|
11453
11457
|
//#region iac/index.ts
|
|
11454
|
-
const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseId, d1DatabaseName, nonInteractive, r2BucketName, workerName }) => {
|
|
11458
|
+
const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseId, d1DatabaseName, nonInteractive, r2BucketName, r2Credentials, workerName }) => {
|
|
11455
11459
|
const cwd = getCwd();
|
|
11456
11460
|
const cloudflarePackagePath = __require.resolve("@hot-updater/cloudflare/package.json", { paths: [cwd] });
|
|
11457
11461
|
const cloudflarePackageRoot = path.dirname(cloudflarePackagePath);
|
|
@@ -11459,6 +11463,7 @@ const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseI
|
|
|
11459
11463
|
const workerRoot = path.join(tmpDir, "worker");
|
|
11460
11464
|
try {
|
|
11461
11465
|
await prepareWorkerDeployment(workerRoot, {
|
|
11466
|
+
accountId,
|
|
11462
11467
|
d1DatabaseId,
|
|
11463
11468
|
d1DatabaseName,
|
|
11464
11469
|
r2BucketName
|
|
@@ -11472,16 +11477,19 @@ const deployWorker = async (apiToken, accountId, { credentialSource, d1DatabaseI
|
|
|
11472
11477
|
});
|
|
11473
11478
|
await wrangler("d1", "migrations", "apply", d1DatabaseName, "--remote");
|
|
11474
11479
|
await wrangler("deploy", "--name", workerName);
|
|
11475
|
-
const
|
|
11480
|
+
const secretWrangler = createWrangler({
|
|
11476
11481
|
stdio: "pipe",
|
|
11477
11482
|
cloudflareApiToken: apiToken,
|
|
11478
11483
|
cwd: workerRoot,
|
|
11479
11484
|
accountId,
|
|
11480
11485
|
nonInteractive
|
|
11481
|
-
})
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11486
|
+
});
|
|
11487
|
+
for (const [name, value] of [["R2_ACCESS_KEY_ID", r2Credentials.accessKeyId], ["R2_SECRET_ACCESS_KEY", r2Credentials.secretAccessKey]]) {
|
|
11488
|
+
const secretCommand = secretWrangler("secret", "put", name, "--name", workerName);
|
|
11489
|
+
if (!secretCommand.stdin) throw new Error("Failed to open Wrangler secret input.");
|
|
11490
|
+
secretCommand.stdin.end(value);
|
|
11491
|
+
await secretCommand;
|
|
11492
|
+
}
|
|
11485
11493
|
return workerName;
|
|
11486
11494
|
} catch (error) {
|
|
11487
11495
|
if (error instanceof Error) throw toCloudflareDeploymentError(error, credentialSource);
|
|
@@ -11813,6 +11821,10 @@ const runInit = async ({ build, envFile }) => {
|
|
|
11813
11821
|
d1DatabaseName,
|
|
11814
11822
|
nonInteractive,
|
|
11815
11823
|
r2BucketName: selectedBucketName,
|
|
11824
|
+
r2Credentials: {
|
|
11825
|
+
accessKeyId,
|
|
11826
|
+
secretAccessKey
|
|
11827
|
+
},
|
|
11816
11828
|
workerName
|
|
11817
11829
|
});
|
|
11818
11830
|
const database = d1Database({
|
package/dist/worker/index.cjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
3
|
-
let
|
|
4
|
-
let _hot_updater_server_plugins_insights = require("@hot-updater/server/plugins/insights");
|
|
5
|
-
let _hot_updater_server_plugins_remote_config = require("@hot-updater/server/plugins/remote-config");
|
|
3
|
+
let _hot_updater_server_plugins = require("@hot-updater/server/plugins");
|
|
6
4
|
//#region src/d1Executor.ts
|
|
7
5
|
var D1ExecutionError = class extends Error {
|
|
8
6
|
name = "D1ExecutionError";
|
|
@@ -68,15 +66,75 @@ const d1Database = (database) => createD1Database({
|
|
|
68
66
|
* client route, and Remote Config. Its Worker imports this list.
|
|
69
67
|
*/
|
|
70
68
|
const plugins = [
|
|
71
|
-
(0,
|
|
72
|
-
(0,
|
|
73
|
-
(0,
|
|
69
|
+
(0, _hot_updater_server_plugins.insights)(),
|
|
70
|
+
(0, _hot_updater_server_plugins.apiKeys)(),
|
|
71
|
+
(0, _hot_updater_server_plugins.remoteConfig)()
|
|
74
72
|
];
|
|
75
73
|
//#endregion
|
|
74
|
+
//#region src/presignR2Url.ts
|
|
75
|
+
const encoder = new TextEncoder();
|
|
76
|
+
const hex = (bytes) => Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
77
|
+
const hmac = async (key, value) => crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", key, {
|
|
78
|
+
hash: "SHA-256",
|
|
79
|
+
name: "HMAC"
|
|
80
|
+
}, false, ["sign"]), encoder.encode(value));
|
|
81
|
+
/** RFC 3986 encoding, which S3 signs keys and query values in. */
|
|
82
|
+
const encodeRfc3986 = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
83
|
+
/**
|
|
84
|
+
* A presigned GET URL for an object on R2's S3-compatible API: AWS Signature
|
|
85
|
+
* Version 4 on Web Crypto, in the form `getSignedUrl` from
|
|
86
|
+
* `@aws-sdk/s3-request-presigner` signs, so a Worker presigns without the
|
|
87
|
+
* AWS SDK.
|
|
88
|
+
*/
|
|
89
|
+
const presignR2GetUrl = async ({ accountId, bucketName, credentials, expiresInSeconds, key, signingDate = /* @__PURE__ */ new Date() }) => {
|
|
90
|
+
const host = `${accountId}.r2.cloudflarestorage.com`;
|
|
91
|
+
const path = `/${bucketName}/${key.split("/").map(encodeRfc3986).join("/")}`;
|
|
92
|
+
const amzDate = signingDate.toISOString().replace(/[-:]|\.\d{3}/g, "");
|
|
93
|
+
const scope = `${amzDate.slice(0, 8)}/auto/s3/aws4_request`;
|
|
94
|
+
const query = [
|
|
95
|
+
["X-Amz-Algorithm", "AWS4-HMAC-SHA256"],
|
|
96
|
+
["X-Amz-Content-Sha256", "UNSIGNED-PAYLOAD"],
|
|
97
|
+
["X-Amz-Credential", `${credentials.accessKeyId}/${scope}`],
|
|
98
|
+
["X-Amz-Date", amzDate],
|
|
99
|
+
["X-Amz-Expires", String(expiresInSeconds)],
|
|
100
|
+
["X-Amz-SignedHeaders", "host"],
|
|
101
|
+
["x-id", "GetObject"]
|
|
102
|
+
];
|
|
103
|
+
const queryString = (entries) => entries.map(([name, value]) => `${encodeRfc3986(name)}=${encodeRfc3986(value)}`).join("&");
|
|
104
|
+
const canonicalRequest = [
|
|
105
|
+
"GET",
|
|
106
|
+
path,
|
|
107
|
+
queryString(query),
|
|
108
|
+
`host:${host}\n`,
|
|
109
|
+
"host",
|
|
110
|
+
"UNSIGNED-PAYLOAD"
|
|
111
|
+
].join("\n");
|
|
112
|
+
const stringToSign = [
|
|
113
|
+
"AWS4-HMAC-SHA256",
|
|
114
|
+
amzDate,
|
|
115
|
+
scope,
|
|
116
|
+
hex(await crypto.subtle.digest("SHA-256", encoder.encode(canonicalRequest)))
|
|
117
|
+
].join("\n");
|
|
118
|
+
let signingKey = encoder.encode(`AWS4${credentials.secretAccessKey}`).buffer;
|
|
119
|
+
for (const part of [
|
|
120
|
+
amzDate.slice(0, 8),
|
|
121
|
+
"auto",
|
|
122
|
+
"s3",
|
|
123
|
+
"aws4_request"
|
|
124
|
+
]) signingKey = await hmac(signingKey, part);
|
|
125
|
+
const signature = hex(await hmac(signingKey, stringToSign));
|
|
126
|
+
return `https://${host}${path}?${queryString([
|
|
127
|
+
...query.slice(0, 5),
|
|
128
|
+
["X-Amz-Signature", signature],
|
|
129
|
+
...query.slice(5)
|
|
130
|
+
])}`;
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
76
133
|
//#region src/r2WorkerStorage.ts
|
|
77
|
-
|
|
134
|
+
/** Devices download right after the update check that returns the URL. */
|
|
135
|
+
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
136
|
+
function r2WorkerStorage(config) {
|
|
78
137
|
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
|
|
79
|
-
const getDownloadUrl = (0, _hot_updater_plugin_core.createStorageDownloadUrl)(config.downloadUrlSigningKey);
|
|
80
138
|
const parseAndValidate = (storageUri) => {
|
|
81
139
|
const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
|
|
82
140
|
if (parsed.bucket !== config.bucketName) throw new Error(`Bucket name mismatch: expected "${config.bucketName}", but found "${parsed.bucket}".`);
|
|
@@ -114,10 +172,16 @@ const r2WorkerStorage = (config) => {
|
|
|
114
172
|
headers.set("content-length", String(object.size));
|
|
115
173
|
return { response: new Response(object.body, { headers }) };
|
|
116
174
|
},
|
|
117
|
-
async getDownloadUrl({ storageUri }) {
|
|
118
|
-
parseAndValidate(storageUri);
|
|
119
|
-
return
|
|
120
|
-
|
|
175
|
+
...config.credentials === void 0 ? {} : { async getDownloadUrl({ storageUri }) {
|
|
176
|
+
const { key } = parseAndValidate(storageUri);
|
|
177
|
+
return { url: await presignR2GetUrl({
|
|
178
|
+
accountId: config.accountId,
|
|
179
|
+
bucketName: config.bucketName,
|
|
180
|
+
credentials: config.credentials,
|
|
181
|
+
expiresInSeconds: PRESIGNED_URL_EXPIRES_IN_SECONDS,
|
|
182
|
+
key
|
|
183
|
+
}) };
|
|
184
|
+
} },
|
|
121
185
|
async exists({ storageUri }) {
|
|
122
186
|
const { key } = parseAndValidate(storageUri);
|
|
123
187
|
return { exists: await config.bucket.head(key) !== null };
|
|
@@ -128,7 +192,7 @@ const r2WorkerStorage = (config) => {
|
|
|
128
192
|
return { deleted: true };
|
|
129
193
|
}
|
|
130
194
|
});
|
|
131
|
-
}
|
|
195
|
+
}
|
|
132
196
|
//#endregion
|
|
133
197
|
exports.d1Database = d1Database;
|
|
134
198
|
exports.plugins = plugins;
|
package/dist/worker/index.d.cts
CHANGED
|
@@ -1037,13 +1037,40 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
|
|
|
1037
1037
|
endpoints: readonly import("@hot-updater/plugin-core").PluginEndpoint[];
|
|
1038
1038
|
}, {}>];
|
|
1039
1039
|
//#endregion
|
|
1040
|
+
//#region src/presignR2Url.d.ts
|
|
1041
|
+
/** R2's S3-compatible credentials. */
|
|
1042
|
+
interface R2Credentials {
|
|
1043
|
+
readonly accessKeyId: string;
|
|
1044
|
+
readonly secretAccessKey: string;
|
|
1045
|
+
}
|
|
1046
|
+
//#endregion
|
|
1040
1047
|
//#region src/r2WorkerStorage.d.ts
|
|
1041
|
-
|
|
1048
|
+
type CloudflareWorkerStorageConfig = {
|
|
1042
1049
|
readonly bucket: R2Bucket;
|
|
1043
1050
|
readonly bucketName: string;
|
|
1044
1051
|
readonly basePath?: string;
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1052
|
+
} & ({
|
|
1053
|
+
/** The account whose R2 endpoint download URLs name. */
|
|
1054
|
+
readonly accountId: string;
|
|
1055
|
+
/**
|
|
1056
|
+
* R2's S3-compatible credentials, which presign download URLs, as
|
|
1057
|
+
* `r2Storage` from `@hot-updater/cloudflare` does: what a server needs
|
|
1058
|
+
* to serve downloads.
|
|
1059
|
+
*/
|
|
1060
|
+
readonly credentials: R2Credentials;
|
|
1061
|
+
} | {
|
|
1062
|
+
readonly accountId?: undefined;
|
|
1063
|
+
readonly credentials?: undefined;
|
|
1064
|
+
});
|
|
1065
|
+
/**
|
|
1066
|
+
* Storage on the Worker's R2 binding. With R2's S3-compatible credentials,
|
|
1067
|
+
* its download URLs are presigned and devices download from the bucket;
|
|
1068
|
+
* without them, it has no `getDownloadUrl`, as a Console that only reads
|
|
1069
|
+
* needs.
|
|
1070
|
+
*/
|
|
1071
|
+
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig & {
|
|
1072
|
+
readonly credentials: R2Credentials;
|
|
1073
|
+
}): StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete">;
|
|
1074
|
+
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig): StorageAdapterWith<"put" | "get" | "exists" | "delete">;
|
|
1048
1075
|
//#endregion
|
|
1049
1076
|
export { type CloudflareWorkerStorageConfig, type D1Like, d1Database, plugins, r2WorkerStorage as r2Storage };
|
package/dist/worker/index.d.mts
CHANGED
|
@@ -1037,13 +1037,40 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
|
|
|
1037
1037
|
endpoints: readonly import("@hot-updater/plugin-core").PluginEndpoint[];
|
|
1038
1038
|
}, {}>];
|
|
1039
1039
|
//#endregion
|
|
1040
|
+
//#region src/presignR2Url.d.ts
|
|
1041
|
+
/** R2's S3-compatible credentials. */
|
|
1042
|
+
interface R2Credentials {
|
|
1043
|
+
readonly accessKeyId: string;
|
|
1044
|
+
readonly secretAccessKey: string;
|
|
1045
|
+
}
|
|
1046
|
+
//#endregion
|
|
1040
1047
|
//#region src/r2WorkerStorage.d.ts
|
|
1041
|
-
|
|
1048
|
+
type CloudflareWorkerStorageConfig = {
|
|
1042
1049
|
readonly bucket: R2Bucket;
|
|
1043
1050
|
readonly bucketName: string;
|
|
1044
1051
|
readonly basePath?: string;
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1052
|
+
} & ({
|
|
1053
|
+
/** The account whose R2 endpoint download URLs name. */
|
|
1054
|
+
readonly accountId: string;
|
|
1055
|
+
/**
|
|
1056
|
+
* R2's S3-compatible credentials, which presign download URLs, as
|
|
1057
|
+
* `r2Storage` from `@hot-updater/cloudflare` does: what a server needs
|
|
1058
|
+
* to serve downloads.
|
|
1059
|
+
*/
|
|
1060
|
+
readonly credentials: R2Credentials;
|
|
1061
|
+
} | {
|
|
1062
|
+
readonly accountId?: undefined;
|
|
1063
|
+
readonly credentials?: undefined;
|
|
1064
|
+
});
|
|
1065
|
+
/**
|
|
1066
|
+
* Storage on the Worker's R2 binding. With R2's S3-compatible credentials,
|
|
1067
|
+
* its download URLs are presigned and devices download from the bucket;
|
|
1068
|
+
* without them, it has no `getDownloadUrl`, as a Console that only reads
|
|
1069
|
+
* needs.
|
|
1070
|
+
*/
|
|
1071
|
+
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig & {
|
|
1072
|
+
readonly credentials: R2Credentials;
|
|
1073
|
+
}): StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete">;
|
|
1074
|
+
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig): StorageAdapterWith<"put" | "get" | "exists" | "delete">;
|
|
1048
1075
|
//#endregion
|
|
1049
1076
|
export { type CloudflareWorkerStorageConfig, type D1Like, d1Database, plugins, r2WorkerStorage as r2Storage };
|