alchemy 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/lib/ai/document.d.ts +12 -1
- package/lib/ai/document.js +10 -1
- package/lib/ai/index.d.ts +0 -2
- package/lib/ai/index.js +0 -2
- package/lib/alchemy.d.ts +5 -0
- package/lib/alchemy.js +44 -1
- package/lib/apply.js +17 -10
- package/lib/aws/account-id.d.ts +4 -1
- package/lib/aws/account-id.js +1 -1
- package/lib/aws/credentials.d.ts +5 -0
- package/lib/aws/credentials.js +0 -0
- package/lib/aws/oidc/oidc-provider.js +2 -2
- package/lib/aws/role.js +35 -23
- package/lib/cloudflare/account-api-token.d.ts +168 -0
- package/lib/cloudflare/account-api-token.js +179 -0
- package/lib/cloudflare/account-id.d.ts +1 -0
- package/lib/cloudflare/account-id.js +0 -0
- package/lib/cloudflare/api.d.ts +38 -1
- package/lib/cloudflare/api.js +55 -4
- package/lib/cloudflare/assets.d.ts +74 -0
- package/lib/cloudflare/assets.js +73 -0
- package/lib/cloudflare/bindings.d.ts +6 -1
- package/lib/cloudflare/bindings.js +6 -0
- package/lib/cloudflare/bucket.d.ts +118 -3
- package/lib/cloudflare/bucket.js +269 -89
- package/lib/cloudflare/custom-domain.d.ts +75 -0
- package/lib/cloudflare/custom-domain.js +154 -0
- package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
- package/lib/cloudflare/index.d.ts +6 -1
- package/lib/cloudflare/index.js +6 -1
- package/lib/cloudflare/permission-groups.d.ts +78 -0
- package/lib/cloudflare/permission-groups.js +48 -0
- package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
- package/lib/cloudflare/r2-rest-state-store.js +55 -24
- package/lib/cloudflare/static-site.d.ts +17 -18
- package/lib/cloudflare/static-site.js +19 -10
- package/lib/cloudflare/worker.d.ts +14 -0
- package/lib/cloudflare/worker.js +150 -3
- package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
- package/lib/{util/encrypt.js → encrypt.js} +1 -1
- package/lib/fs/file-system-state-store.js +1 -1
- package/lib/fs/file.d.ts +18 -0
- package/lib/fs/file.js +6 -0
- package/lib/internal/{providers.d.ts → docs/providers.d.ts} +10 -4
- package/lib/internal/docs/providers.js +196 -0
- package/lib/secret.d.ts +3 -0
- package/lib/secret.js +13 -0
- package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
- package/lib/{util/serde.js → serde.js} +12 -4
- package/lib/test/bun.js +3 -2
- package/lib/util/sha256.d.ts +1 -0
- package/lib/util/sha256.js +4 -0
- package/lib/web/vitepress/config.d.ts +2 -1
- package/lib/web/vitepress/config.js +1 -0
- package/lib/web/vitepress/index.d.ts +1 -0
- package/lib/web/vitepress/index.js +1 -0
- package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
- package/lib/web/vitepress/process-front-matter-files.js +68 -0
- package/lib/web/vitepress/vitepress.js +3 -2
- package/package.json +8 -4
- package/src/ai/document.ts +26 -2
- package/src/ai/index.ts +0 -2
- package/src/alchemy.ts +56 -2
- package/src/apply.ts +21 -20
- package/src/aws/account-id.ts +6 -2
- package/src/aws/credentials.ts +6 -0
- package/src/aws/oidc/oidc-provider.ts +17 -17
- package/src/aws/role.ts +68 -54
- package/src/cloudflare/account-api-token.ts +365 -0
- package/src/cloudflare/account-id.ts +0 -0
- package/src/cloudflare/api.ts +88 -17
- package/src/cloudflare/assets.ts +155 -0
- package/src/cloudflare/bindings.ts +11 -2
- package/src/cloudflare/bucket.ts +493 -133
- package/src/cloudflare/custom-domain.ts +318 -0
- package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
- package/src/cloudflare/index.ts +6 -1
- package/src/cloudflare/permission-groups.ts +137 -0
- package/src/cloudflare/r2-rest-state-store.ts +87 -42
- package/src/cloudflare/static-site.ts +20 -32
- package/src/cloudflare/worker.ts +253 -5
- package/src/dns/import-dns.ts +4 -4
- package/src/{util/encrypt.ts → encrypt.ts} +7 -10
- package/src/fs/file-system-state-store.ts +5 -5
- package/src/fs/file.ts +29 -0
- package/src/internal/docs/providers.ts +281 -0
- package/src/secret.ts +16 -0
- package/src/{util/serde.ts → serde.ts} +16 -10
- package/src/test/bun.ts +8 -7
- package/src/util/sha256.ts +5 -0
- package/src/web/vitepress/config.ts +3 -1
- package/src/web/vitepress/index.ts +1 -0
- package/src/web/vitepress/process-front-matter-files.ts +98 -0
- package/src/web/vitepress/vitepress.ts +3 -2
- package/lib/ai/approve.d.ts +0 -99
- package/lib/ai/approve.js +0 -76
- package/lib/ai/review.d.ts +0 -122
- package/lib/ai/review.js +0 -101
- package/lib/internal/getting-started.d.ts +0 -21
- package/lib/internal/getting-started.js +0 -87
- package/lib/internal/index.d.ts +0 -3
- package/lib/internal/index.js +0 -3
- package/lib/internal/providers.js +0 -172
- package/lib/internal/tutorial.d.ts +0 -104
- package/lib/internal/tutorial.js +0 -251
- package/src/ai/approve.ts +0 -163
- package/src/ai/review.ts +0 -213
- package/src/internal/getting-started.ts +0 -115
- package/src/internal/index.ts +0 -3
- package/src/internal/providers.ts +0 -241
- package/src/internal/tutorial.ts +0 -392
- /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -9,8 +9,10 @@ Resources are simple memoized async functions that can run in any JavaScript run
|
|
|
9
9
|
```ts
|
|
10
10
|
import alchemy from "alchemy";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// initialize the app (with default state $USER)
|
|
13
|
+
const app = alchemy("cloudflare-worker");
|
|
13
14
|
|
|
15
|
+
// create a Cloudflare Worker
|
|
14
16
|
export const worker = await Worker("worker", {
|
|
15
17
|
name: "my-worker",
|
|
16
18
|
entrypoint: "./src/index.ts",
|
|
@@ -18,10 +20,13 @@ export const worker = await Worker("worker", {
|
|
|
18
20
|
COUNTER: counter,
|
|
19
21
|
STORAGE: storage, // Bind the R2 bucket to the worker
|
|
20
22
|
AUTH_STORE: authStore,
|
|
21
|
-
GITHUB_CLIENT_ID: secret(process.env.GITHUB_CLIENT_ID),
|
|
22
|
-
GITHUB_CLIENT_SECRET: secret(process.env.GITHUB_CLIENT_SECRET),
|
|
23
|
+
GITHUB_CLIENT_ID: alchemy.secret(process.env.GITHUB_CLIENT_ID),
|
|
24
|
+
GITHUB_CLIENT_SECRET: alchemy.secret(process.env.GITHUB_CLIENT_SECRET),
|
|
23
25
|
},
|
|
24
26
|
});
|
|
27
|
+
|
|
28
|
+
// finalize the alchemy app (triggering deletion of orphaned resources)
|
|
29
|
+
await app.finalize();
|
|
25
30
|
```
|
|
26
31
|
|
|
27
32
|
# Features
|
package/lib/ai/document.d.ts
CHANGED
|
@@ -10,8 +10,10 @@ import { type ModelConfig } from "./client";
|
|
|
10
10
|
export interface DocumentProps {
|
|
11
11
|
/**
|
|
12
12
|
* Title of the document
|
|
13
|
+
*
|
|
14
|
+
* @default id
|
|
13
15
|
*/
|
|
14
|
-
title
|
|
16
|
+
title?: string;
|
|
15
17
|
/**
|
|
16
18
|
* Optional path to the markdown document
|
|
17
19
|
* If provided, document will be written to this path
|
|
@@ -72,11 +74,20 @@ export interface DocumentProps {
|
|
|
72
74
|
* @default 10000
|
|
73
75
|
*/
|
|
74
76
|
maxTokens?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Freeze the document after creation (do not re-generate on updates)
|
|
79
|
+
* @default false
|
|
80
|
+
*/
|
|
81
|
+
freeze?: boolean;
|
|
75
82
|
}
|
|
76
83
|
/**
|
|
77
84
|
* A markdown document that can be created, updated, and deleted
|
|
78
85
|
*/
|
|
79
86
|
export interface Document extends DocumentProps, Resource<"docs::Document"> {
|
|
87
|
+
/**
|
|
88
|
+
* The title of the document
|
|
89
|
+
*/
|
|
90
|
+
title: string;
|
|
80
91
|
/**
|
|
81
92
|
* Content of the document
|
|
82
93
|
*/
|
package/lib/ai/document.js
CHANGED
|
@@ -99,6 +99,14 @@ export const Document = Resource("docs::Document", async function (id, props) {
|
|
|
99
99
|
if (this.phase === "delete") {
|
|
100
100
|
return this.destroy();
|
|
101
101
|
}
|
|
102
|
+
if (this.phase === "update" && props.freeze) {
|
|
103
|
+
if (props.path) {
|
|
104
|
+
const filePath = props.path;
|
|
105
|
+
const fileId = `${id}-file`;
|
|
106
|
+
await StaticTextFile(fileId, filePath, this.output.content);
|
|
107
|
+
}
|
|
108
|
+
return this(this.output);
|
|
109
|
+
}
|
|
102
110
|
// Use provided system prompt or default
|
|
103
111
|
const system = props.system || DEFAULT_MD_SYSTEM_PROMPT;
|
|
104
112
|
// Generate initial content with rate limit retry
|
|
@@ -137,7 +145,7 @@ export const Document = Resource("docs::Document", async function (id, props) {
|
|
|
137
145
|
});
|
|
138
146
|
const retryResult = extractMarkdownContent(retryText);
|
|
139
147
|
if (retryResult.error) {
|
|
140
|
-
throw new Error(`Failed to generate valid markdown content: ${retryResult.error}`);
|
|
148
|
+
throw new Error(`Failed to generate valid markdown content: ${retryResult.error}\n${retryText}`);
|
|
141
149
|
}
|
|
142
150
|
content = retryResult.content;
|
|
143
151
|
}
|
|
@@ -151,6 +159,7 @@ export const Document = Resource("docs::Document", async function (id, props) {
|
|
|
151
159
|
],
|
|
152
160
|
createdAt: this.output?.createdAt || Date.now(),
|
|
153
161
|
updatedAt: Date.now(),
|
|
162
|
+
title: props.title || id,
|
|
154
163
|
};
|
|
155
164
|
// Write file if path is provided
|
|
156
165
|
if (props.path) {
|
package/lib/ai/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export * from "./approve";
|
|
2
1
|
export * from "./ark";
|
|
3
2
|
export * from "./astro-file";
|
|
4
3
|
export * from "./css-file";
|
|
@@ -6,7 +5,6 @@ export * from "./data";
|
|
|
6
5
|
export * from "./document";
|
|
7
6
|
export * from "./html-file";
|
|
8
7
|
export * from "./json-file";
|
|
9
|
-
export * from "./review";
|
|
10
8
|
export * from "./typescript-file";
|
|
11
9
|
export * from "./vue-file";
|
|
12
10
|
export * from "./yaml-file";
|
package/lib/ai/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export * from "./approve";
|
|
2
1
|
export * from "./ark";
|
|
3
2
|
export * from "./astro-file";
|
|
4
3
|
export * from "./css-file";
|
|
@@ -6,7 +5,6 @@ export * from "./data";
|
|
|
6
5
|
export * from "./document";
|
|
7
6
|
export * from "./html-file";
|
|
8
7
|
export * from "./json-file";
|
|
9
|
-
export * from "./review";
|
|
10
8
|
export * from "./typescript-file";
|
|
11
9
|
export * from "./vue-file";
|
|
12
10
|
export * from "./yaml-file";
|
package/lib/alchemy.d.ts
CHANGED
|
@@ -31,6 +31,10 @@ export interface Alchemy {
|
|
|
31
31
|
scope: typeof scope;
|
|
32
32
|
run: typeof run;
|
|
33
33
|
destroy: typeof destroy;
|
|
34
|
+
/**
|
|
35
|
+
* Get an environment variable and error if it's not set.
|
|
36
|
+
*/
|
|
37
|
+
env: typeof env;
|
|
34
38
|
/**
|
|
35
39
|
* Creates an encrypted secret that can be safely stored in state files.
|
|
36
40
|
* Requires a password to be set either globally in the application options
|
|
@@ -146,4 +150,5 @@ declare function run<T>(...args: [id: string, fn: (this: Scope, scope: Scope) =>
|
|
|
146
150
|
options: AlchemyOptions,
|
|
147
151
|
fn: (this: Scope, scope: Scope) => Promise<T>
|
|
148
152
|
]): Promise<T>;
|
|
153
|
+
export declare function env<T = string>(name: string, value?: T | undefined, error?: string): Promise<T>;
|
|
149
154
|
export {};
|
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
|
-
|
|
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;
|
|
@@ -64,6 +70,22 @@ function _alchemy(...args) {
|
|
|
64
70
|
else if (Array.isArray(value)) {
|
|
65
71
|
return (await Promise.all(value.map(async (value, i) => `${i}. ${await resolve(value)}`))).join("\n");
|
|
66
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
|
+
}
|
|
67
89
|
else {
|
|
68
90
|
// TODO: support other types
|
|
69
91
|
console.log(value);
|
|
@@ -102,6 +124,7 @@ _alchemy.destroy = destroy;
|
|
|
102
124
|
_alchemy.run = run;
|
|
103
125
|
_alchemy.scope = scope;
|
|
104
126
|
_alchemy.secret = secret;
|
|
127
|
+
_alchemy.env = env;
|
|
105
128
|
/**
|
|
106
129
|
* Enter a new scope synchronously.
|
|
107
130
|
*
|
|
@@ -165,3 +188,23 @@ async function run(...args) {
|
|
|
165
188
|
await scope.finalize();
|
|
166
189
|
}
|
|
167
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 {
|
|
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 =
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
})
|
|
47
|
-
if (oldProps === newProps &&
|
|
43
|
+
});
|
|
44
|
+
if (JSON.stringify(oldProps) === JSON.stringify(newProps) &&
|
|
45
|
+
alwaysUpdate !== true) {
|
|
48
46
|
if (!quiet) {
|
|
49
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
|
}
|
package/lib/aws/account-id.d.ts
CHANGED
package/lib/aws/account-id.js
CHANGED
|
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 {
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
161
|
+
console.error(`Error deleting role ${props.roleName}:`, error);
|
|
148
162
|
}
|
|
149
163
|
}
|
|
150
|
-
|
|
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>);
|