alchemy 0.1.13 → 0.1.15

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 (86) hide show
  1. package/lib/aws/account-id.d.ts +5 -0
  2. package/lib/aws/account-id.d.ts.map +1 -0
  3. package/lib/aws/account-id.js +10 -0
  4. package/lib/aws/account-id.js.map +1 -0
  5. package/lib/aws/index.d.ts +1 -0
  6. package/lib/aws/index.d.ts.map +1 -1
  7. package/lib/aws/index.js +1 -0
  8. package/lib/aws/index.js.map +1 -1
  9. package/lib/aws/oidc/github-oidc-provider.d.ts +11 -0
  10. package/lib/aws/oidc/github-oidc-provider.d.ts.map +1 -0
  11. package/lib/aws/oidc/github-oidc-provider.js +22 -0
  12. package/lib/aws/oidc/github-oidc-provider.js.map +1 -0
  13. package/lib/aws/oidc/index.d.ts +3 -0
  14. package/lib/aws/oidc/index.d.ts.map +1 -0
  15. package/lib/aws/oidc/index.js +3 -0
  16. package/lib/aws/oidc/index.js.map +1 -0
  17. package/lib/aws/oidc/oidc-provider.d.ts +60 -0
  18. package/lib/aws/oidc/oidc-provider.d.ts.map +1 -0
  19. package/lib/aws/oidc/oidc-provider.js +140 -0
  20. package/lib/aws/oidc/oidc-provider.js.map +1 -0
  21. package/lib/aws/queue.d.ts +24 -0
  22. package/lib/aws/queue.d.ts.map +1 -1
  23. package/lib/aws/queue.js +6 -1
  24. package/lib/aws/queue.js.map +1 -1
  25. package/lib/aws/role.d.ts.map +1 -1
  26. package/lib/aws/role.js +49 -1
  27. package/lib/aws/role.js.map +1 -1
  28. package/lib/aws/ses.d.ts.map +1 -1
  29. package/lib/aws/ses.js +20 -28
  30. package/lib/aws/ses.js.map +1 -1
  31. package/lib/aws/table.d.ts.map +1 -1
  32. package/lib/aws/table.js +113 -79
  33. package/lib/aws/table.js.map +1 -1
  34. package/lib/cloudflare/bindings.d.ts +7 -2
  35. package/lib/cloudflare/bindings.d.ts.map +1 -1
  36. package/lib/cloudflare/durable-object-namespace.d.ts +2 -5
  37. package/lib/cloudflare/durable-object-namespace.d.ts.map +1 -1
  38. package/lib/cloudflare/durable-object-namespace.js +1 -6
  39. package/lib/cloudflare/durable-object-namespace.js.map +1 -1
  40. package/lib/cloudflare/kv-namespace.d.ts +3 -0
  41. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  42. package/lib/cloudflare/kv-namespace.js +28 -7
  43. package/lib/cloudflare/kv-namespace.js.map +1 -1
  44. package/lib/cloudflare/static-site.d.ts.map +1 -1
  45. package/lib/cloudflare/static-site.js +12 -30
  46. package/lib/cloudflare/static-site.js.map +1 -1
  47. package/lib/cloudflare/worker.d.ts +6 -3
  48. package/lib/cloudflare/worker.d.ts.map +1 -1
  49. package/lib/cloudflare/worker.js +82 -41
  50. package/lib/cloudflare/worker.js.map +1 -1
  51. package/lib/github/client.d.ts +34 -0
  52. package/lib/github/client.d.ts.map +1 -0
  53. package/lib/github/client.js +91 -0
  54. package/lib/github/client.js.map +1 -0
  55. package/lib/github/index.d.ts +2 -0
  56. package/lib/github/index.d.ts.map +1 -0
  57. package/lib/github/index.js +2 -0
  58. package/lib/github/index.js.map +1 -0
  59. package/lib/github/secret.d.ts +46 -0
  60. package/lib/github/secret.d.ts.map +1 -0
  61. package/lib/github/secret.js +103 -0
  62. package/lib/github/secret.js.map +1 -0
  63. package/lib/utils/retry.d.ts +13 -0
  64. package/lib/utils/retry.d.ts.map +1 -0
  65. package/lib/utils/retry.js +32 -0
  66. package/lib/utils/retry.js.map +1 -0
  67. package/package.json +10 -3
  68. package/src/aws/account-id.ts +11 -0
  69. package/src/aws/index.ts +1 -0
  70. package/src/aws/oidc/github-oidc-provider.ts +34 -0
  71. package/src/aws/oidc/index.ts +2 -0
  72. package/src/aws/oidc/oidc-provider.ts +253 -0
  73. package/src/aws/queue.ts +23 -1
  74. package/src/aws/role.ts +69 -0
  75. package/src/aws/ses.ts +27 -31
  76. package/src/aws/table.ts +143 -90
  77. package/src/cloudflare/bindings.ts +8 -2
  78. package/src/cloudflare/durable-object-namespace.ts +4 -10
  79. package/src/cloudflare/kv-namespace.ts +45 -15
  80. package/src/cloudflare/static-site.ts +13 -32
  81. package/src/cloudflare/worker.ts +117 -62
  82. package/src/github/client.ts +115 -0
  83. package/src/github/index.ts +1 -0
  84. package/src/github/secret.ts +172 -0
  85. package/src/utils/retry.ts +37 -0
  86. package/README.md +0 -324
@@ -0,0 +1,103 @@
1
+ import sodium from "libsodium-wrappers";
2
+ import { Resource } from "../resource";
3
+ import { createGitHubClient, verifyGitHubAuth } from "./client";
4
+ /**
5
+ * Encrypt a value for GitHub using libsodium
6
+ * Based on GitHub's documentation for encrypting secrets
7
+ *
8
+ * @param value - The secret value to encrypt
9
+ * @param publicKey - The base64-encoded public key from GitHub
10
+ * @returns The base64-encoded encrypted value
11
+ */
12
+ async function encryptValue(value, publicKey) {
13
+ // Initialize libsodium
14
+ await sodium.ready;
15
+ // Convert the public key from base64 to binary
16
+ const binKey = sodium.from_base64(publicKey, sodium.base64_variants.ORIGINAL);
17
+ // Convert the message to a Uint8Array
18
+ const binMessage = sodium.from_string(value);
19
+ // Encrypt the message with the public key using libsodium's sealed box
20
+ const encryptedBin = sodium.crypto_box_seal(binMessage, binKey);
21
+ // Convert the encrypted message to base64
22
+ return sodium.to_base64(encryptedBin, sodium.base64_variants.ORIGINAL);
23
+ }
24
+ /**
25
+ * Resource for managing GitHub repository secrets
26
+ */
27
+ export class GitHubSecret extends Resource("github::Secret", async (ctx, props) => {
28
+ // Create authenticated Octokit client - will automatically handle token resolution
29
+ const octokit = await createGitHubClient({ token: props.token });
30
+ // Verify authentication and permissions
31
+ if (!ctx.quiet) {
32
+ await verifyGitHubAuth(octokit, props.owner, props.repository);
33
+ }
34
+ if (ctx.event === "delete") {
35
+ if (ctx.output?.id) {
36
+ try {
37
+ // Delete the secret
38
+ await octokit.rest.actions.deleteRepoSecret({
39
+ owner: props.owner,
40
+ repo: props.repository,
41
+ secret_name: props.name,
42
+ });
43
+ }
44
+ catch (error) {
45
+ // Ignore 404 errors (secret already deleted)
46
+ if (error.status === 404) {
47
+ console.log("Secret doesn't exist, ignoring");
48
+ }
49
+ else {
50
+ throw error;
51
+ }
52
+ }
53
+ }
54
+ // Return void (a deleted resource has no content)
55
+ return undefined;
56
+ }
57
+ else {
58
+ try {
59
+ // Get the repository's public key for encrypting secrets
60
+ const { data: publicKey } = await octokit.rest.actions.getRepoPublicKey({
61
+ owner: props.owner,
62
+ repo: props.repository,
63
+ });
64
+ // Encrypt the secret value using libsodium
65
+ const encryptedValue = await encryptValue(props.value, publicKey.key);
66
+ // Create or update the secret with the encrypted value and key_id
67
+ const response = await octokit.rest.actions.createOrUpdateRepoSecret({
68
+ owner: props.owner,
69
+ repo: props.repository,
70
+ secret_name: props.name,
71
+ encrypted_value: encryptedValue,
72
+ key_id: publicKey.key_id,
73
+ });
74
+ // Log success
75
+ if (!ctx.quiet) {
76
+ console.log(`Successfully created/updated secret: ${props.name}`);
77
+ }
78
+ // GitHub doesn't return the secret details on create/update, so we need to construct it
79
+ const output = {
80
+ id: `${props.owner}/${props.repository}/${props.name}`,
81
+ owner: props.owner,
82
+ repository: props.repository,
83
+ name: props.name,
84
+ token: props.token,
85
+ updatedAt: new Date().toISOString(),
86
+ };
87
+ return output;
88
+ }
89
+ catch (error) {
90
+ if (error.status === 403 &&
91
+ error.message?.includes("Must have admin rights")) {
92
+ console.error("\n⚠️ Error creating/updating GitHub secret: You must have admin rights to the repository.");
93
+ console.error("Make sure your GitHub token has the required permissions (repo scope for private repos).\n");
94
+ }
95
+ else {
96
+ console.error("Error creating/updating GitHub secret:", error.message);
97
+ }
98
+ throw error;
99
+ }
100
+ }
101
+ }) {
102
+ }
103
+ //# sourceMappingURL=secret.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret.js","sourceRoot":"","sources":["../../src/github/secret.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAgB,QAAQ,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AA+ChE;;;;;;;GAOG;AACH,KAAK,UAAU,YAAY,CAAC,KAAa,EAAE,SAAiB;IAC1D,uBAAuB;IACvB,MAAM,MAAM,CAAC,KAAK,CAAC;IAEnB,+CAA+C;IAC/C,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,MAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;IAE9E,sCAAsC;IACtC,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAE7C,uEAAuE;IACvE,MAAM,YAAY,GAAG,MAAM,CAAC,eAAe,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAEhE,0CAA0C;IAC1C,OAAO,MAAM,CAAC,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;AACzE,CAAC;AAED;;GAEG;AACH,MAAM,OAAO,YAAa,SAAQ,QAAQ,CACxC,gBAAgB,EAChB,KAAK,EACH,GAAgC,EAChC,KAAwB,EACY,EAAE;IACtC,mFAAmF;IACnF,MAAM,OAAO,GAAG,MAAM,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IAEjE,wCAAwC;IACxC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QACf,MAAM,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACjE,CAAC;IAED,IAAI,GAAG,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC3B,IAAI,GAAG,CAAC,MAAM,EAAE,EAAE,EAAE,CAAC;YACnB,IAAI,CAAC;gBACH,oBAAoB;gBACpB,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC;oBAC1C,KAAK,EAAE,KAAK,CAAC,KAAK;oBAClB,IAAI,EAAE,KAAK,CAAC,UAAU;oBACtB,WAAW,EAAE,KAAK,CAAC,IAAI;iBACxB,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,KAAU,EAAE,CAAC;gBACpB,6CAA6C;gBAC7C,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;oBACzB,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;gBAChD,CAAC;qBAAM,CAAC;oBACN,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC;QACH,CAAC;QAED,kDAAkD;QAClD,OAAO,SAAS,CAAC;IACnB,CAAC;SAAM,CAAC;QACN,IAAI,CAAC;YACH,yDAAyD;YACzD,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,gBAAgB,CACrE;gBACE,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,IAAI,EAAE,KAAK,CAAC,UAAU;aACvB,CACF,CAAC;YAEF,2CAA2C;YAC3C,MAAM,cAAc,GAAG,MAAM,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;YAEtE,kEAAkE;YAClE,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,wBAAwB,CAAC;gBACnE,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,IAAI,EAAE,KAAK,CAAC,UAAU;gBACtB,WAAW,EAAE,KAAK,CAAC,IAAI;gBACvB,eAAe,EAAE,cAAc;gBAC/B,MAAM,EAAE,SAAS,CAAC,MAAM;aACzB,CAAC,CAAC;YAEH,cAAc;YACd,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,GAAG,CAAC,wCAAwC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YACpE,CAAC;YAED,wFAAwF;YACxF,MAAM,MAAM,GAAuB;gBACjC,EAAE,EAAE,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,IAAI,EAAE;gBACtD,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACpC,CAAC;YAEF,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,IACE,KAAK,CAAC,MAAM,KAAK,GAAG;gBACpB,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EACjD,CAAC;gBACD,OAAO,CAAC,KAAK,CACX,2FAA2F,CAC5F,CAAC;gBACF,OAAO,CAAC,KAAK,CACX,4FAA4F,CAC7F,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CACX,wCAAwC,EACxC,KAAK,CAAC,OAAO,CACd,CAAC;YACJ,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;AACH,CAAC,CACF;CAAG"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Utility function for exponential backoff retry
3
+ * Retries an operation with exponential backoff when a retryable error occurs
4
+ *
5
+ * @param operation The async operation to execute and potentially retry
6
+ * @param isRetryable Function to determine if an error should trigger a retry
7
+ * @param maxAttempts Maximum number of attempts before giving up
8
+ * @param initialDelayMs Initial delay in milliseconds before first retry
9
+ * @returns Result of the operation
10
+ * @throws The last error encountered if all retries fail
11
+ */
12
+ export declare function withExponentialBackoff<T>(operation: () => Promise<T>, isRetryable: (error: any) => boolean, maxAttempts?: number, initialDelayMs?: number): Promise<T>;
13
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,WAAW,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,EACpC,WAAW,SAAI,EACf,cAAc,SAAM,GACnB,OAAO,CAAC,CAAC,CAAC,CAoBZ"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Utility function for exponential backoff retry
3
+ * Retries an operation with exponential backoff when a retryable error occurs
4
+ *
5
+ * @param operation The async operation to execute and potentially retry
6
+ * @param isRetryable Function to determine if an error should trigger a retry
7
+ * @param maxAttempts Maximum number of attempts before giving up
8
+ * @param initialDelayMs Initial delay in milliseconds before first retry
9
+ * @returns Result of the operation
10
+ * @throws The last error encountered if all retries fail
11
+ */
12
+ export async function withExponentialBackoff(operation, isRetryable, maxAttempts = 5, initialDelayMs = 100) {
13
+ let attempt = 0;
14
+ let delay = initialDelayMs;
15
+ while (true) {
16
+ try {
17
+ return await operation();
18
+ }
19
+ catch (error) {
20
+ attempt++;
21
+ if (attempt >= maxAttempts || !isRetryable(error)) {
22
+ console.warn("Failed after ", attempt, " attempts");
23
+ throw error;
24
+ }
25
+ // Exponential backoff with jitter
26
+ const jitter = Math.random() * 0.1 * delay;
27
+ await new Promise((resolve) => setTimeout(resolve, delay + jitter));
28
+ delay *= 2; // Double the delay for next attempt
29
+ }
30
+ }
31
+ }
32
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,SAA2B,EAC3B,WAAoC,EACpC,WAAW,GAAG,CAAC,EACf,cAAc,GAAG,GAAG;IAEpB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,KAAK,GAAG,cAAc,CAAC;IAE3B,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,MAAM,SAAS,EAAE,CAAC;QAC3B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,EAAE,CAAC;YACV,IAAI,OAAO,IAAI,WAAW,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;gBAClD,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;gBACpD,MAAM,KAAK,CAAC;YACd,CAAC;YAED,kCAAkC;YAClC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,KAAK,CAAC;YAC3C,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC;YACpE,KAAK,IAAI,CAAC,CAAC,CAAC,oCAAoC;QAClD,CAAC;IACH,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
7
- "check": "tsc -b ./tsconfig.all.json",
8
7
  "publish:npm": "rm -rf ./lib && tsc -b && npm publish"
9
8
  },
10
9
  "files": ["lib", "src"],
@@ -12,10 +11,12 @@
12
11
  ".": "./lib/index.js",
13
12
  "./agent": "./lib/agent/index.js",
14
13
  "./aws": "./lib/aws/index.js",
14
+ "./aws/oidc": "./lib/aws/oidc/index.js",
15
15
  "./cloudflare": "./lib/cloudflare/index.js",
16
16
  "./esbuild": "./lib/esbuild.js",
17
17
  "./markdown": "./lib/markdown/index.js",
18
18
  "./fs": "./lib/fs.js",
19
+ "./github": "./lib/github/index.js",
19
20
  "./stripe": "./lib/stripe/index.js",
20
21
  "./typescript": "./lib/typescript/index.js"
21
22
  },
@@ -28,12 +29,15 @@
28
29
  "@aws-sdk/client-sagemaker": "^3.0.0",
29
30
  "@aws-sdk/client-ses": "^3.0.0",
30
31
  "@aws-sdk/client-sesv2": "^3.0.0",
31
- "@aws-sdk/client-sqs": "^3.0.1",
32
+ "@aws-sdk/client-sqs": "^3.0.0",
33
+ "@aws-sdk/client-sts": "^3.0.0",
34
+ "@octokit/rest": "^21.1.1",
32
35
  "ai": "^4.1.16",
33
36
  "diff": "^7.0.0",
34
37
  "esbuild": "^0.24.2",
35
38
  "glob": "^10.3.10",
36
39
  "hono": "^4.7.4",
40
+ "libsodium-wrappers": "^0.7.15",
37
41
  "jszip": "^3.0.0",
38
42
  "stripe": "^17.7.0",
39
43
  "turndown": "^7.2.0",
@@ -44,12 +48,15 @@
44
48
  "@ai-sdk/openai": "^1.1.9",
45
49
  "@biomejs/biome": "^1.9.4",
46
50
  "@cloudflare/workers-types": "^4.20250303.0",
51
+ "@octokit/rest": "^21.1.1",
47
52
  "@types/bun": "latest",
48
53
  "@types/diff": "^5.0.0",
54
+ "@types/libsodium-wrappers": "^0.7.14",
49
55
  "@types/node": "latest",
50
56
  "@types/turndown": "^5.0.5",
51
57
  "ai": "^4.1.16",
52
58
  "change-case": "^5.4.4",
59
+ "libsodium-wrappers": "^0.7.15",
53
60
  "openpgp": "^6.1.0",
54
61
  "turndown": "^7.2.0",
55
62
  "typescript": "latest",
@@ -0,0 +1,11 @@
1
+ import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
2
+
3
+ const sts = new STSClient({});
4
+
5
+ /**
6
+ * Helper to get the current AWS account ID
7
+ */
8
+ export async function getAccountId(): Promise<string> {
9
+ const identity = await sts.send(new GetCallerIdentityCommand({}));
10
+ return identity.Account!;
11
+ }
package/src/aws/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./account-id";
1
2
  export * from "./bucket";
2
3
  export * from "./function";
3
4
  export * from "./policy";
@@ -0,0 +1,34 @@
1
+ import type { Input } from "../../input";
2
+ import { isOutput } from "../../output";
3
+ import { OIDCProvider, type OIDCProviderProps } from "./oidc-provider";
4
+
5
+ /**
6
+ * Default thumbprint for GitHub's OIDC provider
7
+ * This is the certificate thumbprint that GitHub uses to sign OIDC tokens
8
+ * @see https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-the-identity-provider-to-aws
9
+ */
10
+ const DEFAULT_GITHUB_THUMBPRINT = "6938fd4d98bab03faadb97b34396831e3780aea1";
11
+
12
+ export interface GitHubOIDCProviderProps
13
+ extends Omit<OIDCProviderProps, "thumbprint"> {
14
+ owner: string;
15
+ repository: string;
16
+ thumbprint?: string;
17
+ }
18
+
19
+ export class GitHubOIDCProvider extends OIDCProvider {
20
+ constructor(id: string, props: Input<GitHubOIDCProviderProps>) {
21
+ super(
22
+ id,
23
+ isOutput<GitHubOIDCProviderProps>(props)
24
+ ? props.apply((props) => ({
25
+ ...props,
26
+ thumbprint: props.thumbprint ?? DEFAULT_GITHUB_THUMBPRINT,
27
+ }))
28
+ : ({
29
+ ...props,
30
+ thumbprint: props.thumbprint ?? DEFAULT_GITHUB_THUMBPRINT,
31
+ } as Input<OIDCProviderProps>),
32
+ );
33
+ }
34
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./github-oidc-provider";
2
+ export * from "./oidc-provider";
@@ -0,0 +1,253 @@
1
+ import {
2
+ CreateOpenIDConnectProviderCommand,
3
+ DeleteOpenIDConnectProviderCommand,
4
+ GetOpenIDConnectProviderCommand,
5
+ GetRoleCommand,
6
+ IAMClient,
7
+ type Tag,
8
+ UpdateAssumeRolePolicyCommand,
9
+ } from "@aws-sdk/client-iam";
10
+ import { type Context, Resource } from "../../resource";
11
+ import { getAccountId } from "../account-id";
12
+
13
+ /**
14
+ * Properties for configuring an AWS OIDC provider for GitHub Actions
15
+ */
16
+ export interface OIDCProviderProps {
17
+ /**
18
+ * The GitHub organization or user that owns the repository
19
+ */
20
+ owner: string;
21
+
22
+ /**
23
+ * The name of the GitHub repository
24
+ */
25
+ repository: string;
26
+
27
+ /**
28
+ * Optional list of branches to restrict access to
29
+ * If not provided, all branches will be allowed
30
+ */
31
+ branches?: string[];
32
+
33
+ /**
34
+ * Optional list of environments to restrict access to
35
+ * If not provided, all environments will be allowed
36
+ */
37
+ environments?: string[];
38
+
39
+ /**
40
+ * The ARN of the IAM role to be assumed
41
+ */
42
+ roleArn: string;
43
+
44
+ /**
45
+ * Optional maximum session duration in seconds (default: 3600)
46
+ */
47
+ maxSessionDuration?: number;
48
+
49
+ /**
50
+ * Thumbprint for the OIDC provider
51
+ */
52
+ thumbprint: string;
53
+
54
+ /**
55
+ * Optional AWS region (defaults to AWS_REGION environment variable)
56
+ */
57
+ region?: string;
58
+ }
59
+
60
+ /**
61
+ * Output returned after OIDC provider configuration
62
+ */
63
+ export interface OIDCProviderOutput extends OIDCProviderProps {
64
+ /**
65
+ * The ARN of the OIDC provider
66
+ */
67
+ providerArn: string;
68
+
69
+ /**
70
+ * Time at which the provider was created
71
+ */
72
+ createdAt: number;
73
+ }
74
+
75
+ /**
76
+ * Unique identifier for our trust policy statement
77
+ * Used to track and remove our specific statement without affecting others
78
+ */
79
+ const TRUST_POLICY_SID = "GitHubOIDCTrust";
80
+
81
+ /**
82
+ * Resource for configuring AWS OIDC provider for GitHub Actions
83
+ */
84
+ export class OIDCProvider extends Resource(
85
+ "aws::OIDCProvider",
86
+ async (ctx: Context<OIDCProviderOutput>, props: OIDCProviderProps) => {
87
+ // Initialize AWS SDK client
88
+ const client = new IAMClient({
89
+ region: props.region,
90
+ });
91
+
92
+ if (ctx.event === "delete") {
93
+ if (ctx.output?.providerArn) {
94
+ try {
95
+ // First, remove our trust policy statement from the role
96
+ const getRole = await client.send(
97
+ new GetRoleCommand({
98
+ RoleName: props.roleArn.split("/").pop(),
99
+ }),
100
+ );
101
+
102
+ if (getRole.Role?.AssumeRolePolicyDocument) {
103
+ const policy = JSON.parse(
104
+ decodeURIComponent(getRole.Role.AssumeRolePolicyDocument),
105
+ );
106
+
107
+ // Remove our specific statement while preserving others
108
+ policy.Statement = policy.Statement.filter(
109
+ (stmt: any) => stmt.Sid !== TRUST_POLICY_SID,
110
+ );
111
+
112
+ await client.send(
113
+ new UpdateAssumeRolePolicyCommand({
114
+ RoleName: props.roleArn.split("/").pop(),
115
+ PolicyDocument: JSON.stringify(policy),
116
+ }),
117
+ );
118
+ }
119
+
120
+ // Then delete the OIDC provider if we're the last user
121
+ const provider = await client.send(
122
+ new GetOpenIDConnectProviderCommand({
123
+ OpenIDConnectProviderArn: ctx.output.providerArn,
124
+ }),
125
+ );
126
+
127
+ // Only delete the provider if it exists and has no other tags
128
+ if (provider && (!provider.Tags || provider.Tags.length === 0)) {
129
+ await client.send(
130
+ new DeleteOpenIDConnectProviderCommand({
131
+ OpenIDConnectProviderArn: ctx.output.providerArn,
132
+ }),
133
+ );
134
+ }
135
+ } catch (error) {
136
+ // Log but don't throw on cleanup errors
137
+ console.error("Error during cleanup:", error);
138
+ }
139
+ }
140
+ return;
141
+ }
142
+
143
+ try {
144
+ const url = "https://token.actions.githubusercontent.com";
145
+ const thumbprint = props.thumbprint;
146
+
147
+ // Create or get existing OIDC provider
148
+ let providerArn: string;
149
+ try {
150
+ const createProvider = await client.send(
151
+ new CreateOpenIDConnectProviderCommand({
152
+ Url: url,
153
+ ClientIDList: ["sts.amazonaws.com"],
154
+ ThumbprintList: [thumbprint],
155
+ Tags: [
156
+ {
157
+ Key: "ManagedBy",
158
+ Value: "alchemy",
159
+ },
160
+ ] as Tag[],
161
+ }),
162
+ );
163
+ providerArn = createProvider.OpenIDConnectProviderArn!;
164
+ } catch (error: any) {
165
+ if (error.name === "EntityAlreadyExistsException") {
166
+ // Provider exists, use its ARN
167
+ providerArn = `arn:aws:iam::${await getAccountId()}:oidc-provider/${url}`;
168
+ } else {
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ // Get current role policy
174
+ const getRole = await client.send(
175
+ new GetRoleCommand({
176
+ RoleName: props.roleArn.split("/").pop(),
177
+ }),
178
+ );
179
+
180
+ let policy: any;
181
+ if (getRole.Role?.AssumeRolePolicyDocument) {
182
+ policy = JSON.parse(
183
+ decodeURIComponent(getRole.Role.AssumeRolePolicyDocument),
184
+ );
185
+ } else {
186
+ policy = {
187
+ Version: "2012-10-17",
188
+ Statement: [],
189
+ };
190
+ }
191
+
192
+ // Construct the trust policy conditions
193
+ const conditions: Record<string, any> = {
194
+ StringEquals: {
195
+ "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
196
+ "token.actions.githubusercontent.com:sub": `repo:${props.owner}/${props.repository}:`,
197
+ },
198
+ };
199
+
200
+ if (props.branches?.length) {
201
+ conditions.StringEquals["token.actions.githubusercontent.com:sub"] =
202
+ props.branches.map(
203
+ (branch) =>
204
+ `repo:${props.owner}/${props.repository}:ref:refs/heads/${branch}`,
205
+ );
206
+ }
207
+
208
+ if (props.environments?.length) {
209
+ conditions.StringEquals["token.actions.githubusercontent.com:sub"] =
210
+ props.environments.map(
211
+ (env) =>
212
+ `repo:${props.owner}/${props.repository}:environment:${env}`,
213
+ );
214
+ }
215
+
216
+ // Create our policy statement
217
+ const ourStatement = {
218
+ Sid: TRUST_POLICY_SID,
219
+ Effect: "Allow",
220
+ Principal: {
221
+ Federated: providerArn,
222
+ },
223
+ Action: "sts:AssumeRoleWithWebIdentity",
224
+ Condition: conditions,
225
+ };
226
+
227
+ // Remove any existing statement with our SID and add the new one
228
+ policy.Statement = [
229
+ ...policy.Statement.filter(
230
+ (stmt: any) => stmt.Sid !== TRUST_POLICY_SID,
231
+ ),
232
+ ourStatement,
233
+ ];
234
+
235
+ // Update the role's trust policy
236
+ await client.send(
237
+ new UpdateAssumeRolePolicyCommand({
238
+ RoleName: props.roleArn.split("/").pop(),
239
+ PolicyDocument: JSON.stringify(policy),
240
+ }),
241
+ );
242
+
243
+ return {
244
+ ...props,
245
+ providerArn,
246
+ createdAt: Date.now(),
247
+ };
248
+ } catch (error) {
249
+ console.error("Error configuring OIDC provider:", error);
250
+ throw error;
251
+ }
252
+ },
253
+ ) {}
package/src/aws/queue.ts CHANGED
@@ -8,14 +8,30 @@ import {
8
8
  import { Resource } from "../resource";
9
9
 
10
10
  export interface QueueProps {
11
+ /**
12
+ * Name of the queue
13
+ * For FIFO queues, the name must end with the .fifo suffix
14
+ */
11
15
  queueName: string;
16
+
17
+ /**
18
+ * Whether this is a FIFO queue.
19
+ * If true, the queueName must end with .fifo suffix
20
+ */
12
21
  fifo?: boolean;
22
+
13
23
  visibilityTimeout?: number;
14
24
  messageRetentionPeriod?: number;
15
25
  maximumMessageSize?: number;
16
26
  delaySeconds?: number;
17
27
  receiveMessageWaitTimeSeconds?: number;
28
+
29
+ /**
30
+ * Enables content-based deduplication for FIFO queues.
31
+ * Only applicable when fifo is true.
32
+ */
18
33
  contentBasedDeduplication?: boolean;
34
+
19
35
  deduplicationScope?: "messageGroup" | "queue";
20
36
  fifoThroughputLimit?: "perQueue" | "perMessageGroupId";
21
37
  tags?: Record<string, string>;
@@ -31,7 +47,13 @@ export class Queue extends Resource(
31
47
  "sqs::Queue",
32
48
  async (ctx, props: QueueProps) => {
33
49
  const client = new SQSClient({});
34
- const queueName = props.fifo ? `${props.queueName}.fifo` : props.queueName;
50
+ // Don't automatically add .fifo suffix - user must include it in queueName
51
+ const queueName = props.queueName;
52
+
53
+ // Validate that FIFO queues have .fifo suffix
54
+ if (props.fifo && !queueName.endsWith(".fifo")) {
55
+ throw new Error("FIFO queue names must end with .fifo suffix");
56
+ }
35
57
 
36
58
  if (ctx.event === "delete") {
37
59
  try {
package/src/aws/role.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import {
2
+ AttachRolePolicyCommand,
2
3
  CreateRoleCommand,
3
4
  DeleteRoleCommand,
4
5
  DeleteRolePolicyCommand,
6
+ DetachRolePolicyCommand,
5
7
  EntityAlreadyExistsException,
6
8
  GetRoleCommand,
7
9
  IAMClient,
10
+ ListAttachedRolePoliciesCommand,
8
11
  NoSuchEntityException,
9
12
  PutRolePolicyCommand,
10
13
  type Tag,
@@ -59,6 +62,32 @@ export class Role extends Resource(
59
62
  }
60
63
  }
61
64
 
65
+ // We need to detach managed policies before deleting the role
66
+ // First, get all attached policies
67
+ try {
68
+ const attachedPoliciesResponse = await client.send(
69
+ new ListAttachedRolePoliciesCommand({
70
+ RoleName: props.roleName,
71
+ }),
72
+ );
73
+
74
+ // Detach all managed policies
75
+ const attachedPolicies =
76
+ attachedPoliciesResponse.AttachedPolicies || [];
77
+ for (const policy of attachedPolicies) {
78
+ await client.send(
79
+ new DetachRolePolicyCommand({
80
+ RoleName: props.roleName,
81
+ PolicyArn: policy.PolicyArn!,
82
+ }),
83
+ );
84
+ }
85
+ } catch (error: any) {
86
+ if (error.name !== NoSuchEntityException.name) {
87
+ throw error;
88
+ }
89
+ }
90
+
62
91
  await ignore(NoSuchEntityException.name, () =>
63
92
  client.send(
64
93
  new DeleteRoleCommand({
@@ -229,6 +258,46 @@ export class Role extends Resource(
229
258
  }
230
259
  }
231
260
 
261
+ // Handle managed policy attachments
262
+ // Get currently attached policies
263
+ const attachedPoliciesResponse = await client.send(
264
+ new ListAttachedRolePoliciesCommand({
265
+ RoleName: props.roleName,
266
+ }),
267
+ );
268
+
269
+ const currentAttachedPolicies =
270
+ attachedPoliciesResponse.AttachedPolicies || [];
271
+ const currentPolicyArns = currentAttachedPolicies.map((p) => p.PolicyArn!);
272
+
273
+ // If we're updating, use an empty array as default when managedPolicyArns is undefined
274
+ // to ensure we detach all managed policies
275
+ const desiredPolicyArns = props.managedPolicyArns || [];
276
+
277
+ // Detach policies that are no longer needed
278
+ for (const policyArn of currentPolicyArns) {
279
+ if (!desiredPolicyArns.includes(policyArn)) {
280
+ await client.send(
281
+ new DetachRolePolicyCommand({
282
+ RoleName: props.roleName,
283
+ PolicyArn: policyArn,
284
+ }),
285
+ );
286
+ }
287
+ }
288
+
289
+ // Attach new policies that weren't attached before
290
+ for (const policyArn of desiredPolicyArns) {
291
+ if (!currentPolicyArns.includes(policyArn)) {
292
+ await client.send(
293
+ new AttachRolePolicyCommand({
294
+ RoleName: props.roleName,
295
+ PolicyArn: policyArn,
296
+ }),
297
+ );
298
+ }
299
+ }
300
+
232
301
  if (!role?.Role) {
233
302
  throw new Error(`Failed to create or update role ${props.roleName}`);
234
303
  }