@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 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 _hot_updater_server_plugins_api_keys = require("@hot-updater/server/plugins/api-keys");
30
- let _hot_updater_server_plugins_insights = require("@hot-updater/server/plugins/insights");
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, downloadUrlSigningKey, endpoint, forcePathStyle, region, ...s3Config } = config;
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
- ...getDownloadUrl ? { async getDownloadUrl({ storageUri }) {
196
- parseAndValidate(storageUri);
197
- return getDownloadUrl({ storageUri });
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. The Wrangler fallback was removed.");
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, _hot_updater_server_plugins_insights.insights)(),
236
- (0, _hot_updater_server_plugins_api_keys.apiKeys)(),
237
- (0, _hot_updater_server_plugins_remote_config.remoteConfig)()
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, createStorageDownloadUrl, createStorageKeyBuilder, createStorageUri, createTableStatements, generateEngineSql, parseStorageUri } from "@hot-updater/plugin-core";
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 { apiKeys } from "@hot-updater/server/plugins/api-keys";
6
- import { insights } from "@hot-updater/server/plugins/insights";
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, downloadUrlSigningKey, endpoint, forcePathStyle, region, ...s3Config } = config;
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
- ...getDownloadUrl ? { async getDownloadUrl({ storageUri }) {
172
- parseAndValidate(storageUri);
173
- return getDownloadUrl({ storageUri });
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. The Wrangler fallback was removed.");
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
@@ -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 _hot_updater_server_plugins_api_keys = require("@hot-updater/server/plugins/api-keys");
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, _hot_updater_server_plugins_insights.insights)(),
172
- (0, _hot_updater_server_plugins_api_keys.apiKeys)(),
173
- (0, _hot_updater_server_plugins_remote_config.remoteConfig)()
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: "@hot-updater/cloudflare",
11433
- named: ["plugins"]
11428
+ pkg: "hot-updater/plugins",
11429
+ named: [
11430
+ "apiKeys",
11431
+ "insights",
11432
+ "remoteConfig"
11433
+ ]
11434
11434
  }],
11435
- configString: "plugins"
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, which
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 = { BUCKET_NAME: r2BucketName };
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 secretCommand = createWrangler({
11487
+ const secretWrangler = createWrangler({
11484
11488
  stdio: "pipe",
11485
11489
  cloudflareApiToken: apiToken,
11486
11490
  cwd: workerRoot,
11487
11491
  accountId,
11488
11492
  nonInteractive
11489
- })("secret", "put", "STORAGE_DOWNLOAD_URL_SIGNING_KEY", "--name", workerName);
11490
- if (!secretCommand.stdin) throw new Error("Failed to open Wrangler secret input.");
11491
- secretCommand.stdin.end(crypto.default.randomBytes(32).toString("hex"));
11492
- await secretCommand;
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({
@@ -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/api-keys";
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: "@hot-updater/cloudflare",
11425
- named: ["plugins"]
11421
+ pkg: "hot-updater/plugins",
11422
+ named: [
11423
+ "apiKeys",
11424
+ "insights",
11425
+ "remoteConfig"
11426
+ ]
11426
11427
  }],
11427
- configString: "plugins"
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, which
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 = { BUCKET_NAME: r2BucketName };
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 secretCommand = createWrangler({
11480
+ const secretWrangler = createWrangler({
11476
11481
  stdio: "pipe",
11477
11482
  cloudflareApiToken: apiToken,
11478
11483
  cwd: workerRoot,
11479
11484
  accountId,
11480
11485
  nonInteractive
11481
- })("secret", "put", "STORAGE_DOWNLOAD_URL_SIGNING_KEY", "--name", workerName);
11482
- if (!secretCommand.stdin) throw new Error("Failed to open Wrangler secret input.");
11483
- secretCommand.stdin.end(crypto.randomBytes(32).toString("hex"));
11484
- await secretCommand;
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({
@@ -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 _hot_updater_server_plugins_api_keys = require("@hot-updater/server/plugins/api-keys");
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, _hot_updater_server_plugins_insights.insights)(),
72
- (0, _hot_updater_server_plugins_api_keys.apiKeys)(),
73
- (0, _hot_updater_server_plugins_remote_config.remoteConfig)()
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
- const r2WorkerStorage = (config) => {
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 getDownloadUrl({ storageUri });
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;
@@ -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
- interface CloudflareWorkerStorageConfig {
1048
+ type CloudflareWorkerStorageConfig = {
1042
1049
  readonly bucket: R2Bucket;
1043
1050
  readonly bucketName: string;
1044
1051
  readonly basePath?: string;
1045
- readonly downloadUrlSigningKey: string;
1046
- }
1047
- declare const r2WorkerStorage: (config: CloudflareWorkerStorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete">;
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 };
@@ -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
- interface CloudflareWorkerStorageConfig {
1048
+ type CloudflareWorkerStorageConfig = {
1042
1049
  readonly bucket: R2Bucket;
1043
1050
  readonly bucketName: string;
1044
1051
  readonly basePath?: string;
1045
- readonly downloadUrlSigningKey: string;
1046
- }
1047
- declare const r2WorkerStorage: (config: CloudflareWorkerStorageConfig) => StorageAdapterWith<"put" | "get" | "getDownloadUrl" | "exists" | "delete">;
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 };