alchemy 0.3.1 → 0.4.0
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/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 +5 -1
- package/lib/cloudflare/index.js +5 -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 +3 -2
- package/lib/cloudflare/static-site.d.ts +17 -18
- package/lib/cloudflare/static-site.js +19 -10
- 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/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 +5 -1
- package/src/cloudflare/permission-groups.ts +137 -0
- package/src/cloudflare/r2-rest-state-store.ts +14 -13
- package/src/cloudflare/static-site.ts +20 -32
- 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/src/aws/role.ts
CHANGED
|
@@ -219,58 +219,72 @@ export const Role = Resource(
|
|
|
219
219
|
async function (
|
|
220
220
|
this: Context<Role>,
|
|
221
221
|
id: string,
|
|
222
|
-
props: RoleProps
|
|
222
|
+
props: RoleProps
|
|
223
223
|
): Promise<Role> {
|
|
224
224
|
const client = new IAMClient({});
|
|
225
225
|
|
|
226
226
|
if (this.phase === "delete") {
|
|
227
|
-
// Delete any inline policies first
|
|
228
|
-
if (props.policies) {
|
|
229
|
-
for (const policy of props.policies) {
|
|
230
|
-
await ignore(NoSuchEntityException.name, () =>
|
|
231
|
-
client.send(
|
|
232
|
-
new DeleteRolePolicyCommand({
|
|
233
|
-
RoleName: props.roleName,
|
|
234
|
-
PolicyName: policy.policyName,
|
|
235
|
-
}),
|
|
236
|
-
),
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// We need to detach managed policies before deleting the role
|
|
242
|
-
// First, get all attached policies
|
|
243
227
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
228
|
+
// Delete any inline policies first
|
|
229
|
+
if (props.policies) {
|
|
230
|
+
for (const policy of props.policies) {
|
|
231
|
+
await ignore(NoSuchEntityException.name, () =>
|
|
232
|
+
client.send(
|
|
233
|
+
new DeleteRolePolicyCommand({
|
|
234
|
+
RoleName: props.roleName,
|
|
235
|
+
PolicyName: policy.policyName,
|
|
236
|
+
})
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
249
241
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
new DetachRolePolicyCommand({
|
|
242
|
+
// We need to detach managed policies before deleting the role
|
|
243
|
+
// First, get all attached policies
|
|
244
|
+
try {
|
|
245
|
+
const attachedPoliciesResponse = await client.send(
|
|
246
|
+
new ListAttachedRolePoliciesCommand({
|
|
256
247
|
RoleName: props.roleName,
|
|
257
|
-
|
|
258
|
-
}),
|
|
248
|
+
})
|
|
259
249
|
);
|
|
250
|
+
|
|
251
|
+
// Detach all managed policies
|
|
252
|
+
const attachedPolicies =
|
|
253
|
+
attachedPoliciesResponse.AttachedPolicies || [];
|
|
254
|
+
for (const policy of attachedPolicies) {
|
|
255
|
+
await ignore(NoSuchEntityException.name, () =>
|
|
256
|
+
client.send(
|
|
257
|
+
new DetachRolePolicyCommand({
|
|
258
|
+
RoleName: props.roleName,
|
|
259
|
+
PolicyArn: policy.PolicyArn!,
|
|
260
|
+
})
|
|
261
|
+
)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
} catch (error: any) {
|
|
265
|
+
if (error.name !== NoSuchEntityException.name) {
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
// Role doesn't exist, no need to continue with detaching policies
|
|
260
269
|
}
|
|
270
|
+
|
|
271
|
+
// Try to delete the role, ignoring if it doesn't exist
|
|
272
|
+
await ignore(NoSuchEntityException.name, () =>
|
|
273
|
+
client.send(
|
|
274
|
+
new DeleteRoleCommand({
|
|
275
|
+
RoleName: props.roleName,
|
|
276
|
+
})
|
|
277
|
+
)
|
|
278
|
+
);
|
|
261
279
|
} catch (error: any) {
|
|
280
|
+
// If we get any other error besides NoSuchEntityException, log it but don't fail
|
|
281
|
+
// This ensures the resource is still marked as destroyed
|
|
262
282
|
if (error.name !== NoSuchEntityException.name) {
|
|
263
|
-
|
|
283
|
+
console.error(`Error deleting role ${props.roleName}:`, error);
|
|
264
284
|
}
|
|
265
285
|
}
|
|
266
286
|
|
|
267
|
-
|
|
268
|
-
client.send(
|
|
269
|
-
new DeleteRoleCommand({
|
|
270
|
-
RoleName: props.roleName,
|
|
271
|
-
}),
|
|
272
|
-
),
|
|
273
|
-
);
|
|
287
|
+
// Always return destroyed state regardless of any errors
|
|
274
288
|
return this.destroy();
|
|
275
289
|
}
|
|
276
290
|
|
|
@@ -302,7 +316,7 @@ export const Role = Resource(
|
|
|
302
316
|
Value: this.id,
|
|
303
317
|
},
|
|
304
318
|
],
|
|
305
|
-
})
|
|
319
|
+
})
|
|
306
320
|
);
|
|
307
321
|
}
|
|
308
322
|
} catch (error: any) {
|
|
@@ -314,7 +328,7 @@ export const Role = Resource(
|
|
|
314
328
|
const existingRole = await client.send(
|
|
315
329
|
new GetRoleCommand({
|
|
316
330
|
RoleName: props.roleName,
|
|
317
|
-
})
|
|
331
|
+
})
|
|
318
332
|
);
|
|
319
333
|
const roleTags =
|
|
320
334
|
existingRole.Role?.Tags?.reduce(
|
|
@@ -322,7 +336,7 @@ export const Role = Resource(
|
|
|
322
336
|
acc[tag.Key!] = tag.Value!;
|
|
323
337
|
return acc;
|
|
324
338
|
},
|
|
325
|
-
{} as Record<string, string
|
|
339
|
+
{} as Record<string, string>
|
|
326
340
|
) || {};
|
|
327
341
|
|
|
328
342
|
if (
|
|
@@ -340,7 +354,7 @@ export const Role = Resource(
|
|
|
340
354
|
role = await client.send(
|
|
341
355
|
new GetRoleCommand({
|
|
342
356
|
RoleName: props.roleName,
|
|
343
|
-
})
|
|
357
|
+
})
|
|
344
358
|
);
|
|
345
359
|
|
|
346
360
|
// Update assume role policy if it changed
|
|
@@ -349,7 +363,7 @@ export const Role = Resource(
|
|
|
349
363
|
new UpdateAssumeRolePolicyCommand({
|
|
350
364
|
RoleName: props.roleName,
|
|
351
365
|
PolicyDocument: assumeRolePolicyDocument,
|
|
352
|
-
})
|
|
366
|
+
})
|
|
353
367
|
);
|
|
354
368
|
}
|
|
355
369
|
|
|
@@ -363,7 +377,7 @@ export const Role = Resource(
|
|
|
363
377
|
RoleName: props.roleName,
|
|
364
378
|
Description: props.description,
|
|
365
379
|
MaxSessionDuration: props.maxSessionDuration,
|
|
366
|
-
})
|
|
380
|
+
})
|
|
367
381
|
);
|
|
368
382
|
}
|
|
369
383
|
|
|
@@ -381,7 +395,7 @@ export const Role = Resource(
|
|
|
381
395
|
new TagRoleCommand({
|
|
382
396
|
RoleName: props.roleName,
|
|
383
397
|
Tags: tags,
|
|
384
|
-
})
|
|
398
|
+
})
|
|
385
399
|
);
|
|
386
400
|
|
|
387
401
|
// Handle policy changes
|
|
@@ -393,7 +407,7 @@ export const Role = Resource(
|
|
|
393
407
|
for (const oldPolicy of previousPolicies) {
|
|
394
408
|
if (
|
|
395
409
|
!currentPolicies.some(
|
|
396
|
-
(p: { policyName: string }) => p.policyName === oldPolicy.policyName
|
|
410
|
+
(p: { policyName: string }) => p.policyName === oldPolicy.policyName
|
|
397
411
|
)
|
|
398
412
|
) {
|
|
399
413
|
await ignore(NoSuchEntityException.name, () =>
|
|
@@ -401,8 +415,8 @@ export const Role = Resource(
|
|
|
401
415
|
new DeleteRolePolicyCommand({
|
|
402
416
|
RoleName: props.roleName,
|
|
403
417
|
PolicyName: oldPolicy.policyName,
|
|
404
|
-
})
|
|
405
|
-
)
|
|
418
|
+
})
|
|
419
|
+
)
|
|
406
420
|
);
|
|
407
421
|
}
|
|
408
422
|
}
|
|
@@ -410,7 +424,7 @@ export const Role = Resource(
|
|
|
410
424
|
// Update or create policies
|
|
411
425
|
for (const policy of currentPolicies) {
|
|
412
426
|
const oldPolicy = previousPolicies.find(
|
|
413
|
-
(p) => p.policyName === policy.policyName
|
|
427
|
+
(p) => p.policyName === policy.policyName
|
|
414
428
|
);
|
|
415
429
|
if (
|
|
416
430
|
!oldPolicy ||
|
|
@@ -422,7 +436,7 @@ export const Role = Resource(
|
|
|
422
436
|
RoleName: props.roleName,
|
|
423
437
|
PolicyName: policy.policyName,
|
|
424
438
|
PolicyDocument: JSON.stringify(policy.policyDocument),
|
|
425
|
-
})
|
|
439
|
+
})
|
|
426
440
|
);
|
|
427
441
|
}
|
|
428
442
|
}
|
|
@@ -432,7 +446,7 @@ export const Role = Resource(
|
|
|
432
446
|
const attachedPoliciesResponse = await client.send(
|
|
433
447
|
new ListAttachedRolePoliciesCommand({
|
|
434
448
|
RoleName: props.roleName,
|
|
435
|
-
})
|
|
449
|
+
})
|
|
436
450
|
);
|
|
437
451
|
|
|
438
452
|
const currentAttachedPolicies =
|
|
@@ -450,7 +464,7 @@ export const Role = Resource(
|
|
|
450
464
|
new DetachRolePolicyCommand({
|
|
451
465
|
RoleName: props.roleName,
|
|
452
466
|
PolicyArn: policyArn,
|
|
453
|
-
})
|
|
467
|
+
})
|
|
454
468
|
);
|
|
455
469
|
}
|
|
456
470
|
}
|
|
@@ -462,7 +476,7 @@ export const Role = Resource(
|
|
|
462
476
|
new AttachRolePolicyCommand({
|
|
463
477
|
RoleName: props.roleName,
|
|
464
478
|
PolicyArn: policyArn,
|
|
465
|
-
})
|
|
479
|
+
})
|
|
466
480
|
);
|
|
467
481
|
}
|
|
468
482
|
}
|
|
@@ -479,5 +493,5 @@ export const Role = Resource(
|
|
|
479
493
|
roleName: role.Role.RoleName ?? props.roleName,
|
|
480
494
|
createDate: role.Role.CreateDate!,
|
|
481
495
|
});
|
|
482
|
-
}
|
|
496
|
+
}
|
|
483
497
|
);
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { alchemy } from "../alchemy";
|
|
2
|
+
import type { Context } from "../context";
|
|
3
|
+
import { Resource } from "../resource";
|
|
4
|
+
import { Secret } from "../secret";
|
|
5
|
+
import { sha256 } from "../util/sha256";
|
|
6
|
+
import { createCloudflareApi } from "./api";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Permission group for a token policy
|
|
10
|
+
*/
|
|
11
|
+
export interface TokenPolicyPermissionGroup {
|
|
12
|
+
/**
|
|
13
|
+
* ID of the permission group
|
|
14
|
+
*/
|
|
15
|
+
id: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Optional metadata for the permission group
|
|
19
|
+
*/
|
|
20
|
+
meta?: Record<string, any>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Policy that defines what the token can access
|
|
25
|
+
*/
|
|
26
|
+
export interface TokenPolicy {
|
|
27
|
+
/**
|
|
28
|
+
* Effect of the policy
|
|
29
|
+
*/
|
|
30
|
+
effect: "allow" | "deny";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Permission groups to include in the policy
|
|
34
|
+
*/
|
|
35
|
+
permissionGroups: TokenPolicyPermissionGroup[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resources the policy applies to
|
|
39
|
+
*/
|
|
40
|
+
resources: Record<string, string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Condition for token usage (e.g., IP restrictions)
|
|
45
|
+
*/
|
|
46
|
+
export interface TokenCondition {
|
|
47
|
+
/**
|
|
48
|
+
* IP address conditions
|
|
49
|
+
*/
|
|
50
|
+
requestIp?: {
|
|
51
|
+
/**
|
|
52
|
+
* IP ranges to allow
|
|
53
|
+
*/
|
|
54
|
+
in?: string[];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* IP ranges to deny
|
|
58
|
+
*/
|
|
59
|
+
notIn?: string[];
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Properties for creating or updating an Account API Token
|
|
65
|
+
*/
|
|
66
|
+
export interface AccountApiTokenProps {
|
|
67
|
+
/**
|
|
68
|
+
* Name of the token
|
|
69
|
+
*/
|
|
70
|
+
name: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Policies that define what the token can access
|
|
74
|
+
*/
|
|
75
|
+
policies: TokenPolicy[];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Optional expiration date for the token (ISO format)
|
|
79
|
+
*/
|
|
80
|
+
expiresOn?: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Optional "not before" date (token is not valid before this date) (ISO format)
|
|
84
|
+
*/
|
|
85
|
+
notBefore?: string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Optional conditions for token use (like IP restrictions)
|
|
89
|
+
*/
|
|
90
|
+
condition?: TokenCondition;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Cloudflare API token format as returned by the API
|
|
95
|
+
*/
|
|
96
|
+
interface CloudflareApiToken {
|
|
97
|
+
id: string;
|
|
98
|
+
name: string;
|
|
99
|
+
status: string;
|
|
100
|
+
policies: {
|
|
101
|
+
effect: "allow" | "deny";
|
|
102
|
+
permission_groups: {
|
|
103
|
+
id: string;
|
|
104
|
+
meta: Record<string, any>;
|
|
105
|
+
}[];
|
|
106
|
+
resources: Record<string, string>;
|
|
107
|
+
}[];
|
|
108
|
+
expires_on?: string;
|
|
109
|
+
not_before?: string;
|
|
110
|
+
condition?: {
|
|
111
|
+
request_ip?: {
|
|
112
|
+
in?: string[];
|
|
113
|
+
not_in?: string[];
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
value?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Output returned after Account API Token creation/update
|
|
121
|
+
*/
|
|
122
|
+
export interface AccountApiToken
|
|
123
|
+
extends Resource<"cloudflare::AccountApiToken">,
|
|
124
|
+
AccountApiTokenProps {
|
|
125
|
+
/**
|
|
126
|
+
* The ID of the token
|
|
127
|
+
*
|
|
128
|
+
* Equiv. to ACCESS_KEY_ID
|
|
129
|
+
*/
|
|
130
|
+
id: string;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Status of the token
|
|
134
|
+
*/
|
|
135
|
+
status: string;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Actual token value (only available on creation)
|
|
139
|
+
* Stored as a Secret for security
|
|
140
|
+
*
|
|
141
|
+
* Equiv. to SECRET_ACCESS_KEY
|
|
142
|
+
*/
|
|
143
|
+
value?: Secret;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Access key ID for the token
|
|
147
|
+
*
|
|
148
|
+
* An alias of {@link id}
|
|
149
|
+
*/
|
|
150
|
+
accessKeyId: string;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Secret access key for the token
|
|
154
|
+
*
|
|
155
|
+
* The SHA-256 hash of the token {@link value}
|
|
156
|
+
*
|
|
157
|
+
* @see https://developers.cloudflare.com/r2/api/tokens/#get-s3-api-credentials-from-an-api-token
|
|
158
|
+
*/
|
|
159
|
+
secretAccessKey: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Creates a Cloudflare Account API Token with specified permissions.
|
|
164
|
+
*
|
|
165
|
+
* @see https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/create/
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* // First, fetch all permission groups
|
|
169
|
+
* const permissions = await PermissionGroups("cloudflare-permissions", {
|
|
170
|
+
* accountId: cfAccountId,
|
|
171
|
+
* });
|
|
172
|
+
*
|
|
173
|
+
* // Create a token with read-only permissions for specific zones
|
|
174
|
+
* const readOnlyToken = await AccountApiToken("readonly-token", {
|
|
175
|
+
* name: "Readonly Zone Token",
|
|
176
|
+
* policies: [
|
|
177
|
+
* {
|
|
178
|
+
* effect: "allow",
|
|
179
|
+
* permissionGroups: [
|
|
180
|
+
* { id: permissions["Zone Read"].id },
|
|
181
|
+
* { id: permissions["Analytics Read"].id }
|
|
182
|
+
* ],
|
|
183
|
+
* resources: {
|
|
184
|
+
* "com.cloudflare.api.account.zone.22b1de5f1c0e4b3ea97bb1e963b06a43": "*",
|
|
185
|
+
* "com.cloudflare.api.account.zone.eb78d65290b24279ba6f44721b3ea3c4": "*"
|
|
186
|
+
* }
|
|
187
|
+
* }
|
|
188
|
+
* ],
|
|
189
|
+
* expiresOn: "2024-12-31T23:59:59Z"
|
|
190
|
+
* });
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* // Create a token with time and IP restrictions
|
|
194
|
+
* const restrictedToken = await AccountApiToken("restricted-token", {
|
|
195
|
+
* name: "Restricted Access Token",
|
|
196
|
+
* policies: [
|
|
197
|
+
* {
|
|
198
|
+
* effect: "allow",
|
|
199
|
+
* permissionGroups: [
|
|
200
|
+
* { id: permissions["Worker Routes Edit"].id }
|
|
201
|
+
* ],
|
|
202
|
+
* resources: {
|
|
203
|
+
* "com.cloudflare.api.account.worker.route.*": "*"
|
|
204
|
+
* }
|
|
205
|
+
* }
|
|
206
|
+
* ],
|
|
207
|
+
* notBefore: "2023-01-01T00:00:00Z",
|
|
208
|
+
* expiresOn: "2023-12-31T23:59:59Z",
|
|
209
|
+
* condition: {
|
|
210
|
+
* requestIp: {
|
|
211
|
+
* in: ["192.168.1.0/24", "10.0.0.0/8"],
|
|
212
|
+
* notIn: ["192.168.1.100/32"]
|
|
213
|
+
* }
|
|
214
|
+
* }
|
|
215
|
+
* });
|
|
216
|
+
*/
|
|
217
|
+
export const AccountApiToken = Resource(
|
|
218
|
+
"cloudflare::AccountApiToken",
|
|
219
|
+
async function (
|
|
220
|
+
this: Context<AccountApiToken>,
|
|
221
|
+
id: string,
|
|
222
|
+
props: AccountApiTokenProps
|
|
223
|
+
): Promise<AccountApiToken> {
|
|
224
|
+
// Create Cloudflare API client with automatic account discovery
|
|
225
|
+
const api = await createCloudflareApi();
|
|
226
|
+
|
|
227
|
+
if (this.phase === "delete") {
|
|
228
|
+
// Delete token if we have an ID
|
|
229
|
+
if (this.output?.id) {
|
|
230
|
+
try {
|
|
231
|
+
const deleteResponse = await api.delete(
|
|
232
|
+
`/accounts/${api.accountId}/tokens/${this.output.id}`
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (!deleteResponse.ok && deleteResponse.status !== 404) {
|
|
236
|
+
const errorData: any = await deleteResponse.json().catch(() => ({
|
|
237
|
+
errors: [{ message: deleteResponse.statusText }],
|
|
238
|
+
}));
|
|
239
|
+
console.error(`Error deleting token '${props.name}':`, errorData);
|
|
240
|
+
}
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error(`Error deleting token '${props.name}':`, error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Return destroyed state
|
|
247
|
+
return this.destroy();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Transform our properties to API format
|
|
251
|
+
const apiPayload = {
|
|
252
|
+
name: props.name,
|
|
253
|
+
policies: props.policies.map((policy) => ({
|
|
254
|
+
effect: policy.effect,
|
|
255
|
+
permission_groups: policy.permissionGroups.map((pg) => ({
|
|
256
|
+
id: pg.id,
|
|
257
|
+
meta: pg.meta || {},
|
|
258
|
+
})),
|
|
259
|
+
resources: policy.resources,
|
|
260
|
+
})),
|
|
261
|
+
// Format dates for Cloudflare API (removing milliseconds)
|
|
262
|
+
...(props.expiresOn
|
|
263
|
+
? { expires_on: formatCloudflareDate(props.expiresOn) }
|
|
264
|
+
: {}),
|
|
265
|
+
...(props.notBefore
|
|
266
|
+
? { not_before: formatCloudflareDate(props.notBefore) }
|
|
267
|
+
: {}),
|
|
268
|
+
...(props.condition
|
|
269
|
+
? {
|
|
270
|
+
condition: {
|
|
271
|
+
request_ip: props.condition.requestIp
|
|
272
|
+
? {
|
|
273
|
+
in: props.condition.requestIp.in || [],
|
|
274
|
+
not_in: props.condition.requestIp.notIn || [],
|
|
275
|
+
}
|
|
276
|
+
: undefined,
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
: {}),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Formats a date string for Cloudflare API by removing milliseconds
|
|
284
|
+
* Converts from "2023-01-01T00:00:00.000Z" to "2023-01-01T00:00:00Z"
|
|
285
|
+
*/
|
|
286
|
+
function formatCloudflareDate(dateStr: string): string {
|
|
287
|
+
return dateStr.replace(/\.\d{3}Z$/, "Z");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let response;
|
|
291
|
+
let tokenValue;
|
|
292
|
+
|
|
293
|
+
if (this.phase === "update" && this.output?.id) {
|
|
294
|
+
// Update existing token
|
|
295
|
+
response = await api.put(
|
|
296
|
+
`/accounts/${api.accountId}/tokens/${this.output.id}`,
|
|
297
|
+
apiPayload
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
// Create new token
|
|
301
|
+
response = await api.post(
|
|
302
|
+
`/accounts/${api.accountId}/tokens`,
|
|
303
|
+
apiPayload
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
const errorData: any = await response.json().catch(() => ({
|
|
309
|
+
errors: [{ message: response.statusText }],
|
|
310
|
+
}));
|
|
311
|
+
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Error ${this.phase === "update" ? "updating" : "creating"} token '${props.name}': ${
|
|
314
|
+
errorData.errors?.[0]?.message || response.statusText
|
|
315
|
+
}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const result: { result: CloudflareApiToken } = await response.json();
|
|
320
|
+
const tokenData = result.result;
|
|
321
|
+
|
|
322
|
+
if (tokenData.value) {
|
|
323
|
+
tokenValue = alchemy.secret(tokenData.value);
|
|
324
|
+
} else {
|
|
325
|
+
if (!this.output?.value) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`Token '${props.name}' was created but we have no record of its value. Try deleting and recreating the token.`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
tokenValue = this.output?.value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Transform API response to our format
|
|
334
|
+
return this({
|
|
335
|
+
id: tokenData.id,
|
|
336
|
+
name: tokenData.name,
|
|
337
|
+
status: tokenData.status,
|
|
338
|
+
policies: tokenData.policies.map((policy) => ({
|
|
339
|
+
effect: policy.effect,
|
|
340
|
+
permissionGroups: policy.permission_groups.map((pg) => ({
|
|
341
|
+
id: pg.id,
|
|
342
|
+
meta: pg.meta,
|
|
343
|
+
})),
|
|
344
|
+
resources: policy.resources,
|
|
345
|
+
})),
|
|
346
|
+
...(tokenData.expires_on ? { expiresOn: tokenData.expires_on } : {}),
|
|
347
|
+
...(tokenData.not_before ? { notBefore: tokenData.not_before } : {}),
|
|
348
|
+
...(tokenData.condition
|
|
349
|
+
? {
|
|
350
|
+
condition: {
|
|
351
|
+
requestIp: tokenData.condition.request_ip
|
|
352
|
+
? {
|
|
353
|
+
in: tokenData.condition.request_ip.in || [],
|
|
354
|
+
notIn: tokenData.condition.request_ip.not_in || [],
|
|
355
|
+
}
|
|
356
|
+
: undefined,
|
|
357
|
+
},
|
|
358
|
+
}
|
|
359
|
+
: {}),
|
|
360
|
+
value: tokenValue,
|
|
361
|
+
accessKeyId: tokenData.id,
|
|
362
|
+
secretAccessKey: sha256(tokenValue.unencrypted),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
);
|
|
File without changes
|