@hot-updater/cloudflare 0.31.4 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,7 +28,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
28
28
  var __getOwnPropNames = Object.getOwnPropertyNames;
29
29
  var __getProtoOf = Object.getPrototypeOf;
30
30
  var __hasOwnProp = Object.prototype.hasOwnProperty;
31
- var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
31
+ var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
32
32
  var __copyProps = (to, from, except, desc) => {
33
33
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
34
34
  key = keys[i];
@@ -10838,7 +10838,10 @@ const getConfigScaffold = (build) => {
10838
10838
  configString: `r2Storage({
10839
10839
  bucketName: process.env.HOT_UPDATER_CLOUDFLARE_R2_BUCKET_NAME!,
10840
10840
  accountId: process.env.HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID!,
10841
- cloudflareApiToken: process.env.HOT_UPDATER_CLOUDFLARE_API_TOKEN!,
10841
+ credentials: {
10842
+ accessKeyId: process.env.HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID!,
10843
+ secretAccessKey: process.env.HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY!,
10844
+ },
10842
10845
  })`
10843
10846
  }).setDatabase({
10844
10847
  imports: [{
@@ -10863,7 +10866,50 @@ export default HotUpdater.wrap({
10863
10866
  baseURL: "%%source%%",
10864
10867
  updateStrategy: "appVersion", // or "fingerprint"
10865
10868
  })(App);`;
10866
- const deployWorker = async (oauth_token, accountId, { d1DatabaseId, d1DatabaseName, r2BucketName }) => {
10869
+ const HOT_UPDATER_ENV_PATH = ".env.hotupdater";
10870
+ const unquoteEnvValue = (value) => {
10871
+ const trimmed = value.trim();
10872
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
10873
+ return trimmed;
10874
+ };
10875
+ const readHotUpdaterEnv = async (cwd) => {
10876
+ const envPath = path.join(cwd, HOT_UPDATER_ENV_PATH);
10877
+ const content = await fs.readFile(envPath, "utf-8").catch(() => "");
10878
+ const env = {};
10879
+ for (const line of content.split("\n")) {
10880
+ const trimmed = line.trim();
10881
+ if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
10882
+ const [key, ...valueParts] = trimmed.split("=");
10883
+ if (!key) continue;
10884
+ env[key.trim()] = unquoteEnvValue(valueParts.join("="));
10885
+ }
10886
+ return env;
10887
+ };
10888
+ const getEnvValue = (env, key) => {
10889
+ return process.env[key]?.trim() || env[key]?.trim() || void 0;
10890
+ };
10891
+ const inputR2ApiCredentials = async ({ accountId, bucketName, accessKeyId, secretAccessKey }) => {
10892
+ p.log.step(`R2 API Tokens dashboard: ${link(`https://dash.cloudflare.com/${accountId}/r2/api-tokens`)}`);
10893
+ p.log.step("Required permission: Object Read & Write");
10894
+ p.log.step(`Target bucket: ${bucketName}`);
10895
+ let resolvedAccessKeyId = accessKeyId;
10896
+ if (!resolvedAccessKeyId) {
10897
+ const inputR2AccessKeyId = await p.password({ message: "Enter the R2 Access Key ID" });
10898
+ if (p.isCancel(inputR2AccessKeyId)) process.exit(1);
10899
+ resolvedAccessKeyId = inputR2AccessKeyId;
10900
+ }
10901
+ let resolvedSecretAccessKey = secretAccessKey;
10902
+ if (!resolvedSecretAccessKey) {
10903
+ const inputR2SecretAccessKey = await p.password({ message: "Enter the R2 Secret Access Key" });
10904
+ if (p.isCancel(inputR2SecretAccessKey)) process.exit(1);
10905
+ resolvedSecretAccessKey = inputR2SecretAccessKey;
10906
+ }
10907
+ return {
10908
+ accessKeyId: resolvedAccessKeyId,
10909
+ secretAccessKey: resolvedSecretAccessKey
10910
+ };
10911
+ };
10912
+ const deployWorker = async (oauth_token, accountId, { d1DatabaseId, d1DatabaseName, r2BucketName, workerName }) => {
10867
10913
  const cwd = getCwd();
10868
10914
  const cloudflarePackagePath = __require.resolve("@hot-updater/cloudflare/package.json", { paths: [cwd] });
10869
10915
  const { tmpDir, removeTmpDir } = await copyDirToTmp(path.dirname(cloudflarePackagePath));
@@ -10895,14 +10941,19 @@ const deployWorker = async (oauth_token, accountId, { d1DatabaseId, d1DatabaseNa
10895
10941
  await fs.writeFile(filePath, transformTemplate(content, { BUCKET_NAME: r2BucketName }));
10896
10942
  }
10897
10943
  await wrangler("d1", "migrations", "apply", d1DatabaseName, "--remote");
10898
- const workerName = await p.text({
10899
- message: "Enter the name of the worker",
10900
- defaultValue: "hot-updater",
10901
- placeholder: "hot-updater"
10902
- });
10903
- if (p.isCancel(workerName)) process.exit(1);
10904
- await wrangler("deploy", "--name", workerName);
10905
- return workerName;
10944
+ let resolvedWorkerName = workerName;
10945
+ if (resolvedWorkerName) p.log.info("Using existing Cloudflare Worker name.");
10946
+ else {
10947
+ const inputWorkerName = await p.text({
10948
+ message: "Enter the name of the worker",
10949
+ defaultValue: "hot-updater",
10950
+ placeholder: "hot-updater"
10951
+ });
10952
+ if (p.isCancel(inputWorkerName)) process.exit(1);
10953
+ resolvedWorkerName = inputWorkerName;
10954
+ }
10955
+ await wrangler("deploy", "--name", resolvedWorkerName);
10956
+ return resolvedWorkerName;
10906
10957
  } catch (error) {
10907
10958
  throw new Error("Failed to deploy worker", { cause: error });
10908
10959
  } finally {
@@ -10911,6 +10962,7 @@ const deployWorker = async (oauth_token, accountId, { d1DatabaseId, d1DatabaseNa
10911
10962
  };
10912
10963
  const runInit = async ({ build }) => {
10913
10964
  const cwd = getCwd();
10965
+ const existingEnv = await readHotUpdaterEnv(cwd);
10914
10966
  let auth = getWranglerLoginAuthToken();
10915
10967
  if (!auth || (0, import_dayjs_min.default)(auth?.expiration_time).isBefore((0, import_dayjs_min.default)())) {
10916
10968
  await execa("npx", [
@@ -10928,70 +10980,117 @@ const runInit = async ({ build }) => {
10928
10980
  if (!auth) throw new Error("'npx wrangler login' is required to use this command");
10929
10981
  const cf = new Cloudflare({ apiToken: auth.oauth_token });
10930
10982
  const createKey = `create/${Math.random().toString(36).substring(2, 15)}`;
10931
- const accounts = [];
10932
- try {
10933
- await p.tasks([{
10934
- title: "Checking Account List...",
10935
- task: async () => {
10936
- accounts.push(...(await cf.accounts.list()).result.map((account) => ({
10937
- id: account.id,
10938
- name: account.name
10939
- })));
10940
- }
10941
- }]);
10942
- } catch (e) {
10943
- if (e instanceof Error) p.log.error(e.message);
10944
- throw e;
10983
+ let accountId = getEnvValue(existingEnv, "HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID");
10984
+ if (accountId) p.log.info("Using existing Cloudflare account ID.");
10985
+ else {
10986
+ const accounts = [];
10987
+ try {
10988
+ await p.tasks([{
10989
+ title: "Checking Account List...",
10990
+ task: async () => {
10991
+ accounts.push(...(await cf.accounts.list()).result.map((account) => ({
10992
+ id: account.id,
10993
+ name: account.name
10994
+ })));
10995
+ }
10996
+ }]);
10997
+ } catch (e) {
10998
+ if (e instanceof Error) p.log.error(e.message);
10999
+ throw e;
11000
+ }
11001
+ const selectedAccountId = await p.select({
11002
+ message: "Account List",
11003
+ options: accounts.map((account) => ({
11004
+ value: account.id,
11005
+ label: `${account.name} (${account.id})`
11006
+ }))
11007
+ });
11008
+ if (p.isCancel(selectedAccountId)) process.exit(1);
11009
+ accountId = selectedAccountId;
10945
11010
  }
10946
- const accountId = await p.select({
10947
- message: "Account List",
10948
- options: accounts.map((account) => ({
10949
- value: account.id,
10950
- label: `${account.name} (${account.id})`
10951
- }))
10952
- });
10953
- if (p.isCancel(accountId)) process.exit(1);
10954
- p.log.step(`Please visit this link to create an API Token: ${link(`https://dash.cloudflare.com/${accountId}/api-tokens`)}`);
10955
- p.log.step("You need edit permissions for both D1 and R2");
10956
- const apiToken = await p.password({ message: "Enter the API Token" });
10957
- if (!apiToken) p.log.warn("Skipping API Token. You can set it later in .env HOT_UPDATER_CLOUDFLARE_API_TOKEN file.");
10958
- if (p.isCancel(apiToken)) process.exit(1);
10959
- const availableBuckets = [];
10960
- try {
10961
- await p.tasks([{
10962
- title: "Checking R2 Buckets...",
10963
- task: async () => {
10964
- const buckets = (await cf.r2.buckets.list({ account_id: accountId })).buckets ?? [];
10965
- availableBuckets.push(...buckets.filter((bucket) => bucket.name).map((bucket) => ({ name: bucket.name })));
10966
- }
10967
- }]);
10968
- } catch (e) {
10969
- if (e instanceof Error) p.log.error(e.message);
10970
- throw e;
11011
+ let apiToken = getEnvValue(existingEnv, "HOT_UPDATER_CLOUDFLARE_API_TOKEN");
11012
+ if (apiToken) p.log.info("Using existing Cloudflare API token.");
11013
+ else {
11014
+ p.log.step(`D1 API Token dashboard: ${link(`https://dash.cloudflare.com/${accountId}/api-tokens`)}`);
11015
+ p.log.step("Required permission: D1 Edit");
11016
+ p.log.step("Used for bundle metadata writes after init.");
11017
+ const inputApiToken = await p.password({ message: "Enter the D1 API Token" });
11018
+ if (p.isCancel(inputApiToken)) process.exit(1);
11019
+ apiToken = inputApiToken;
11020
+ if (!apiToken) p.log.warn("Skipping API Token. You can set it later in .env HOT_UPDATER_CLOUDFLARE_API_TOKEN file.");
11021
+ }
11022
+ const existingBucketName = getEnvValue(existingEnv, "HOT_UPDATER_CLOUDFLARE_R2_BUCKET_NAME");
11023
+ let selectedBucketName;
11024
+ if (existingBucketName) {
11025
+ selectedBucketName = existingBucketName;
11026
+ p.log.info("Using existing Cloudflare R2 bucket name.");
11027
+ } else {
11028
+ const availableBuckets = [];
11029
+ try {
11030
+ await p.tasks([{
11031
+ title: "Checking R2 Buckets...",
11032
+ task: async () => {
11033
+ const buckets = (await cf.r2.buckets.list({ account_id: accountId })).buckets ?? [];
11034
+ availableBuckets.push(...buckets.filter((bucket) => bucket.name).map((bucket) => ({ name: bucket.name })));
11035
+ }
11036
+ }]);
11037
+ } catch (e) {
11038
+ if (e instanceof Error) p.log.error(e.message);
11039
+ throw e;
11040
+ }
11041
+ if (availableBuckets.length === 1) {
11042
+ selectedBucketName = availableBuckets[0].name;
11043
+ p.log.info("Using the only Cloudflare R2 bucket.");
11044
+ } else {
11045
+ const selectedR2BucketName = await p.select({
11046
+ message: "R2 List",
11047
+ options: [...availableBuckets.map((bucket) => ({
11048
+ value: bucket.name,
11049
+ label: bucket.name
11050
+ })), {
11051
+ value: createKey,
11052
+ label: "Create New R2 Bucket"
11053
+ }]
11054
+ });
11055
+ if (p.isCancel(selectedR2BucketName)) process.exit(1);
11056
+ selectedBucketName = selectedR2BucketName;
11057
+ }
11058
+ if (selectedBucketName === createKey) {
11059
+ const name = await p.text({ message: "Enter the name of the new R2 Bucket" });
11060
+ if (p.isCancel(name)) process.exit(1);
11061
+ const newR2 = await cf.r2.buckets.create({
11062
+ account_id: accountId,
11063
+ name
11064
+ });
11065
+ if (!newR2.name) throw new Error("Failed to create new R2 Bucket");
11066
+ selectedBucketName = newR2.name;
11067
+ }
10971
11068
  }
10972
- let selectedBucketName = await p.select({
10973
- message: "R2 List",
10974
- options: [...availableBuckets.map((bucket) => ({
10975
- value: bucket.name,
10976
- label: bucket.name
10977
- })), {
10978
- value: createKey,
10979
- label: "Create New R2 Bucket"
10980
- }]
10981
- });
10982
- if (p.isCancel(selectedBucketName)) process.exit(1);
10983
- if (selectedBucketName === createKey) {
10984
- const name = await p.text({ message: "Enter the name of the new R2 Bucket" });
10985
- if (p.isCancel(name)) process.exit(1);
10986
- const newR2 = await cf.r2.buckets.create({
10987
- account_id: accountId,
10988
- name
11069
+ p.log.info(`Selected R2: ${selectedBucketName}`);
11070
+ const existingR2AccessKeyId = getEnvValue(existingEnv, "HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID");
11071
+ const existingR2SecretAccessKey = getEnvValue(existingEnv, "HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY");
11072
+ let r2AccessKeyId = existingR2AccessKeyId;
11073
+ let r2SecretAccessKey = existingR2SecretAccessKey;
11074
+ if (r2AccessKeyId && r2SecretAccessKey) p.log.info("Using existing Cloudflare R2 API credentials.");
11075
+ else if (r2AccessKeyId || r2SecretAccessKey) {
11076
+ p.log.warn("Existing Cloudflare R2 API credentials are incomplete.");
11077
+ const credentials = await inputR2ApiCredentials({
11078
+ accountId,
11079
+ bucketName: selectedBucketName,
11080
+ accessKeyId: r2AccessKeyId,
11081
+ secretAccessKey: r2SecretAccessKey
11082
+ });
11083
+ r2AccessKeyId = credentials.accessKeyId;
11084
+ r2SecretAccessKey = credentials.secretAccessKey;
11085
+ } else {
11086
+ const credentials = await inputR2ApiCredentials({
11087
+ accountId,
11088
+ bucketName: selectedBucketName
10989
11089
  });
10990
- if (!newR2.name) throw new Error("Failed to create new R2 Bucket");
10991
- selectedBucketName = newR2.name;
11090
+ r2AccessKeyId = credentials.accessKeyId;
11091
+ r2SecretAccessKey = credentials.secretAccessKey;
10992
11092
  }
10993
- p.log.info(`Selected R2: ${selectedBucketName}`);
10994
- if ((await cf.r2.buckets.domains.managed.list(selectedBucketName, { account_id: accountId })).enabled) {
11093
+ if ((existingBucketName ? { enabled: false } : await cf.r2.buckets.domains.managed.list(selectedBucketName, { account_id: accountId })).enabled) {
10995
11094
  if (await p.confirm({ message: "Make R2 bucket private?" })) try {
10996
11095
  await p.tasks([{
10997
11096
  title: "Making R2 bucket private...",
@@ -11023,17 +11122,27 @@ const runInit = async ({ build }) => {
11023
11122
  if (e instanceof Error) p.log.error(e.message);
11024
11123
  throw e;
11025
11124
  }
11026
- let selectedD1DatabaseId = await p.select({
11027
- message: "D1 List",
11028
- options: [...availableD1List.map((d1) => ({
11029
- value: d1.uuid,
11030
- label: `${d1.name} (${d1.uuid})`
11031
- })), {
11032
- value: createKey,
11033
- label: "Create New D1 Database"
11034
- }]
11035
- });
11036
- if (p.isCancel(selectedD1DatabaseId)) process.exit(1);
11125
+ const existingD1DatabaseId = getEnvValue(existingEnv, "HOT_UPDATER_CLOUDFLARE_D1_DATABASE_ID");
11126
+ const hasExistingD1Database = availableD1List.some((d1) => d1.uuid === existingD1DatabaseId);
11127
+ let selectedD1DatabaseId;
11128
+ if (existingD1DatabaseId && hasExistingD1Database) {
11129
+ selectedD1DatabaseId = existingD1DatabaseId;
11130
+ p.log.info("Using existing Cloudflare D1 database ID.");
11131
+ } else {
11132
+ if (existingD1DatabaseId) p.log.warn("Existing Cloudflare D1 database ID was not found. Select a database again.");
11133
+ const selectedD1 = await p.select({
11134
+ message: "D1 List",
11135
+ options: [...availableD1List.map((d1) => ({
11136
+ value: d1.uuid,
11137
+ label: `${d1.name} (${d1.uuid})`
11138
+ })), {
11139
+ value: createKey,
11140
+ label: "Create New D1 Database"
11141
+ }]
11142
+ });
11143
+ if (p.isCancel(selectedD1)) process.exit(1);
11144
+ selectedD1DatabaseId = selectedD1;
11145
+ }
11037
11146
  if (selectedD1DatabaseId === createKey) {
11038
11147
  const name = await p.text({ message: "Enter the name of the new D1 Database" });
11039
11148
  if (p.isCancel(name)) process.exit(1);
@@ -11052,17 +11161,22 @@ const runInit = async ({ build }) => {
11052
11161
  const d1DatabaseName = availableD1List.find((d1) => d1.uuid === selectedD1DatabaseId)?.name;
11053
11162
  if (!d1DatabaseName) throw new Error("Failed to get D1 Database name");
11054
11163
  const subdomains = await cf.workers.subdomains.get({ account_id: accountId });
11164
+ const existingWorkerName = getEnvValue(existingEnv, "HOT_UPDATER_CLOUDFLARE_WORKER_NAME");
11055
11165
  const workerName = await deployWorker(auth.oauth_token, accountId, {
11056
11166
  d1DatabaseId: selectedD1DatabaseId,
11057
11167
  d1DatabaseName,
11058
- r2BucketName: selectedBucketName
11168
+ r2BucketName: selectedBucketName,
11169
+ workerName: existingWorkerName
11059
11170
  });
11060
11171
  const configWriteResult = await writeHotUpdaterConfig(getConfigScaffold(build));
11061
11172
  await makeEnv({
11062
11173
  HOT_UPDATER_CLOUDFLARE_API_TOKEN: apiToken,
11063
11174
  HOT_UPDATER_CLOUDFLARE_ACCOUNT_ID: accountId,
11064
11175
  HOT_UPDATER_CLOUDFLARE_R2_BUCKET_NAME: selectedBucketName,
11065
- HOT_UPDATER_CLOUDFLARE_D1_DATABASE_ID: selectedD1DatabaseId
11176
+ HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID: r2AccessKeyId,
11177
+ HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY: r2SecretAccessKey,
11178
+ HOT_UPDATER_CLOUDFLARE_D1_DATABASE_ID: selectedD1DatabaseId,
11179
+ HOT_UPDATER_CLOUDFLARE_WORKER_NAME: workerName
11066
11180
  });
11067
11181
  p.log.success("Generated '.env.hotupdater' file with Cloudflare settings.");
11068
11182
  if (configWriteResult.status === "created") p.log.success("Generated 'hot-updater.config.ts' file with Cloudflare settings.");
package/dist/index.cjs CHANGED
@@ -6,7 +6,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
9
+ var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
10
10
  var __copyProps = (to, from, except, desc) => {
11
11
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
12
  key = keys[i];
@@ -25,21 +25,25 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  let _hot_updater_core = require("@hot-updater/core");
26
26
  let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
27
27
  let cloudflare = require("cloudflare");
28
- cloudflare = __toESM(cloudflare, 1);
28
+ cloudflare = __toESM(cloudflare);
29
29
  let node_fs_promises = require("node:fs/promises");
30
- node_fs_promises = __toESM(node_fs_promises, 1);
30
+ node_fs_promises = __toESM(node_fs_promises);
31
31
  let node_path = require("node:path");
32
- node_path = __toESM(node_path, 1);
32
+ node_path = __toESM(node_path);
33
+ let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
34
+ let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
35
+ let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
36
+ let node_os = require("node:os");
37
+ node_os = __toESM(node_os);
33
38
  let node_url = require("node:url");
34
39
  let node_child_process = require("node:child_process");
35
40
  let node_string_decoder = require("node:string_decoder");
36
41
  let node_util = require("node:util");
37
42
  let node_process = require("node:process");
38
- node_process = __toESM(node_process, 1);
43
+ node_process = __toESM(node_process);
39
44
  let node_tty = require("node:tty");
40
- node_tty = __toESM(node_tty, 1);
45
+ node_tty = __toESM(node_tty);
41
46
  let node_timers_promises = require("node:timers/promises");
42
- let node_os = require("node:os");
43
47
  let node_events = require("node:events");
44
48
  let node_v8 = require("node:v8");
45
49
  let node_fs = require("node:fs");
@@ -669,6 +673,112 @@ const d1Database = (0, _hot_updater_plugin_core.createDatabasePlugin)({
669
673
  }
670
674
  });
671
675
  //#endregion
676
+ //#region src/r2S3Storage.ts
677
+ const ensureExpectedR2Bucket$1 = (bucket, bucketName) => {
678
+ if (bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`);
679
+ };
680
+ const isS3ObjectNotFoundError = (error) => {
681
+ if (error instanceof Error) return error.name === "NotFound" || error.name === "NoSuchKey";
682
+ if (typeof error === "object" && error !== null && "$metadata" in error) return error.$metadata?.httpStatusCode === 404;
683
+ return false;
684
+ };
685
+ const createS3Client = (config) => {
686
+ const { accountId, basePath: _basePath, bucketName: _bucketName, endpoint, forcePathStyle, region, ...s3Config } = config;
687
+ return new _aws_sdk_client_s3.S3Client({
688
+ ...s3Config,
689
+ endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
690
+ forcePathStyle: forcePathStyle ?? true,
691
+ region: region ?? "auto"
692
+ });
693
+ };
694
+ const createS3StorageProfile = (config) => {
695
+ const { bucketName } = config;
696
+ const client = createS3Client(config);
697
+ const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
698
+ return {
699
+ async delete(storageUri) {
700
+ const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
701
+ ensureExpectedR2Bucket$1(bucket, bucketName);
702
+ await client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
703
+ Bucket: bucketName,
704
+ Key: key
705
+ }));
706
+ },
707
+ async upload(key, filePath) {
708
+ const Body = await node_fs_promises.default.readFile(filePath);
709
+ const ContentType = (0, _hot_updater_plugin_core.getContentType)(filePath);
710
+ const Key = getStorageKey(key, node_path.default.basename(filePath));
711
+ await new _aws_sdk_lib_storage.Upload({
712
+ client,
713
+ params: {
714
+ Body,
715
+ Bucket: bucketName,
716
+ CacheControl: "max-age=31536000",
717
+ ContentType,
718
+ Key
719
+ }
720
+ }).done();
721
+ return { storageUri: `r2://${bucketName}/${Key}` };
722
+ },
723
+ async exists(storageUri) {
724
+ const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
725
+ ensureExpectedR2Bucket$1(bucket, bucketName);
726
+ try {
727
+ await client.send(new _aws_sdk_client_s3.HeadObjectCommand({
728
+ Bucket: bucketName,
729
+ Key: key
730
+ }));
731
+ return true;
732
+ } catch (error) {
733
+ if (isS3ObjectNotFoundError(error)) return false;
734
+ throw error;
735
+ }
736
+ },
737
+ async downloadFile(storageUri, filePath) {
738
+ const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
739
+ ensureExpectedR2Bucket$1(bucket, bucketName);
740
+ const response = await client.send(new _aws_sdk_client_s3.GetObjectCommand({
741
+ Bucket: bucketName,
742
+ Key: key
743
+ }));
744
+ if (!response.Body) throw new Error("R2 object body is empty");
745
+ await node_fs_promises.default.mkdir(node_path.default.dirname(filePath), { recursive: true });
746
+ await node_fs_promises.default.writeFile(filePath, await response.Body.transformToByteArray());
747
+ }
748
+ };
749
+ };
750
+ const createS3RuntimeStorageProfile = (config) => {
751
+ const { bucketName } = config;
752
+ const client = createS3Client(config);
753
+ return {
754
+ async readText(storageUri) {
755
+ const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
756
+ ensureExpectedR2Bucket$1(bucket, bucketName);
757
+ try {
758
+ const response = await client.send(new _aws_sdk_client_s3.GetObjectCommand({
759
+ Bucket: bucketName,
760
+ Key: key
761
+ }));
762
+ if (!response.Body) return null;
763
+ return response.Body.transformToString();
764
+ } catch (error) {
765
+ if (isS3ObjectNotFoundError(error)) return null;
766
+ throw error;
767
+ }
768
+ },
769
+ async getDownloadUrl(storageUri) {
770
+ const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
771
+ ensureExpectedR2Bucket$1(bucket, bucketName);
772
+ const signedUrl = await (0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.GetObjectCommand({
773
+ Bucket: bucketName,
774
+ Key: key
775
+ }), { expiresIn: 3600 });
776
+ if (!signedUrl) throw new Error("Failed to presign R2 URL");
777
+ return { fileUrl: signedUrl };
778
+ }
779
+ };
780
+ };
781
+ //#endregion
672
782
  //#region ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
673
783
  function isPlainObject(value) {
674
784
  if (typeof value !== "object" || value === null) return false;
@@ -6842,55 +6952,108 @@ const createWrangler = ({ stdio, accountId, cloudflareApiToken, cwd }) => {
6842
6952
  return (...command) => $("npx", ["wrangler", ...command]);
6843
6953
  };
6844
6954
  //#endregion
6955
+ //#region src/r2WranglerStorage.ts
6956
+ const ensureExpectedR2Bucket = (bucket, bucketName) => {
6957
+ if (bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`);
6958
+ };
6959
+ const isR2ObjectNotFoundError = (error) => {
6960
+ const output = [
6961
+ error.stderr,
6962
+ error.stdout,
6963
+ error.shortMessage,
6964
+ error.message
6965
+ ].filter(Boolean).join("\n").toLowerCase();
6966
+ return output.includes("not found") || output.includes("no such object") || output.includes("does not exist");
6967
+ };
6968
+ const createWranglerStorageProfile = (config) => {
6969
+ const { bucketName, cloudflareApiToken, accountId } = config;
6970
+ const wrangler = createWrangler({
6971
+ accountId,
6972
+ cloudflareApiToken,
6973
+ cwd: process.cwd()
6974
+ });
6975
+ const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
6976
+ return {
6977
+ async delete(storageUri) {
6978
+ const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
6979
+ ensureExpectedR2Bucket(bucket, bucketName);
6980
+ try {
6981
+ await wrangler("r2", "object", "delete", [bucketName, key].join("/"), "--remote");
6982
+ } catch {
6983
+ throw new Error("Can not delete bundle");
6984
+ }
6985
+ },
6986
+ async upload(key, filePath) {
6987
+ const contentType = (0, _hot_updater_plugin_core.getContentType)(filePath);
6988
+ const Key = getStorageKey(key, node_path.default.basename(filePath));
6989
+ try {
6990
+ const { stderr, exitCode } = await wrangler("r2", "object", "put", [bucketName, Key].join("/"), "--file", filePath, "--content-type", contentType, "--remote");
6991
+ if (exitCode !== 0 && stderr) throw new Error(stderr);
6992
+ } catch (error) {
6993
+ if (error instanceof ExecaError) throw new Error(error.stderr || error.stdout);
6994
+ throw error;
6995
+ }
6996
+ return { storageUri: `r2://${bucketName}/${Key}` };
6997
+ },
6998
+ async exists(storageUri) {
6999
+ const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
7000
+ ensureExpectedR2Bucket(bucket, bucketName);
7001
+ const tempDir = await node_fs_promises.default.mkdtemp(node_path.default.join(node_os.default.tmpdir(), "hot-updater-r2-exists-"));
7002
+ const tempFilePath = node_path.default.join(tempDir, "object");
7003
+ try {
7004
+ await wrangler("r2", "object", "get", [bucketName, key].join("/"), "--file", tempFilePath, "--remote");
7005
+ return true;
7006
+ } catch (error) {
7007
+ if (error instanceof ExecaError && isR2ObjectNotFoundError(error)) return false;
7008
+ throw error;
7009
+ } finally {
7010
+ await node_fs_promises.default.rm(tempDir, {
7011
+ force: true,
7012
+ recursive: true
7013
+ });
7014
+ }
7015
+ },
7016
+ async downloadFile(storageUri, filePath) {
7017
+ const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
7018
+ ensureExpectedR2Bucket(bucket, bucketName);
7019
+ try {
7020
+ await node_fs_promises.default.mkdir(node_path.default.dirname(filePath), { recursive: true });
7021
+ const { stderr, exitCode } = await wrangler("r2", "object", "get", [bucketName, key].join("/"), "--file", filePath, "--remote");
7022
+ if (exitCode !== 0 && stderr) throw new Error(stderr);
7023
+ } catch (error) {
7024
+ if (error instanceof ExecaError) throw new Error(error.stderr || error.stdout);
7025
+ throw error;
7026
+ }
7027
+ }
7028
+ };
7029
+ };
7030
+ const createWranglerRuntimeStorageProfile = () => {
7031
+ const error = /* @__PURE__ */ new Error("r2Storage runtime profile requires R2 S3 credentials. Wrangler-based R2 access is only supported by the node profile.");
7032
+ return {
7033
+ async readText() {
7034
+ throw error;
7035
+ },
7036
+ async getDownloadUrl() {
7037
+ throw error;
7038
+ }
7039
+ };
7040
+ };
7041
+ //#endregion
6845
7042
  //#region src/r2Storage.ts
6846
- /**
6847
- * Cloudflare R2 storage plugin for Hot Updater.
6848
- */
6849
- const r2Storage = (0, _hot_updater_plugin_core.createNodeStoragePlugin)({
7043
+ const hasS3Credentials = (config) => {
7044
+ return Boolean(config.credentials);
7045
+ };
7046
+ const r2Storage = (0, _hot_updater_plugin_core.createUniversalStoragePlugin)({
6850
7047
  name: "r2Storage",
6851
7048
  supportedProtocol: "r2",
6852
7049
  factory: (config) => {
6853
- const { bucketName, cloudflareApiToken, accountId } = config;
6854
- const wrangler = createWrangler({
6855
- accountId,
6856
- cloudflareApiToken,
6857
- cwd: process.cwd()
6858
- });
6859
- const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
7050
+ if (hasS3Credentials(config)) return {
7051
+ node: createS3StorageProfile(config),
7052
+ runtime: createS3RuntimeStorageProfile(config)
7053
+ };
6860
7054
  return {
6861
- async delete(storageUri) {
6862
- const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
6863
- if (bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`);
6864
- try {
6865
- await wrangler("r2", "object", "delete", [bucketName, key].join("/"), "--remote");
6866
- } catch {
6867
- throw new Error("Can not delete bundle");
6868
- }
6869
- },
6870
- async upload(key, filePath) {
6871
- const contentType = (0, _hot_updater_plugin_core.getContentType)(filePath);
6872
- const Key = getStorageKey(key, node_path.default.basename(filePath));
6873
- try {
6874
- const { stderr, exitCode } = await wrangler("r2", "object", "put", [bucketName, Key].join("/"), "--file", filePath, "--content-type", contentType, "--remote");
6875
- if (exitCode !== 0 && stderr) throw new Error(stderr);
6876
- } catch (error) {
6877
- if (error instanceof ExecaError) throw new Error(error.stderr || error.stdout);
6878
- throw error;
6879
- }
6880
- return { storageUri: `r2://${bucketName}/${Key}` };
6881
- },
6882
- async downloadFile(storageUri, filePath) {
6883
- const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
6884
- if (bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`);
6885
- try {
6886
- await node_fs_promises.default.mkdir(node_path.default.dirname(filePath), { recursive: true });
6887
- const { stderr, exitCode } = await wrangler("r2", "object", "get", [bucketName, key].join("/"), "--file", filePath, "--remote");
6888
- if (exitCode !== 0 && stderr) throw new Error(stderr);
6889
- } catch (error) {
6890
- if (error instanceof ExecaError) throw new Error(error.stderr || error.stdout);
6891
- throw error;
6892
- }
6893
- }
7055
+ node: createWranglerStorageProfile(config),
7056
+ runtime: createWranglerRuntimeStorageProfile()
6894
7057
  };
6895
7058
  }
6896
7059
  });