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,318 @@
|
|
|
1
|
+
import type { Context } from "../context";
|
|
2
|
+
import { Resource } from "../resource";
|
|
3
|
+
import { CloudflareApi, createCloudflareApi, handleApiError } from "./api";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Properties for creating or updating a CustomDomain
|
|
7
|
+
*/
|
|
8
|
+
export interface CustomDomainProps {
|
|
9
|
+
/**
|
|
10
|
+
* The domain name to bind to the worker
|
|
11
|
+
*/
|
|
12
|
+
name: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Cloudflare Zone ID for the domain
|
|
16
|
+
*/
|
|
17
|
+
zoneId: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Name of the worker to bind to the domain
|
|
21
|
+
*/
|
|
22
|
+
workerName: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Worker environment (defaults to production)
|
|
26
|
+
* @default "production"
|
|
27
|
+
*/
|
|
28
|
+
environment?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Cloudflare Domain object structure from API
|
|
33
|
+
*/
|
|
34
|
+
interface CloudflareDomain {
|
|
35
|
+
id: string;
|
|
36
|
+
zone_id: string;
|
|
37
|
+
zone_name: string;
|
|
38
|
+
hostname: string;
|
|
39
|
+
service: string;
|
|
40
|
+
environment: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Output returned after CustomDomain creation/update
|
|
45
|
+
*/
|
|
46
|
+
export interface CustomDomain
|
|
47
|
+
extends Resource<"cloudflare::CustomDomain">,
|
|
48
|
+
CustomDomainProps {
|
|
49
|
+
/**
|
|
50
|
+
* The unique identifier for the Cloudflare domain binding.
|
|
51
|
+
*/
|
|
52
|
+
id: string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Time at which the domain binding was created (approximated if not returned by API)
|
|
56
|
+
*/
|
|
57
|
+
createdAt: number;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Time at which the domain binding was last updated
|
|
61
|
+
*/
|
|
62
|
+
updatedAt: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Configure custom domain for a Cloudflare Worker using the Cloudflare Custom Domains API
|
|
67
|
+
* This attaches a worker (either a standard Worker or the one backing a StaticSite)
|
|
68
|
+
* to a specific hostname within a zone.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // Bind a domain to a standard Cloudflare Worker
|
|
72
|
+
* const apiWorker = await Worker("api", {
|
|
73
|
+
* name: "my-api-worker",
|
|
74
|
+
* entrypoint: "./src/api-worker.ts"
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* const apiDomain = await CustomDomain("api-domain-binding", {
|
|
78
|
+
* name: "api.example.com",
|
|
79
|
+
* zoneId: "YOUR_ZONE_ID", // Replace with actual Zone ID
|
|
80
|
+
* workerName: apiWorker.name // Use the name from the Worker resource
|
|
81
|
+
* });
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // Bind a domain to a Cloudflare Static Site
|
|
85
|
+
* const mySite = await StaticSite("landing-page", {
|
|
86
|
+
* name: "my-landing-page-site", // This becomes the underlying worker name
|
|
87
|
+
* dir: "./dist/landing-page"
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* const siteDomain = await CustomDomain("site-domain-binding", {
|
|
91
|
+
* name: "www.example.com",
|
|
92
|
+
* zoneId: "YOUR_ZONE_ID", // Replace with actual Zone ID
|
|
93
|
+
* workerName: mySite.name // Use the name from the StaticSite resource
|
|
94
|
+
* });
|
|
95
|
+
*
|
|
96
|
+
* @see https://developers.cloudflare.com/api/resources/workers/subresources/domains/
|
|
97
|
+
*/
|
|
98
|
+
export const CustomDomain = Resource(
|
|
99
|
+
"cloudflare::CustomDomain",
|
|
100
|
+
async function (
|
|
101
|
+
this: Context<CustomDomain>,
|
|
102
|
+
logicalId: string, // Changed param name from id to logicalId for clarity
|
|
103
|
+
props: CustomDomainProps
|
|
104
|
+
): Promise<CustomDomain> {
|
|
105
|
+
// Create Cloudflare API client with automatic account discovery
|
|
106
|
+
const api = await createCloudflareApi();
|
|
107
|
+
|
|
108
|
+
// Validate required properties
|
|
109
|
+
if (!props.name) {
|
|
110
|
+
throw new Error("Domain name (props.name) is required");
|
|
111
|
+
}
|
|
112
|
+
if (!props.zoneId) {
|
|
113
|
+
throw new Error("Zone ID (props.zoneId) is required");
|
|
114
|
+
}
|
|
115
|
+
if (!props.workerName) {
|
|
116
|
+
throw new Error("Worker name (props.workerName) is required");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this.phase === "delete") {
|
|
120
|
+
await deleteCustomDomain(this, api, logicalId, props);
|
|
121
|
+
return this.destroy();
|
|
122
|
+
} else {
|
|
123
|
+
// Create or Update phase
|
|
124
|
+
return await ensureCustomDomain(this, api, logicalId, props);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Helper function to delete the custom domain binding
|
|
130
|
+
async function deleteCustomDomain(
|
|
131
|
+
context: Context<CustomDomain>,
|
|
132
|
+
api: CloudflareApi,
|
|
133
|
+
logicalId: string,
|
|
134
|
+
props: CustomDomainProps
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
const domainHostname = props.name;
|
|
137
|
+
const domainIdToDelete = context.output?.id;
|
|
138
|
+
|
|
139
|
+
if (!domainIdToDelete) {
|
|
140
|
+
console.warn(
|
|
141
|
+
`Cannot delete CustomDomain ${logicalId} (${domainHostname}): Missing domain ID in state. Assuming already deleted.`
|
|
142
|
+
);
|
|
143
|
+
return; // Exit early if no ID
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(
|
|
147
|
+
`Deleting CustomDomain binding ${domainIdToDelete} for ${domainHostname}`
|
|
148
|
+
);
|
|
149
|
+
const response = await api.delete(
|
|
150
|
+
`/accounts/${api.accountId}/workers/domains/${domainIdToDelete}`
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
console.log(
|
|
154
|
+
`Delete result for ${domainIdToDelete} (${domainHostname}):`,
|
|
155
|
+
response.status,
|
|
156
|
+
response.statusText
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// 404 is acceptable during deletion for idempotency
|
|
160
|
+
if (!response.ok && response.status !== 404) {
|
|
161
|
+
await handleApiError(
|
|
162
|
+
response,
|
|
163
|
+
"deleting",
|
|
164
|
+
"custom domain binding",
|
|
165
|
+
domainIdToDelete
|
|
166
|
+
);
|
|
167
|
+
// Throw after handling to ensure failure is reported
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Failed to delete custom domain binding ${domainIdToDelete}: ${response.statusText}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Helper function to create or update the custom domain binding
|
|
175
|
+
async function ensureCustomDomain(
|
|
176
|
+
context: Context<CustomDomain>,
|
|
177
|
+
api: CloudflareApi,
|
|
178
|
+
logicalId: string,
|
|
179
|
+
props: CustomDomainProps
|
|
180
|
+
): Promise<CustomDomain> {
|
|
181
|
+
const environment = props.environment || "production";
|
|
182
|
+
const domainHostname = props.name;
|
|
183
|
+
|
|
184
|
+
// Check if domain binding already exists for this account
|
|
185
|
+
console.log(`Checking existing domain bindings for account ${api.accountId}`);
|
|
186
|
+
const listResponse = await api.get(
|
|
187
|
+
`/accounts/${api.accountId}/workers/domains`
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (!listResponse.ok) {
|
|
191
|
+
// Fix: Added the 4th argument (resource identifier/context)
|
|
192
|
+
await handleApiError(
|
|
193
|
+
listResponse,
|
|
194
|
+
"listing",
|
|
195
|
+
"worker domains",
|
|
196
|
+
`Account ${api.accountId}`
|
|
197
|
+
);
|
|
198
|
+
// If listing fails, we cannot proceed reliably
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Failed to list worker domains for account ${api.accountId}: ${listResponse.statusText}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const listData = (await listResponse.json()) as {
|
|
205
|
+
result?: CloudflareDomain[];
|
|
206
|
+
success: boolean;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (!listData.success || !listData.result) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Failed to parse list worker domains response: ${JSON.stringify(listData)}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Find the specific binding by hostname AND zoneId
|
|
216
|
+
const existingBinding = listData.result.find(
|
|
217
|
+
(b) => b.hostname === domainHostname && b.zone_id === props.zoneId
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
let currentDomainId = existingBinding?.id;
|
|
221
|
+
const bindingExists = !!existingBinding;
|
|
222
|
+
|
|
223
|
+
console.log(
|
|
224
|
+
`Domain binding status for ${domainHostname} (Zone: ${props.zoneId}):`,
|
|
225
|
+
bindingExists
|
|
226
|
+
? `Found (ID: ${currentDomainId}, Worker: ${existingBinding.service}, Env: ${existingBinding.environment})`
|
|
227
|
+
: "Not found"
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Determine if we need to update (binding exists but has different service or environment)
|
|
231
|
+
const needsUpdate =
|
|
232
|
+
bindingExists &&
|
|
233
|
+
(existingBinding.service !== props.workerName ||
|
|
234
|
+
existingBinding.environment !== environment);
|
|
235
|
+
|
|
236
|
+
let operationPerformed: "create" | "update" | "none" = "none";
|
|
237
|
+
let resultantBinding: CloudflareDomain | undefined = existingBinding;
|
|
238
|
+
|
|
239
|
+
// Create or Update the binding using PUT
|
|
240
|
+
// Cloudflare's PUT /accounts/{account_id}/workers/domains acts as an upsert
|
|
241
|
+
if (!bindingExists || needsUpdate) {
|
|
242
|
+
operationPerformed = bindingExists ? "update" : "create";
|
|
243
|
+
console.log(
|
|
244
|
+
`${operationPerformed === "update" ? "Updating" : "Creating"} domain binding: ${domainHostname} (Zone: ${props.zoneId}) → ${props.workerName}:${environment}`
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const putPayload = {
|
|
248
|
+
zone_id: props.zoneId,
|
|
249
|
+
hostname: domainHostname,
|
|
250
|
+
service: props.workerName,
|
|
251
|
+
environment: environment,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const putResponse = await api.put(
|
|
255
|
+
`/accounts/${api.accountId}/workers/domains`,
|
|
256
|
+
putPayload
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (!putResponse.ok) {
|
|
260
|
+
await handleApiError(
|
|
261
|
+
putResponse,
|
|
262
|
+
operationPerformed === "update" ? "updating" : "creating",
|
|
263
|
+
"custom domain binding",
|
|
264
|
+
domainHostname
|
|
265
|
+
);
|
|
266
|
+
// Throw after handling to prevent inconsistent state
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Failed to ${operationPerformed} custom domain binding: ${putResponse.statusText}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const putResult = (await putResponse.json()) as {
|
|
273
|
+
result?: CloudflareDomain;
|
|
274
|
+
success: boolean;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (!putResult.success || !putResult.result) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`Failed to parse ${operationPerformed} domain binding response: ${JSON.stringify(putResult)}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
resultantBinding = putResult.result;
|
|
284
|
+
currentDomainId = resultantBinding.id; // Update ID from the PUT response
|
|
285
|
+
console.log(
|
|
286
|
+
`Successfully ${operationPerformed}d binding, new ID: ${currentDomainId}`
|
|
287
|
+
);
|
|
288
|
+
} else {
|
|
289
|
+
console.log(
|
|
290
|
+
`Domain binding already exists and is up to date: ${domainHostname} (ID: ${currentDomainId}) → ${props.workerName}:${environment}`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Ensure we have the final binding details
|
|
295
|
+
if (!resultantBinding || !currentDomainId) {
|
|
296
|
+
// This case should ideally not happen if API calls succeed
|
|
297
|
+
console.error("Error: Could not determine final domain binding state.", {
|
|
298
|
+
existingBinding,
|
|
299
|
+
resultantBinding,
|
|
300
|
+
currentDomainId,
|
|
301
|
+
});
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Failed to get final state for custom domain ${domainHostname}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const now = Date.now();
|
|
308
|
+
|
|
309
|
+
// Construct the output state
|
|
310
|
+
return context({
|
|
311
|
+
...props, // Include all input props
|
|
312
|
+
id: currentDomainId, // Use the definitive ID
|
|
313
|
+
environment: resultantBinding.environment, // Use actual environment from CF
|
|
314
|
+
createdAt: context.output?.createdAt || now, // Preserve create time or set new
|
|
315
|
+
updatedAt:
|
|
316
|
+
operationPerformed !== "none" ? now : context.output?.updatedAt || now, // Update time only if changed
|
|
317
|
+
});
|
|
318
|
+
}
|
|
@@ -124,7 +124,7 @@ export const DnsRecords = Resource(
|
|
|
124
124
|
async function (
|
|
125
125
|
this: Context<DnsRecords>,
|
|
126
126
|
id: string,
|
|
127
|
-
props: DnsRecordsProps
|
|
127
|
+
props: DnsRecordsProps
|
|
128
128
|
): Promise<DnsRecords> {
|
|
129
129
|
// Create Cloudflare API client
|
|
130
130
|
const api = await createCloudflareApi();
|
|
@@ -139,17 +139,17 @@ export const DnsRecords = Resource(
|
|
|
139
139
|
this.output.records.map(async (record) => {
|
|
140
140
|
try {
|
|
141
141
|
const response = await api.delete(
|
|
142
|
-
`/zones/${zoneId}/dns_records/${record.id}
|
|
142
|
+
`/zones/${zoneId}/dns_records/${record.id}`
|
|
143
143
|
);
|
|
144
144
|
if (!response.ok && response.status !== 404) {
|
|
145
145
|
console.error(
|
|
146
|
-
`Failed to delete DNS record ${record.name}: ${response.statusText}
|
|
146
|
+
`Failed to delete DNS record ${record.name}: ${response.statusText}`
|
|
147
147
|
);
|
|
148
148
|
}
|
|
149
149
|
} catch (error) {
|
|
150
150
|
console.error(`Error deleting DNS record ${record.name}:`, error);
|
|
151
151
|
}
|
|
152
|
-
})
|
|
152
|
+
})
|
|
153
153
|
);
|
|
154
154
|
}
|
|
155
155
|
return this.destroy();
|
|
@@ -165,8 +165,8 @@ export const DnsRecords = Resource(
|
|
|
165
165
|
(current) =>
|
|
166
166
|
!desiredRecords.some(
|
|
167
167
|
(desired) =>
|
|
168
|
-
desired.name === current.name && desired.type === current.type
|
|
169
|
-
)
|
|
168
|
+
desired.name === current.name && desired.type === current.type
|
|
169
|
+
)
|
|
170
170
|
);
|
|
171
171
|
|
|
172
172
|
// Delete orphaned records
|
|
@@ -174,17 +174,17 @@ export const DnsRecords = Resource(
|
|
|
174
174
|
recordsToDelete.map(async (record) => {
|
|
175
175
|
try {
|
|
176
176
|
const response = await api.delete(
|
|
177
|
-
`/zones/${zoneId}/dns_records/${record.id}
|
|
177
|
+
`/zones/${zoneId}/dns_records/${record.id}`
|
|
178
178
|
);
|
|
179
179
|
if (!response.ok && response.status !== 404) {
|
|
180
180
|
console.error(
|
|
181
|
-
`Failed to delete DNS record ${record.name}: ${response.statusText}
|
|
181
|
+
`Failed to delete DNS record ${record.name}: ${response.statusText}`
|
|
182
182
|
);
|
|
183
183
|
}
|
|
184
184
|
} catch (error) {
|
|
185
185
|
console.error(`Error deleting DNS record ${record.name}:`, error);
|
|
186
186
|
}
|
|
187
|
-
})
|
|
187
|
+
})
|
|
188
188
|
);
|
|
189
189
|
|
|
190
190
|
// Update or create records
|
|
@@ -193,7 +193,7 @@ export const DnsRecords = Resource(
|
|
|
193
193
|
// Find matching existing record
|
|
194
194
|
const existing = currentRecords.find(
|
|
195
195
|
(current) =>
|
|
196
|
-
current.name === desired.name && current.type === desired.type
|
|
196
|
+
current.name === desired.name && current.type === desired.type
|
|
197
197
|
);
|
|
198
198
|
|
|
199
199
|
if (existing) {
|
|
@@ -212,7 +212,7 @@ export const DnsRecords = Resource(
|
|
|
212
212
|
// Create new record
|
|
213
213
|
return createOrUpdateRecord(api, zoneId, desired);
|
|
214
214
|
}
|
|
215
|
-
})
|
|
215
|
+
})
|
|
216
216
|
);
|
|
217
217
|
|
|
218
218
|
return this({
|
|
@@ -224,22 +224,36 @@ export const DnsRecords = Resource(
|
|
|
224
224
|
// Create new records
|
|
225
225
|
const uniqueRecords = props.records.reduce(
|
|
226
226
|
(acc, record) => {
|
|
227
|
-
|
|
227
|
+
// For record types that can have multiple entries with the same name (MX, TXT, NS, etc.),
|
|
228
|
+
// include content and/or priority in the key to avoid deduplication
|
|
229
|
+
let key = `${record.name}-${record.type}`;
|
|
230
|
+
|
|
231
|
+
// If it's a record type that can have multiple entries with the same name, make the key unique
|
|
232
|
+
if (["MX", "TXT", "NS", "SRV", "CAA"].includes(record.type)) {
|
|
233
|
+
// For MX, include priority in the key
|
|
234
|
+
if (record.type === "MX" || record.type === "SRV") {
|
|
235
|
+
key = `${key}-${record.priority}-${record.content}`;
|
|
236
|
+
} else {
|
|
237
|
+
// For other multi-record types, content is the differentiator
|
|
238
|
+
key = `${key}-${record.content}`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
228
242
|
acc[key] = record;
|
|
229
243
|
return acc;
|
|
230
244
|
},
|
|
231
|
-
{} as Record<string, DnsRecordProps
|
|
245
|
+
{} as Record<string, DnsRecordProps>
|
|
232
246
|
);
|
|
233
247
|
|
|
234
248
|
const createdRecords = await Promise.all(
|
|
235
249
|
Object.values(uniqueRecords).map(async (record) => {
|
|
236
250
|
// First check if record exists
|
|
237
251
|
const listResponse = await api.get(
|
|
238
|
-
`/zones/${zoneId}/dns_records?type=${record.type}&name=${record.name}
|
|
252
|
+
`/zones/${zoneId}/dns_records?type=${record.type}&name=${record.name}`
|
|
239
253
|
);
|
|
240
254
|
if (!listResponse.ok) {
|
|
241
255
|
throw new Error(
|
|
242
|
-
`Failed to check existing DNS records: ${listResponse.statusText}
|
|
256
|
+
`Failed to check existing DNS records: ${listResponse.statusText}`
|
|
243
257
|
);
|
|
244
258
|
}
|
|
245
259
|
|
|
@@ -249,14 +263,14 @@ export const DnsRecords = Resource(
|
|
|
249
263
|
const existingRecord = listResult.result[0];
|
|
250
264
|
|
|
251
265
|
return createOrUpdateRecord(api, zoneId, record, existingRecord?.id);
|
|
252
|
-
})
|
|
266
|
+
})
|
|
253
267
|
);
|
|
254
268
|
|
|
255
269
|
return this({
|
|
256
270
|
zoneId,
|
|
257
271
|
records: createdRecords,
|
|
258
272
|
});
|
|
259
|
-
}
|
|
273
|
+
}
|
|
260
274
|
);
|
|
261
275
|
|
|
262
276
|
/**
|
|
@@ -266,7 +280,7 @@ async function createOrUpdateRecord(
|
|
|
266
280
|
api: CloudflareApi,
|
|
267
281
|
zoneId: string,
|
|
268
282
|
record: DnsRecordProps,
|
|
269
|
-
existingId?: string
|
|
283
|
+
existingId?: string
|
|
270
284
|
): Promise<DnsRecord> {
|
|
271
285
|
const payload = getRecordPayload(record);
|
|
272
286
|
|
|
@@ -282,12 +296,12 @@ async function createOrUpdateRecord(
|
|
|
282
296
|
try {
|
|
283
297
|
const createResponse = await api.post(
|
|
284
298
|
`/zones/${zoneId}/dns_records`,
|
|
285
|
-
payload
|
|
299
|
+
payload
|
|
286
300
|
);
|
|
287
301
|
if (createResponse.ok) {
|
|
288
302
|
return convertCloudflareRecord(
|
|
289
303
|
((await createResponse.json()) as any).result,
|
|
290
|
-
zoneId
|
|
304
|
+
zoneId
|
|
291
305
|
);
|
|
292
306
|
}
|
|
293
307
|
} catch (err) {
|
|
@@ -296,7 +310,7 @@ async function createOrUpdateRecord(
|
|
|
296
310
|
}
|
|
297
311
|
|
|
298
312
|
throw new Error(
|
|
299
|
-
`Failed to ${existingId ? "update" : "create"} DNS record ${record.name}: ${response.statusText}\nResponse: ${errorBody}
|
|
313
|
+
`Failed to ${existingId ? "update" : "create"} DNS record ${record.name}: ${response.statusText}\nResponse: ${errorBody}`
|
|
300
314
|
);
|
|
301
315
|
}
|
|
302
316
|
|
|
@@ -325,7 +339,7 @@ function getRecordPayload(record: DnsRecordProps) {
|
|
|
325
339
|
*/
|
|
326
340
|
function convertCloudflareRecord(
|
|
327
341
|
record: CloudflareDnsRecord,
|
|
328
|
-
zoneId: string
|
|
342
|
+
zoneId: string
|
|
329
343
|
): DnsRecord {
|
|
330
344
|
return {
|
|
331
345
|
id: record.id,
|
package/src/cloudflare/index.ts
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,137 @@
|
|
|
1
|
+
import type { Context } from "../context";
|
|
2
|
+
import { Resource } from "../resource";
|
|
3
|
+
import { createCloudflareApi, type CloudflareApiOptions } from "./api";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cloudflare permission group as returned by the API
|
|
7
|
+
*/
|
|
8
|
+
export interface PermissionGroup {
|
|
9
|
+
/**
|
|
10
|
+
* Unique identifier for the permission group
|
|
11
|
+
*/
|
|
12
|
+
id: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Human-readable name of the permission group
|
|
16
|
+
*/
|
|
17
|
+
name: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Scopes included in this permission group
|
|
21
|
+
*/
|
|
22
|
+
scopes: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Response from the Cloudflare permission groups API
|
|
27
|
+
*/
|
|
28
|
+
interface PermissionGroupsResponse {
|
|
29
|
+
result: PermissionGroup[];
|
|
30
|
+
success: boolean;
|
|
31
|
+
errors: any[];
|
|
32
|
+
messages: any[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* All Cloudflare permission groups mapped by name to ID
|
|
37
|
+
*
|
|
38
|
+
* @see https://developers.cloudflare.com/r2/api/tokens/#permissions
|
|
39
|
+
*/
|
|
40
|
+
export type PermissionGroups = Resource<"cloudflare::PermissionGroups"> & {
|
|
41
|
+
/**
|
|
42
|
+
* Admin Read & Write - Allows create, list, delete buckets and edit bucket configurations
|
|
43
|
+
* plus list, write, and read object access
|
|
44
|
+
*/
|
|
45
|
+
"Workers R2 Storage Write": PermissionGroup;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Admin Read only - Allows list buckets and view bucket configuration
|
|
49
|
+
* plus list and read object access
|
|
50
|
+
*/
|
|
51
|
+
"Workers R2 Storage Read": PermissionGroup;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Object Read & Write - Allows read, write, and list objects in specific buckets
|
|
55
|
+
*/
|
|
56
|
+
"Workers R2 Storage Bucket Item Write": PermissionGroup;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Object Read only - Allows read and list objects in specific buckets
|
|
60
|
+
*/
|
|
61
|
+
"Workers R2 Storage Bucket Item Read": PermissionGroup;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Dynamically discovered permission groups
|
|
65
|
+
*/
|
|
66
|
+
[name: string]: PermissionGroup;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Lists all permission groups available for the Cloudflare account
|
|
71
|
+
* and returns a typed map of permission names to their IDs.
|
|
72
|
+
*
|
|
73
|
+
* This is primarily used when creating API tokens for Cloudflare services like R2.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Get all permission groups including those for R2
|
|
77
|
+
* const permissions = await PermissionGroups("cloudflare-permissions");
|
|
78
|
+
*
|
|
79
|
+
* // Use with AccountApiToken to create a token with proper permissions
|
|
80
|
+
* const token = await AccountApiToken("r2-token", {
|
|
81
|
+
* name: "R2 Read-Only Token",
|
|
82
|
+
* policies: [
|
|
83
|
+
* {
|
|
84
|
+
* effect: "allow",
|
|
85
|
+
* resources: {
|
|
86
|
+
* "com.cloudflare.edge.r2.bucket.abc123_default_my-bucket": "*"
|
|
87
|
+
* },
|
|
88
|
+
* permissionGroups: [
|
|
89
|
+
* {
|
|
90
|
+
* id: permissions["Workers R2 Storage Bucket Item Read"]
|
|
91
|
+
* }
|
|
92
|
+
* ]
|
|
93
|
+
* }
|
|
94
|
+
* ]
|
|
95
|
+
* });
|
|
96
|
+
*/
|
|
97
|
+
export const PermissionGroups = Resource(
|
|
98
|
+
"cloudflare::PermissionGroups",
|
|
99
|
+
async function (
|
|
100
|
+
this: Context<PermissionGroups>,
|
|
101
|
+
id: string,
|
|
102
|
+
options?: CloudflareApiOptions
|
|
103
|
+
): Promise<PermissionGroups> {
|
|
104
|
+
// Only create and update phases are supported
|
|
105
|
+
if (this.phase === "delete") {
|
|
106
|
+
return this.destroy();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Initialize API client
|
|
110
|
+
const api = await createCloudflareApi(options);
|
|
111
|
+
|
|
112
|
+
// Fetch permission groups from Cloudflare API
|
|
113
|
+
const response = await api.get(
|
|
114
|
+
`/accounts/${api.accountId}/tokens/permission_groups`
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Failed to fetch permission groups: ${response.statusText}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const data = (await response.json()) as PermissionGroupsResponse;
|
|
124
|
+
|
|
125
|
+
if (!data.success || !data.result) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`API returned error: ${data.errors?.[0]?.message || "Unknown error"}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return this(
|
|
132
|
+
Object.fromEntries(
|
|
133
|
+
data.result.map((group) => [group.name, group])
|
|
134
|
+
) as PermissionGroups
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
);
|