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.
Files changed (104) hide show
  1. package/README.md +8 -3
  2. package/lib/ai/document.d.ts +12 -1
  3. package/lib/ai/document.js +10 -1
  4. package/lib/ai/index.d.ts +0 -2
  5. package/lib/ai/index.js +0 -2
  6. package/lib/alchemy.d.ts +5 -0
  7. package/lib/alchemy.js +44 -1
  8. package/lib/apply.js +17 -10
  9. package/lib/aws/account-id.d.ts +4 -1
  10. package/lib/aws/account-id.js +1 -1
  11. package/lib/aws/credentials.d.ts +5 -0
  12. package/lib/aws/credentials.js +0 -0
  13. package/lib/aws/oidc/oidc-provider.js +2 -2
  14. package/lib/aws/role.js +35 -23
  15. package/lib/cloudflare/account-api-token.d.ts +168 -0
  16. package/lib/cloudflare/account-api-token.js +179 -0
  17. package/lib/cloudflare/account-id.d.ts +1 -0
  18. package/lib/cloudflare/account-id.js +0 -0
  19. package/lib/cloudflare/api.d.ts +38 -1
  20. package/lib/cloudflare/api.js +55 -4
  21. package/lib/cloudflare/bucket.d.ts +118 -3
  22. package/lib/cloudflare/bucket.js +269 -89
  23. package/lib/cloudflare/custom-domain.d.ts +75 -0
  24. package/lib/cloudflare/custom-domain.js +154 -0
  25. package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
  26. package/lib/cloudflare/index.d.ts +5 -1
  27. package/lib/cloudflare/index.js +5 -1
  28. package/lib/cloudflare/permission-groups.d.ts +78 -0
  29. package/lib/cloudflare/permission-groups.js +48 -0
  30. package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
  31. package/lib/cloudflare/r2-rest-state-store.js +3 -2
  32. package/lib/cloudflare/static-site.d.ts +17 -18
  33. package/lib/cloudflare/static-site.js +19 -10
  34. package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
  35. package/lib/{util/encrypt.js → encrypt.js} +1 -1
  36. package/lib/fs/file-system-state-store.js +1 -1
  37. package/lib/fs/file.d.ts +18 -0
  38. package/lib/fs/file.js +6 -0
  39. package/lib/internal/{providers.d.ts → docs/providers.d.ts} +10 -4
  40. package/lib/internal/docs/providers.js +196 -0
  41. package/lib/secret.d.ts +3 -0
  42. package/lib/secret.js +13 -0
  43. package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
  44. package/lib/{util/serde.js → serde.js} +12 -4
  45. package/lib/test/bun.js +3 -2
  46. package/lib/util/sha256.d.ts +1 -0
  47. package/lib/util/sha256.js +4 -0
  48. package/lib/web/vitepress/config.d.ts +2 -1
  49. package/lib/web/vitepress/config.js +1 -0
  50. package/lib/web/vitepress/index.d.ts +1 -0
  51. package/lib/web/vitepress/index.js +1 -0
  52. package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
  53. package/lib/web/vitepress/process-front-matter-files.js +68 -0
  54. package/lib/web/vitepress/vitepress.js +3 -2
  55. package/package.json +8 -4
  56. package/src/ai/document.ts +26 -2
  57. package/src/ai/index.ts +0 -2
  58. package/src/alchemy.ts +56 -2
  59. package/src/apply.ts +21 -20
  60. package/src/aws/account-id.ts +6 -2
  61. package/src/aws/credentials.ts +6 -0
  62. package/src/aws/oidc/oidc-provider.ts +17 -17
  63. package/src/aws/role.ts +68 -54
  64. package/src/cloudflare/account-api-token.ts +365 -0
  65. package/src/cloudflare/account-id.ts +0 -0
  66. package/src/cloudflare/api.ts +88 -17
  67. package/src/cloudflare/bucket.ts +493 -133
  68. package/src/cloudflare/custom-domain.ts +318 -0
  69. package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
  70. package/src/cloudflare/index.ts +5 -1
  71. package/src/cloudflare/permission-groups.ts +137 -0
  72. package/src/cloudflare/r2-rest-state-store.ts +14 -13
  73. package/src/cloudflare/static-site.ts +20 -32
  74. package/src/dns/import-dns.ts +4 -4
  75. package/src/{util/encrypt.ts → encrypt.ts} +7 -10
  76. package/src/fs/file-system-state-store.ts +5 -5
  77. package/src/fs/file.ts +29 -0
  78. package/src/internal/docs/providers.ts +281 -0
  79. package/src/secret.ts +16 -0
  80. package/src/{util/serde.ts → serde.ts} +16 -10
  81. package/src/test/bun.ts +8 -7
  82. package/src/util/sha256.ts +5 -0
  83. package/src/web/vitepress/config.ts +3 -1
  84. package/src/web/vitepress/index.ts +1 -0
  85. package/src/web/vitepress/process-front-matter-files.ts +98 -0
  86. package/src/web/vitepress/vitepress.ts +3 -2
  87. package/lib/ai/approve.d.ts +0 -99
  88. package/lib/ai/approve.js +0 -76
  89. package/lib/ai/review.d.ts +0 -122
  90. package/lib/ai/review.js +0 -101
  91. package/lib/internal/getting-started.d.ts +0 -21
  92. package/lib/internal/getting-started.js +0 -87
  93. package/lib/internal/index.d.ts +0 -3
  94. package/lib/internal/index.js +0 -3
  95. package/lib/internal/providers.js +0 -172
  96. package/lib/internal/tutorial.d.ts +0 -104
  97. package/lib/internal/tutorial.js +0 -251
  98. package/src/ai/approve.ts +0 -163
  99. package/src/ai/review.ts +0 -213
  100. package/src/internal/getting-started.ts +0 -115
  101. package/src/internal/index.ts +0 -3
  102. package/src/internal/providers.ts +0 -241
  103. package/src/internal/tutorial.ts +0 -392
  104. /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;
@@ -0,0 +1,4 @@
1
+ import { createHash } from "node:crypto";
2
+ export function sha256(value) {
3
+ return createHash("sha256").update(value).digest("hex");
4
+ }
@@ -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()],
@@ -1,4 +1,5 @@
1
1
  export * from "./config";
2
2
  export * from "./custom-theme";
3
3
  export * from "./home-page";
4
+ export * from "./process-front-matter-files";
4
5
  export * from "./vitepress";
@@ -1,4 +1,5 @@
1
1
  export * from "./config";
2
2
  export * from "./custom-theme";
3
3
  export * from "./home-page";
4
+ export * from "./process-front-matter-files";
4
5
  export * from "./vitepress";
@@ -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
- // Delete the entire project directory
18
- await execAsync(`rm -rf ${dir}`);
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.1",
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",
@@ -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: string;
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
- return scope(undefined, {
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 { Secret } from "./secret";
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 = JSON.stringify(
60
- state.props,
61
- (_, value) =>
62
- value instanceof Secret
63
- ? {
64
- "@secret": value.unencrypted,
65
- }
66
- : value,
67
- 2
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
  }
@@ -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 getAccountId(): Promise<string> {
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
  }
@@ -0,0 +1,6 @@
1
+ import type { Secret } from "../secret";
2
+
3
+ export interface AwsCredentials {
4
+ accessKeyId: Secret;
5
+ secretAccessKey: Secret;
6
+ }
@@ -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 { getAccountId } from "../account-id";
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 getAccountId()}:oidc-provider/${url}`;
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
  );