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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { alchemy } from "../alchemy";
|
|
2
|
+
import { Resource } from "../resource";
|
|
3
|
+
import { Secret } from "../secret";
|
|
4
|
+
import { sha256 } from "../util/sha256";
|
|
5
|
+
import { createCloudflareApi } from "./api";
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Cloudflare Account API Token with specified permissions.
|
|
8
|
+
*
|
|
9
|
+
* @see https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/create/
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // First, fetch all permission groups
|
|
13
|
+
* const permissions = await PermissionGroups("cloudflare-permissions", {
|
|
14
|
+
* accountId: cfAccountId,
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // Create a token with read-only permissions for specific zones
|
|
18
|
+
* const readOnlyToken = await AccountApiToken("readonly-token", {
|
|
19
|
+
* name: "Readonly Zone Token",
|
|
20
|
+
* policies: [
|
|
21
|
+
* {
|
|
22
|
+
* effect: "allow",
|
|
23
|
+
* permissionGroups: [
|
|
24
|
+
* { id: permissions["Zone Read"].id },
|
|
25
|
+
* { id: permissions["Analytics Read"].id }
|
|
26
|
+
* ],
|
|
27
|
+
* resources: {
|
|
28
|
+
* "com.cloudflare.api.account.zone.22b1de5f1c0e4b3ea97bb1e963b06a43": "*",
|
|
29
|
+
* "com.cloudflare.api.account.zone.eb78d65290b24279ba6f44721b3ea3c4": "*"
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
* ],
|
|
33
|
+
* expiresOn: "2024-12-31T23:59:59Z"
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // Create a token with time and IP restrictions
|
|
38
|
+
* const restrictedToken = await AccountApiToken("restricted-token", {
|
|
39
|
+
* name: "Restricted Access Token",
|
|
40
|
+
* policies: [
|
|
41
|
+
* {
|
|
42
|
+
* effect: "allow",
|
|
43
|
+
* permissionGroups: [
|
|
44
|
+
* { id: permissions["Worker Routes Edit"].id }
|
|
45
|
+
* ],
|
|
46
|
+
* resources: {
|
|
47
|
+
* "com.cloudflare.api.account.worker.route.*": "*"
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
* ],
|
|
51
|
+
* notBefore: "2023-01-01T00:00:00Z",
|
|
52
|
+
* expiresOn: "2023-12-31T23:59:59Z",
|
|
53
|
+
* condition: {
|
|
54
|
+
* requestIp: {
|
|
55
|
+
* in: ["192.168.1.0/24", "10.0.0.0/8"],
|
|
56
|
+
* notIn: ["192.168.1.100/32"]
|
|
57
|
+
* }
|
|
58
|
+
* }
|
|
59
|
+
* });
|
|
60
|
+
*/
|
|
61
|
+
export const AccountApiToken = Resource("cloudflare::AccountApiToken", async function (id, props) {
|
|
62
|
+
// Create Cloudflare API client with automatic account discovery
|
|
63
|
+
const api = await createCloudflareApi();
|
|
64
|
+
if (this.phase === "delete") {
|
|
65
|
+
// Delete token if we have an ID
|
|
66
|
+
if (this.output?.id) {
|
|
67
|
+
try {
|
|
68
|
+
const deleteResponse = await api.delete(`/accounts/${api.accountId}/tokens/${this.output.id}`);
|
|
69
|
+
if (!deleteResponse.ok && deleteResponse.status !== 404) {
|
|
70
|
+
const errorData = await deleteResponse.json().catch(() => ({
|
|
71
|
+
errors: [{ message: deleteResponse.statusText }],
|
|
72
|
+
}));
|
|
73
|
+
console.error(`Error deleting token '${props.name}':`, errorData);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error(`Error deleting token '${props.name}':`, error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Return destroyed state
|
|
81
|
+
return this.destroy();
|
|
82
|
+
}
|
|
83
|
+
// Transform our properties to API format
|
|
84
|
+
const apiPayload = {
|
|
85
|
+
name: props.name,
|
|
86
|
+
policies: props.policies.map((policy) => ({
|
|
87
|
+
effect: policy.effect,
|
|
88
|
+
permission_groups: policy.permissionGroups.map((pg) => ({
|
|
89
|
+
id: pg.id,
|
|
90
|
+
meta: pg.meta || {},
|
|
91
|
+
})),
|
|
92
|
+
resources: policy.resources,
|
|
93
|
+
})),
|
|
94
|
+
// Format dates for Cloudflare API (removing milliseconds)
|
|
95
|
+
...(props.expiresOn
|
|
96
|
+
? { expires_on: formatCloudflareDate(props.expiresOn) }
|
|
97
|
+
: {}),
|
|
98
|
+
...(props.notBefore
|
|
99
|
+
? { not_before: formatCloudflareDate(props.notBefore) }
|
|
100
|
+
: {}),
|
|
101
|
+
...(props.condition
|
|
102
|
+
? {
|
|
103
|
+
condition: {
|
|
104
|
+
request_ip: props.condition.requestIp
|
|
105
|
+
? {
|
|
106
|
+
in: props.condition.requestIp.in || [],
|
|
107
|
+
not_in: props.condition.requestIp.notIn || [],
|
|
108
|
+
}
|
|
109
|
+
: undefined,
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Formats a date string for Cloudflare API by removing milliseconds
|
|
116
|
+
* Converts from "2023-01-01T00:00:00.000Z" to "2023-01-01T00:00:00Z"
|
|
117
|
+
*/
|
|
118
|
+
function formatCloudflareDate(dateStr) {
|
|
119
|
+
return dateStr.replace(/\.\d{3}Z$/, "Z");
|
|
120
|
+
}
|
|
121
|
+
let response;
|
|
122
|
+
let tokenValue;
|
|
123
|
+
if (this.phase === "update" && this.output?.id) {
|
|
124
|
+
// Update existing token
|
|
125
|
+
response = await api.put(`/accounts/${api.accountId}/tokens/${this.output.id}`, apiPayload);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Create new token
|
|
129
|
+
response = await api.post(`/accounts/${api.accountId}/tokens`, apiPayload);
|
|
130
|
+
}
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const errorData = await response.json().catch(() => ({
|
|
133
|
+
errors: [{ message: response.statusText }],
|
|
134
|
+
}));
|
|
135
|
+
throw new Error(`Error ${this.phase === "update" ? "updating" : "creating"} token '${props.name}': ${errorData.errors?.[0]?.message || response.statusText}`);
|
|
136
|
+
}
|
|
137
|
+
const result = await response.json();
|
|
138
|
+
const tokenData = result.result;
|
|
139
|
+
if (tokenData.value) {
|
|
140
|
+
tokenValue = alchemy.secret(tokenData.value);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
if (!this.output?.value) {
|
|
144
|
+
throw new Error(`Token '${props.name}' was created but we have no record of its value. Try deleting and recreating the token.`);
|
|
145
|
+
}
|
|
146
|
+
tokenValue = this.output?.value;
|
|
147
|
+
}
|
|
148
|
+
// Transform API response to our format
|
|
149
|
+
return this({
|
|
150
|
+
id: tokenData.id,
|
|
151
|
+
name: tokenData.name,
|
|
152
|
+
status: tokenData.status,
|
|
153
|
+
policies: tokenData.policies.map((policy) => ({
|
|
154
|
+
effect: policy.effect,
|
|
155
|
+
permissionGroups: policy.permission_groups.map((pg) => ({
|
|
156
|
+
id: pg.id,
|
|
157
|
+
meta: pg.meta,
|
|
158
|
+
})),
|
|
159
|
+
resources: policy.resources,
|
|
160
|
+
})),
|
|
161
|
+
...(tokenData.expires_on ? { expiresOn: tokenData.expires_on } : {}),
|
|
162
|
+
...(tokenData.not_before ? { notBefore: tokenData.not_before } : {}),
|
|
163
|
+
...(tokenData.condition
|
|
164
|
+
? {
|
|
165
|
+
condition: {
|
|
166
|
+
requestIp: tokenData.condition.request_ip
|
|
167
|
+
? {
|
|
168
|
+
in: tokenData.condition.request_ip.in || [],
|
|
169
|
+
notIn: tokenData.condition.request_ip.not_in || [],
|
|
170
|
+
}
|
|
171
|
+
: undefined,
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
: {}),
|
|
175
|
+
value: tokenValue,
|
|
176
|
+
accessKeyId: tokenData.id,
|
|
177
|
+
secretAccessKey: sha256(tokenValue.unencrypted),
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
File without changes
|
package/lib/cloudflare/api.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Secret } from "../secret";
|
|
1
2
|
/**
|
|
2
3
|
* Options for Cloudflare API requests
|
|
3
4
|
*/
|
|
@@ -5,7 +6,7 @@ export interface CloudflareApiOptions {
|
|
|
5
6
|
/**
|
|
6
7
|
* API Key to use (overrides CLOUDFLARE_API_KEY env var)
|
|
7
8
|
*/
|
|
8
|
-
apiKey?:
|
|
9
|
+
apiKey?: Secret;
|
|
9
10
|
/**
|
|
10
11
|
* Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
|
|
11
12
|
* If not provided, will be automatically retrieved from the Cloudflare API
|
|
@@ -21,6 +22,38 @@ export interface CloudflareApiOptions {
|
|
|
21
22
|
*/
|
|
22
23
|
email?: string;
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Custom error class for Cloudflare API errors
|
|
27
|
+
* Includes HTTP status information from the Response
|
|
28
|
+
*/
|
|
29
|
+
export declare class CloudflareApiError extends Error {
|
|
30
|
+
/**
|
|
31
|
+
* HTTP status code
|
|
32
|
+
*/
|
|
33
|
+
status: number;
|
|
34
|
+
/**
|
|
35
|
+
* HTTP status text
|
|
36
|
+
*/
|
|
37
|
+
statusText: string;
|
|
38
|
+
/**
|
|
39
|
+
* Raw error data from the API
|
|
40
|
+
*/
|
|
41
|
+
errorData?: any;
|
|
42
|
+
/**
|
|
43
|
+
* Create a new CloudflareApiError
|
|
44
|
+
*/
|
|
45
|
+
constructor(message: string, response: Response, errorData?: any);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Helper function to handle API errors
|
|
49
|
+
*
|
|
50
|
+
* @param response The fetch Response object
|
|
51
|
+
* @param action The action being performed (e.g., "creating", "deleting")
|
|
52
|
+
* @param resourceType The type of resource being acted upon (e.g., "R2 bucket", "Worker")
|
|
53
|
+
* @param resourceName The name/identifier of the specific resource
|
|
54
|
+
* @returns Never returns - always throws an error
|
|
55
|
+
*/
|
|
56
|
+
export declare function handleApiError(response: Response, action: string, resourceType: string, resourceName: string): Promise<never>;
|
|
24
57
|
/**
|
|
25
58
|
* Creates a CloudflareApi instance with automatic account ID discovery if not provided
|
|
26
59
|
*
|
|
@@ -28,6 +61,10 @@ export interface CloudflareApiOptions {
|
|
|
28
61
|
* @returns Promise resolving to a CloudflareApi instance
|
|
29
62
|
*/
|
|
30
63
|
export declare function createCloudflareApi(options?: CloudflareApiOptions): Promise<CloudflareApi>;
|
|
64
|
+
export type CloudflareAccountId = string & {
|
|
65
|
+
readonly __brand: "CloudflareAccountId";
|
|
66
|
+
};
|
|
67
|
+
export declare function CloudflareAccountId(options?: CloudflareApiOptions): Promise<CloudflareAccountId>;
|
|
31
68
|
/**
|
|
32
69
|
* Cloudflare API client using raw fetch
|
|
33
70
|
*/
|
package/lib/cloudflare/api.js
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
import { getCloudflareHeaders } from "./auth";
|
|
2
|
+
/**
|
|
3
|
+
* Custom error class for Cloudflare API errors
|
|
4
|
+
* Includes HTTP status information from the Response
|
|
5
|
+
*/
|
|
6
|
+
export class CloudflareApiError extends Error {
|
|
7
|
+
/**
|
|
8
|
+
* HTTP status code
|
|
9
|
+
*/
|
|
10
|
+
status;
|
|
11
|
+
/**
|
|
12
|
+
* HTTP status text
|
|
13
|
+
*/
|
|
14
|
+
statusText;
|
|
15
|
+
/**
|
|
16
|
+
* Raw error data from the API
|
|
17
|
+
*/
|
|
18
|
+
errorData;
|
|
19
|
+
/**
|
|
20
|
+
* Create a new CloudflareApiError
|
|
21
|
+
*/
|
|
22
|
+
constructor(message, response, errorData) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "CloudflareApiError";
|
|
25
|
+
this.status = response.status;
|
|
26
|
+
this.statusText = response.statusText;
|
|
27
|
+
this.errorData = errorData;
|
|
28
|
+
// Ensure instanceof works correctly
|
|
29
|
+
Object.setPrototypeOf(this, CloudflareApiError.prototype);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Helper function to handle API errors
|
|
34
|
+
*
|
|
35
|
+
* @param response The fetch Response object
|
|
36
|
+
* @param action The action being performed (e.g., "creating", "deleting")
|
|
37
|
+
* @param resourceType The type of resource being acted upon (e.g., "R2 bucket", "Worker")
|
|
38
|
+
* @param resourceName The name/identifier of the specific resource
|
|
39
|
+
* @returns Never returns - always throws an error
|
|
40
|
+
*/
|
|
41
|
+
export async function handleApiError(response, action, resourceType, resourceName) {
|
|
42
|
+
const json = await response.json();
|
|
43
|
+
console.log(json);
|
|
44
|
+
const errorData = json.errors || [{ message: response.statusText }];
|
|
45
|
+
const errorMessage = `Error ${action} ${resourceType} '${resourceName}': ${errorData.errors?.[0]?.message || response.statusText}`;
|
|
46
|
+
throw new CloudflareApiError(errorMessage, response, errorData);
|
|
47
|
+
}
|
|
2
48
|
/**
|
|
3
49
|
* Creates a CloudflareApi instance with automatic account ID discovery if not provided
|
|
4
50
|
*
|
|
@@ -9,9 +55,7 @@ export async function createCloudflareApi(options = {}) {
|
|
|
9
55
|
try {
|
|
10
56
|
return new CloudflareApi({
|
|
11
57
|
...options,
|
|
12
|
-
accountId: options
|
|
13
|
-
process.env.CLOUDFLARE_ACCOUNT_ID ||
|
|
14
|
-
(await fetchAccountId()),
|
|
58
|
+
accountId: await CloudflareAccountId(options),
|
|
15
59
|
});
|
|
16
60
|
}
|
|
17
61
|
catch (error) {
|
|
@@ -19,6 +63,11 @@ export async function createCloudflareApi(options = {}) {
|
|
|
19
63
|
throw new Error("Failed to automatically discover Cloudflare account ID. Please provide an account ID explicitly or ensure your API token/key has sufficient permissions.");
|
|
20
64
|
}
|
|
21
65
|
}
|
|
66
|
+
export async function CloudflareAccountId(options = {}) {
|
|
67
|
+
return (options.accountId ||
|
|
68
|
+
process.env.CLOUDFLARE_ACCOUNT_ID ||
|
|
69
|
+
(await fetchAccountId()));
|
|
70
|
+
}
|
|
22
71
|
/**
|
|
23
72
|
* Cloudflare API client using raw fetch
|
|
24
73
|
*/
|
|
@@ -44,7 +93,9 @@ export class CloudflareApi {
|
|
|
44
93
|
*/
|
|
45
94
|
constructor(options = {}) {
|
|
46
95
|
this.baseUrl = "https://api.cloudflare.com/client/v4";
|
|
47
|
-
const apiKey = options.apiKey ||
|
|
96
|
+
const apiKey = options.apiKey?.unencrypted ||
|
|
97
|
+
process.env.CLOUDFLARE_API_KEY ||
|
|
98
|
+
undefined;
|
|
48
99
|
if (!apiKey) {
|
|
49
100
|
throw new Error("No API key provided. Use createCloudflareApi() instead for automatic account discovery.");
|
|
50
101
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Context } from "../context";
|
|
2
|
+
import { Resource } from "../resource";
|
|
3
|
+
/**
|
|
4
|
+
* Properties for creating or updating Assets
|
|
5
|
+
*/
|
|
6
|
+
export interface AssetsProps {
|
|
7
|
+
/**
|
|
8
|
+
* Path to a directory containing static assets to be uploaded
|
|
9
|
+
* These files will be served by Cloudflare's Workers runtime
|
|
10
|
+
*/
|
|
11
|
+
path: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Output returned after Assets creation/update
|
|
15
|
+
*/
|
|
16
|
+
export interface Assets extends Resource<"cloudflare::Asset">, AssetsProps {
|
|
17
|
+
/**
|
|
18
|
+
* The type of binding
|
|
19
|
+
*/
|
|
20
|
+
type: "assets";
|
|
21
|
+
/**
|
|
22
|
+
* The ID of the assets bundle
|
|
23
|
+
*/
|
|
24
|
+
id: string;
|
|
25
|
+
/**
|
|
26
|
+
* Asset files that were found
|
|
27
|
+
*/
|
|
28
|
+
files: AssetFile[];
|
|
29
|
+
/**
|
|
30
|
+
* Time at which the assets were created
|
|
31
|
+
*/
|
|
32
|
+
createdAt: number;
|
|
33
|
+
/**
|
|
34
|
+
* Time at which the assets were last updated
|
|
35
|
+
*/
|
|
36
|
+
updatedAt: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Represents a single asset file
|
|
40
|
+
*/
|
|
41
|
+
export interface AssetFile {
|
|
42
|
+
/**
|
|
43
|
+
* Path relative to the assets directory
|
|
44
|
+
*/
|
|
45
|
+
path: string;
|
|
46
|
+
/**
|
|
47
|
+
* Full filesystem path to the file
|
|
48
|
+
*/
|
|
49
|
+
filePath: string;
|
|
50
|
+
/**
|
|
51
|
+
* Content type of the file
|
|
52
|
+
*/
|
|
53
|
+
contentType: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Cloudflare Assets represent a collection of static files that can be uploaded and served
|
|
57
|
+
* by Cloudflare Workers.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // Create a basic assets bundle from a local directory
|
|
61
|
+
* const staticAssets = await Assets("static", {
|
|
62
|
+
* path: "./src/assets"
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* // Use these assets with a worker
|
|
66
|
+
* const worker = await Worker("frontend", {
|
|
67
|
+
* name: "frontend-worker",
|
|
68
|
+
* entrypoint: "./src/worker.ts",
|
|
69
|
+
* bindings: {
|
|
70
|
+
* ASSETS: staticAssets
|
|
71
|
+
* }
|
|
72
|
+
* });
|
|
73
|
+
*/
|
|
74
|
+
export declare const Assets: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<Assets>, id: string, props: AssetsProps) => Promise<Assets>);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { Resource } from "../resource";
|
|
4
|
+
import { getContentType } from "../util/content-type";
|
|
5
|
+
/**
|
|
6
|
+
* Cloudflare Assets represent a collection of static files that can be uploaded and served
|
|
7
|
+
* by Cloudflare Workers.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Create a basic assets bundle from a local directory
|
|
11
|
+
* const staticAssets = await Assets("static", {
|
|
12
|
+
* path: "./src/assets"
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* // Use these assets with a worker
|
|
16
|
+
* const worker = await Worker("frontend", {
|
|
17
|
+
* name: "frontend-worker",
|
|
18
|
+
* entrypoint: "./src/worker.ts",
|
|
19
|
+
* bindings: {
|
|
20
|
+
* ASSETS: staticAssets
|
|
21
|
+
* }
|
|
22
|
+
* });
|
|
23
|
+
*/
|
|
24
|
+
export const Assets = Resource("cloudflare::Asset", async function (id, props) {
|
|
25
|
+
if (this.phase === "delete") {
|
|
26
|
+
return this.destroy();
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
// Check if the assets directory exists
|
|
30
|
+
const stats = await fs.stat(props.path);
|
|
31
|
+
if (!stats.isDirectory()) {
|
|
32
|
+
throw new Error(`Assets path ${props.path} is not a directory`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`Assets directory ${props.path} does not exist or is not accessible`);
|
|
37
|
+
}
|
|
38
|
+
// Recursively get all files in the assets directory
|
|
39
|
+
const filesList = await getFilesRecursively(props.path);
|
|
40
|
+
// Create asset file objects
|
|
41
|
+
const files = filesList.map((filePath) => {
|
|
42
|
+
const relativePath = path.relative(props.path, filePath);
|
|
43
|
+
const normalizedPath = relativePath.split(path.sep).join("/"); // Ensure forward slashes for URLs
|
|
44
|
+
return {
|
|
45
|
+
path: normalizedPath,
|
|
46
|
+
filePath,
|
|
47
|
+
contentType: getContentType(filePath),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
// Get current timestamp
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
// Construct the output
|
|
53
|
+
return this({
|
|
54
|
+
id,
|
|
55
|
+
type: "assets",
|
|
56
|
+
path: props.path,
|
|
57
|
+
files,
|
|
58
|
+
createdAt: this.output?.createdAt || now,
|
|
59
|
+
updatedAt: now,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
// Helper functions for file operations
|
|
63
|
+
async function getFilesRecursively(dir) {
|
|
64
|
+
const files = await fs.readdir(dir, { withFileTypes: true });
|
|
65
|
+
const allFiles = await Promise.all(files.map(async (file) => {
|
|
66
|
+
const path = `${dir}/${file.name}`;
|
|
67
|
+
if (file.isDirectory()) {
|
|
68
|
+
return getFilesRecursively(path);
|
|
69
|
+
}
|
|
70
|
+
return path;
|
|
71
|
+
}));
|
|
72
|
+
return allFiles.flat();
|
|
73
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/
|
|
5
5
|
*/
|
|
6
6
|
import type { Secret } from "../secret";
|
|
7
|
+
import type { Assets } from "./assets";
|
|
7
8
|
import type { R2Bucket } from "./bucket";
|
|
8
9
|
import type { DurableObjectNamespace } from "./durable-object-namespace";
|
|
9
10
|
import type { KVNamespace } from "./kv-namespace";
|
|
@@ -14,8 +15,12 @@ export type Bindings = {
|
|
|
14
15
|
/**
|
|
15
16
|
* L2 Binding Resources.
|
|
16
17
|
*/
|
|
17
|
-
export type Binding = DurableObjectNamespace | KVNamespace | Worker | R2Bucket | Secret | string;
|
|
18
|
+
export type Binding = DurableObjectNamespace | KVNamespace | Worker | R2Bucket | Secret | string | Assets;
|
|
18
19
|
export declare function isDurableObjectNamespace(binding: Binding): binding is DurableObjectNamespace;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a binding is an Assets resource
|
|
22
|
+
*/
|
|
23
|
+
export declare function isAssets(binding: Binding): binding is Assets;
|
|
19
24
|
/**
|
|
20
25
|
* Union type for all Worker binding types (API spec)
|
|
21
26
|
*/
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
export function isDurableObjectNamespace(binding) {
|
|
2
2
|
return (typeof binding === "object" && binding.type === "durable_object_namespace");
|
|
3
3
|
}
|
|
4
|
+
/**
|
|
5
|
+
* Check if a binding is an Assets resource
|
|
6
|
+
*/
|
|
7
|
+
export function isAssets(binding) {
|
|
8
|
+
return typeof binding === "object" && binding.type === "assets";
|
|
9
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { AwsClient } from "aws4fetch";
|
|
1
2
|
import type { Context } from "../context";
|
|
2
3
|
import { Resource } from "../resource";
|
|
4
|
+
import { CloudflareApi, type CloudflareApiOptions } from "./api";
|
|
3
5
|
/**
|
|
4
6
|
* Properties for creating or updating an R2 Bucket
|
|
5
7
|
*/
|
|
6
|
-
export interface BucketProps {
|
|
8
|
+
export interface BucketProps extends CloudflareApiOptions {
|
|
7
9
|
/**
|
|
8
10
|
* Name of the bucket
|
|
9
11
|
* Names can only contain lowercase letters (a-z), numbers (0-9), and hyphens (-)
|
|
@@ -25,6 +27,12 @@ export interface BucketProps {
|
|
|
25
27
|
* Only for development purposes - use custom domains for production
|
|
26
28
|
*/
|
|
27
29
|
allowPublicAccess?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Whether to delete the bucket and its contents during resource deletion
|
|
32
|
+
* If set to false, the bucket will remain but the resource will be removed from state
|
|
33
|
+
* Default: true
|
|
34
|
+
*/
|
|
35
|
+
delete?: boolean;
|
|
28
36
|
}
|
|
29
37
|
/**
|
|
30
38
|
* Output returned after R2 Bucket creation/update
|
|
@@ -39,9 +47,9 @@ export interface R2Bucket extends Resource<"cloudflare::R2Bucket">, BucketProps
|
|
|
39
47
|
*/
|
|
40
48
|
location: string;
|
|
41
49
|
/**
|
|
42
|
-
* Time at which the bucket was created
|
|
50
|
+
* Time at which the bucket was created
|
|
43
51
|
*/
|
|
44
|
-
creationDate:
|
|
52
|
+
creationDate: Date;
|
|
45
53
|
}
|
|
46
54
|
/**
|
|
47
55
|
* Creates and manages Cloudflare R2 Buckets for object storage.
|
|
@@ -80,3 +88,110 @@ export interface R2Bucket extends Resource<"cloudflare::R2Bucket">, BucketProps
|
|
|
80
88
|
* @see https://developers.cloudflare.com/r2/buckets/
|
|
81
89
|
*/
|
|
82
90
|
export declare const R2Bucket: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<R2Bucket>, id: string, props: BucketProps) => Promise<R2Bucket>);
|
|
91
|
+
/**
|
|
92
|
+
* Configuration for R2 client to connect to Cloudflare R2
|
|
93
|
+
*/
|
|
94
|
+
export interface R2ClientConfig {
|
|
95
|
+
accountId?: string;
|
|
96
|
+
accessKeyId?: string;
|
|
97
|
+
secretAccessKey?: string;
|
|
98
|
+
jurisdiction?: string;
|
|
99
|
+
}
|
|
100
|
+
type R2Client = AwsClient & {
|
|
101
|
+
accountId: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Creates an aws4fetch client configured for Cloudflare R2
|
|
105
|
+
*
|
|
106
|
+
* @see https://developers.cloudflare.com/r2/examples/aws/aws-sdk-js-v3/
|
|
107
|
+
*/
|
|
108
|
+
export declare function createR2Client(config?: R2ClientConfig): Promise<R2Client>;
|
|
109
|
+
interface CloudflareBucketResponse {
|
|
110
|
+
result: {
|
|
111
|
+
name: string;
|
|
112
|
+
location?: string;
|
|
113
|
+
creation_date: string;
|
|
114
|
+
};
|
|
115
|
+
success: boolean;
|
|
116
|
+
errors: Array<{
|
|
117
|
+
code: number;
|
|
118
|
+
message: string;
|
|
119
|
+
}>;
|
|
120
|
+
messages: string[];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Adds jurisdiction header to the headers object if specified in props
|
|
124
|
+
*
|
|
125
|
+
* @param headers Headers object to modify
|
|
126
|
+
* @param props Props or jurisdiction string
|
|
127
|
+
* @returns Modified headers object
|
|
128
|
+
*/
|
|
129
|
+
export declare function withJurisdiction(headers: Record<string, string> | undefined, props: BucketProps | {
|
|
130
|
+
jurisdiction?: string;
|
|
131
|
+
} | string | undefined): Record<string, string>;
|
|
132
|
+
/**
|
|
133
|
+
* Create a new bucket
|
|
134
|
+
*/
|
|
135
|
+
export declare function createBucket(api: CloudflareApi, bucketName: string, props: BucketProps): Promise<CloudflareBucketResponse>;
|
|
136
|
+
/**
|
|
137
|
+
* Delete a bucket
|
|
138
|
+
*/
|
|
139
|
+
export declare function deleteBucket(api: CloudflareApi, bucketName: string, props: BucketProps): Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* List objects in an R2 bucket
|
|
142
|
+
*
|
|
143
|
+
* @param r2 R2Client instance
|
|
144
|
+
* @param bucketName Name of the bucket
|
|
145
|
+
* @param continuationToken Optional token for pagination
|
|
146
|
+
* @param jurisdiction Optional jurisdiction for the bucket
|
|
147
|
+
* @returns Object containing the list of objects and the next continuation token
|
|
148
|
+
*/
|
|
149
|
+
export declare function listObjects(r2: R2Client, bucketName: string, continuationToken?: string, jurisdiction?: string): Promise<{
|
|
150
|
+
objects: {
|
|
151
|
+
Key: string;
|
|
152
|
+
}[];
|
|
153
|
+
continuationToken?: string;
|
|
154
|
+
}>;
|
|
155
|
+
/**
|
|
156
|
+
* Helper function to empty a bucket by deleting all objects
|
|
157
|
+
*/
|
|
158
|
+
export declare function emptyBucket(r2: R2Client, bucketName: string, jurisdiction?: string): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* Update public access setting for a bucket
|
|
161
|
+
*
|
|
162
|
+
* This operation is not available through the S3 API for R2,
|
|
163
|
+
* so we still use the Cloudflare API directly.
|
|
164
|
+
*/
|
|
165
|
+
export declare function updatePublicAccess(api: CloudflareApi, bucketName: string, allowPublicAccess: boolean, jurisdiction?: string): Promise<void>;
|
|
166
|
+
/**
|
|
167
|
+
* Set CORS configuration for a bucket using aws4fetch
|
|
168
|
+
*/
|
|
169
|
+
export declare function setCorsConfiguration(r2: R2Client, bucketName: string, allowedOrigins?: string[], allowedMethods?: string[], allowedHeaders?: string[], maxAgeSeconds?: number, jurisdiction?: string): Promise<void>;
|
|
170
|
+
/**
|
|
171
|
+
* Information about an R2 bucket returned by list operations
|
|
172
|
+
*/
|
|
173
|
+
export interface R2BucketInfo {
|
|
174
|
+
/**
|
|
175
|
+
* Name of the bucket
|
|
176
|
+
*/
|
|
177
|
+
Name: string;
|
|
178
|
+
/**
|
|
179
|
+
* Creation date of the bucket
|
|
180
|
+
*/
|
|
181
|
+
CreationDate: Date;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* List all R2 buckets in an account
|
|
185
|
+
*
|
|
186
|
+
* @param api CloudflareApi instance
|
|
187
|
+
* @param options Optional listing options
|
|
188
|
+
* @returns Array of bucket information
|
|
189
|
+
*/
|
|
190
|
+
export declare function listBuckets(api: CloudflareApi, options?: {
|
|
191
|
+
nameContains?: string;
|
|
192
|
+
perPage?: number;
|
|
193
|
+
cursor?: string;
|
|
194
|
+
direction?: "asc" | "desc";
|
|
195
|
+
jurisdiction?: string;
|
|
196
|
+
}): Promise<R2BucketInfo[]>;
|
|
197
|
+
export {};
|