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
package/lib/cloudflare/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
export * from "./account-api-token";
|
|
2
|
+
export * from "./api";
|
|
3
|
+
export * from "./assets";
|
|
1
4
|
export * from "./bindings";
|
|
2
5
|
export * from "./bucket";
|
|
3
|
-
export * from "./
|
|
6
|
+
export * from "./custom-domain";
|
|
7
|
+
export * from "./dns-records";
|
|
4
8
|
export * from "./durable-object-namespace";
|
|
5
9
|
export * from "./kv-namespace";
|
|
10
|
+
export * from "./permission-groups";
|
|
6
11
|
export * from "./r2-rest-state-store";
|
|
7
12
|
export * from "./static-site";
|
|
8
13
|
export * from "./worker";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Context } from "../context";
|
|
2
|
+
import { Resource } from "../resource";
|
|
3
|
+
import { type CloudflareApiOptions } from "./api";
|
|
4
|
+
/**
|
|
5
|
+
* Cloudflare permission group as returned by the API
|
|
6
|
+
*/
|
|
7
|
+
export interface PermissionGroup {
|
|
8
|
+
/**
|
|
9
|
+
* Unique identifier for the permission group
|
|
10
|
+
*/
|
|
11
|
+
id: string;
|
|
12
|
+
/**
|
|
13
|
+
* Human-readable name of the permission group
|
|
14
|
+
*/
|
|
15
|
+
name: string;
|
|
16
|
+
/**
|
|
17
|
+
* Scopes included in this permission group
|
|
18
|
+
*/
|
|
19
|
+
scopes: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* All Cloudflare permission groups mapped by name to ID
|
|
23
|
+
*
|
|
24
|
+
* @see https://developers.cloudflare.com/r2/api/tokens/#permissions
|
|
25
|
+
*/
|
|
26
|
+
export type PermissionGroups = Resource<"cloudflare::PermissionGroups"> & {
|
|
27
|
+
/**
|
|
28
|
+
* Admin Read & Write - Allows create, list, delete buckets and edit bucket configurations
|
|
29
|
+
* plus list, write, and read object access
|
|
30
|
+
*/
|
|
31
|
+
"Workers R2 Storage Write": PermissionGroup;
|
|
32
|
+
/**
|
|
33
|
+
* Admin Read only - Allows list buckets and view bucket configuration
|
|
34
|
+
* plus list and read object access
|
|
35
|
+
*/
|
|
36
|
+
"Workers R2 Storage Read": PermissionGroup;
|
|
37
|
+
/**
|
|
38
|
+
* Object Read & Write - Allows read, write, and list objects in specific buckets
|
|
39
|
+
*/
|
|
40
|
+
"Workers R2 Storage Bucket Item Write": PermissionGroup;
|
|
41
|
+
/**
|
|
42
|
+
* Object Read only - Allows read and list objects in specific buckets
|
|
43
|
+
*/
|
|
44
|
+
"Workers R2 Storage Bucket Item Read": PermissionGroup;
|
|
45
|
+
/**
|
|
46
|
+
* Dynamically discovered permission groups
|
|
47
|
+
*/
|
|
48
|
+
[name: string]: PermissionGroup;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Lists all permission groups available for the Cloudflare account
|
|
52
|
+
* and returns a typed map of permission names to their IDs.
|
|
53
|
+
*
|
|
54
|
+
* This is primarily used when creating API tokens for Cloudflare services like R2.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Get all permission groups including those for R2
|
|
58
|
+
* const permissions = await PermissionGroups("cloudflare-permissions");
|
|
59
|
+
*
|
|
60
|
+
* // Use with AccountApiToken to create a token with proper permissions
|
|
61
|
+
* const token = await AccountApiToken("r2-token", {
|
|
62
|
+
* name: "R2 Read-Only Token",
|
|
63
|
+
* policies: [
|
|
64
|
+
* {
|
|
65
|
+
* effect: "allow",
|
|
66
|
+
* resources: {
|
|
67
|
+
* "com.cloudflare.edge.r2.bucket.abc123_default_my-bucket": "*"
|
|
68
|
+
* },
|
|
69
|
+
* permissionGroups: [
|
|
70
|
+
* {
|
|
71
|
+
* id: permissions["Workers R2 Storage Bucket Item Read"]
|
|
72
|
+
* }
|
|
73
|
+
* ]
|
|
74
|
+
* }
|
|
75
|
+
* ]
|
|
76
|
+
* });
|
|
77
|
+
*/
|
|
78
|
+
export declare const PermissionGroups: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<PermissionGroups>, id: string, options?: CloudflareApiOptions) => Promise<PermissionGroups>);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Resource } from "../resource";
|
|
2
|
+
import { createCloudflareApi } from "./api";
|
|
3
|
+
/**
|
|
4
|
+
* Lists all permission groups available for the Cloudflare account
|
|
5
|
+
* and returns a typed map of permission names to their IDs.
|
|
6
|
+
*
|
|
7
|
+
* This is primarily used when creating API tokens for Cloudflare services like R2.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Get all permission groups including those for R2
|
|
11
|
+
* const permissions = await PermissionGroups("cloudflare-permissions");
|
|
12
|
+
*
|
|
13
|
+
* // Use with AccountApiToken to create a token with proper permissions
|
|
14
|
+
* const token = await AccountApiToken("r2-token", {
|
|
15
|
+
* name: "R2 Read-Only Token",
|
|
16
|
+
* policies: [
|
|
17
|
+
* {
|
|
18
|
+
* effect: "allow",
|
|
19
|
+
* resources: {
|
|
20
|
+
* "com.cloudflare.edge.r2.bucket.abc123_default_my-bucket": "*"
|
|
21
|
+
* },
|
|
22
|
+
* permissionGroups: [
|
|
23
|
+
* {
|
|
24
|
+
* id: permissions["Workers R2 Storage Bucket Item Read"]
|
|
25
|
+
* }
|
|
26
|
+
* ]
|
|
27
|
+
* }
|
|
28
|
+
* ]
|
|
29
|
+
* });
|
|
30
|
+
*/
|
|
31
|
+
export const PermissionGroups = Resource("cloudflare::PermissionGroups", async function (id, options) {
|
|
32
|
+
// Only create and update phases are supported
|
|
33
|
+
if (this.phase === "delete") {
|
|
34
|
+
return this.destroy();
|
|
35
|
+
}
|
|
36
|
+
// Initialize API client
|
|
37
|
+
const api = await createCloudflareApi(options);
|
|
38
|
+
// Fetch permission groups from Cloudflare API
|
|
39
|
+
const response = await api.get(`/accounts/${api.accountId}/tokens/permission_groups`);
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`Failed to fetch permission groups: ${response.statusText}`);
|
|
42
|
+
}
|
|
43
|
+
const data = (await response.json());
|
|
44
|
+
if (!data.success || !data.result) {
|
|
45
|
+
throw new Error(`API returned error: ${data.errors?.[0]?.message || "Unknown error"}`);
|
|
46
|
+
}
|
|
47
|
+
return this(Object.fromEntries(data.result.map((group) => [group.name, group])));
|
|
48
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Scope } from "../scope";
|
|
2
|
+
import type { Secret } from "../secret";
|
|
2
3
|
import type { State, StateStore } from "../state";
|
|
3
4
|
/**
|
|
4
5
|
* Options for CloudflareR2StateStore
|
|
@@ -17,7 +18,7 @@ export interface CloudflareR2StateStoreOptions {
|
|
|
17
18
|
/**
|
|
18
19
|
* API key to use (overrides CLOUDFLARE_API_KEY env var)
|
|
19
20
|
*/
|
|
20
|
-
apiKey?:
|
|
21
|
+
apiKey?: Secret;
|
|
21
22
|
/**
|
|
22
23
|
* Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
|
|
23
24
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { deserialize, serialize } from "../serde";
|
|
1
2
|
import { withExponentialBackoff } from "../util/retry";
|
|
2
|
-
import { deserialize, serialize } from "../util/serde";
|
|
3
3
|
import { createCloudflareApi } from "./api";
|
|
4
4
|
/**
|
|
5
5
|
* State store implementation using Cloudflare R2 API
|
|
@@ -74,13 +74,22 @@ export class R2RestStateStore {
|
|
|
74
74
|
params.append("cursor", cursor);
|
|
75
75
|
}
|
|
76
76
|
const listPath = `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects?${params.toString()}`;
|
|
77
|
-
const response = await
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
const response = await withExponentialBackoff(async () => {
|
|
78
|
+
const response = await this.api.get(listPath);
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const errorData = await response.json().catch(() => ({
|
|
81
|
+
errors: [{ message: response.statusText }],
|
|
82
|
+
}));
|
|
83
|
+
throw new Error(`Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`);
|
|
84
|
+
}
|
|
85
|
+
return response;
|
|
86
|
+
},
|
|
87
|
+
// Retry on transient errors
|
|
88
|
+
(error) => error.message?.includes("502") ||
|
|
89
|
+
error.message?.includes("503") ||
|
|
90
|
+
error.message?.includes("timeout"), 5, // 5 retry attempts
|
|
91
|
+
1000 // Start with 1 second delay
|
|
92
|
+
);
|
|
84
93
|
const data = (await response.json());
|
|
85
94
|
// The result structure may be under "result" key in Cloudflare's API
|
|
86
95
|
const result = data.result || data;
|
|
@@ -114,15 +123,27 @@ export class R2RestStateStore {
|
|
|
114
123
|
async get(key) {
|
|
115
124
|
await this.ensureInitialized();
|
|
116
125
|
try {
|
|
117
|
-
const response = await
|
|
118
|
-
|
|
119
|
-
if (response.
|
|
120
|
-
|
|
126
|
+
const response = await withExponentialBackoff(async () => {
|
|
127
|
+
const response = await this.api.get(`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`);
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
if (response.status === 404) {
|
|
130
|
+
return response;
|
|
131
|
+
}
|
|
132
|
+
const errorData = await response.json().catch(() => ({
|
|
133
|
+
errors: [{ message: response.statusText }],
|
|
134
|
+
}));
|
|
135
|
+
throw new Error(`Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`);
|
|
121
136
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
return response;
|
|
138
|
+
},
|
|
139
|
+
// Retry on transient errors
|
|
140
|
+
(error) => error.message?.includes("502") ||
|
|
141
|
+
error.message?.includes("503") ||
|
|
142
|
+
error.message?.includes("timeout"), 5, // 5 retry attempts
|
|
143
|
+
1000 // Start with 1 second delay
|
|
144
|
+
);
|
|
145
|
+
if (response.status === 404) {
|
|
146
|
+
return undefined;
|
|
126
147
|
}
|
|
127
148
|
// Parse and deserialize the state data
|
|
128
149
|
const rawData = await response.json();
|
|
@@ -198,7 +219,8 @@ export class R2RestStateStore {
|
|
|
198
219
|
},
|
|
199
220
|
// Retry on transient errors
|
|
200
221
|
(error) => error.message?.includes("503") || error.message?.includes("timeout"), 5, // 5 retry attempts
|
|
201
|
-
1000
|
|
222
|
+
1000 // Start with 1 second delay
|
|
223
|
+
);
|
|
202
224
|
}
|
|
203
225
|
/**
|
|
204
226
|
* Delete a state by key
|
|
@@ -207,13 +229,22 @@ export class R2RestStateStore {
|
|
|
207
229
|
*/
|
|
208
230
|
async delete(key) {
|
|
209
231
|
await this.ensureInitialized();
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
232
|
+
await withExponentialBackoff(async () => {
|
|
233
|
+
const response = await this.api.delete(`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`);
|
|
234
|
+
if (!response.ok && response.status !== 404) {
|
|
235
|
+
const errorData = await response.json().catch(() => ({
|
|
236
|
+
errors: [{ message: response.statusText }],
|
|
237
|
+
}));
|
|
238
|
+
throw new Error(`Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`);
|
|
239
|
+
}
|
|
240
|
+
return response;
|
|
241
|
+
},
|
|
242
|
+
// Retry on transient errors
|
|
243
|
+
(error) => error.message?.includes("502") ||
|
|
244
|
+
error.message?.includes("503") ||
|
|
245
|
+
error.message?.includes("timeout"), 5, // 5 retry attempts
|
|
246
|
+
1000 // Start with 1 second delay
|
|
247
|
+
);
|
|
217
248
|
}
|
|
218
249
|
/**
|
|
219
250
|
* Convert key for storage by replacing slashes with colons
|
|
@@ -72,13 +72,6 @@ export interface StaticSiteProps {
|
|
|
72
72
|
* @default true
|
|
73
73
|
*/
|
|
74
74
|
production?: boolean;
|
|
75
|
-
/**
|
|
76
|
-
* Custom domain for the site
|
|
77
|
-
*/
|
|
78
|
-
domain?: string | {
|
|
79
|
-
name: string;
|
|
80
|
-
redirects?: string[];
|
|
81
|
-
};
|
|
82
75
|
build?: {
|
|
83
76
|
/**
|
|
84
77
|
* Command to run before deploying the site
|
|
@@ -137,13 +130,6 @@ export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
|
|
|
137
130
|
* List of uploaded asset files
|
|
138
131
|
*/
|
|
139
132
|
assets: string[];
|
|
140
|
-
/**
|
|
141
|
-
* Custom domain for the site
|
|
142
|
-
*/
|
|
143
|
-
domain?: string | {
|
|
144
|
-
name: string;
|
|
145
|
-
redirects?: string[];
|
|
146
|
-
};
|
|
147
133
|
/**
|
|
148
134
|
* Whether the site is deployed to production
|
|
149
135
|
*/
|
|
@@ -198,13 +184,26 @@ export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
|
|
|
198
184
|
* });
|
|
199
185
|
*
|
|
200
186
|
* @example
|
|
201
|
-
* // Create a static site with custom
|
|
202
|
-
* const
|
|
187
|
+
* // Create a static site with a custom domain using the CustomDomain resource
|
|
188
|
+
* const site = await StaticSite("custom-site", {
|
|
203
189
|
* name: "custom-site",
|
|
204
190
|
* dir: "./www",
|
|
205
191
|
* errorPage: "404.html",
|
|
206
|
-
* indexPage: "home.html"
|
|
207
|
-
*
|
|
192
|
+
* indexPage: "home.html"
|
|
193
|
+
* });
|
|
194
|
+
*
|
|
195
|
+
* // Then configure the custom domain separately
|
|
196
|
+
* const domain = await CustomDomain("custom-domain", {
|
|
197
|
+
* name: "www.example.com",
|
|
198
|
+
* zoneId: "abcdef123456789",
|
|
199
|
+
* workerName: site.name
|
|
200
|
+
* });
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* // Create a static site with multiple domains (primary + redirects)
|
|
204
|
+
* const site = await StaticSite("multi-domain-site", {
|
|
205
|
+
* name: "multi-domain-site",
|
|
206
|
+
* dir: "./public"
|
|
208
207
|
* });
|
|
209
208
|
*
|
|
210
209
|
* @see https://developers.cloudflare.com/workers/platform/sites
|
|
@@ -50,13 +50,26 @@ const __dirname = import.meta.dirname;
|
|
|
50
50
|
* });
|
|
51
51
|
*
|
|
52
52
|
* @example
|
|
53
|
-
* // Create a static site with custom
|
|
54
|
-
* const
|
|
53
|
+
* // Create a static site with a custom domain using the CustomDomain resource
|
|
54
|
+
* const site = await StaticSite("custom-site", {
|
|
55
55
|
* name: "custom-site",
|
|
56
56
|
* dir: "./www",
|
|
57
57
|
* errorPage: "404.html",
|
|
58
|
-
* indexPage: "home.html"
|
|
59
|
-
*
|
|
58
|
+
* indexPage: "home.html"
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* // Then configure the custom domain separately
|
|
62
|
+
* const domain = await CustomDomain("custom-domain", {
|
|
63
|
+
* name: "www.example.com",
|
|
64
|
+
* zoneId: "abcdef123456789",
|
|
65
|
+
* workerName: site.name
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // Create a static site with multiple domains (primary + redirects)
|
|
70
|
+
* const site = await StaticSite("multi-domain-site", {
|
|
71
|
+
* name: "multi-domain-site",
|
|
72
|
+
* dir: "./public"
|
|
60
73
|
* });
|
|
61
74
|
*
|
|
62
75
|
* @see https://developers.cloudflare.com/workers/platform/sites
|
|
@@ -64,6 +77,8 @@ const __dirname = import.meta.dirname;
|
|
|
64
77
|
export const StaticSite = Resource("cloudflare::StaticSite", {
|
|
65
78
|
alwaysUpdate: true,
|
|
66
79
|
}, async function (id, props) {
|
|
80
|
+
// Create Cloudflare API client with automatic account discovery
|
|
81
|
+
const api = await createCloudflareApi();
|
|
67
82
|
if (this.phase === "delete") {
|
|
68
83
|
// For delete operations, we'll rely on the Worker delete to clean up
|
|
69
84
|
// Return empty output for deleted state
|
|
@@ -113,8 +128,6 @@ export const StaticSite = Resource("cloudflare::StaticSite", {
|
|
|
113
128
|
// Use the provided name
|
|
114
129
|
const siteName = props.name;
|
|
115
130
|
const indexPage = props.indexPage || "index.html";
|
|
116
|
-
// Create Cloudflare API client with automatic account discovery
|
|
117
|
-
const api = await createCloudflareApi();
|
|
118
131
|
// Step 1: Create or get the KV namespace for assets
|
|
119
132
|
const [kv, assetManifest] = await Promise.all([
|
|
120
133
|
KVNamespace("assets", {
|
|
@@ -122,9 +135,6 @@ export const StaticSite = Resource("cloudflare::StaticSite", {
|
|
|
122
135
|
}),
|
|
123
136
|
generateAssetManifest(props.dir),
|
|
124
137
|
]);
|
|
125
|
-
console.log({
|
|
126
|
-
assetManifest,
|
|
127
|
-
});
|
|
128
138
|
// Step 3: Upload assets to KV
|
|
129
139
|
await uploadAssetManifest(api, kv.namespaceId, assetManifest);
|
|
130
140
|
// Prepare the bindings for the worker
|
|
@@ -196,7 +206,6 @@ export const StaticSite = Resource("cloudflare::StaticSite", {
|
|
|
196
206
|
assets: assetManifest.map((item) => item.key),
|
|
197
207
|
createdAt: this.output?.createdAt || now,
|
|
198
208
|
updatedAt: now,
|
|
199
|
-
domain: props.domain,
|
|
200
209
|
production: props.production !== false,
|
|
201
210
|
url: worker.url,
|
|
202
211
|
routes,
|
|
@@ -166,6 +166,20 @@ export interface Worker<B extends Bindings = Bindings> extends Resource<"cloudfl
|
|
|
166
166
|
* }
|
|
167
167
|
* });
|
|
168
168
|
*
|
|
169
|
+
* @example
|
|
170
|
+
* // Create a worker with static assets:
|
|
171
|
+
* const staticAssets = await Assets("static", {
|
|
172
|
+
* path: "./src/assets"
|
|
173
|
+
* });
|
|
174
|
+
*
|
|
175
|
+
* const frontendWorker = await Worker("frontend", {
|
|
176
|
+
* name: "frontend-worker",
|
|
177
|
+
* entrypoint: "./src/worker.ts",
|
|
178
|
+
* bindings: {
|
|
179
|
+
* ASSETS: staticAssets
|
|
180
|
+
* }
|
|
181
|
+
* });
|
|
182
|
+
*
|
|
169
183
|
* @see https://developers.cloudflare.com/workers/
|
|
170
184
|
*/
|
|
171
185
|
export declare const Worker: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | (<const B extends Bindings>(this: Context<Worker<NoInfer<B>>>, id: string, props: WorkerProps<B>) => Promise<Worker<B>>);
|
package/lib/cloudflare/worker.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
1
2
|
import * as fs from "fs/promises";
|
|
2
3
|
import { Bundle } from "../esbuild/bundle";
|
|
3
4
|
import { Resource } from "../resource";
|
|
4
5
|
import { isSecret } from "../secret";
|
|
6
|
+
import { getContentType } from "../util/content-type";
|
|
5
7
|
import { withExponentialBackoff } from "../util/retry";
|
|
6
8
|
import { slugify } from "../util/slugify";
|
|
7
9
|
import { createCloudflareApi } from "./api";
|
|
8
|
-
import { isDurableObjectNamespace, } from "./bindings";
|
|
10
|
+
import { isAssets, isDurableObjectNamespace, } from "./bindings";
|
|
9
11
|
import { isKVNamespace } from "./kv-namespace";
|
|
10
12
|
/**
|
|
11
13
|
* A Cloudflare Worker is a serverless function that can be deployed to the Cloudflare network.
|
|
@@ -68,6 +70,20 @@ import { isKVNamespace } from "./kv-namespace";
|
|
|
68
70
|
* }
|
|
69
71
|
* });
|
|
70
72
|
*
|
|
73
|
+
* @example
|
|
74
|
+
* // Create a worker with static assets:
|
|
75
|
+
* const staticAssets = await Assets("static", {
|
|
76
|
+
* path: "./src/assets"
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* const frontendWorker = await Worker("frontend", {
|
|
80
|
+
* name: "frontend-worker",
|
|
81
|
+
* entrypoint: "./src/worker.ts",
|
|
82
|
+
* bindings: {
|
|
83
|
+
* ASSETS: staticAssets
|
|
84
|
+
* }
|
|
85
|
+
* });
|
|
86
|
+
*
|
|
71
87
|
* @see https://developers.cloudflare.com/workers/
|
|
72
88
|
*/
|
|
73
89
|
export const Worker = Resource("cloudflare::Worker", {
|
|
@@ -89,9 +105,28 @@ export const Worker = Resource("cloudflare::Worker", {
|
|
|
89
105
|
await assertWorkerDoesNotExist(this, api, workerName);
|
|
90
106
|
}
|
|
91
107
|
const oldBindings = await this.get("bindings");
|
|
92
|
-
const scriptMetadata = await prepareWorkerMetadata(this, oldBindings, props);
|
|
93
108
|
// Get the script content - either from props.script, or by bundling
|
|
94
109
|
const scriptContent = props.script ?? (await bundleWorkerScript(props));
|
|
110
|
+
// Find any assets bindings
|
|
111
|
+
const assetsBindings = [];
|
|
112
|
+
if (props.bindings) {
|
|
113
|
+
for (const [bindingName, binding] of Object.entries(props.bindings)) {
|
|
114
|
+
if (isAssets(binding)) {
|
|
115
|
+
assetsBindings.push({ name: bindingName, assets: binding });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Upload any assets and get completion tokens
|
|
120
|
+
let assetUploadResult;
|
|
121
|
+
if (assetsBindings.length > 0) {
|
|
122
|
+
// We'll use the first asset binding for now
|
|
123
|
+
// In the future, we might want to support multiple asset bindings
|
|
124
|
+
const assetBinding = assetsBindings[0];
|
|
125
|
+
// Upload the assets and get the completion token
|
|
126
|
+
assetUploadResult = await uploadAssets(api, workerName, assetBinding.assets);
|
|
127
|
+
}
|
|
128
|
+
// Prepare metadata with bindings
|
|
129
|
+
const scriptMetadata = await prepareWorkerMetadata(this, oldBindings, props, assetUploadResult);
|
|
95
130
|
// Upload the worker script
|
|
96
131
|
await putWorker(api, workerName, scriptContent, scriptMetadata);
|
|
97
132
|
// TODO: it is less than ideal that this can fail, resulting in state problem
|
|
@@ -206,7 +241,7 @@ class NotFoundError extends Error {
|
|
|
206
241
|
this.name = "NotFoundError";
|
|
207
242
|
}
|
|
208
243
|
}
|
|
209
|
-
async function prepareWorkerMetadata(ctx, oldBindings, props) {
|
|
244
|
+
async function prepareWorkerMetadata(ctx, oldBindings, props, assetUploadResult) {
|
|
210
245
|
// Prepare metadata with bindings
|
|
211
246
|
const meta = {
|
|
212
247
|
bindings: [],
|
|
@@ -223,6 +258,15 @@ async function prepareWorkerMetadata(ctx, oldBindings, props) {
|
|
|
223
258
|
new_sqlite_classes: props.migrations?.new_sqlite_classes ?? [],
|
|
224
259
|
},
|
|
225
260
|
};
|
|
261
|
+
// If we have asset upload results, add them to the metadata
|
|
262
|
+
if (assetUploadResult) {
|
|
263
|
+
meta.assets = {
|
|
264
|
+
jwt: assetUploadResult.completionToken,
|
|
265
|
+
};
|
|
266
|
+
if (assetUploadResult.assetConfig) {
|
|
267
|
+
meta.assets.config = assetUploadResult.assetConfig;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
226
270
|
const bindings = (props.bindings ?? {});
|
|
227
271
|
// Convert bindings to the format expected by the API
|
|
228
272
|
for (const [bindingName, binding] of Object.entries(bindings)) {
|
|
@@ -284,6 +328,12 @@ async function prepareWorkerMetadata(ctx, oldBindings, props) {
|
|
|
284
328
|
bucket_name: binding.name,
|
|
285
329
|
});
|
|
286
330
|
}
|
|
331
|
+
else if (isAssets(binding)) {
|
|
332
|
+
meta.bindings.push({
|
|
333
|
+
type: "assets",
|
|
334
|
+
name: bindingName,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
287
337
|
else if (isSecret(binding)) {
|
|
288
338
|
meta.bindings.push({
|
|
289
339
|
type: "secret_text",
|
|
@@ -468,3 +518,100 @@ async function getWorkerBindings(api, workerName, environment = "production") {
|
|
|
468
518
|
const data = await response.json();
|
|
469
519
|
return data.result;
|
|
470
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Uploads assets to Cloudflare and returns a completion token
|
|
523
|
+
*
|
|
524
|
+
* @param api CloudflareApi instance
|
|
525
|
+
* @param workerName Name of the worker
|
|
526
|
+
* @param assets Assets resource containing files to upload
|
|
527
|
+
* @returns Completion token for the assets upload
|
|
528
|
+
*/
|
|
529
|
+
async function uploadAssets(api, workerName, assets) {
|
|
530
|
+
// Generate the file manifest
|
|
531
|
+
const fileMetadata = {};
|
|
532
|
+
// Process each file in the assets
|
|
533
|
+
for (const file of assets.files) {
|
|
534
|
+
const { hash, size } = await calculateFileMetadata(file.filePath);
|
|
535
|
+
// Use the relative path as the key, ensuring it starts with a slash
|
|
536
|
+
const key = file.path.startsWith("/") ? file.path : `/${file.path}`;
|
|
537
|
+
fileMetadata[key] = { hash, size };
|
|
538
|
+
}
|
|
539
|
+
// Start the upload session
|
|
540
|
+
const uploadSessionUrl = `/accounts/${api.accountId}/workers/scripts/${workerName}/assets-upload-session`;
|
|
541
|
+
const uploadSessionResponse = await api.post(uploadSessionUrl, JSON.stringify({ manifest: fileMetadata }), {
|
|
542
|
+
headers: { "Content-Type": "application/json" },
|
|
543
|
+
});
|
|
544
|
+
if (!uploadSessionResponse.ok) {
|
|
545
|
+
throw new Error(`Failed to start assets upload session: ${uploadSessionResponse.status} ${uploadSessionResponse.statusText}`);
|
|
546
|
+
}
|
|
547
|
+
const sessionData = (await uploadSessionResponse.json());
|
|
548
|
+
// If there are no buckets, assets are already uploaded or empty
|
|
549
|
+
if (!sessionData.result.buckets || sessionData.result.buckets.length === 0) {
|
|
550
|
+
return { completionToken: sessionData.result.jwt };
|
|
551
|
+
}
|
|
552
|
+
// Upload the files in batches as specified by the API
|
|
553
|
+
let completionToken = sessionData.result.jwt;
|
|
554
|
+
const buckets = sessionData.result.buckets;
|
|
555
|
+
// Process each bucket of files
|
|
556
|
+
for (const bucket of buckets) {
|
|
557
|
+
const formData = new FormData();
|
|
558
|
+
// Add each file in the bucket to the form
|
|
559
|
+
for (const fileHash of bucket) {
|
|
560
|
+
// Find the file with this hash
|
|
561
|
+
const file = assets.files.find((f) => {
|
|
562
|
+
const filePath = f.path.startsWith("/") ? f.path : `/${f.path}`;
|
|
563
|
+
return fileMetadata[filePath]?.hash === fileHash;
|
|
564
|
+
});
|
|
565
|
+
if (!file) {
|
|
566
|
+
throw new Error(`Could not find file with hash ${fileHash}`);
|
|
567
|
+
}
|
|
568
|
+
// Read the file content
|
|
569
|
+
const fileContent = await fs.readFile(file.filePath);
|
|
570
|
+
// Convert to base64 as required by the API when using base64=true
|
|
571
|
+
const base64Content = fileContent.toString("base64");
|
|
572
|
+
// Add the file to the form with the hash as the key and set the correct content type
|
|
573
|
+
const blob = new Blob([base64Content], {
|
|
574
|
+
type: getContentType(file.filePath),
|
|
575
|
+
});
|
|
576
|
+
formData.append(fileHash, blob, fileHash);
|
|
577
|
+
}
|
|
578
|
+
// Upload this batch of files
|
|
579
|
+
const uploadResponse = await api.post(`/accounts/${api.accountId}/workers/assets/upload?base64=true`, formData, {
|
|
580
|
+
headers: {
|
|
581
|
+
Authorization: `Bearer ${completionToken}`,
|
|
582
|
+
"Content-Type": "multipart/form-data",
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
if (!uploadResponse.ok) {
|
|
586
|
+
throw new Error(`Failed to upload asset files: ${uploadResponse.status} ${uploadResponse.statusText}`);
|
|
587
|
+
}
|
|
588
|
+
const uploadData = (await uploadResponse.json());
|
|
589
|
+
// Update the completion token for the next batch
|
|
590
|
+
if (uploadData.result.jwt) {
|
|
591
|
+
completionToken = uploadData.result.jwt;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// Return the final completion token
|
|
595
|
+
return {
|
|
596
|
+
completionToken,
|
|
597
|
+
assetConfig: {
|
|
598
|
+
html_handling: "auto-trailing-slash",
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Calculate the SHA-256 hash and size of a file
|
|
604
|
+
*
|
|
605
|
+
* @param filePath Path to the file
|
|
606
|
+
* @returns Hash (first 32 chars of SHA-256) and size of the file
|
|
607
|
+
*/
|
|
608
|
+
async function calculateFileMetadata(filePath) {
|
|
609
|
+
const hash = crypto.createHash("sha256");
|
|
610
|
+
const fileContent = await fs.readFile(filePath);
|
|
611
|
+
hash.update(fileContent);
|
|
612
|
+
const fileHash = hash.digest("hex").substring(0, 32); // First 32 chars of hash
|
|
613
|
+
return {
|
|
614
|
+
hash: fileHash,
|
|
615
|
+
size: fileContent.length,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @param key - The encryption key
|
|
6
6
|
* @returns The base64-encoded encrypted value with nonce
|
|
7
7
|
*/
|
|
8
|
-
export declare function
|
|
8
|
+
export declare function encrypt(value: string, key: string): Promise<string>;
|
|
9
9
|
/**
|
|
10
10
|
* Decrypt a value encrypted with a symmetric key
|
|
11
11
|
*
|
|
@@ -6,7 +6,7 @@ import sodium from "libsodium-wrappers";
|
|
|
6
6
|
* @param key - The encryption key
|
|
7
7
|
* @returns The base64-encoded encrypted value with nonce
|
|
8
8
|
*/
|
|
9
|
-
export async function
|
|
9
|
+
export async function encrypt(value, key) {
|
|
10
10
|
// Initialize libsodium
|
|
11
11
|
await sodium.ready;
|
|
12
12
|
// Derive a key from the passphrase
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { deserialize, serialize } from "../serde";
|
|
3
4
|
import { ignore } from "../util/ignore";
|
|
4
|
-
import { deserialize, serialize } from "../util/serde";
|
|
5
5
|
const stateRootDir = path.join(process.cwd(), ".alchemy");
|
|
6
6
|
export class FileSystemStateStore {
|
|
7
7
|
scope;
|