openwrangler 0.0.2 → 0.0.4
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.d.mts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.mjs +794 -3
- package/package.json +4 -4
package/dist/index.d.mts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { R2Bucket, KVNamespace, D1Database } from '@cloudflare/workers-types/experimental';
|
|
2
2
|
|
|
3
|
+
interface R2Config {
|
|
4
|
+
accountId: string;
|
|
5
|
+
r2AccessKeyId: string;
|
|
6
|
+
r2SecretAccessKey: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
interface BindingsConfig {
|
|
4
10
|
accountId: string;
|
|
5
11
|
apiToken: string;
|
|
6
12
|
}
|
|
7
|
-
|
|
13
|
+
|
|
14
|
+
declare function createR2Binding(config: R2Config, bucketName: string): R2Bucket;
|
|
8
15
|
declare function createKVBinding(config: BindingsConfig, namespaceId: string): KVNamespace;
|
|
9
16
|
declare function createD1Binding(config: BindingsConfig, databaseId: string): D1Database;
|
|
10
17
|
|
|
11
18
|
export { createD1Binding, createKVBinding, createR2Binding };
|
|
12
|
-
export type { BindingsConfig };
|
|
19
|
+
export type { BindingsConfig, R2Config };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { R2Bucket, KVNamespace, D1Database } from '@cloudflare/workers-types/experimental';
|
|
2
2
|
|
|
3
|
+
interface R2Config {
|
|
4
|
+
accountId: string;
|
|
5
|
+
r2AccessKeyId: string;
|
|
6
|
+
r2SecretAccessKey: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
interface BindingsConfig {
|
|
4
10
|
accountId: string;
|
|
5
11
|
apiToken: string;
|
|
6
12
|
}
|
|
7
|
-
|
|
13
|
+
|
|
14
|
+
declare function createR2Binding(config: R2Config, bucketName: string): R2Bucket;
|
|
8
15
|
declare function createKVBinding(config: BindingsConfig, namespaceId: string): KVNamespace;
|
|
9
16
|
declare function createD1Binding(config: BindingsConfig, databaseId: string): D1Database;
|
|
10
17
|
|
|
11
18
|
export { createD1Binding, createKVBinding, createR2Binding };
|
|
12
|
-
export type { BindingsConfig };
|
|
19
|
+
export type { BindingsConfig, R2Config };
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,802 @@
|
|
|
1
|
+
class CloudflareAPIClient {
|
|
2
|
+
baseUrl = "https://api.cloudflare.com/client/v4";
|
|
3
|
+
accountId;
|
|
4
|
+
apiToken;
|
|
5
|
+
constructor(accountId, apiToken) {
|
|
6
|
+
this.accountId = accountId;
|
|
7
|
+
this.apiToken = apiToken;
|
|
8
|
+
}
|
|
9
|
+
async request(method, endpoint, body, headers) {
|
|
10
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
11
|
+
const response = await fetch(url, {
|
|
12
|
+
method,
|
|
13
|
+
headers: {
|
|
14
|
+
"Authorization": `Bearer ${this.apiToken}`,
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
...headers
|
|
17
|
+
},
|
|
18
|
+
body: body ? JSON.stringify(body) : void 0
|
|
19
|
+
});
|
|
20
|
+
const data = await response.json();
|
|
21
|
+
if (!response.ok || !data.success) {
|
|
22
|
+
const errorMessage = data.errors?.[0]?.message || `HTTP ${response.status}`;
|
|
23
|
+
throw new Error(`Cloudflare API Error: ${errorMessage}`);
|
|
24
|
+
}
|
|
25
|
+
return data.result;
|
|
26
|
+
}
|
|
27
|
+
async get(endpoint, headers) {
|
|
28
|
+
return this.request("GET", endpoint, void 0, headers);
|
|
29
|
+
}
|
|
30
|
+
async post(endpoint, body, headers) {
|
|
31
|
+
return this.request("POST", endpoint, body, headers);
|
|
32
|
+
}
|
|
33
|
+
async put(endpoint, body, headers) {
|
|
34
|
+
return this.request("PUT", endpoint, body, headers);
|
|
35
|
+
}
|
|
36
|
+
async delete(endpoint, body, headers) {
|
|
37
|
+
return this.request("DELETE", endpoint, body, headers);
|
|
38
|
+
}
|
|
39
|
+
getAccountId() {
|
|
40
|
+
return this.accountId;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createKVBinding$1(client, namespaceId) {
|
|
45
|
+
const baseEndpoint = `/accounts/${client.getAccountId()}/storage/kv/namespaces/${namespaceId}`;
|
|
46
|
+
async function getValue(key, type, cacheTtl) {
|
|
47
|
+
try {
|
|
48
|
+
const url = `${baseEndpoint}/values/${encodeURIComponent(key)}`;
|
|
49
|
+
const headers = {};
|
|
50
|
+
if (cacheTtl !== void 0) {
|
|
51
|
+
headers["Cache-Control"] = `max-age=${cacheTtl}`;
|
|
52
|
+
}
|
|
53
|
+
const response = await fetch(`https://api.cloudflare.com/client/v4${url}`, {
|
|
54
|
+
method: "GET",
|
|
55
|
+
headers: {
|
|
56
|
+
"Authorization": `Bearer ${client.apiToken}`,
|
|
57
|
+
...headers
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
if (response.status === 404) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`KV GET failed: ${response.status}`);
|
|
65
|
+
}
|
|
66
|
+
switch (type) {
|
|
67
|
+
case "json":
|
|
68
|
+
return await response.json();
|
|
69
|
+
case "arrayBuffer":
|
|
70
|
+
return await response.arrayBuffer();
|
|
71
|
+
case "stream":
|
|
72
|
+
return response.body;
|
|
73
|
+
case "text":
|
|
74
|
+
default:
|
|
75
|
+
return await response.text();
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error?.message?.includes("404")) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function getValueWithMetadata(key, type) {
|
|
85
|
+
try {
|
|
86
|
+
const url = `${baseEndpoint}/values/${encodeURIComponent(key)}`;
|
|
87
|
+
const response = await fetch(`https://api.cloudflare.com/client/v4${url}`, {
|
|
88
|
+
method: "GET",
|
|
89
|
+
headers: {
|
|
90
|
+
"Authorization": `Bearer ${client.apiToken}`
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
if (response.status === 404) {
|
|
94
|
+
return { value: null, metadata: null, cacheStatus: null };
|
|
95
|
+
}
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(`KV GET failed: ${response.status}`);
|
|
98
|
+
}
|
|
99
|
+
const metadata = response.headers.get("cf-kv-metadata");
|
|
100
|
+
const parsedMetadata = metadata ? JSON.parse(metadata) : null;
|
|
101
|
+
let value;
|
|
102
|
+
switch (type) {
|
|
103
|
+
case "json":
|
|
104
|
+
value = await response.json();
|
|
105
|
+
break;
|
|
106
|
+
case "arrayBuffer":
|
|
107
|
+
value = await response.arrayBuffer();
|
|
108
|
+
break;
|
|
109
|
+
case "stream":
|
|
110
|
+
value = response.body;
|
|
111
|
+
break;
|
|
112
|
+
case "text":
|
|
113
|
+
default:
|
|
114
|
+
value = await response.text();
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
value,
|
|
118
|
+
metadata: parsedMetadata,
|
|
119
|
+
cacheStatus: response.headers.get("cf-cache-status")
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error?.message?.includes("404")) {
|
|
123
|
+
return { value: null, metadata: null, cacheStatus: null };
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const kvNamespace = {
|
|
129
|
+
async get(key, options) {
|
|
130
|
+
if (Array.isArray(key)) {
|
|
131
|
+
const type2 = typeof options === "string" ? options : options?.type || "text";
|
|
132
|
+
const results = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const k of key) {
|
|
134
|
+
const value = await getValue(k, type2, options?.cacheTtl);
|
|
135
|
+
results.set(k, value);
|
|
136
|
+
}
|
|
137
|
+
return results;
|
|
138
|
+
}
|
|
139
|
+
const type = typeof options === "string" ? options : options?.type;
|
|
140
|
+
return getValue(key, type, options?.cacheTtl);
|
|
141
|
+
},
|
|
142
|
+
async put(key, value, options) {
|
|
143
|
+
const url = `${baseEndpoint}/values/${encodeURIComponent(key)}`;
|
|
144
|
+
const headers = {
|
|
145
|
+
"Authorization": `Bearer ${client.apiToken}`
|
|
146
|
+
};
|
|
147
|
+
let body;
|
|
148
|
+
if (typeof value === "string") {
|
|
149
|
+
body = value;
|
|
150
|
+
} else if (value instanceof ArrayBuffer) {
|
|
151
|
+
body = value;
|
|
152
|
+
} else if (ArrayBuffer.isView(value)) {
|
|
153
|
+
body = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
|
154
|
+
} else if (value instanceof ReadableStream) {
|
|
155
|
+
const reader = value.getReader();
|
|
156
|
+
const chunks = [];
|
|
157
|
+
while (true) {
|
|
158
|
+
const { done, value: chunk } = await reader.read();
|
|
159
|
+
if (done) break;
|
|
160
|
+
chunks.push(chunk);
|
|
161
|
+
}
|
|
162
|
+
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
163
|
+
const result = new Uint8Array(totalLength);
|
|
164
|
+
let offset = 0;
|
|
165
|
+
for (const chunk of chunks) {
|
|
166
|
+
result.set(chunk, offset);
|
|
167
|
+
offset += chunk.length;
|
|
168
|
+
}
|
|
169
|
+
body = result.buffer;
|
|
170
|
+
} else {
|
|
171
|
+
throw new Error("Unsupported value type");
|
|
172
|
+
}
|
|
173
|
+
const queryParams = new URLSearchParams();
|
|
174
|
+
if (options?.expiration) {
|
|
175
|
+
queryParams.set("expiration", options.expiration.toString());
|
|
176
|
+
}
|
|
177
|
+
if (options?.expirationTtl) {
|
|
178
|
+
queryParams.set("expiration_ttl", options.expirationTtl.toString());
|
|
179
|
+
}
|
|
180
|
+
const queryString = queryParams.toString();
|
|
181
|
+
const fullUrl = queryString ? `${url}?${queryString}` : url;
|
|
182
|
+
const response = await fetch(`https://api.cloudflare.com/client/v4${fullUrl}`, {
|
|
183
|
+
method: "PUT",
|
|
184
|
+
headers,
|
|
185
|
+
body
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new Error(`KV PUT failed: ${response.status}`);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
async delete(key) {
|
|
192
|
+
const url = `${baseEndpoint}/values/${encodeURIComponent(key)}`;
|
|
193
|
+
const response = await fetch(`https://api.cloudflare.com/client/v4${url}`, {
|
|
194
|
+
method: "DELETE",
|
|
195
|
+
headers: {
|
|
196
|
+
"Authorization": `Bearer ${client.apiToken}`
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
if (!response.ok && response.status !== 404) {
|
|
200
|
+
throw new Error(`KV DELETE failed: ${response.status}`);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
async deleteBulk(keys) {
|
|
204
|
+
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
205
|
+
const url = `${baseEndpoint}/bulk`;
|
|
206
|
+
const response = await fetch(`https://api.cloudflare.com/client/v4${url}`, {
|
|
207
|
+
method: "DELETE",
|
|
208
|
+
headers: {
|
|
209
|
+
"Authorization": `Bearer ${client.apiToken}`,
|
|
210
|
+
"Content-Type": "application/json"
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify(keyArray)
|
|
213
|
+
});
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
throw new Error(`KV DELETE BULK failed: ${response.status}`);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
async list(options) {
|
|
219
|
+
const queryParams = new URLSearchParams();
|
|
220
|
+
if (options?.limit) {
|
|
221
|
+
queryParams.set("limit", options.limit.toString());
|
|
222
|
+
}
|
|
223
|
+
if (options?.prefix) {
|
|
224
|
+
queryParams.set("prefix", options.prefix);
|
|
225
|
+
}
|
|
226
|
+
if (options?.cursor) {
|
|
227
|
+
queryParams.set("cursor", options.cursor);
|
|
228
|
+
}
|
|
229
|
+
const queryString = queryParams.toString();
|
|
230
|
+
const url = queryString ? `${baseEndpoint}/keys?${queryString}` : `${baseEndpoint}/keys`;
|
|
231
|
+
const data = await client.get(url);
|
|
232
|
+
if (data.list_complete) {
|
|
233
|
+
return {
|
|
234
|
+
list_complete: true,
|
|
235
|
+
keys: data.keys.map((k) => ({
|
|
236
|
+
name: k.name,
|
|
237
|
+
expiration: k.expiration,
|
|
238
|
+
metadata: k.metadata
|
|
239
|
+
})),
|
|
240
|
+
cacheStatus: null
|
|
241
|
+
};
|
|
242
|
+
} else {
|
|
243
|
+
return {
|
|
244
|
+
list_complete: false,
|
|
245
|
+
keys: data.keys.map((k) => ({
|
|
246
|
+
name: k.name,
|
|
247
|
+
expiration: k.expiration,
|
|
248
|
+
metadata: k.metadata
|
|
249
|
+
})),
|
|
250
|
+
cursor: data.cursor,
|
|
251
|
+
cacheStatus: null
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
async getWithMetadata(key, options) {
|
|
256
|
+
if (Array.isArray(key)) {
|
|
257
|
+
const type2 = typeof options === "string" ? options : options?.type || "text";
|
|
258
|
+
const results = /* @__PURE__ */ new Map();
|
|
259
|
+
for (const k of key) {
|
|
260
|
+
const result = await getValueWithMetadata(k, type2);
|
|
261
|
+
results.set(k, result);
|
|
262
|
+
}
|
|
263
|
+
return results;
|
|
264
|
+
}
|
|
265
|
+
const type = typeof options === "string" ? options : options?.type;
|
|
266
|
+
return getValueWithMetadata(key, type);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
return kvNamespace;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
class D1PreparedStatementImpl {
|
|
273
|
+
constructor(query, client, databaseId) {
|
|
274
|
+
this.query = query;
|
|
275
|
+
this.client = client;
|
|
276
|
+
this.databaseId = databaseId;
|
|
277
|
+
}
|
|
278
|
+
bindings = [];
|
|
279
|
+
bind(...values) {
|
|
280
|
+
this.bindings = values;
|
|
281
|
+
return this;
|
|
282
|
+
}
|
|
283
|
+
async run() {
|
|
284
|
+
const endpoint = `/accounts/${this.client.getAccountId()}/d1/database/${this.databaseId}/query`;
|
|
285
|
+
const data = await this.client.post(endpoint, {
|
|
286
|
+
sql: this.query,
|
|
287
|
+
params: this.bindings
|
|
288
|
+
});
|
|
289
|
+
const result = data[0];
|
|
290
|
+
return {
|
|
291
|
+
success: result.success,
|
|
292
|
+
results: result.results,
|
|
293
|
+
meta: {
|
|
294
|
+
served_by: result.meta.served_by || "",
|
|
295
|
+
duration: result.meta.duration || 0,
|
|
296
|
+
changes: result.meta.changes || 0,
|
|
297
|
+
last_row_id: result.meta.last_row_id || 0,
|
|
298
|
+
changed_db: result.meta.changed_db || false,
|
|
299
|
+
size_after: result.meta.size_after || 0,
|
|
300
|
+
rows_read: result.meta.rows_read || 0,
|
|
301
|
+
rows_written: result.meta.rows_written || 0
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
async all() {
|
|
306
|
+
return this.run();
|
|
307
|
+
}
|
|
308
|
+
async first(colName) {
|
|
309
|
+
const result = await this.run();
|
|
310
|
+
if (!result.results || result.results.length === 0) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
const firstRow = result.results[0];
|
|
314
|
+
if (colName && typeof firstRow === "object" && firstRow !== null) {
|
|
315
|
+
return firstRow[colName] ?? null;
|
|
316
|
+
}
|
|
317
|
+
return firstRow;
|
|
318
|
+
}
|
|
319
|
+
async raw(options) {
|
|
320
|
+
const result = await this.run();
|
|
321
|
+
if (!result.results || result.results.length === 0) {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
const raw = result.results.map((row) => {
|
|
325
|
+
if (typeof row === "object" && row !== null) {
|
|
326
|
+
return Object.values(row);
|
|
327
|
+
}
|
|
328
|
+
return row;
|
|
329
|
+
});
|
|
330
|
+
if (options?.columnNames && result.results.length > 0) {
|
|
331
|
+
const firstRow = result.results[0];
|
|
332
|
+
if (typeof firstRow === "object" && firstRow !== null) {
|
|
333
|
+
const columnNames = Object.keys(firstRow);
|
|
334
|
+
return [columnNames, ...raw];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return raw;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function createD1Binding$1(client, databaseId) {
|
|
341
|
+
return {
|
|
342
|
+
prepare(query) {
|
|
343
|
+
return new D1PreparedStatementImpl(query, client, databaseId);
|
|
344
|
+
},
|
|
345
|
+
async batch(statements) {
|
|
346
|
+
const endpoint = `/accounts/${client.getAccountId()}/d1/database/${databaseId}/query`;
|
|
347
|
+
const queries = statements.map((stmt) => {
|
|
348
|
+
const impl = stmt;
|
|
349
|
+
return {
|
|
350
|
+
sql: impl.query,
|
|
351
|
+
params: impl.bindings
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
const data = await client.post(endpoint, queries);
|
|
355
|
+
return data.map((result) => ({
|
|
356
|
+
success: result.success,
|
|
357
|
+
results: result.results,
|
|
358
|
+
meta: {
|
|
359
|
+
served_by: result.meta.served_by || "",
|
|
360
|
+
duration: result.meta.duration || 0,
|
|
361
|
+
changes: result.meta.changes || 0,
|
|
362
|
+
last_row_id: result.meta.last_row_id || 0,
|
|
363
|
+
changed_db: result.meta.changed_db || false,
|
|
364
|
+
size_after: result.meta.size_after || 0,
|
|
365
|
+
rows_read: result.meta.rows_read || 0,
|
|
366
|
+
rows_written: result.meta.rows_written || 0
|
|
367
|
+
}
|
|
368
|
+
}));
|
|
369
|
+
},
|
|
370
|
+
async exec(query) {
|
|
371
|
+
const endpoint = `/accounts/${client.getAccountId()}/d1/database/${databaseId}/query`;
|
|
372
|
+
const data = await client.post(endpoint, {
|
|
373
|
+
sql: query
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
count: data.length,
|
|
377
|
+
duration: data.reduce((acc, r) => acc + (r.meta.duration || 0), 0)
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
dump() {
|
|
381
|
+
throw new Error("D1 dump() is deprecated and not supported");
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function sha256(data) {
|
|
387
|
+
const encoder = new TextEncoder();
|
|
388
|
+
const dataBuffer = typeof data === "string" ? encoder.encode(data) : data;
|
|
389
|
+
return await crypto.subtle.digest("SHA-256", dataBuffer);
|
|
390
|
+
}
|
|
391
|
+
function hex(buffer) {
|
|
392
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
393
|
+
}
|
|
394
|
+
async function hmac(key, data) {
|
|
395
|
+
const encoder = new TextEncoder();
|
|
396
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
397
|
+
"raw",
|
|
398
|
+
key,
|
|
399
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
400
|
+
false,
|
|
401
|
+
["sign"]
|
|
402
|
+
);
|
|
403
|
+
return await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(data));
|
|
404
|
+
}
|
|
405
|
+
async function getSignatureKey(secretAccessKey, dateStamp, region, service) {
|
|
406
|
+
const encoder = new TextEncoder();
|
|
407
|
+
const kDate = await hmac(encoder.encode(`AWS4${secretAccessKey}`), dateStamp);
|
|
408
|
+
const kRegion = await hmac(kDate, region);
|
|
409
|
+
const kService = await hmac(kRegion, service);
|
|
410
|
+
const kSigning = await hmac(kService, "aws4_request");
|
|
411
|
+
return kSigning;
|
|
412
|
+
}
|
|
413
|
+
async function signRequest(params) {
|
|
414
|
+
const {
|
|
415
|
+
method,
|
|
416
|
+
url,
|
|
417
|
+
headers,
|
|
418
|
+
body,
|
|
419
|
+
accessKeyId,
|
|
420
|
+
secretAccessKey,
|
|
421
|
+
region = "auto",
|
|
422
|
+
service = "s3"
|
|
423
|
+
} = params;
|
|
424
|
+
const urlObj = new URL(url);
|
|
425
|
+
const host = urlObj.host;
|
|
426
|
+
const path = urlObj.pathname || "/";
|
|
427
|
+
const queryString = urlObj.search.slice(1);
|
|
428
|
+
const now = /* @__PURE__ */ new Date();
|
|
429
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
430
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
431
|
+
const canonicalHeaders = {
|
|
432
|
+
host,
|
|
433
|
+
"x-amz-date": amzDate,
|
|
434
|
+
...headers
|
|
435
|
+
};
|
|
436
|
+
const signedHeaders = Object.keys(canonicalHeaders).sort().join(";");
|
|
437
|
+
const canonicalHeadersString = Object.keys(canonicalHeaders).sort().map((key) => `${key}:${canonicalHeaders[key]}`).join("\n");
|
|
438
|
+
const payloadHash = body ? hex(await sha256(body)) : hex(await sha256(""));
|
|
439
|
+
const canonicalRequest = [
|
|
440
|
+
method,
|
|
441
|
+
path,
|
|
442
|
+
queryString,
|
|
443
|
+
canonicalHeadersString + "\n",
|
|
444
|
+
signedHeaders,
|
|
445
|
+
payloadHash
|
|
446
|
+
].join("\n");
|
|
447
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
448
|
+
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
449
|
+
const stringToSign = [
|
|
450
|
+
algorithm,
|
|
451
|
+
amzDate,
|
|
452
|
+
credentialScope,
|
|
453
|
+
hex(await sha256(canonicalRequest))
|
|
454
|
+
].join("\n");
|
|
455
|
+
const signingKey = await getSignatureKey(secretAccessKey, dateStamp, region, service);
|
|
456
|
+
const signature = hex(await hmac(signingKey, stringToSign));
|
|
457
|
+
const authorization = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
458
|
+
return {
|
|
459
|
+
"Authorization": authorization,
|
|
460
|
+
"x-amz-date": amzDate,
|
|
461
|
+
"x-amz-content-sha256": payloadHash
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function createR2Binding$1(config, bucketName) {
|
|
466
|
+
const { accountId, r2AccessKeyId, r2SecretAccessKey } = config;
|
|
467
|
+
const baseUrl = `https://${accountId}.r2.cloudflarestorage.com/${bucketName}`;
|
|
468
|
+
async function signedFetch(method, key, options) {
|
|
469
|
+
const queryString = options?.queryParams ? "?" + new URLSearchParams(options.queryParams).toString() : "";
|
|
470
|
+
const url = `${baseUrl}/${encodeURIComponent(key)}${queryString}`;
|
|
471
|
+
let body;
|
|
472
|
+
if (options?.body instanceof ReadableStream) {
|
|
473
|
+
const reader = options.body.getReader();
|
|
474
|
+
const chunks = [];
|
|
475
|
+
while (true) {
|
|
476
|
+
const { done, value } = await reader.read();
|
|
477
|
+
if (done) break;
|
|
478
|
+
chunks.push(value);
|
|
479
|
+
}
|
|
480
|
+
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
481
|
+
const result = new Uint8Array(totalLength);
|
|
482
|
+
let offset = 0;
|
|
483
|
+
for (const chunk of chunks) {
|
|
484
|
+
result.set(chunk, offset);
|
|
485
|
+
offset += chunk.length;
|
|
486
|
+
}
|
|
487
|
+
body = result.buffer;
|
|
488
|
+
} else {
|
|
489
|
+
body = options?.body;
|
|
490
|
+
}
|
|
491
|
+
const signatureHeaders = await signRequest({
|
|
492
|
+
method,
|
|
493
|
+
url,
|
|
494
|
+
headers: options?.headers || {},
|
|
495
|
+
body,
|
|
496
|
+
accessKeyId: r2AccessKeyId,
|
|
497
|
+
secretAccessKey: r2SecretAccessKey
|
|
498
|
+
});
|
|
499
|
+
return fetch(url, {
|
|
500
|
+
method,
|
|
501
|
+
headers: {
|
|
502
|
+
...options?.headers,
|
|
503
|
+
...signatureHeaders
|
|
504
|
+
},
|
|
505
|
+
body
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
async function signedFetchBucket(method, options) {
|
|
509
|
+
const queryString = options?.queryParams ? "?" + new URLSearchParams(options.queryParams).toString() : "";
|
|
510
|
+
const url = `${baseUrl}${queryString}`;
|
|
511
|
+
const signatureHeaders = await signRequest({
|
|
512
|
+
method,
|
|
513
|
+
url,
|
|
514
|
+
headers: options?.headers || {},
|
|
515
|
+
accessKeyId: r2AccessKeyId,
|
|
516
|
+
secretAccessKey: r2SecretAccessKey
|
|
517
|
+
});
|
|
518
|
+
return fetch(url, {
|
|
519
|
+
method,
|
|
520
|
+
headers: {
|
|
521
|
+
...options?.headers,
|
|
522
|
+
...signatureHeaders
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const r2Bucket = {
|
|
527
|
+
async get(key, options) {
|
|
528
|
+
const headers = {};
|
|
529
|
+
if (options?.range) {
|
|
530
|
+
if (typeof options.range === "object") {
|
|
531
|
+
const { offset, length, suffix } = options.range;
|
|
532
|
+
if (suffix) {
|
|
533
|
+
headers["Range"] = `bytes=-${suffix}`;
|
|
534
|
+
} else if (offset !== void 0) {
|
|
535
|
+
headers["Range"] = length ? `bytes=${offset}-${offset + length - 1}` : `bytes=${offset}-`;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (options?.onlyIf) {
|
|
540
|
+
if (options.onlyIf.etagMatches) {
|
|
541
|
+
headers["If-Match"] = options.onlyIf.etagMatches;
|
|
542
|
+
}
|
|
543
|
+
if (options.onlyIf.etagDoesNotMatch) {
|
|
544
|
+
headers["If-None-Match"] = options.onlyIf.etagDoesNotMatch;
|
|
545
|
+
}
|
|
546
|
+
if (options.onlyIf.uploadedBefore) {
|
|
547
|
+
headers["If-Unmodified-Since"] = options.onlyIf.uploadedBefore.toUTCString();
|
|
548
|
+
}
|
|
549
|
+
if (options.onlyIf.uploadedAfter) {
|
|
550
|
+
headers["If-Modified-Since"] = options.onlyIf.uploadedAfter.toUTCString();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const response = await signedFetch("GET", key, { headers });
|
|
554
|
+
if (response.status === 404) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
if (!response.ok) {
|
|
558
|
+
throw new Error(`R2 GET failed: ${response.status}`);
|
|
559
|
+
}
|
|
560
|
+
const customMetadata = {};
|
|
561
|
+
response.headers.forEach((value, key2) => {
|
|
562
|
+
if (key2.toLowerCase().startsWith("x-amz-meta-")) {
|
|
563
|
+
const metaKey = key2.slice("x-amz-meta-".length);
|
|
564
|
+
customMetadata[metaKey] = value;
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
const r2Object = {
|
|
568
|
+
key,
|
|
569
|
+
version: response.headers.get("x-amz-version-id") || "",
|
|
570
|
+
size: parseInt(response.headers.get("content-length") || "0", 10),
|
|
571
|
+
etag: response.headers.get("etag") || "",
|
|
572
|
+
httpEtag: response.headers.get("etag") || "",
|
|
573
|
+
checksums: {},
|
|
574
|
+
uploaded: new Date(response.headers.get("last-modified") || Date.now()),
|
|
575
|
+
httpMetadata: {
|
|
576
|
+
contentType: response.headers.get("content-type") || void 0,
|
|
577
|
+
contentLanguage: response.headers.get("content-language") || void 0,
|
|
578
|
+
contentDisposition: response.headers.get("content-disposition") || void 0,
|
|
579
|
+
contentEncoding: response.headers.get("content-encoding") || void 0,
|
|
580
|
+
cacheControl: response.headers.get("cache-control") || void 0,
|
|
581
|
+
cacheExpiry: response.headers.get("expires") ? new Date(response.headers.get("expires")) : void 0
|
|
582
|
+
},
|
|
583
|
+
customMetadata,
|
|
584
|
+
range: options?.range ? {
|
|
585
|
+
offset: 0,
|
|
586
|
+
length: parseInt(response.headers.get("content-length") || "0", 10)
|
|
587
|
+
} : void 0,
|
|
588
|
+
body: response.body,
|
|
589
|
+
bodyUsed: false,
|
|
590
|
+
arrayBuffer: () => response.arrayBuffer(),
|
|
591
|
+
text: () => response.text(),
|
|
592
|
+
json: () => response.json(),
|
|
593
|
+
blob: () => response.blob(),
|
|
594
|
+
writeHttpMetadata: (headers2) => {
|
|
595
|
+
if (r2Object.httpMetadata.contentType) {
|
|
596
|
+
headers2.set("content-type", r2Object.httpMetadata.contentType);
|
|
597
|
+
}
|
|
598
|
+
if (r2Object.httpMetadata.contentLanguage) {
|
|
599
|
+
headers2.set("content-language", r2Object.httpMetadata.contentLanguage);
|
|
600
|
+
}
|
|
601
|
+
if (r2Object.httpMetadata.contentDisposition) {
|
|
602
|
+
headers2.set("content-disposition", r2Object.httpMetadata.contentDisposition);
|
|
603
|
+
}
|
|
604
|
+
if (r2Object.httpMetadata.contentEncoding) {
|
|
605
|
+
headers2.set("content-encoding", r2Object.httpMetadata.contentEncoding);
|
|
606
|
+
}
|
|
607
|
+
if (r2Object.httpMetadata.cacheControl) {
|
|
608
|
+
headers2.set("cache-control", r2Object.httpMetadata.cacheControl);
|
|
609
|
+
}
|
|
610
|
+
if (r2Object.httpMetadata.cacheExpiry) {
|
|
611
|
+
headers2.set("expires", r2Object.httpMetadata.cacheExpiry.toUTCString());
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
return r2Object;
|
|
616
|
+
},
|
|
617
|
+
async put(key, value, options) {
|
|
618
|
+
const headers = {};
|
|
619
|
+
if (options?.httpMetadata) {
|
|
620
|
+
if (options.httpMetadata.contentType) {
|
|
621
|
+
headers["Content-Type"] = options.httpMetadata.contentType;
|
|
622
|
+
}
|
|
623
|
+
if (options.httpMetadata.contentLanguage) {
|
|
624
|
+
headers["Content-Language"] = options.httpMetadata.contentLanguage;
|
|
625
|
+
}
|
|
626
|
+
if (options.httpMetadata.contentDisposition) {
|
|
627
|
+
headers["Content-Disposition"] = options.httpMetadata.contentDisposition;
|
|
628
|
+
}
|
|
629
|
+
if (options.httpMetadata.contentEncoding) {
|
|
630
|
+
headers["Content-Encoding"] = options.httpMetadata.contentEncoding;
|
|
631
|
+
}
|
|
632
|
+
if (options.httpMetadata.cacheControl) {
|
|
633
|
+
headers["Cache-Control"] = options.httpMetadata.cacheControl;
|
|
634
|
+
}
|
|
635
|
+
if (options.httpMetadata.cacheExpiry) {
|
|
636
|
+
headers["Expires"] = options.httpMetadata.cacheExpiry.toUTCString();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (options?.customMetadata) {
|
|
640
|
+
for (const [key2, value2] of Object.entries(options.customMetadata)) {
|
|
641
|
+
headers[`x-amz-meta-${key2}`] = value2;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
let body;
|
|
645
|
+
if (ArrayBuffer.isView(value)) {
|
|
646
|
+
body = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
|
647
|
+
} else {
|
|
648
|
+
body = value;
|
|
649
|
+
}
|
|
650
|
+
const response = await signedFetch("PUT", key, { headers, body });
|
|
651
|
+
if (!response.ok) {
|
|
652
|
+
throw new Error(`R2 PUT failed: ${response.status}`);
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
key,
|
|
656
|
+
version: response.headers.get("x-amz-version-id") || "",
|
|
657
|
+
size: typeof value === "string" ? value.length : 0,
|
|
658
|
+
etag: response.headers.get("etag") || "",
|
|
659
|
+
httpEtag: response.headers.get("etag") || "",
|
|
660
|
+
checksums: {},
|
|
661
|
+
uploaded: /* @__PURE__ */ new Date(),
|
|
662
|
+
httpMetadata: options?.httpMetadata || {},
|
|
663
|
+
customMetadata: options?.customMetadata || {}
|
|
664
|
+
};
|
|
665
|
+
},
|
|
666
|
+
async delete(keys) {
|
|
667
|
+
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
668
|
+
if (keyArray.length === 1) {
|
|
669
|
+
const response = await signedFetch("DELETE", keyArray[0]);
|
|
670
|
+
if (!response.ok && response.status !== 404) {
|
|
671
|
+
throw new Error(`R2 DELETE failed: ${response.status}`);
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
675
|
+
<Delete>
|
|
676
|
+
${keyArray.map((key) => `<Object><Key>${key}</Key></Object>`).join("")}
|
|
677
|
+
</Delete>`;
|
|
678
|
+
const response = await signedFetchBucket("POST", {
|
|
679
|
+
queryParams: { delete: "" },
|
|
680
|
+
headers: { "Content-Type": "application/xml" }
|
|
681
|
+
});
|
|
682
|
+
if (!response.ok) {
|
|
683
|
+
throw new Error(`R2 DELETE BULK failed: ${response.status}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
async head(key) {
|
|
688
|
+
const response = await signedFetch("HEAD", key);
|
|
689
|
+
if (response.status === 404) {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
if (!response.ok) {
|
|
693
|
+
throw new Error(`R2 HEAD failed: ${response.status}`);
|
|
694
|
+
}
|
|
695
|
+
const customMetadata = {};
|
|
696
|
+
response.headers.forEach((value, key2) => {
|
|
697
|
+
if (key2.toLowerCase().startsWith("x-amz-meta-")) {
|
|
698
|
+
const metaKey = key2.slice("x-amz-meta-".length);
|
|
699
|
+
customMetadata[metaKey] = value;
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
return {
|
|
703
|
+
key,
|
|
704
|
+
version: response.headers.get("x-amz-version-id") || "",
|
|
705
|
+
size: parseInt(response.headers.get("content-length") || "0", 10),
|
|
706
|
+
etag: response.headers.get("etag") || "",
|
|
707
|
+
httpEtag: response.headers.get("etag") || "",
|
|
708
|
+
checksums: {},
|
|
709
|
+
uploaded: new Date(response.headers.get("last-modified") || Date.now()),
|
|
710
|
+
httpMetadata: {
|
|
711
|
+
contentType: response.headers.get("content-type") || void 0,
|
|
712
|
+
contentLanguage: response.headers.get("content-language") || void 0,
|
|
713
|
+
contentDisposition: response.headers.get("content-disposition") || void 0,
|
|
714
|
+
contentEncoding: response.headers.get("content-encoding") || void 0,
|
|
715
|
+
cacheControl: response.headers.get("cache-control") || void 0,
|
|
716
|
+
cacheExpiry: response.headers.get("expires") ? new Date(response.headers.get("expires")) : void 0
|
|
717
|
+
},
|
|
718
|
+
customMetadata
|
|
719
|
+
};
|
|
720
|
+
},
|
|
721
|
+
async list(options) {
|
|
722
|
+
const queryParams = {
|
|
723
|
+
"list-type": "2"
|
|
724
|
+
};
|
|
725
|
+
if (options?.limit) {
|
|
726
|
+
queryParams["max-keys"] = options.limit.toString();
|
|
727
|
+
}
|
|
728
|
+
if (options?.prefix) {
|
|
729
|
+
queryParams["prefix"] = options.prefix;
|
|
730
|
+
}
|
|
731
|
+
if (options?.cursor) {
|
|
732
|
+
queryParams["continuation-token"] = options.cursor;
|
|
733
|
+
}
|
|
734
|
+
if (options?.delimiter) {
|
|
735
|
+
queryParams["delimiter"] = options.delimiter;
|
|
736
|
+
}
|
|
737
|
+
if (options?.startAfter) {
|
|
738
|
+
queryParams["start-after"] = options.startAfter;
|
|
739
|
+
}
|
|
740
|
+
const response = await signedFetchBucket("GET", { queryParams });
|
|
741
|
+
if (!response.ok) {
|
|
742
|
+
throw new Error(`R2 LIST failed: ${response.status}`);
|
|
743
|
+
}
|
|
744
|
+
const xml = await response.text();
|
|
745
|
+
const objects = [];
|
|
746
|
+
const delimitedPrefixes = [];
|
|
747
|
+
const contentsRegex = /<Contents>(.*?)<\/Contents>/gs;
|
|
748
|
+
let match;
|
|
749
|
+
while ((match = contentsRegex.exec(xml)) !== null) {
|
|
750
|
+
const content = match[1];
|
|
751
|
+
const key = content.match(/<Key>(.*?)<\/Key>/)?.[1] || "";
|
|
752
|
+
const size = parseInt(content.match(/<Size>(.*?)<\/Size>/)?.[1] || "0", 10);
|
|
753
|
+
const etag = content.match(/<ETag>(.*?)<\/ETag>/)?.[1] || "";
|
|
754
|
+
const lastModified = content.match(/<LastModified>(.*?)<\/LastModified>/)?.[1] || "";
|
|
755
|
+
objects.push({
|
|
756
|
+
key,
|
|
757
|
+
version: "",
|
|
758
|
+
size,
|
|
759
|
+
etag,
|
|
760
|
+
httpEtag: etag,
|
|
761
|
+
checksums: {},
|
|
762
|
+
uploaded: new Date(lastModified),
|
|
763
|
+
httpMetadata: {},
|
|
764
|
+
customMetadata: {}
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
const prefixRegex = /<CommonPrefixes>.*?<Prefix>(.*?)<\/Prefix>.*?<\/CommonPrefixes>/gs;
|
|
768
|
+
while ((match = prefixRegex.exec(xml)) !== null) {
|
|
769
|
+
delimitedPrefixes.push(match[1]);
|
|
770
|
+
}
|
|
771
|
+
const isTruncated = xml.includes("<IsTruncated>true</IsTruncated>");
|
|
772
|
+
const nextContinuationToken = xml.match(/<NextContinuationToken>(.*?)<\/NextContinuationToken>/)?.[1];
|
|
773
|
+
return {
|
|
774
|
+
objects,
|
|
775
|
+
truncated: isTruncated,
|
|
776
|
+
cursor: nextContinuationToken,
|
|
777
|
+
delimitedPrefixes
|
|
778
|
+
};
|
|
779
|
+
},
|
|
780
|
+
createMultipartUpload(key, options) {
|
|
781
|
+
throw new Error("R2 multipart upload not yet implemented");
|
|
782
|
+
},
|
|
783
|
+
resumeMultipartUpload(key, uploadId) {
|
|
784
|
+
throw new Error("R2 multipart upload not yet implemented");
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
return r2Bucket;
|
|
788
|
+
}
|
|
789
|
+
|
|
1
790
|
function createR2Binding(config, bucketName) {
|
|
2
|
-
|
|
791
|
+
return createR2Binding$1(config, bucketName);
|
|
3
792
|
}
|
|
4
793
|
function createKVBinding(config, namespaceId) {
|
|
5
|
-
|
|
794
|
+
const client = new CloudflareAPIClient(config.accountId, config.apiToken);
|
|
795
|
+
return createKVBinding$1(client, namespaceId);
|
|
6
796
|
}
|
|
7
797
|
function createD1Binding(config, databaseId) {
|
|
8
|
-
|
|
798
|
+
const client = new CloudflareAPIClient(config.accountId, config.apiToken);
|
|
799
|
+
return createD1Binding$1(client, databaseId);
|
|
9
800
|
}
|
|
10
801
|
|
|
11
802
|
export { createD1Binding, createKVBinding, createR2Binding };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openwrangler",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"@cloudflare/workers-types": "^4.20251231.0",
|
|
18
18
|
"nuxt": "^3.15.0",
|
|
19
19
|
"typescript": "^5.7.2",
|
|
20
|
+
"wrangler": "^4.54.0",
|
|
20
21
|
"unbuild": "^3.3.1",
|
|
21
|
-
"
|
|
22
|
-
"@binochoi/nitro-cloudflare-dev": "npm:@bino0216/nitro-cloudflare-dev@0.2.4"
|
|
22
|
+
"@bino0216/nitro-cloudflare-dev": "0.2.6"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"dev": "
|
|
25
|
+
"dev": "cd ./playground && pnpm dev",
|
|
26
26
|
"build": "unbuild"
|
|
27
27
|
}
|
|
28
28
|
}
|