alchemy 0.3.0 → 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.
Files changed (154) hide show
  1. package/README.md +8 -3
  2. package/lib/ai/astro-file.js +7 -24
  3. package/lib/ai/client.d.ts +7 -0
  4. package/lib/ai/client.js +45 -0
  5. package/lib/ai/css-file.js +7 -24
  6. package/lib/ai/data.d.ts +26 -21
  7. package/lib/ai/data.js +33 -22
  8. package/lib/ai/document.d.ts +76 -5
  9. package/lib/ai/document.js +112 -56
  10. package/lib/ai/html-file.js +7 -24
  11. package/lib/ai/json-file.js +11 -32
  12. package/lib/ai/typescript-file.d.ts +1 -2
  13. package/lib/ai/typescript-file.js +8 -39
  14. package/lib/ai/vue-file.js +7 -24
  15. package/lib/ai/yaml-file.js +12 -48
  16. package/lib/alchemy.d.ts +5 -0
  17. package/lib/alchemy.js +61 -3
  18. package/lib/apply.js +18 -11
  19. package/lib/aws/account-id.d.ts +4 -1
  20. package/lib/aws/account-id.js +1 -1
  21. package/lib/aws/credentials.d.ts +5 -0
  22. package/lib/aws/credentials.js +0 -0
  23. package/lib/aws/oidc/oidc-provider.js +2 -2
  24. package/lib/aws/role.js +35 -23
  25. package/lib/cloudflare/account-api-token.d.ts +168 -0
  26. package/lib/cloudflare/account-api-token.js +179 -0
  27. package/lib/cloudflare/account-id.d.ts +1 -0
  28. package/lib/cloudflare/account-id.js +0 -0
  29. package/lib/cloudflare/api.d.ts +38 -1
  30. package/lib/cloudflare/api.js +55 -4
  31. package/lib/cloudflare/bucket.d.ts +118 -3
  32. package/lib/cloudflare/bucket.js +269 -89
  33. package/lib/cloudflare/custom-domain.d.ts +75 -0
  34. package/lib/cloudflare/custom-domain.js +154 -0
  35. package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
  36. package/lib/cloudflare/generate-asset-manifest.js +1 -1
  37. package/lib/cloudflare/index.d.ts +5 -1
  38. package/lib/cloudflare/index.js +5 -1
  39. package/lib/cloudflare/permission-groups.d.ts +78 -0
  40. package/lib/cloudflare/permission-groups.js +48 -0
  41. package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
  42. package/lib/cloudflare/r2-rest-state-store.js +3 -2
  43. package/lib/cloudflare/static-site-router.js +16 -7
  44. package/lib/cloudflare/static-site.d.ts +17 -18
  45. package/lib/cloudflare/static-site.js +19 -7
  46. package/lib/cloudflare/upload-asset-manifest.js +4 -2
  47. package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
  48. package/lib/{util/encrypt.js → encrypt.js} +1 -1
  49. package/lib/fs/copy-file.d.ts +54 -0
  50. package/lib/fs/copy-file.js +63 -0
  51. package/lib/fs/file-system-state-store.js +1 -1
  52. package/lib/fs/file.d.ts +31 -0
  53. package/lib/fs/file.js +30 -5
  54. package/lib/fs/index.d.ts +6 -0
  55. package/lib/fs/index.js +6 -0
  56. package/lib/fs/static-astro-file.d.ts +34 -0
  57. package/lib/fs/static-astro-file.js +39 -0
  58. package/lib/fs/static-css-file.d.ts +24 -0
  59. package/lib/fs/static-css-file.js +29 -0
  60. package/lib/fs/static-html-file.d.ts +31 -0
  61. package/lib/fs/static-html-file.js +36 -0
  62. package/lib/fs/static-json-file.d.ts +2 -2
  63. package/lib/fs/static-json-file.js +3 -2
  64. package/lib/fs/static-text-file.d.ts +1 -1
  65. package/lib/fs/static-text-file.js +3 -2
  66. package/lib/fs/static-typescript-file.d.ts +2 -2
  67. package/lib/fs/static-typescript-file.js +3 -2
  68. package/lib/fs/static-vue-file.d.ts +28 -0
  69. package/lib/fs/static-vue-file.js +33 -0
  70. package/lib/fs/static-yaml-file.d.ts +11 -8
  71. package/lib/fs/static-yaml-file.js +3 -2
  72. package/lib/internal/{providers.d.ts → docs/providers.d.ts} +14 -4
  73. package/lib/internal/docs/providers.js +196 -0
  74. package/lib/secret.d.ts +3 -0
  75. package/lib/secret.js +13 -0
  76. package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
  77. package/lib/{util/serde.js → serde.js} +12 -4
  78. package/lib/test/bun.js +3 -2
  79. package/lib/util/sha256.d.ts +1 -0
  80. package/lib/util/sha256.js +4 -0
  81. package/lib/web/vitepress/config.d.ts +11 -0
  82. package/lib/web/vitepress/config.js +32 -0
  83. package/lib/web/vitepress/home-page.d.ts +5 -3
  84. package/lib/web/vitepress/home-page.js +48 -35
  85. package/lib/web/vitepress/index.d.ts +2 -0
  86. package/lib/web/vitepress/index.js +2 -0
  87. package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
  88. package/lib/web/vitepress/process-front-matter-files.js +68 -0
  89. package/lib/web/vitepress/vitepress.d.ts +5 -4
  90. package/lib/web/vitepress/vitepress.js +20 -36
  91. package/package.json +8 -4
  92. package/src/ai/astro-file.ts +11 -30
  93. package/src/ai/client.ts +59 -0
  94. package/src/ai/css-file.ts +11 -30
  95. package/src/ai/data.ts +50 -25
  96. package/src/ai/document.ts +171 -66
  97. package/src/ai/html-file.ts +11 -30
  98. package/src/ai/json-file.ts +13 -37
  99. package/src/ai/typescript-file.ts +14 -48
  100. package/src/ai/vue-file.ts +11 -30
  101. package/src/ai/yaml-file.ts +13 -53
  102. package/src/alchemy.ts +74 -9
  103. package/src/apply.ts +27 -26
  104. package/src/aws/account-id.ts +6 -2
  105. package/src/aws/credentials.ts +6 -0
  106. package/src/aws/oidc/oidc-provider.ts +17 -17
  107. package/src/aws/role.ts +68 -54
  108. package/src/cloudflare/account-api-token.ts +365 -0
  109. package/src/cloudflare/account-id.ts +0 -0
  110. package/src/cloudflare/api.ts +88 -17
  111. package/src/cloudflare/bucket.ts +493 -133
  112. package/src/cloudflare/custom-domain.ts +318 -0
  113. package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
  114. package/src/cloudflare/generate-asset-manifest.ts +4 -4
  115. package/src/cloudflare/index.ts +5 -1
  116. package/src/cloudflare/permission-groups.ts +137 -0
  117. package/src/cloudflare/r2-rest-state-store.ts +14 -13
  118. package/src/cloudflare/static-site-router.ts +18 -8
  119. package/src/cloudflare/static-site.ts +27 -35
  120. package/src/cloudflare/upload-asset-manifest.ts +6 -4
  121. package/src/cloudflare/worker.ts +44 -43
  122. package/src/dns/import-dns.ts +4 -4
  123. package/src/{util/encrypt.ts → encrypt.ts} +7 -10
  124. package/src/fs/copy-file.ts +115 -0
  125. package/src/fs/file-system-state-store.ts +5 -5
  126. package/src/fs/file.ts +64 -9
  127. package/src/fs/index.ts +6 -0
  128. package/src/fs/static-astro-file.ts +45 -0
  129. package/src/fs/static-css-file.ts +35 -0
  130. package/src/fs/static-html-file.ts +42 -0
  131. package/src/fs/static-json-file.ts +4 -3
  132. package/src/fs/static-text-file.ts +5 -2
  133. package/src/fs/static-typescript-file.ts +4 -3
  134. package/src/fs/static-vue-file.ts +39 -0
  135. package/src/fs/static-yaml-file.ts +13 -9
  136. package/src/internal/docs/providers.ts +281 -0
  137. package/src/secret.ts +16 -0
  138. package/src/{util/serde.ts → serde.ts} +16 -10
  139. package/src/test/bun.ts +8 -7
  140. package/src/util/sha256.ts +5 -0
  141. package/src/web/vitepress/config.ts +50 -0
  142. package/src/web/vitepress/home-page.ts +54 -42
  143. package/src/web/vitepress/index.ts +2 -0
  144. package/src/web/vitepress/process-front-matter-files.ts +98 -0
  145. package/src/web/vitepress/vitepress.ts +34 -50
  146. package/lib/internal/getting-started.d.ts +0 -10
  147. package/lib/internal/getting-started.js +0 -94
  148. package/lib/internal/project.d.ts +0 -13
  149. package/lib/internal/project.js +0 -101
  150. package/lib/internal/providers.js +0 -148
  151. package/src/internal/getting-started.ts +0 -107
  152. package/src/internal/project.ts +0 -119
  153. package/src/internal/providers.ts +0 -209
  154. /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
package/lib/alchemy.js CHANGED
@@ -13,11 +13,17 @@ export const alchemy = _alchemy;
13
13
  function _alchemy(...args) {
14
14
  if (typeof args[0] === "string") {
15
15
  const [appName, options] = args;
16
- return scope(undefined, {
16
+ const root = scope(undefined, {
17
17
  ...options,
18
18
  appName,
19
19
  stage: options?.stage,
20
20
  });
21
+ if (options?.phase === "destroy") {
22
+ return destroy(root).finally(() => {
23
+ process.exit(0);
24
+ });
25
+ }
26
+ return root;
21
27
  }
22
28
  else {
23
29
  const [template, ...values] = args;
@@ -33,6 +39,17 @@ function _alchemy(...args) {
33
39
  if (typeof value === "string") {
34
40
  return indent + value;
35
41
  }
42
+ else if (value === null) {
43
+ return "null";
44
+ }
45
+ else if (value === undefined) {
46
+ return "undefined";
47
+ }
48
+ else if (typeof value === "number" ||
49
+ typeof value === "boolean" ||
50
+ typeof value === "bigint") {
51
+ return value.toString();
52
+ }
36
53
  else if (value instanceof Promise) {
37
54
  return resolve(await value);
38
55
  }
@@ -53,9 +70,26 @@ function _alchemy(...args) {
53
70
  else if (Array.isArray(value)) {
54
71
  return (await Promise.all(value.map(async (value, i) => `${i}. ${await resolve(value)}`))).join("\n");
55
72
  }
73
+ else if (typeof value === "object" &&
74
+ typeof value.path === "string") {
75
+ if (typeof value.content === "string") {
76
+ appendices[value.path] = value.content;
77
+ return `[${path.basename(value.path)}](${value.path})`;
78
+ }
79
+ else {
80
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
81
+ return `[${path.basename(value.path)}](${value.path})`;
82
+ }
83
+ }
84
+ else if (typeof value === "object") {
85
+ return (await Promise.all(Object.entries(value).map(async ([key, value]) => {
86
+ return `* ${key}: ${await resolve(value)}`;
87
+ }))).join("\n");
88
+ }
56
89
  else {
57
90
  // TODO: support other types
58
- throw new Error(`Unsupported value type: ${JSON.stringify(value)}`);
91
+ console.log(value);
92
+ throw new Error(`Unsupported value type: ${value}`);
59
93
  }
60
94
  }));
61
95
  // Construct the string template by joining template parts with interpolated values
@@ -90,6 +124,7 @@ _alchemy.destroy = destroy;
90
124
  _alchemy.run = run;
91
125
  _alchemy.scope = scope;
92
126
  _alchemy.secret = secret;
127
+ _alchemy.env = env;
93
128
  /**
94
129
  * Enter a new scope synchronously.
95
130
  *
@@ -104,7 +139,10 @@ _alchemy.secret = secret;
104
139
  * apiKey: alchemy.secret(process.env.API_KEY)
105
140
  * });
106
141
  */
107
- function scope(id, options) {
142
+ function scope(id, options
143
+ // TODO: maybe we want to allow using _ = await alchemy.scope(import.meta)
144
+ // | [meta: ImportMeta]
145
+ ) {
108
146
  const scope = new Scope({
109
147
  ...options,
110
148
  appName: options?.appName,
@@ -150,3 +188,23 @@ async function run(...args) {
150
188
  await scope.finalize();
151
189
  }
152
190
  }
191
+ export async function env(name, value, error) {
192
+ if (value !== undefined) {
193
+ return value;
194
+ }
195
+ else if (typeof process !== undefined) {
196
+ // we are in a node environment
197
+ return process.env[name];
198
+ }
199
+ else {
200
+ // we are in a browser environment
201
+ try {
202
+ const { env } = await import("cloudflare:workers");
203
+ if (name in env) {
204
+ return env[name];
205
+ }
206
+ }
207
+ catch (error) { }
208
+ }
209
+ throw new Error(error ?? `Environment variable ${name} is not set`);
210
+ }
package/lib/apply.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { alchemy } from "./alchemy";
2
2
  import { context } from "./context";
3
3
  import { PROVIDERS, } from "./resource";
4
- import { Secret } from "./secret";
5
- import { serialize } from "./util/serde";
4
+ import { serialize } from "./serde";
6
5
  export async function apply(resource, props, options) {
7
6
  const scope = resource.Scope;
8
7
  try {
@@ -36,20 +35,27 @@ export async function apply(resource, props, options) {
36
35
  const alwaysUpdate = options?.alwaysUpdate ?? provider.options?.alwaysUpdate ?? false;
37
36
  // Skip update if inputs haven't changed and resource is in a stable state
38
37
  if (state.status === "created" || state.status === "updated") {
39
- const oldProps = JSON.stringify(state.props, (_, value) => value instanceof Secret
40
- ? {
41
- "@secret": value.unencrypted,
42
- }
43
- : value, 2);
44
- const newProps = JSON.stringify(await serialize(scope, props, {
38
+ const oldProps = await serialize(scope, state.props, {
39
+ encrypt: false,
40
+ });
41
+ const newProps = await serialize(scope, props, {
45
42
  encrypt: false,
46
- }), null, 2);
47
- if (oldProps === newProps && alwaysUpdate !== true) {
43
+ });
44
+ if (JSON.stringify(oldProps) === JSON.stringify(newProps) &&
45
+ alwaysUpdate !== true) {
48
46
  if (!quiet) {
49
- console.log(`Skip: "${resource.FQN}" (no changes)`);
47
+ // console.log(`Skip: "${resource.FQN}" (no changes)`);
50
48
  }
51
49
  return state.output;
52
50
  }
51
+ const diff = await import("diff");
52
+ const yaml = await import("yaml");
53
+ const old = yaml.stringify(oldProps);
54
+ const news = yaml.stringify(newProps);
55
+ const patch = diff.createPatch(resource.ID, old, news);
56
+ // console.log(patch);
57
+ // if (resource.ID === "what-is-alchemy") {
58
+ // }
53
59
  }
54
60
  const phase = state.status === "creating" ? "create" : "update";
55
61
  state.status = phase === "create" ? "creating" : "updating";
@@ -98,6 +104,7 @@ export async function apply(resource, props, options) {
98
104
  return output;
99
105
  }
100
106
  catch (error) {
107
+ console.error(new Error().stack);
101
108
  scope.fail();
102
109
  throw error;
103
110
  }
@@ -1,4 +1,7 @@
1
+ export type AccountId = string & {
2
+ readonly __brand: "AccountId";
3
+ };
1
4
  /**
2
5
  * Helper to get the current AWS account ID
3
6
  */
4
- export declare function getAccountId(): Promise<string>;
7
+ export declare function AccountId(): Promise<AccountId>;
@@ -3,7 +3,7 @@ const sts = new STSClient({});
3
3
  /**
4
4
  * Helper to get the current AWS account ID
5
5
  */
6
- export async function getAccountId() {
6
+ export async function AccountId() {
7
7
  const identity = await sts.send(new GetCallerIdentityCommand({}));
8
8
  return identity.Account;
9
9
  }
@@ -0,0 +1,5 @@
1
+ import type { Secret } from "../secret";
2
+ export interface AwsCredentials {
3
+ accessKeyId: Secret;
4
+ secretAccessKey: Secret;
5
+ }
File without changes
@@ -1,6 +1,6 @@
1
1
  import { CreateOpenIDConnectProviderCommand, DeleteOpenIDConnectProviderCommand, GetOpenIDConnectProviderCommand, GetRoleCommand, IAMClient, UpdateAssumeRolePolicyCommand, } from "@aws-sdk/client-iam";
2
2
  import { Resource } from "../../resource";
3
- import { getAccountId } from "../account-id";
3
+ import { AccountId } from "../account-id";
4
4
  /**
5
5
  * AWS OIDC Provider Resource for GitHub Actions
6
6
  *
@@ -91,7 +91,7 @@ export const OIDCProvider = Resource("aws::OIDCProvider", async function (id, pr
91
91
  catch (error) {
92
92
  if (error.name === "EntityAlreadyExistsException") {
93
93
  // Provider exists, use its ARN
94
- providerArn = `arn:aws:iam::${await getAccountId()}:oidc-provider/${url}`;
94
+ providerArn = `arn:aws:iam::${await AccountId()}:oidc-provider/${url}`;
95
95
  }
96
96
  else {
97
97
  throw error;
package/lib/aws/role.js CHANGED
@@ -118,38 +118,50 @@ import { ignore } from "../util/ignore";
118
118
  export const Role = Resource("iam::Role", async function (id, props) {
119
119
  const client = new IAMClient({});
120
120
  if (this.phase === "delete") {
121
- // Delete any inline policies first
122
- if (props.policies) {
123
- for (const policy of props.policies) {
124
- await ignore(NoSuchEntityException.name, () => client.send(new DeleteRolePolicyCommand({
125
- RoleName: props.roleName,
126
- PolicyName: policy.policyName,
127
- })));
128
- }
129
- }
130
- // We need to detach managed policies before deleting the role
131
- // First, get all attached policies
132
121
  try {
133
- const attachedPoliciesResponse = await client.send(new ListAttachedRolePoliciesCommand({
134
- RoleName: props.roleName,
135
- }));
136
- // Detach all managed policies
137
- const attachedPolicies = attachedPoliciesResponse.AttachedPolicies || [];
138
- for (const policy of attachedPolicies) {
139
- await client.send(new DetachRolePolicyCommand({
122
+ // Delete any inline policies first
123
+ if (props.policies) {
124
+ for (const policy of props.policies) {
125
+ await ignore(NoSuchEntityException.name, () => client.send(new DeleteRolePolicyCommand({
126
+ RoleName: props.roleName,
127
+ PolicyName: policy.policyName,
128
+ })));
129
+ }
130
+ }
131
+ // We need to detach managed policies before deleting the role
132
+ // First, get all attached policies
133
+ try {
134
+ const attachedPoliciesResponse = await client.send(new ListAttachedRolePoliciesCommand({
140
135
  RoleName: props.roleName,
141
- PolicyArn: policy.PolicyArn,
142
136
  }));
137
+ // Detach all managed policies
138
+ const attachedPolicies = attachedPoliciesResponse.AttachedPolicies || [];
139
+ for (const policy of attachedPolicies) {
140
+ await ignore(NoSuchEntityException.name, () => client.send(new DetachRolePolicyCommand({
141
+ RoleName: props.roleName,
142
+ PolicyArn: policy.PolicyArn,
143
+ })));
144
+ }
143
145
  }
146
+ catch (error) {
147
+ if (error.name !== NoSuchEntityException.name) {
148
+ throw error;
149
+ }
150
+ // Role doesn't exist, no need to continue with detaching policies
151
+ }
152
+ // Try to delete the role, ignoring if it doesn't exist
153
+ await ignore(NoSuchEntityException.name, () => client.send(new DeleteRoleCommand({
154
+ RoleName: props.roleName,
155
+ })));
144
156
  }
145
157
  catch (error) {
158
+ // If we get any other error besides NoSuchEntityException, log it but don't fail
159
+ // This ensures the resource is still marked as destroyed
146
160
  if (error.name !== NoSuchEntityException.name) {
147
- throw error;
161
+ console.error(`Error deleting role ${props.roleName}:`, error);
148
162
  }
149
163
  }
150
- await ignore(NoSuchEntityException.name, () => client.send(new DeleteRoleCommand({
151
- RoleName: props.roleName,
152
- })));
164
+ // Always return destroyed state regardless of any errors
153
165
  return this.destroy();
154
166
  }
155
167
  const assumeRolePolicyDocument = JSON.stringify(props.assumeRolePolicy);
@@ -0,0 +1,168 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ import { Secret } from "../secret";
4
+ /**
5
+ * Permission group for a token policy
6
+ */
7
+ export interface TokenPolicyPermissionGroup {
8
+ /**
9
+ * ID of the permission group
10
+ */
11
+ id: string;
12
+ /**
13
+ * Optional metadata for the permission group
14
+ */
15
+ meta?: Record<string, any>;
16
+ }
17
+ /**
18
+ * Policy that defines what the token can access
19
+ */
20
+ export interface TokenPolicy {
21
+ /**
22
+ * Effect of the policy
23
+ */
24
+ effect: "allow" | "deny";
25
+ /**
26
+ * Permission groups to include in the policy
27
+ */
28
+ permissionGroups: TokenPolicyPermissionGroup[];
29
+ /**
30
+ * Resources the policy applies to
31
+ */
32
+ resources: Record<string, string>;
33
+ }
34
+ /**
35
+ * Condition for token usage (e.g., IP restrictions)
36
+ */
37
+ export interface TokenCondition {
38
+ /**
39
+ * IP address conditions
40
+ */
41
+ requestIp?: {
42
+ /**
43
+ * IP ranges to allow
44
+ */
45
+ in?: string[];
46
+ /**
47
+ * IP ranges to deny
48
+ */
49
+ notIn?: string[];
50
+ };
51
+ }
52
+ /**
53
+ * Properties for creating or updating an Account API Token
54
+ */
55
+ export interface AccountApiTokenProps {
56
+ /**
57
+ * Name of the token
58
+ */
59
+ name: string;
60
+ /**
61
+ * Policies that define what the token can access
62
+ */
63
+ policies: TokenPolicy[];
64
+ /**
65
+ * Optional expiration date for the token (ISO format)
66
+ */
67
+ expiresOn?: string;
68
+ /**
69
+ * Optional "not before" date (token is not valid before this date) (ISO format)
70
+ */
71
+ notBefore?: string;
72
+ /**
73
+ * Optional conditions for token use (like IP restrictions)
74
+ */
75
+ condition?: TokenCondition;
76
+ }
77
+ /**
78
+ * Output returned after Account API Token creation/update
79
+ */
80
+ export interface AccountApiToken extends Resource<"cloudflare::AccountApiToken">, AccountApiTokenProps {
81
+ /**
82
+ * The ID of the token
83
+ *
84
+ * Equiv. to ACCESS_KEY_ID
85
+ */
86
+ id: string;
87
+ /**
88
+ * Status of the token
89
+ */
90
+ status: string;
91
+ /**
92
+ * Actual token value (only available on creation)
93
+ * Stored as a Secret for security
94
+ *
95
+ * Equiv. to SECRET_ACCESS_KEY
96
+ */
97
+ value?: Secret;
98
+ /**
99
+ * Access key ID for the token
100
+ *
101
+ * An alias of {@link id}
102
+ */
103
+ accessKeyId: string;
104
+ /**
105
+ * Secret access key for the token
106
+ *
107
+ * The SHA-256 hash of the token {@link value}
108
+ *
109
+ * @see https://developers.cloudflare.com/r2/api/tokens/#get-s3-api-credentials-from-an-api-token
110
+ */
111
+ secretAccessKey: string;
112
+ }
113
+ /**
114
+ * Creates a Cloudflare Account API Token with specified permissions.
115
+ *
116
+ * @see https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/create/
117
+ *
118
+ * @example
119
+ * // First, fetch all permission groups
120
+ * const permissions = await PermissionGroups("cloudflare-permissions", {
121
+ * accountId: cfAccountId,
122
+ * });
123
+ *
124
+ * // Create a token with read-only permissions for specific zones
125
+ * const readOnlyToken = await AccountApiToken("readonly-token", {
126
+ * name: "Readonly Zone Token",
127
+ * policies: [
128
+ * {
129
+ * effect: "allow",
130
+ * permissionGroups: [
131
+ * { id: permissions["Zone Read"].id },
132
+ * { id: permissions["Analytics Read"].id }
133
+ * ],
134
+ * resources: {
135
+ * "com.cloudflare.api.account.zone.22b1de5f1c0e4b3ea97bb1e963b06a43": "*",
136
+ * "com.cloudflare.api.account.zone.eb78d65290b24279ba6f44721b3ea3c4": "*"
137
+ * }
138
+ * }
139
+ * ],
140
+ * expiresOn: "2024-12-31T23:59:59Z"
141
+ * });
142
+ *
143
+ * @example
144
+ * // Create a token with time and IP restrictions
145
+ * const restrictedToken = await AccountApiToken("restricted-token", {
146
+ * name: "Restricted Access Token",
147
+ * policies: [
148
+ * {
149
+ * effect: "allow",
150
+ * permissionGroups: [
151
+ * { id: permissions["Worker Routes Edit"].id }
152
+ * ],
153
+ * resources: {
154
+ * "com.cloudflare.api.account.worker.route.*": "*"
155
+ * }
156
+ * }
157
+ * ],
158
+ * notBefore: "2023-01-01T00:00:00Z",
159
+ * expiresOn: "2023-12-31T23:59:59Z",
160
+ * condition: {
161
+ * requestIp: {
162
+ * in: ["192.168.1.0/24", "10.0.0.0/8"],
163
+ * notIn: ["192.168.1.100/32"]
164
+ * }
165
+ * }
166
+ * });
167
+ */
168
+ export declare const AccountApiToken: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<AccountApiToken>, id: string, props: AccountApiTokenProps) => Promise<AccountApiToken>);
@@ -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