alchemy 0.3.1 → 0.4.1
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/README.md +8 -3
- package/lib/ai/document.d.ts +12 -1
- package/lib/ai/document.js +10 -1
- package/lib/ai/index.d.ts +0 -2
- package/lib/ai/index.js +0 -2
- package/lib/alchemy.d.ts +5 -0
- package/lib/alchemy.js +44 -1
- package/lib/apply.js +17 -10
- package/lib/aws/account-id.d.ts +4 -1
- package/lib/aws/account-id.js +1 -1
- package/lib/aws/credentials.d.ts +5 -0
- package/lib/aws/credentials.js +0 -0
- package/lib/aws/oidc/oidc-provider.js +2 -2
- package/lib/aws/role.js +35 -23
- package/lib/cloudflare/account-api-token.d.ts +168 -0
- package/lib/cloudflare/account-api-token.js +179 -0
- package/lib/cloudflare/account-id.d.ts +1 -0
- package/lib/cloudflare/account-id.js +0 -0
- package/lib/cloudflare/api.d.ts +38 -1
- package/lib/cloudflare/api.js +55 -4
- package/lib/cloudflare/assets.d.ts +74 -0
- package/lib/cloudflare/assets.js +73 -0
- package/lib/cloudflare/bindings.d.ts +6 -1
- package/lib/cloudflare/bindings.js +6 -0
- package/lib/cloudflare/bucket.d.ts +118 -3
- package/lib/cloudflare/bucket.js +269 -89
- package/lib/cloudflare/custom-domain.d.ts +75 -0
- package/lib/cloudflare/custom-domain.js +154 -0
- package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
- package/lib/cloudflare/index.d.ts +6 -1
- package/lib/cloudflare/index.js +6 -1
- package/lib/cloudflare/permission-groups.d.ts +78 -0
- package/lib/cloudflare/permission-groups.js +48 -0
- package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
- package/lib/cloudflare/r2-rest-state-store.js +55 -24
- package/lib/cloudflare/static-site.d.ts +17 -18
- package/lib/cloudflare/static-site.js +19 -10
- package/lib/cloudflare/worker.d.ts +14 -0
- package/lib/cloudflare/worker.js +150 -3
- package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
- package/lib/{util/encrypt.js → encrypt.js} +1 -1
- package/lib/fs/file-system-state-store.js +1 -1
- package/lib/fs/file.d.ts +18 -0
- package/lib/fs/file.js +6 -0
- package/lib/internal/{providers.d.ts → docs/providers.d.ts} +10 -4
- package/lib/internal/docs/providers.js +196 -0
- package/lib/secret.d.ts +3 -0
- package/lib/secret.js +13 -0
- package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
- package/lib/{util/serde.js → serde.js} +12 -4
- package/lib/test/bun.js +3 -2
- package/lib/util/sha256.d.ts +1 -0
- package/lib/util/sha256.js +4 -0
- package/lib/web/vitepress/config.d.ts +2 -1
- package/lib/web/vitepress/config.js +1 -0
- package/lib/web/vitepress/index.d.ts +1 -0
- package/lib/web/vitepress/index.js +1 -0
- package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
- package/lib/web/vitepress/process-front-matter-files.js +68 -0
- package/lib/web/vitepress/vitepress.js +3 -2
- package/package.json +8 -4
- package/src/ai/document.ts +26 -2
- package/src/ai/index.ts +0 -2
- package/src/alchemy.ts +56 -2
- package/src/apply.ts +21 -20
- package/src/aws/account-id.ts +6 -2
- package/src/aws/credentials.ts +6 -0
- package/src/aws/oidc/oidc-provider.ts +17 -17
- package/src/aws/role.ts +68 -54
- package/src/cloudflare/account-api-token.ts +365 -0
- package/src/cloudflare/account-id.ts +0 -0
- package/src/cloudflare/api.ts +88 -17
- package/src/cloudflare/assets.ts +155 -0
- package/src/cloudflare/bindings.ts +11 -2
- package/src/cloudflare/bucket.ts +493 -133
- package/src/cloudflare/custom-domain.ts +318 -0
- package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
- package/src/cloudflare/index.ts +6 -1
- package/src/cloudflare/permission-groups.ts +137 -0
- package/src/cloudflare/r2-rest-state-store.ts +87 -42
- package/src/cloudflare/static-site.ts +20 -32
- package/src/cloudflare/worker.ts +253 -5
- package/src/dns/import-dns.ts +4 -4
- package/src/{util/encrypt.ts → encrypt.ts} +7 -10
- package/src/fs/file-system-state-store.ts +5 -5
- package/src/fs/file.ts +29 -0
- package/src/internal/docs/providers.ts +281 -0
- package/src/secret.ts +16 -0
- package/src/{util/serde.ts → serde.ts} +16 -10
- package/src/test/bun.ts +8 -7
- package/src/util/sha256.ts +5 -0
- package/src/web/vitepress/config.ts +3 -1
- package/src/web/vitepress/index.ts +1 -0
- package/src/web/vitepress/process-front-matter-files.ts +98 -0
- package/src/web/vitepress/vitepress.ts +3 -2
- package/lib/ai/approve.d.ts +0 -99
- package/lib/ai/approve.js +0 -76
- package/lib/ai/review.d.ts +0 -122
- package/lib/ai/review.js +0 -101
- package/lib/internal/getting-started.d.ts +0 -21
- package/lib/internal/getting-started.js +0 -87
- package/lib/internal/index.d.ts +0 -3
- package/lib/internal/index.js +0 -3
- package/lib/internal/providers.js +0 -172
- package/lib/internal/tutorial.d.ts +0 -104
- package/lib/internal/tutorial.js +0 -251
- package/src/ai/approve.ts +0 -163
- package/src/ai/review.ts +0 -213
- package/src/internal/getting-started.ts +0 -115
- package/src/internal/index.ts +0 -3
- package/src/internal/providers.ts +0 -241
- package/src/internal/tutorial.ts +0 -392
- /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Scope } from "../scope";
|
|
2
|
+
import type { Secret } from "../secret";
|
|
3
|
+
import { deserialize, serialize } from "../serde";
|
|
2
4
|
import type { State, StateStore } from "../state";
|
|
3
5
|
import { withExponentialBackoff } from "../util/retry";
|
|
4
|
-
import { deserialize, serialize } from "../util/serde";
|
|
5
6
|
import { type CloudflareApi, createCloudflareApi } from "./api";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -23,7 +24,7 @@ export interface CloudflareR2StateStoreOptions {
|
|
|
23
24
|
/**
|
|
24
25
|
* API key to use (overrides CLOUDFLARE_API_KEY env var)
|
|
25
26
|
*/
|
|
26
|
-
apiKey?:
|
|
27
|
+
apiKey?: Secret;
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
|
|
@@ -44,7 +45,7 @@ export class R2RestStateStore implements StateStore {
|
|
|
44
45
|
private api: CloudflareApi;
|
|
45
46
|
private prefix: string;
|
|
46
47
|
private bucketName: string;
|
|
47
|
-
private apiKey:
|
|
48
|
+
private apiKey: Secret | undefined;
|
|
48
49
|
private accountId: string | undefined;
|
|
49
50
|
private email: string | undefined;
|
|
50
51
|
private initialized = false;
|
|
@@ -57,7 +58,7 @@ export class R2RestStateStore implements StateStore {
|
|
|
57
58
|
*/
|
|
58
59
|
constructor(
|
|
59
60
|
public readonly scope: Scope,
|
|
60
|
-
options: CloudflareR2StateStoreOptions
|
|
61
|
+
options: CloudflareR2StateStoreOptions
|
|
61
62
|
) {
|
|
62
63
|
// Use the scope's chain to build the prefix, similar to how FileSystemStateStore builds its directory
|
|
63
64
|
const scopePath = scope.chain.join("/");
|
|
@@ -122,16 +123,30 @@ export class R2RestStateStore implements StateStore {
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
const listPath = `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects?${params.toString()}`;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
|
|
127
|
+
const response = await withExponentialBackoff(
|
|
128
|
+
async () => {
|
|
129
|
+
const response = await this.api.get(listPath);
|
|
130
|
+
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const errorData: any = await response.json().catch(() => ({
|
|
133
|
+
errors: [{ message: response.statusText }],
|
|
134
|
+
}));
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return response;
|
|
141
|
+
},
|
|
142
|
+
// Retry on transient errors
|
|
143
|
+
(error) =>
|
|
144
|
+
error.message?.includes("502") ||
|
|
145
|
+
error.message?.includes("503") ||
|
|
146
|
+
error.message?.includes("timeout"),
|
|
147
|
+
5, // 5 retry attempts
|
|
148
|
+
1000 // Start with 1 second delay
|
|
149
|
+
);
|
|
135
150
|
|
|
136
151
|
const data = (await response.json()) as any;
|
|
137
152
|
|
|
@@ -144,7 +159,7 @@ export class R2RestStateStore implements StateStore {
|
|
|
144
159
|
objects.map((obj: any) => {
|
|
145
160
|
const keyName = obj.key || obj.name;
|
|
146
161
|
return this.convertKeyFromStorage(keyName.slice(this.prefix.length));
|
|
147
|
-
})
|
|
162
|
+
})
|
|
148
163
|
);
|
|
149
164
|
|
|
150
165
|
// Update cursor for next page if available
|
|
@@ -175,21 +190,38 @@ export class R2RestStateStore implements StateStore {
|
|
|
175
190
|
await this.ensureInitialized();
|
|
176
191
|
|
|
177
192
|
try {
|
|
178
|
-
const response = await
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
const response = await withExponentialBackoff(
|
|
194
|
+
async () => {
|
|
195
|
+
const response = await this.api.get(
|
|
196
|
+
`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`
|
|
197
|
+
);
|
|
181
198
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
if (response.status === 404) {
|
|
201
|
+
return response;
|
|
202
|
+
}
|
|
186
203
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
const errorData: any = await response.json().catch(() => ({
|
|
205
|
+
errors: [{ message: response.statusText }],
|
|
206
|
+
}));
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return response;
|
|
213
|
+
},
|
|
214
|
+
// Retry on transient errors
|
|
215
|
+
(error) =>
|
|
216
|
+
error.message?.includes("502") ||
|
|
217
|
+
error.message?.includes("503") ||
|
|
218
|
+
error.message?.includes("timeout"),
|
|
219
|
+
5, // 5 retry attempts
|
|
220
|
+
1000 // Start with 1 second delay
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (response.status === 404) {
|
|
224
|
+
return undefined;
|
|
193
225
|
}
|
|
194
226
|
|
|
195
227
|
// Parse and deserialize the state data
|
|
@@ -267,7 +299,7 @@ export class R2RestStateStore implements StateStore {
|
|
|
267
299
|
headers: {
|
|
268
300
|
"Content-Type": "application/json",
|
|
269
301
|
},
|
|
270
|
-
}
|
|
302
|
+
}
|
|
271
303
|
);
|
|
272
304
|
|
|
273
305
|
if (!response.ok) {
|
|
@@ -275,7 +307,7 @@ export class R2RestStateStore implements StateStore {
|
|
|
275
307
|
errors: [{ message: response.statusText }],
|
|
276
308
|
}));
|
|
277
309
|
throw new Error(
|
|
278
|
-
`Error writing to R2: ${errorData.errors?.[0]?.message || response.statusText}
|
|
310
|
+
`Error writing to R2: ${errorData.errors?.[0]?.message || response.statusText}`
|
|
279
311
|
);
|
|
280
312
|
}
|
|
281
313
|
|
|
@@ -285,7 +317,7 @@ export class R2RestStateStore implements StateStore {
|
|
|
285
317
|
(error) =>
|
|
286
318
|
error.message?.includes("503") || error.message?.includes("timeout"),
|
|
287
319
|
5, // 5 retry attempts
|
|
288
|
-
1000
|
|
320
|
+
1000 // Start with 1 second delay
|
|
289
321
|
);
|
|
290
322
|
}
|
|
291
323
|
|
|
@@ -297,18 +329,31 @@ export class R2RestStateStore implements StateStore {
|
|
|
297
329
|
async delete(key: string): Promise<void> {
|
|
298
330
|
await this.ensureInitialized();
|
|
299
331
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
332
|
+
await withExponentialBackoff(
|
|
333
|
+
async () => {
|
|
334
|
+
const response = await this.api.delete(
|
|
335
|
+
`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`
|
|
336
|
+
);
|
|
303
337
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
338
|
+
if (!response.ok && response.status !== 404) {
|
|
339
|
+
const errorData: any = await response.json().catch(() => ({
|
|
340
|
+
errors: [{ message: response.statusText }],
|
|
341
|
+
}));
|
|
342
|
+
throw new Error(
|
|
343
|
+
`Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return response;
|
|
348
|
+
},
|
|
349
|
+
// Retry on transient errors
|
|
350
|
+
(error) =>
|
|
351
|
+
error.message?.includes("502") ||
|
|
352
|
+
error.message?.includes("503") ||
|
|
353
|
+
error.message?.includes("timeout"),
|
|
354
|
+
5, // 5 retry attempts
|
|
355
|
+
1000 // Start with 1 second delay
|
|
356
|
+
);
|
|
312
357
|
}
|
|
313
358
|
|
|
314
359
|
/**
|
|
@@ -96,16 +96,6 @@ export interface StaticSiteProps {
|
|
|
96
96
|
*/
|
|
97
97
|
production?: boolean;
|
|
98
98
|
|
|
99
|
-
/**
|
|
100
|
-
* Custom domain for the site
|
|
101
|
-
*/
|
|
102
|
-
domain?:
|
|
103
|
-
| string
|
|
104
|
-
| {
|
|
105
|
-
name: string;
|
|
106
|
-
redirects?: string[];
|
|
107
|
-
};
|
|
108
|
-
|
|
109
99
|
build?: {
|
|
110
100
|
/**
|
|
111
101
|
* Command to run before deploying the site
|
|
@@ -176,16 +166,6 @@ export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
|
|
|
176
166
|
*/
|
|
177
167
|
assets: string[];
|
|
178
168
|
|
|
179
|
-
/**
|
|
180
|
-
* Custom domain for the site
|
|
181
|
-
*/
|
|
182
|
-
domain?:
|
|
183
|
-
| string
|
|
184
|
-
| {
|
|
185
|
-
name: string;
|
|
186
|
-
redirects?: string[];
|
|
187
|
-
};
|
|
188
|
-
|
|
189
169
|
/**
|
|
190
170
|
* Whether the site is deployed to production
|
|
191
171
|
*/
|
|
@@ -243,13 +223,26 @@ export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
|
|
|
243
223
|
* });
|
|
244
224
|
*
|
|
245
225
|
* @example
|
|
246
|
-
* // Create a static site with custom
|
|
247
|
-
* const
|
|
226
|
+
* // Create a static site with a custom domain using the CustomDomain resource
|
|
227
|
+
* const site = await StaticSite("custom-site", {
|
|
248
228
|
* name: "custom-site",
|
|
249
229
|
* dir: "./www",
|
|
250
230
|
* errorPage: "404.html",
|
|
251
|
-
* indexPage: "home.html"
|
|
252
|
-
*
|
|
231
|
+
* indexPage: "home.html"
|
|
232
|
+
* });
|
|
233
|
+
*
|
|
234
|
+
* // Then configure the custom domain separately
|
|
235
|
+
* const domain = await CustomDomain("custom-domain", {
|
|
236
|
+
* name: "www.example.com",
|
|
237
|
+
* zoneId: "abcdef123456789",
|
|
238
|
+
* workerName: site.name
|
|
239
|
+
* });
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* // Create a static site with multiple domains (primary + redirects)
|
|
243
|
+
* const site = await StaticSite("multi-domain-site", {
|
|
244
|
+
* name: "multi-domain-site",
|
|
245
|
+
* dir: "./public"
|
|
253
246
|
* });
|
|
254
247
|
*
|
|
255
248
|
* @see https://developers.cloudflare.com/workers/platform/sites
|
|
@@ -264,6 +257,9 @@ export const StaticSite = Resource(
|
|
|
264
257
|
id: string,
|
|
265
258
|
props: StaticSiteProps
|
|
266
259
|
) {
|
|
260
|
+
// Create Cloudflare API client with automatic account discovery
|
|
261
|
+
const api = await createCloudflareApi();
|
|
262
|
+
|
|
267
263
|
if (this.phase === "delete") {
|
|
268
264
|
// For delete operations, we'll rely on the Worker delete to clean up
|
|
269
265
|
// Return empty output for deleted state
|
|
@@ -322,9 +318,6 @@ export const StaticSite = Resource(
|
|
|
322
318
|
const siteName = props.name;
|
|
323
319
|
const indexPage = props.indexPage || "index.html";
|
|
324
320
|
|
|
325
|
-
// Create Cloudflare API client with automatic account discovery
|
|
326
|
-
const api = await createCloudflareApi();
|
|
327
|
-
|
|
328
321
|
// Step 1: Create or get the KV namespace for assets
|
|
329
322
|
const [kv, assetManifest] = await Promise.all([
|
|
330
323
|
KVNamespace("assets", {
|
|
@@ -333,10 +326,6 @@ export const StaticSite = Resource(
|
|
|
333
326
|
generateAssetManifest(props.dir),
|
|
334
327
|
]);
|
|
335
328
|
|
|
336
|
-
console.log({
|
|
337
|
-
assetManifest,
|
|
338
|
-
});
|
|
339
|
-
|
|
340
329
|
// Step 3: Upload assets to KV
|
|
341
330
|
await uploadAssetManifest(api, kv.namespaceId, assetManifest);
|
|
342
331
|
|
|
@@ -419,7 +408,6 @@ export const StaticSite = Resource(
|
|
|
419
408
|
assets: assetManifest.map((item) => item.key),
|
|
420
409
|
createdAt: this.output?.createdAt || now,
|
|
421
410
|
updatedAt: now,
|
|
422
|
-
domain: props.domain,
|
|
423
411
|
production: props.production !== false,
|
|
424
412
|
url: worker.url,
|
|
425
413
|
routes,
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
1
2
|
import * as fs from "fs/promises";
|
|
2
3
|
import type { Context } from "../context";
|
|
3
4
|
import { Bundle, type BundleProps } from "../esbuild/bundle";
|
|
4
5
|
import { Resource } from "../resource";
|
|
5
6
|
import { isSecret } from "../secret";
|
|
7
|
+
import { getContentType } from "../util/content-type";
|
|
6
8
|
import { withExponentialBackoff } from "../util/retry";
|
|
7
9
|
import { slugify } from "../util/slugify";
|
|
8
10
|
import { type CloudflareApi, createCloudflareApi } from "./api";
|
|
11
|
+
import type { Assets } from "./assets";
|
|
9
12
|
import {
|
|
10
13
|
type Bindings,
|
|
11
14
|
type WorkerBindingSpec,
|
|
15
|
+
isAssets,
|
|
12
16
|
isDurableObjectNamespace,
|
|
13
17
|
} from "./bindings";
|
|
14
18
|
import type { Bound } from "./bound";
|
|
@@ -200,6 +204,20 @@ export interface Worker<B extends Bindings = Bindings>
|
|
|
200
204
|
* }
|
|
201
205
|
* });
|
|
202
206
|
*
|
|
207
|
+
* @example
|
|
208
|
+
* // Create a worker with static assets:
|
|
209
|
+
* const staticAssets = await Assets("static", {
|
|
210
|
+
* path: "./src/assets"
|
|
211
|
+
* });
|
|
212
|
+
*
|
|
213
|
+
* const frontendWorker = await Worker("frontend", {
|
|
214
|
+
* name: "frontend-worker",
|
|
215
|
+
* entrypoint: "./src/worker.ts",
|
|
216
|
+
* bindings: {
|
|
217
|
+
* ASSETS: staticAssets
|
|
218
|
+
* }
|
|
219
|
+
* });
|
|
220
|
+
*
|
|
203
221
|
* @see https://developers.cloudflare.com/workers/
|
|
204
222
|
*/
|
|
205
223
|
export const Worker = Resource(
|
|
@@ -232,15 +250,42 @@ export const Worker = Resource(
|
|
|
232
250
|
|
|
233
251
|
const oldBindings = await this.get<Bindings>("bindings");
|
|
234
252
|
|
|
253
|
+
// Get the script content - either from props.script, or by bundling
|
|
254
|
+
const scriptContent = props.script ?? (await bundleWorkerScript(props));
|
|
255
|
+
|
|
256
|
+
// Find any assets bindings
|
|
257
|
+
const assetsBindings: { name: string; assets: Assets }[] = [];
|
|
258
|
+
if (props.bindings) {
|
|
259
|
+
for (const [bindingName, binding] of Object.entries(props.bindings)) {
|
|
260
|
+
if (isAssets(binding)) {
|
|
261
|
+
assetsBindings.push({ name: bindingName, assets: binding });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Upload any assets and get completion tokens
|
|
267
|
+
let assetUploadResult: AssetUploadResult | undefined;
|
|
268
|
+
if (assetsBindings.length > 0) {
|
|
269
|
+
// We'll use the first asset binding for now
|
|
270
|
+
// In the future, we might want to support multiple asset bindings
|
|
271
|
+
const assetBinding = assetsBindings[0];
|
|
272
|
+
|
|
273
|
+
// Upload the assets and get the completion token
|
|
274
|
+
assetUploadResult = await uploadAssets(
|
|
275
|
+
api,
|
|
276
|
+
workerName,
|
|
277
|
+
assetBinding.assets
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Prepare metadata with bindings
|
|
235
282
|
const scriptMetadata = await prepareWorkerMetadata(
|
|
236
283
|
this,
|
|
237
284
|
oldBindings,
|
|
238
|
-
props
|
|
285
|
+
props,
|
|
286
|
+
assetUploadResult
|
|
239
287
|
);
|
|
240
288
|
|
|
241
|
-
// Get the script content - either from props.script, or by bundling
|
|
242
|
-
const scriptContent = props.script ?? (await bundleWorkerScript(props));
|
|
243
|
-
|
|
244
289
|
// Upload the worker script
|
|
245
290
|
await putWorker(api, workerName, scriptContent, scriptMetadata);
|
|
246
291
|
|
|
@@ -437,12 +482,29 @@ interface WorkerMetadata {
|
|
|
437
482
|
main_module?: string;
|
|
438
483
|
body_part?: string;
|
|
439
484
|
tags?: string[];
|
|
485
|
+
assets?: {
|
|
486
|
+
jwt?: string;
|
|
487
|
+
keep_assets?: boolean;
|
|
488
|
+
config?: {
|
|
489
|
+
html_handling?: "auto-trailing-slash" | "none";
|
|
490
|
+
not_found_handling?: "none" | "fall-through";
|
|
491
|
+
};
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
interface AssetUploadResult {
|
|
496
|
+
completionToken: string;
|
|
497
|
+
assetConfig?: {
|
|
498
|
+
html_handling?: "auto-trailing-slash" | "none";
|
|
499
|
+
not_found_handling?: "none" | "fall-through";
|
|
500
|
+
};
|
|
440
501
|
}
|
|
441
502
|
|
|
442
503
|
async function prepareWorkerMetadata<B extends Bindings>(
|
|
443
504
|
ctx: Context<Worker<B>>,
|
|
444
505
|
oldBindings: Bindings | undefined,
|
|
445
|
-
props: WorkerProps
|
|
506
|
+
props: WorkerProps,
|
|
507
|
+
assetUploadResult?: AssetUploadResult
|
|
446
508
|
): Promise<WorkerMetadata> {
|
|
447
509
|
// Prepare metadata with bindings
|
|
448
510
|
const meta: WorkerMetadata = {
|
|
@@ -461,6 +523,17 @@ async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
461
523
|
},
|
|
462
524
|
};
|
|
463
525
|
|
|
526
|
+
// If we have asset upload results, add them to the metadata
|
|
527
|
+
if (assetUploadResult) {
|
|
528
|
+
meta.assets = {
|
|
529
|
+
jwt: assetUploadResult.completionToken,
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
if (assetUploadResult.assetConfig) {
|
|
533
|
+
meta.assets.config = assetUploadResult.assetConfig;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
464
537
|
const bindings = (props.bindings ?? {}) as Bindings;
|
|
465
538
|
|
|
466
539
|
// Convert bindings to the format expected by the API
|
|
@@ -522,6 +595,11 @@ async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
522
595
|
name: bindingName,
|
|
523
596
|
bucket_name: binding.name,
|
|
524
597
|
});
|
|
598
|
+
} else if (isAssets(binding)) {
|
|
599
|
+
meta.bindings.push({
|
|
600
|
+
type: "assets",
|
|
601
|
+
name: bindingName,
|
|
602
|
+
});
|
|
525
603
|
} else if (isSecret(binding)) {
|
|
526
604
|
meta.bindings.push({
|
|
527
605
|
type: "secret_text",
|
|
@@ -802,3 +880,173 @@ async function getWorkerBindings(
|
|
|
802
880
|
|
|
803
881
|
return data.result;
|
|
804
882
|
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Interface for a file's metadata to be uploaded
|
|
886
|
+
*/
|
|
887
|
+
interface FileMetadata {
|
|
888
|
+
hash: string;
|
|
889
|
+
size: number;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Response from the assets upload session API
|
|
894
|
+
*/
|
|
895
|
+
interface UploadSessionResponse {
|
|
896
|
+
result: {
|
|
897
|
+
jwt: string;
|
|
898
|
+
buckets: string[][];
|
|
899
|
+
};
|
|
900
|
+
success: boolean;
|
|
901
|
+
errors: any[];
|
|
902
|
+
messages: any[];
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Response from the file upload API
|
|
907
|
+
*/
|
|
908
|
+
interface UploadResponse {
|
|
909
|
+
result: {
|
|
910
|
+
jwt: string;
|
|
911
|
+
buckets?: string[][];
|
|
912
|
+
};
|
|
913
|
+
success: boolean;
|
|
914
|
+
errors: any[];
|
|
915
|
+
messages: any[];
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Uploads assets to Cloudflare and returns a completion token
|
|
920
|
+
*
|
|
921
|
+
* @param api CloudflareApi instance
|
|
922
|
+
* @param workerName Name of the worker
|
|
923
|
+
* @param assets Assets resource containing files to upload
|
|
924
|
+
* @returns Completion token for the assets upload
|
|
925
|
+
*/
|
|
926
|
+
async function uploadAssets(
|
|
927
|
+
api: CloudflareApi,
|
|
928
|
+
workerName: string,
|
|
929
|
+
assets: Assets
|
|
930
|
+
): Promise<AssetUploadResult> {
|
|
931
|
+
// Generate the file manifest
|
|
932
|
+
const fileMetadata: Record<string, FileMetadata> = {};
|
|
933
|
+
|
|
934
|
+
// Process each file in the assets
|
|
935
|
+
for (const file of assets.files) {
|
|
936
|
+
const { hash, size } = await calculateFileMetadata(file.filePath);
|
|
937
|
+
// Use the relative path as the key, ensuring it starts with a slash
|
|
938
|
+
const key = file.path.startsWith("/") ? file.path : `/${file.path}`;
|
|
939
|
+
fileMetadata[key] = { hash, size };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Start the upload session
|
|
943
|
+
const uploadSessionUrl = `/accounts/${api.accountId}/workers/scripts/${workerName}/assets-upload-session`;
|
|
944
|
+
const uploadSessionResponse = await api.post(
|
|
945
|
+
uploadSessionUrl,
|
|
946
|
+
JSON.stringify({ manifest: fileMetadata }),
|
|
947
|
+
{
|
|
948
|
+
headers: { "Content-Type": "application/json" },
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
if (!uploadSessionResponse.ok) {
|
|
953
|
+
throw new Error(
|
|
954
|
+
`Failed to start assets upload session: ${uploadSessionResponse.status} ${uploadSessionResponse.statusText}`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const sessionData =
|
|
959
|
+
(await uploadSessionResponse.json()) as UploadSessionResponse;
|
|
960
|
+
|
|
961
|
+
// If there are no buckets, assets are already uploaded or empty
|
|
962
|
+
if (!sessionData.result.buckets || sessionData.result.buckets.length === 0) {
|
|
963
|
+
return { completionToken: sessionData.result.jwt };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Upload the files in batches as specified by the API
|
|
967
|
+
let completionToken = sessionData.result.jwt;
|
|
968
|
+
const buckets = sessionData.result.buckets;
|
|
969
|
+
|
|
970
|
+
// Process each bucket of files
|
|
971
|
+
for (const bucket of buckets) {
|
|
972
|
+
const formData = new FormData();
|
|
973
|
+
|
|
974
|
+
// Add each file in the bucket to the form
|
|
975
|
+
for (const fileHash of bucket) {
|
|
976
|
+
// Find the file with this hash
|
|
977
|
+
const file = assets.files.find((f) => {
|
|
978
|
+
const filePath = f.path.startsWith("/") ? f.path : `/${f.path}`;
|
|
979
|
+
return fileMetadata[filePath]?.hash === fileHash;
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
if (!file) {
|
|
983
|
+
throw new Error(`Could not find file with hash ${fileHash}`);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Read the file content
|
|
987
|
+
const fileContent = await fs.readFile(file.filePath);
|
|
988
|
+
|
|
989
|
+
// Convert to base64 as required by the API when using base64=true
|
|
990
|
+
const base64Content = fileContent.toString("base64");
|
|
991
|
+
|
|
992
|
+
// Add the file to the form with the hash as the key and set the correct content type
|
|
993
|
+
const blob = new Blob([base64Content], {
|
|
994
|
+
type: getContentType(file.filePath),
|
|
995
|
+
});
|
|
996
|
+
formData.append(fileHash, blob, fileHash);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Upload this batch of files
|
|
1000
|
+
const uploadResponse = await api.post(
|
|
1001
|
+
`/accounts/${api.accountId}/workers/assets/upload?base64=true`,
|
|
1002
|
+
formData,
|
|
1003
|
+
{
|
|
1004
|
+
headers: {
|
|
1005
|
+
Authorization: `Bearer ${completionToken}`,
|
|
1006
|
+
"Content-Type": "multipart/form-data",
|
|
1007
|
+
},
|
|
1008
|
+
}
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
if (!uploadResponse.ok) {
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
`Failed to upload asset files: ${uploadResponse.status} ${uploadResponse.statusText}`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const uploadData = (await uploadResponse.json()) as UploadResponse;
|
|
1018
|
+
// Update the completion token for the next batch
|
|
1019
|
+
if (uploadData.result.jwt) {
|
|
1020
|
+
completionToken = uploadData.result.jwt;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Return the final completion token
|
|
1025
|
+
return {
|
|
1026
|
+
completionToken,
|
|
1027
|
+
assetConfig: {
|
|
1028
|
+
html_handling: "auto-trailing-slash",
|
|
1029
|
+
},
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Calculate the SHA-256 hash and size of a file
|
|
1035
|
+
*
|
|
1036
|
+
* @param filePath Path to the file
|
|
1037
|
+
* @returns Hash (first 32 chars of SHA-256) and size of the file
|
|
1038
|
+
*/
|
|
1039
|
+
async function calculateFileMetadata(
|
|
1040
|
+
filePath: string
|
|
1041
|
+
): Promise<{ hash: string; size: number }> {
|
|
1042
|
+
const hash = crypto.createHash("sha256");
|
|
1043
|
+
const fileContent = await fs.readFile(filePath);
|
|
1044
|
+
|
|
1045
|
+
hash.update(fileContent);
|
|
1046
|
+
const fileHash = hash.digest("hex").substring(0, 32); // First 32 chars of hash
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
hash: fileHash,
|
|
1050
|
+
size: fileContent.length,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
package/src/dns/import-dns.ts
CHANGED
|
@@ -134,7 +134,7 @@ export const ImportDnsRecords = Resource(
|
|
|
134
134
|
async function (
|
|
135
135
|
this: Context<ImportDnsRecords>,
|
|
136
136
|
id: string,
|
|
137
|
-
props: ImportDnsRecordsProps
|
|
137
|
+
props: ImportDnsRecordsProps
|
|
138
138
|
): Promise<ImportDnsRecords> {
|
|
139
139
|
// For delete phase, just return destroyed state since this is a read-only resource
|
|
140
140
|
if (this.phase === "delete") {
|
|
@@ -152,7 +152,7 @@ export const ImportDnsRecords = Resource(
|
|
|
152
152
|
headers: {
|
|
153
153
|
accept: "application/dns-json",
|
|
154
154
|
},
|
|
155
|
-
}
|
|
155
|
+
}
|
|
156
156
|
);
|
|
157
157
|
|
|
158
158
|
if (!res.ok) {
|
|
@@ -197,7 +197,7 @@ export const ImportDnsRecords = Resource(
|
|
|
197
197
|
} catch (error) {
|
|
198
198
|
console.warn(
|
|
199
199
|
`Failed to fetch ${type} records for ${props.domain}:`,
|
|
200
|
-
error
|
|
200
|
+
error
|
|
201
201
|
);
|
|
202
202
|
}
|
|
203
203
|
}
|
|
@@ -209,5 +209,5 @@ export const ImportDnsRecords = Resource(
|
|
|
209
209
|
records: allRecords,
|
|
210
210
|
importedAt: Date.now(),
|
|
211
211
|
});
|
|
212
|
-
}
|
|
212
|
+
}
|
|
213
213
|
);
|