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/lib/test/bun.js
CHANGED
|
@@ -32,12 +32,13 @@ alchemy.test = test;
|
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
34
|
export function test(meta, defaultOptions) {
|
|
35
|
+
let isFailed = false;
|
|
35
36
|
defaultOptions = defaultOptions ?? {};
|
|
36
37
|
if (defaultOptions.stateStore === undefined &&
|
|
37
38
|
// process.env.CI &&
|
|
38
39
|
process.env.ALCHEMY_STATE_STORE === "cloudflare") {
|
|
39
40
|
defaultOptions.stateStore = (scope) => new R2RestStateStore(scope, {
|
|
40
|
-
apiKey: process.env.CLOUDFLARE_API_KEY,
|
|
41
|
+
apiKey: alchemy.secret(process.env.CLOUDFLARE_API_KEY),
|
|
41
42
|
email: process.env.CLOUDFLARE_EMAIL,
|
|
42
43
|
bucketName: process.env.CLOUDFLARE_BUCKET_NAME,
|
|
43
44
|
});
|
|
@@ -70,7 +71,7 @@ export function test(meta, defaultOptions) {
|
|
|
70
71
|
};
|
|
71
72
|
// Clean up test scope after all tests complete
|
|
72
73
|
afterAll(async () => {
|
|
73
|
-
if (defaultOptions?.destroy !== false) {
|
|
74
|
+
if (defaultOptions?.destroy !== false && !isFailed) {
|
|
74
75
|
await alchemy.destroy(test.scope);
|
|
75
76
|
}
|
|
76
77
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sha256(value: string): string;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { DefaultTheme, ThemeOptions } from "vitepress";
|
|
1
|
+
import type { DefaultTheme, HeadConfig, ThemeOptions } from "vitepress";
|
|
2
2
|
export type VitePressConfigProps = {
|
|
3
3
|
cwd: string;
|
|
4
4
|
title?: string;
|
|
5
5
|
description?: string;
|
|
6
|
+
head?: HeadConfig[];
|
|
6
7
|
theme?: ThemeOptions;
|
|
7
8
|
themeConfig: DefaultTheme.Config;
|
|
8
9
|
};
|
|
@@ -9,6 +9,7 @@ import { defineConfig } from "vitepress";
|
|
|
9
9
|
export default defineConfig({
|
|
10
10
|
title: ${JSON.stringify(props.title || "Alchemy")},
|
|
11
11
|
description: ${JSON.stringify(props.description || "Alchemy Docs")},
|
|
12
|
+
head: ${JSON.stringify(props.head || [])},
|
|
12
13
|
markdown: {
|
|
13
14
|
// @ts-ignore
|
|
14
15
|
codeTransformers: [transformerTwoslash()],
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process markdown files with frontmatter to generate navigation items
|
|
3
|
+
* @param directoryPath The directory containing markdown files
|
|
4
|
+
* @param linkPrefix The prefix for navigation links
|
|
5
|
+
* @returns Sorted array of navigation items with text and link properties
|
|
6
|
+
*/
|
|
7
|
+
export declare function processFrontmatterFiles(directoryPath: string, linkPrefix: string): Promise<{
|
|
8
|
+
text: string;
|
|
9
|
+
link: string;
|
|
10
|
+
items?: {
|
|
11
|
+
text: string;
|
|
12
|
+
link: string;
|
|
13
|
+
items?: {
|
|
14
|
+
text: string;
|
|
15
|
+
link: string;
|
|
16
|
+
}[];
|
|
17
|
+
}[];
|
|
18
|
+
}[]>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Process markdown files with frontmatter to generate navigation items
|
|
5
|
+
* @param directoryPath The directory containing markdown files
|
|
6
|
+
* @param linkPrefix The prefix for navigation links
|
|
7
|
+
* @returns Sorted array of navigation items with text and link properties
|
|
8
|
+
*/
|
|
9
|
+
export async function processFrontmatterFiles(directoryPath, linkPrefix) {
|
|
10
|
+
const entries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
11
|
+
const processedEntries = await Promise.all(entries
|
|
12
|
+
.filter((entry) => !entry.name.endsWith("index.md"))
|
|
13
|
+
.map(async (entry) => {
|
|
14
|
+
const fullPath = path.join(directoryPath, entry.name);
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
// Process subdirectory recursively
|
|
17
|
+
const items = await processFrontmatterFiles(fullPath, `${linkPrefix}/${entry.name}`);
|
|
18
|
+
// Create section for subdirectory
|
|
19
|
+
return {
|
|
20
|
+
text: entry.name.charAt(0).toUpperCase() + entry.name.slice(1),
|
|
21
|
+
collapsed: false,
|
|
22
|
+
items,
|
|
23
|
+
order: 10000, // Default order for directories
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// Process markdown file
|
|
27
|
+
if (!entry.name.endsWith(".md")) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
31
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
32
|
+
let order = 10000;
|
|
33
|
+
let title;
|
|
34
|
+
if (frontmatterMatch) {
|
|
35
|
+
const frontmatter = frontmatterMatch[1];
|
|
36
|
+
const orderMatch = frontmatter.match(/order:\s*(\d+)/);
|
|
37
|
+
if (orderMatch) {
|
|
38
|
+
order = parseInt(orderMatch[1]);
|
|
39
|
+
}
|
|
40
|
+
const titleMatch = frontmatter.match(/title:\s*(.+)/);
|
|
41
|
+
if (titleMatch) {
|
|
42
|
+
title = titleMatch[1].trim();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// If no title in frontmatter, try to get first heading
|
|
46
|
+
if (!title) {
|
|
47
|
+
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
48
|
+
if (headingMatch) {
|
|
49
|
+
title = headingMatch[1].trim();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Fall back to filename if no title found
|
|
53
|
+
if (!title) {
|
|
54
|
+
const name = entry.name.replace(".md", "");
|
|
55
|
+
title = name.charAt(0).toUpperCase() + name.slice(1);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
text: title,
|
|
59
|
+
link: `${linkPrefix}/${entry.name}`,
|
|
60
|
+
order,
|
|
61
|
+
};
|
|
62
|
+
}));
|
|
63
|
+
// Filter out null entries and sort by order
|
|
64
|
+
return processedEntries
|
|
65
|
+
.filter((entry) => entry !== null)
|
|
66
|
+
.sort((a, b) => a.order - b.order)
|
|
67
|
+
.map(({ text, link, items }) => items ? { text, items, collapsed: true } : { text, link });
|
|
68
|
+
}
|
|
@@ -14,8 +14,9 @@ export const VitepressProject = Resource("project::VitepressProject", {
|
|
|
14
14
|
try {
|
|
15
15
|
if (props.delete !== false) {
|
|
16
16
|
if (await fs.exists(dir)) {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
18
|
+
// // Delete the entire project directory
|
|
19
|
+
// await execAsync(`rm -rf ${dir}`);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "./lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -38,16 +38,18 @@
|
|
|
38
38
|
"@aws-sdk/client-sesv2": "^3.0.0",
|
|
39
39
|
"@aws-sdk/client-sqs": "^3.0.0",
|
|
40
40
|
"@aws-sdk/client-sts": "^3.0.0",
|
|
41
|
-
"arktype": "^2.0.0",
|
|
42
|
-
"prettier": "^3.0.0",
|
|
43
41
|
"@octokit/rest": "^21.1.1",
|
|
44
42
|
"ai": "^4.0.0",
|
|
43
|
+
"arktype": "^2.0.0",
|
|
44
|
+
"aws4fetch": "^1.0.20",
|
|
45
|
+
"cloudflare": "^4.0.0",
|
|
45
46
|
"diff": "^7.0.0",
|
|
46
47
|
"esbuild": "^0.25.1",
|
|
47
48
|
"glob": "^10.0.0",
|
|
48
49
|
"hono": "^4.0.0",
|
|
49
|
-
"libsodium-wrappers": "^0.7.15",
|
|
50
50
|
"jszip": "^3.0.0",
|
|
51
|
+
"libsodium-wrappers": "^0.7.15",
|
|
52
|
+
"prettier": "^3.0.0",
|
|
51
53
|
"stripe": "^17.0.0",
|
|
52
54
|
"turndown": "^7.0.0",
|
|
53
55
|
"yaml": "^2.0.0",
|
|
@@ -56,6 +58,7 @@
|
|
|
56
58
|
"devDependencies": {
|
|
57
59
|
"@ai-sdk/anthropic": "^1.1.6",
|
|
58
60
|
"@ai-sdk/openai": "^1.1.9",
|
|
61
|
+
"@aws-sdk/client-s3": "3.726.1",
|
|
59
62
|
"@biomejs/biome": "^1.9.4",
|
|
60
63
|
"@cloudflare/workers-types": "^4.20250303.0",
|
|
61
64
|
"@octokit/rest": "^21.1.1",
|
|
@@ -67,6 +70,7 @@
|
|
|
67
70
|
"ai": "^4.1.16",
|
|
68
71
|
"arktype": "^2.1.16",
|
|
69
72
|
"change-case": "^5.4.4",
|
|
73
|
+
"cloudflare": "^4.2.0",
|
|
70
74
|
"libsodium-wrappers": "^0.7.15",
|
|
71
75
|
"openpgp": "^6.1.0",
|
|
72
76
|
"prettier": "^3.5.3",
|
package/src/ai/document.ts
CHANGED
|
@@ -11,8 +11,10 @@ import { createModel, withRateLimitRetry, type ModelConfig } from "./client";
|
|
|
11
11
|
export interface DocumentProps {
|
|
12
12
|
/**
|
|
13
13
|
* Title of the document
|
|
14
|
+
*
|
|
15
|
+
* @default id
|
|
14
16
|
*/
|
|
15
|
-
title
|
|
17
|
+
title?: string;
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Optional path to the markdown document
|
|
@@ -82,12 +84,23 @@ export interface DocumentProps {
|
|
|
82
84
|
* @default 10000
|
|
83
85
|
*/
|
|
84
86
|
maxTokens?: number;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Freeze the document after creation (do not re-generate on updates)
|
|
90
|
+
* @default false
|
|
91
|
+
*/
|
|
92
|
+
freeze?: boolean;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
/**
|
|
88
96
|
* A markdown document that can be created, updated, and deleted
|
|
89
97
|
*/
|
|
90
98
|
export interface Document extends DocumentProps, Resource<"docs::Document"> {
|
|
99
|
+
/**
|
|
100
|
+
* The title of the document
|
|
101
|
+
*/
|
|
102
|
+
title: string;
|
|
103
|
+
|
|
91
104
|
/**
|
|
92
105
|
* Content of the document
|
|
93
106
|
*/
|
|
@@ -221,6 +234,16 @@ export const Document = Resource(
|
|
|
221
234
|
return this.destroy();
|
|
222
235
|
}
|
|
223
236
|
|
|
237
|
+
if (this.phase === "update" && props.freeze) {
|
|
238
|
+
if (props.path) {
|
|
239
|
+
const filePath = props.path;
|
|
240
|
+
const fileId = `${id}-file`;
|
|
241
|
+
|
|
242
|
+
await StaticTextFile(fileId, filePath, this.output!.content);
|
|
243
|
+
}
|
|
244
|
+
return this(this.output);
|
|
245
|
+
}
|
|
246
|
+
|
|
224
247
|
// Use provided system prompt or default
|
|
225
248
|
const system = props.system || DEFAULT_MD_SYSTEM_PROMPT;
|
|
226
249
|
|
|
@@ -266,7 +289,7 @@ export const Document = Resource(
|
|
|
266
289
|
|
|
267
290
|
if (retryResult.error) {
|
|
268
291
|
throw new Error(
|
|
269
|
-
`Failed to generate valid markdown content: ${retryResult.error}`
|
|
292
|
+
`Failed to generate valid markdown content: ${retryResult.error}\n${retryText}`
|
|
270
293
|
);
|
|
271
294
|
}
|
|
272
295
|
|
|
@@ -283,6 +306,7 @@ export const Document = Resource(
|
|
|
283
306
|
],
|
|
284
307
|
createdAt: this.output?.createdAt || Date.now(),
|
|
285
308
|
updatedAt: Date.now(),
|
|
309
|
+
title: props.title || id,
|
|
286
310
|
};
|
|
287
311
|
|
|
288
312
|
// Write file if path is provided
|
package/src/ai/index.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/src/alchemy.ts
CHANGED
|
@@ -40,6 +40,12 @@ export interface Alchemy {
|
|
|
40
40
|
scope: typeof scope;
|
|
41
41
|
run: typeof run;
|
|
42
42
|
destroy: typeof destroy;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get an environment variable and error if it's not set.
|
|
46
|
+
*/
|
|
47
|
+
env: typeof env;
|
|
48
|
+
|
|
43
49
|
/**
|
|
44
50
|
* Creates an encrypted secret that can be safely stored in state files.
|
|
45
51
|
* Requires a password to be set either globally in the application options
|
|
@@ -88,12 +94,18 @@ function _alchemy(
|
|
|
88
94
|
| [appName: string, options?: Omit<AlchemyOptions, "appName">]
|
|
89
95
|
): any {
|
|
90
96
|
if (typeof args[0] === "string") {
|
|
91
|
-
const [appName, options] = args;
|
|
92
|
-
|
|
97
|
+
const [appName, options] = args as [string, AlchemyOptions?];
|
|
98
|
+
const root = scope(undefined, {
|
|
93
99
|
...options,
|
|
94
100
|
appName,
|
|
95
101
|
stage: options?.stage,
|
|
96
102
|
});
|
|
103
|
+
if (options?.phase === "destroy") {
|
|
104
|
+
return destroy(root).finally(() => {
|
|
105
|
+
process.exit(0);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return root;
|
|
97
109
|
} else {
|
|
98
110
|
const [template, ...values] = args;
|
|
99
111
|
const [, secondLine] = template[0].split("\n");
|
|
@@ -141,6 +153,25 @@ function _alchemy(
|
|
|
141
153
|
value.map(async (value, i) => `${i}. ${await resolve(value)}`)
|
|
142
154
|
)
|
|
143
155
|
).join("\n");
|
|
156
|
+
} else if (
|
|
157
|
+
typeof value === "object" &&
|
|
158
|
+
typeof value.path === "string"
|
|
159
|
+
) {
|
|
160
|
+
if (typeof value.content === "string") {
|
|
161
|
+
appendices[value.path] = value.content;
|
|
162
|
+
return `[${path.basename(value.path)}](${value.path})`;
|
|
163
|
+
} else {
|
|
164
|
+
appendices[value.path] = await fs.readFile(value.path, "utf-8");
|
|
165
|
+
return `[${path.basename(value.path)}](${value.path})`;
|
|
166
|
+
}
|
|
167
|
+
} else if (typeof value === "object") {
|
|
168
|
+
return (
|
|
169
|
+
await Promise.all(
|
|
170
|
+
Object.entries(value).map(async ([key, value]) => {
|
|
171
|
+
return `* ${key}: ${await resolve(value)}`;
|
|
172
|
+
})
|
|
173
|
+
)
|
|
174
|
+
).join("\n");
|
|
144
175
|
} else {
|
|
145
176
|
// TODO: support other types
|
|
146
177
|
console.log(value);
|
|
@@ -189,6 +220,7 @@ _alchemy.destroy = destroy;
|
|
|
189
220
|
_alchemy.run = run;
|
|
190
221
|
_alchemy.scope = scope;
|
|
191
222
|
_alchemy.secret = secret;
|
|
223
|
+
_alchemy.env = env;
|
|
192
224
|
|
|
193
225
|
export interface AlchemyOptions {
|
|
194
226
|
/**
|
|
@@ -311,3 +343,25 @@ async function run<T>(
|
|
|
311
343
|
await scope.finalize();
|
|
312
344
|
}
|
|
313
345
|
}
|
|
346
|
+
|
|
347
|
+
export async function env<T = string>(
|
|
348
|
+
name: string,
|
|
349
|
+
value?: T | undefined,
|
|
350
|
+
error?: string
|
|
351
|
+
): Promise<T> {
|
|
352
|
+
if (value !== undefined) {
|
|
353
|
+
return value;
|
|
354
|
+
} else if (typeof process !== undefined) {
|
|
355
|
+
// we are in a node environment
|
|
356
|
+
return process.env[name]! as T;
|
|
357
|
+
} else {
|
|
358
|
+
// we are in a browser environment
|
|
359
|
+
try {
|
|
360
|
+
const { env } = await import("cloudflare:workers");
|
|
361
|
+
if (name in env) {
|
|
362
|
+
return env[name as keyof typeof env];
|
|
363
|
+
}
|
|
364
|
+
} catch (error) {}
|
|
365
|
+
}
|
|
366
|
+
throw new Error(error ?? `Environment variable ${name} is not set`);
|
|
367
|
+
}
|
package/src/apply.ts
CHANGED
|
@@ -7,9 +7,8 @@ import {
|
|
|
7
7
|
type Resource,
|
|
8
8
|
type ResourceProps,
|
|
9
9
|
} from "./resource";
|
|
10
|
-
import {
|
|
10
|
+
import { serialize } from "./serde";
|
|
11
11
|
import type { State } from "./state";
|
|
12
|
-
import { serialize } from "./util/serde";
|
|
13
12
|
|
|
14
13
|
export interface ApplyOptions {
|
|
15
14
|
quiet?: boolean;
|
|
@@ -56,29 +55,30 @@ export async function apply<Out extends Resource>(
|
|
|
56
55
|
|
|
57
56
|
// Skip update if inputs haven't changed and resource is in a stable state
|
|
58
57
|
if (state.status === "created" || state.status === "updated") {
|
|
59
|
-
const oldProps =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
const newProps = JSON.stringify(
|
|
70
|
-
await serialize(scope, props, {
|
|
71
|
-
encrypt: false,
|
|
72
|
-
}),
|
|
73
|
-
null,
|
|
74
|
-
2
|
|
75
|
-
);
|
|
76
|
-
if (oldProps === newProps && alwaysUpdate !== true) {
|
|
58
|
+
const oldProps = await serialize(scope, state.props, {
|
|
59
|
+
encrypt: false,
|
|
60
|
+
});
|
|
61
|
+
const newProps = await serialize(scope, props, {
|
|
62
|
+
encrypt: false,
|
|
63
|
+
});
|
|
64
|
+
if (
|
|
65
|
+
JSON.stringify(oldProps) === JSON.stringify(newProps) &&
|
|
66
|
+
alwaysUpdate !== true
|
|
67
|
+
) {
|
|
77
68
|
if (!quiet) {
|
|
78
69
|
// console.log(`Skip: "${resource.FQN}" (no changes)`);
|
|
79
70
|
}
|
|
80
71
|
return state.output as Awaited<Out>;
|
|
81
72
|
}
|
|
73
|
+
|
|
74
|
+
const diff = await import("diff");
|
|
75
|
+
const yaml = await import("yaml");
|
|
76
|
+
const old = yaml.stringify(oldProps);
|
|
77
|
+
const news = yaml.stringify(newProps);
|
|
78
|
+
const patch = diff.createPatch(resource.ID, old, news);
|
|
79
|
+
// console.log(patch);
|
|
80
|
+
// if (resource.ID === "what-is-alchemy") {
|
|
81
|
+
// }
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
const phase = state.status === "creating" ? "create" : "update";
|
|
@@ -141,6 +141,7 @@ export async function apply<Out extends Resource>(
|
|
|
141
141
|
// }
|
|
142
142
|
return output as any;
|
|
143
143
|
} catch (error) {
|
|
144
|
+
console.error(new Error().stack);
|
|
144
145
|
scope.fail();
|
|
145
146
|
throw error;
|
|
146
147
|
}
|
package/src/aws/account-id.ts
CHANGED
|
@@ -2,10 +2,14 @@ import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
|
|
|
2
2
|
|
|
3
3
|
const sts = new STSClient({});
|
|
4
4
|
|
|
5
|
+
export type AccountId = string & {
|
|
6
|
+
readonly __brand: "AccountId";
|
|
7
|
+
};
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Helper to get the current AWS account ID
|
|
7
11
|
*/
|
|
8
|
-
export async function
|
|
12
|
+
export async function AccountId(): Promise<AccountId> {
|
|
9
13
|
const identity = await sts.send(new GetCallerIdentityCommand({}));
|
|
10
|
-
return identity.Account
|
|
14
|
+
return identity.Account! as AccountId;
|
|
11
15
|
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@aws-sdk/client-iam";
|
|
10
10
|
import type { Context } from "../../context";
|
|
11
11
|
import { Resource } from "../../resource";
|
|
12
|
-
import {
|
|
12
|
+
import { AccountId } from "../account-id";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Properties for configuring an AWS OIDC provider for GitHub Actions
|
|
@@ -121,7 +121,7 @@ export const OIDCProvider = Resource(
|
|
|
121
121
|
async function (
|
|
122
122
|
this: Context<OIDCProvider>,
|
|
123
123
|
id: string,
|
|
124
|
-
props: OIDCProviderProps
|
|
124
|
+
props: OIDCProviderProps
|
|
125
125
|
) {
|
|
126
126
|
// Initialize AWS SDK client
|
|
127
127
|
const client = new IAMClient({
|
|
@@ -135,24 +135,24 @@ export const OIDCProvider = Resource(
|
|
|
135
135
|
const getRole = await client.send(
|
|
136
136
|
new GetRoleCommand({
|
|
137
137
|
RoleName: props.roleArn.split("/").pop(),
|
|
138
|
-
})
|
|
138
|
+
})
|
|
139
139
|
);
|
|
140
140
|
|
|
141
141
|
if (getRole.Role?.AssumeRolePolicyDocument) {
|
|
142
142
|
const policy = JSON.parse(
|
|
143
|
-
decodeURIComponent(getRole.Role.AssumeRolePolicyDocument)
|
|
143
|
+
decodeURIComponent(getRole.Role.AssumeRolePolicyDocument)
|
|
144
144
|
);
|
|
145
145
|
|
|
146
146
|
// Remove our specific statement while preserving others
|
|
147
147
|
policy.Statement = policy.Statement.filter(
|
|
148
|
-
(stmt: any) => stmt.Sid !== TRUST_POLICY_SID
|
|
148
|
+
(stmt: any) => stmt.Sid !== TRUST_POLICY_SID
|
|
149
149
|
);
|
|
150
150
|
|
|
151
151
|
await client.send(
|
|
152
152
|
new UpdateAssumeRolePolicyCommand({
|
|
153
153
|
RoleName: props.roleArn.split("/").pop(),
|
|
154
154
|
PolicyDocument: JSON.stringify(policy),
|
|
155
|
-
})
|
|
155
|
+
})
|
|
156
156
|
);
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -160,7 +160,7 @@ export const OIDCProvider = Resource(
|
|
|
160
160
|
const provider = await client.send(
|
|
161
161
|
new GetOpenIDConnectProviderCommand({
|
|
162
162
|
OpenIDConnectProviderArn: this.output.providerArn,
|
|
163
|
-
})
|
|
163
|
+
})
|
|
164
164
|
);
|
|
165
165
|
|
|
166
166
|
// Only delete the provider if it exists and has no other tags
|
|
@@ -168,7 +168,7 @@ export const OIDCProvider = Resource(
|
|
|
168
168
|
await client.send(
|
|
169
169
|
new DeleteOpenIDConnectProviderCommand({
|
|
170
170
|
OpenIDConnectProviderArn: this.output.providerArn,
|
|
171
|
-
})
|
|
171
|
+
})
|
|
172
172
|
);
|
|
173
173
|
}
|
|
174
174
|
} catch (error) {
|
|
@@ -197,13 +197,13 @@ export const OIDCProvider = Resource(
|
|
|
197
197
|
Value: "alchemy",
|
|
198
198
|
},
|
|
199
199
|
] as Tag[],
|
|
200
|
-
})
|
|
200
|
+
})
|
|
201
201
|
);
|
|
202
202
|
providerArn = createProvider.OpenIDConnectProviderArn!;
|
|
203
203
|
} catch (error: any) {
|
|
204
204
|
if (error.name === "EntityAlreadyExistsException") {
|
|
205
205
|
// Provider exists, use its ARN
|
|
206
|
-
providerArn = `arn:aws:iam::${await
|
|
206
|
+
providerArn = `arn:aws:iam::${await AccountId()}:oidc-provider/${url}`;
|
|
207
207
|
} else {
|
|
208
208
|
throw error;
|
|
209
209
|
}
|
|
@@ -213,13 +213,13 @@ export const OIDCProvider = Resource(
|
|
|
213
213
|
const getRole = await client.send(
|
|
214
214
|
new GetRoleCommand({
|
|
215
215
|
RoleName: props.roleArn.split("/").pop(),
|
|
216
|
-
})
|
|
216
|
+
})
|
|
217
217
|
);
|
|
218
218
|
|
|
219
219
|
let policy: any;
|
|
220
220
|
if (getRole.Role?.AssumeRolePolicyDocument) {
|
|
221
221
|
policy = JSON.parse(
|
|
222
|
-
decodeURIComponent(getRole.Role.AssumeRolePolicyDocument)
|
|
222
|
+
decodeURIComponent(getRole.Role.AssumeRolePolicyDocument)
|
|
223
223
|
);
|
|
224
224
|
} else {
|
|
225
225
|
policy = {
|
|
@@ -240,7 +240,7 @@ export const OIDCProvider = Resource(
|
|
|
240
240
|
conditions.StringEquals["token.actions.githubusercontent.com:sub"] =
|
|
241
241
|
props.branches.map(
|
|
242
242
|
(branch) =>
|
|
243
|
-
`repo:${props.owner}/${props.repository}:ref:refs/heads/${branch}
|
|
243
|
+
`repo:${props.owner}/${props.repository}:ref:refs/heads/${branch}`
|
|
244
244
|
);
|
|
245
245
|
}
|
|
246
246
|
|
|
@@ -248,7 +248,7 @@ export const OIDCProvider = Resource(
|
|
|
248
248
|
conditions.StringEquals["token.actions.githubusercontent.com:sub"] =
|
|
249
249
|
props.environments.map(
|
|
250
250
|
(env) =>
|
|
251
|
-
`repo:${props.owner}/${props.repository}:environment:${env}
|
|
251
|
+
`repo:${props.owner}/${props.repository}:environment:${env}`
|
|
252
252
|
);
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -266,7 +266,7 @@ export const OIDCProvider = Resource(
|
|
|
266
266
|
// Remove any existing statement with our SID and add the new one
|
|
267
267
|
policy.Statement = [
|
|
268
268
|
...policy.Statement.filter(
|
|
269
|
-
(stmt: any) => stmt.Sid !== TRUST_POLICY_SID
|
|
269
|
+
(stmt: any) => stmt.Sid !== TRUST_POLICY_SID
|
|
270
270
|
),
|
|
271
271
|
ourStatement,
|
|
272
272
|
];
|
|
@@ -276,7 +276,7 @@ export const OIDCProvider = Resource(
|
|
|
276
276
|
new UpdateAssumeRolePolicyCommand({
|
|
277
277
|
RoleName: props.roleArn.split("/").pop(),
|
|
278
278
|
PolicyDocument: JSON.stringify(policy),
|
|
279
|
-
})
|
|
279
|
+
})
|
|
280
280
|
);
|
|
281
281
|
|
|
282
282
|
return this({
|
|
@@ -288,5 +288,5 @@ export const OIDCProvider = Resource(
|
|
|
288
288
|
console.error("Error configuring OIDC provider:", error);
|
|
289
289
|
throw error;
|
|
290
290
|
}
|
|
291
|
-
}
|
|
291
|
+
}
|
|
292
292
|
);
|