@hot-updater/cloudflare 0.31.4 → 0.33.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.
@@ -0,0 +1,193 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ createStorageKeyBuilder,
7
+ getContentType,
8
+ type NodeStorageProfile,
9
+ parseStorageUri,
10
+ type RuntimeStorageProfile,
11
+ } from "@hot-updater/plugin-core";
12
+ import { ExecaError } from "execa";
13
+
14
+ import { createWrangler } from "./utils/createWrangler";
15
+
16
+ /**
17
+ * @deprecated `cloudflareApiToken` uses the Wrangler CLI for R2 operations,
18
+ * which is slower than direct S3-compatible API access. Create R2
19
+ * S3-compatible credentials in the Cloudflare dashboard and pass them with
20
+ * `r2Storage({ credentials })` instead.
21
+ */
22
+ export interface R2WranglerStorageConfig {
23
+ accountId: string;
24
+ bucketName: string;
25
+ /**
26
+ * @deprecated This token keeps R2 access on the slower Wrangler CLI path.
27
+ * Create R2 S3-compatible credentials in the Cloudflare dashboard and use
28
+ * `credentials` instead.
29
+ */
30
+ cloudflareApiToken: string;
31
+ /**
32
+ * Base path where bundles will be stored in the bucket
33
+ */
34
+ basePath?: string;
35
+ credentials?: never;
36
+ }
37
+
38
+ const ensureExpectedR2Bucket = (bucket: string, bucketName: string) => {
39
+ if (bucket !== bucketName) {
40
+ throw new Error(
41
+ `Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`,
42
+ );
43
+ }
44
+ };
45
+
46
+ const isR2ObjectNotFoundError = (error: ExecaError) => {
47
+ const output = [error.stderr, error.stdout, error.shortMessage, error.message]
48
+ .filter(Boolean)
49
+ .join("\n")
50
+ .toLowerCase();
51
+
52
+ return (
53
+ output.includes("not found") ||
54
+ output.includes("no such object") ||
55
+ output.includes("does not exist")
56
+ );
57
+ };
58
+
59
+ export const createWranglerStorageProfile = (
60
+ config: R2WranglerStorageConfig,
61
+ ): NodeStorageProfile => {
62
+ const { bucketName, cloudflareApiToken, accountId } = config;
63
+ const wrangler = createWrangler({
64
+ accountId,
65
+ cloudflareApiToken,
66
+ cwd: process.cwd(),
67
+ });
68
+ const getStorageKey = createStorageKeyBuilder(config.basePath);
69
+
70
+ return {
71
+ async delete(storageUri) {
72
+ const { bucket, key } = parseStorageUri(storageUri, "r2");
73
+ ensureExpectedR2Bucket(bucket, bucketName);
74
+
75
+ try {
76
+ await wrangler(
77
+ "r2",
78
+ "object",
79
+ "delete",
80
+ [bucketName, key].join("/"),
81
+ "--remote",
82
+ );
83
+ } catch {
84
+ throw new Error("Can not delete bundle");
85
+ }
86
+ },
87
+ async upload(key, filePath) {
88
+ const contentType = getContentType(filePath);
89
+
90
+ const filename = path.basename(filePath);
91
+
92
+ const Key = getStorageKey(key, filename);
93
+ try {
94
+ const { stderr, exitCode } = await wrangler(
95
+ "r2",
96
+ "object",
97
+ "put",
98
+ [bucketName, Key].join("/"),
99
+ "--file",
100
+ filePath,
101
+ "--content-type",
102
+ contentType,
103
+ "--remote",
104
+ );
105
+ if (exitCode !== 0 && stderr) {
106
+ throw new Error(stderr);
107
+ }
108
+ } catch (error) {
109
+ if (error instanceof ExecaError) {
110
+ throw new Error(error.stderr || error.stdout);
111
+ }
112
+
113
+ throw error;
114
+ }
115
+
116
+ return {
117
+ storageUri: `r2://${bucketName}/${Key}`,
118
+ };
119
+ },
120
+ async exists(storageUri: string) {
121
+ const { bucket, key } = parseStorageUri(storageUri, "r2");
122
+ ensureExpectedR2Bucket(bucket, bucketName);
123
+
124
+ const tempDir = await fs.mkdtemp(
125
+ path.join(os.tmpdir(), "hot-updater-r2-exists-"),
126
+ );
127
+ const tempFilePath = path.join(tempDir, "object");
128
+
129
+ try {
130
+ await wrangler(
131
+ "r2",
132
+ "object",
133
+ "get",
134
+ [bucketName, key].join("/"),
135
+ "--file",
136
+ tempFilePath,
137
+ "--remote",
138
+ );
139
+ return true;
140
+ } catch (error) {
141
+ if (error instanceof ExecaError && isR2ObjectNotFoundError(error)) {
142
+ return false;
143
+ }
144
+
145
+ throw error;
146
+ } finally {
147
+ await fs.rm(tempDir, { force: true, recursive: true });
148
+ }
149
+ },
150
+ async downloadFile(storageUri, filePath) {
151
+ const { bucket, key } = parseStorageUri(storageUri, "r2");
152
+ ensureExpectedR2Bucket(bucket, bucketName);
153
+
154
+ try {
155
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
156
+ const { stderr, exitCode } = await wrangler(
157
+ "r2",
158
+ "object",
159
+ "get",
160
+ [bucketName, key].join("/"),
161
+ "--file",
162
+ filePath,
163
+ "--remote",
164
+ );
165
+ if (exitCode !== 0 && stderr) {
166
+ throw new Error(stderr);
167
+ }
168
+ } catch (error) {
169
+ if (error instanceof ExecaError) {
170
+ throw new Error(error.stderr || error.stdout);
171
+ }
172
+
173
+ throw error;
174
+ }
175
+ },
176
+ };
177
+ };
178
+
179
+ export const createWranglerRuntimeStorageProfile =
180
+ (): RuntimeStorageProfile => {
181
+ const error = new Error(
182
+ "r2Storage runtime profile requires R2 S3 credentials. Wrangler-based R2 access is only supported by the node profile.",
183
+ );
184
+
185
+ return {
186
+ async readText() {
187
+ throw error;
188
+ },
189
+ async getDownloadUrl() {
190
+ throw error;
191
+ },
192
+ };
193
+ };
@@ -1 +1 @@
1
- This folder contains the built output assets for the worker "hot-updater" generated at 2026-05-18T15:42:26.357Z.
1
+ This folder contains the built output assets for the worker "hot-updater" generated at 2026-06-11T16:55:59.241Z.