alchemy 0.53.0 → 0.55.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 (214) hide show
  1. package/bin/alchemy.js +196353 -0
  2. package/bin/alchemy.ts +2 -2
  3. package/bin/commands/init.ts +1190 -0
  4. package/bin/constants.ts +18 -0
  5. package/bin/services/execute-alchemy.ts +7 -2
  6. package/bin/services/package-manager.ts +1 -1
  7. package/bin/types.ts +14 -5
  8. package/lib/aws/ec2/index.d.ts +10 -0
  9. package/lib/aws/ec2/index.d.ts.map +1 -0
  10. package/lib/aws/ec2/index.js +10 -0
  11. package/lib/aws/ec2/index.js.map +1 -0
  12. package/lib/aws/ec2/internet-gateway-attachment.d.ts +150 -0
  13. package/lib/aws/ec2/internet-gateway-attachment.d.ts.map +1 -0
  14. package/lib/aws/ec2/internet-gateway-attachment.js +248 -0
  15. package/lib/aws/ec2/internet-gateway-attachment.js.map +1 -0
  16. package/lib/aws/ec2/internet-gateway.d.ts +132 -0
  17. package/lib/aws/ec2/internet-gateway.d.ts.map +1 -0
  18. package/lib/aws/ec2/internet-gateway.js +311 -0
  19. package/lib/aws/ec2/internet-gateway.js.map +1 -0
  20. package/lib/aws/ec2/nat-gateway.d.ts +145 -0
  21. package/lib/aws/ec2/nat-gateway.d.ts.map +1 -0
  22. package/lib/aws/ec2/nat-gateway.js +618 -0
  23. package/lib/aws/ec2/nat-gateway.js.map +1 -0
  24. package/lib/aws/ec2/route-table-association.d.ts +134 -0
  25. package/lib/aws/ec2/route-table-association.d.ts.map +1 -0
  26. package/lib/aws/ec2/route-table-association.js +364 -0
  27. package/lib/aws/ec2/route-table-association.js.map +1 -0
  28. package/lib/aws/ec2/route-table.d.ts +94 -0
  29. package/lib/aws/ec2/route-table.d.ts.map +1 -0
  30. package/lib/aws/ec2/route-table.js +253 -0
  31. package/lib/aws/ec2/route-table.js.map +1 -0
  32. package/lib/aws/ec2/route.d.ts +130 -0
  33. package/lib/aws/ec2/route.d.ts.map +1 -0
  34. package/lib/aws/ec2/route.js +253 -0
  35. package/lib/aws/ec2/route.js.map +1 -0
  36. package/lib/aws/ec2/security-group-rule.d.ts +84 -0
  37. package/lib/aws/ec2/security-group-rule.d.ts.map +1 -0
  38. package/lib/aws/ec2/security-group-rule.js +123 -0
  39. package/lib/aws/ec2/security-group-rule.js.map +1 -0
  40. package/lib/aws/ec2/security-group.d.ts +84 -0
  41. package/lib/aws/ec2/security-group.d.ts.map +1 -0
  42. package/lib/aws/ec2/security-group.js +207 -0
  43. package/lib/aws/ec2/security-group.js.map +1 -0
  44. package/lib/aws/ec2/subnet.d.ts +110 -0
  45. package/lib/aws/ec2/subnet.d.ts.map +1 -0
  46. package/lib/aws/ec2/subnet.js +336 -0
  47. package/lib/aws/ec2/subnet.js.map +1 -0
  48. package/lib/aws/ec2/utils.d.ts +15 -0
  49. package/lib/aws/ec2/utils.d.ts.map +1 -0
  50. package/lib/aws/ec2/utils.js +79 -0
  51. package/lib/aws/ec2/utils.js.map +1 -0
  52. package/lib/aws/ec2/vpc.d.ts +160 -0
  53. package/lib/aws/ec2/vpc.d.ts.map +1 -0
  54. package/lib/aws/ec2/vpc.js +453 -0
  55. package/lib/aws/ec2/vpc.js.map +1 -0
  56. package/lib/aws/index.d.ts +7 -0
  57. package/lib/aws/index.d.ts.map +1 -1
  58. package/lib/aws/index.js +8 -0
  59. package/lib/aws/index.js.map +1 -1
  60. package/lib/aws/utils.d.ts +25 -0
  61. package/lib/aws/utils.d.ts.map +1 -0
  62. package/lib/aws/utils.js +89 -0
  63. package/lib/aws/utils.js.map +1 -0
  64. package/lib/build-date.d.ts +6 -0
  65. package/lib/build-date.d.ts.map +1 -0
  66. package/lib/build-date.js +8 -0
  67. package/lib/build-date.js.map +1 -0
  68. package/lib/cloudflare/astro.d.ts.map +1 -1
  69. package/lib/cloudflare/astro.js +4 -3
  70. package/lib/cloudflare/astro.js.map +1 -1
  71. package/lib/cloudflare/bundle/externals.d.ts +3 -0
  72. package/lib/cloudflare/bundle/externals.d.ts.map +1 -0
  73. package/lib/cloudflare/bundle/externals.js +35 -0
  74. package/lib/cloudflare/bundle/externals.js.map +1 -0
  75. package/lib/cloudflare/bundle/internal-worker-bundle.d.ts +8 -1
  76. package/lib/cloudflare/bundle/internal-worker-bundle.d.ts.map +1 -1
  77. package/lib/cloudflare/bundle/internal-worker-bundle.js +11 -4
  78. package/lib/cloudflare/bundle/internal-worker-bundle.js.map +1 -1
  79. package/lib/cloudflare/bundle/plugin-hot-reload.d.ts +4 -1
  80. package/lib/cloudflare/bundle/plugin-hot-reload.d.ts.map +1 -1
  81. package/lib/cloudflare/bundle/plugin-hot-reload.js +27 -11
  82. package/lib/cloudflare/bundle/plugin-hot-reload.js.map +1 -1
  83. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js +5 -1
  84. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js.map +1 -1
  85. package/lib/cloudflare/bundle/plugin-wasm.d.ts +6 -2
  86. package/lib/cloudflare/bundle/plugin-wasm.d.ts.map +1 -1
  87. package/lib/cloudflare/bundle/plugin-wasm.js +21 -47
  88. package/lib/cloudflare/bundle/plugin-wasm.js.map +1 -1
  89. package/lib/cloudflare/bundle/validate-node-compat.d.ts +2 -1
  90. package/lib/cloudflare/bundle/validate-node-compat.d.ts.map +1 -1
  91. package/lib/cloudflare/bundle/validate-node-compat.js +5 -4
  92. package/lib/cloudflare/bundle/validate-node-compat.js.map +1 -1
  93. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  94. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  95. package/lib/cloudflare/d1-database.js +1 -1
  96. package/lib/cloudflare/d1-database.js.map +1 -1
  97. package/lib/cloudflare/d1-migrations.d.ts.map +1 -1
  98. package/lib/cloudflare/d1-migrations.js +8 -10
  99. package/lib/cloudflare/d1-migrations.js.map +1 -1
  100. package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts +3 -3
  101. package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts.map +1 -1
  102. package/lib/cloudflare/miniflare/miniflare-worker-options.js +23 -6
  103. package/lib/cloudflare/miniflare/miniflare-worker-options.js.map +1 -1
  104. package/lib/cloudflare/miniflare/miniflare.d.ts.map +1 -1
  105. package/lib/cloudflare/miniflare/miniflare.js +2 -4
  106. package/lib/cloudflare/miniflare/miniflare.js.map +1 -1
  107. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
  108. package/lib/cloudflare/miniflare/remote-binding-proxy.js +4 -6
  109. package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
  110. package/lib/cloudflare/react-router.d.ts +4 -0
  111. package/lib/cloudflare/react-router.d.ts.map +1 -1
  112. package/lib/cloudflare/react-router.js +8 -5
  113. package/lib/cloudflare/react-router.js.map +1 -1
  114. package/lib/cloudflare/worker-bundle.d.ts +80 -0
  115. package/lib/cloudflare/worker-bundle.d.ts.map +1 -0
  116. package/lib/cloudflare/worker-bundle.js +266 -0
  117. package/lib/cloudflare/worker-bundle.js.map +1 -0
  118. package/lib/cloudflare/worker.d.ts +1 -1
  119. package/lib/cloudflare/worker.d.ts.map +1 -1
  120. package/lib/cloudflare/worker.js +81 -195
  121. package/lib/cloudflare/worker.js.map +1 -1
  122. package/lib/esbuild/bundle.d.ts +1 -0
  123. package/lib/esbuild/bundle.d.ts.map +1 -1
  124. package/lib/state/cloudflare-state-store.js +1 -5
  125. package/lib/state/cloudflare-state-store.js.map +1 -1
  126. package/lib/state/dofs-state-store/store.d.ts.map +1 -1
  127. package/lib/state/dofs-state-store/store.js +4 -4
  128. package/lib/state/dofs-state-store/store.js.map +1 -1
  129. package/lib/util/params.d.ts +5 -0
  130. package/lib/util/params.d.ts.map +1 -0
  131. package/lib/util/params.js +30 -0
  132. package/lib/util/params.js.map +1 -0
  133. package/lib/util/timeout.d.ts +31 -0
  134. package/lib/util/timeout.d.ts.map +1 -0
  135. package/lib/util/timeout.js +109 -0
  136. package/lib/util/timeout.js.map +1 -0
  137. package/lib/util/xml.d.ts +5 -0
  138. package/lib/util/xml.d.ts.map +1 -0
  139. package/lib/util/xml.js +8 -0
  140. package/lib/util/xml.js.map +1 -0
  141. package/package.json +12 -5
  142. package/src/aws/ec2/index.ts +9 -0
  143. package/src/aws/ec2/internet-gateway-attachment.ts +468 -0
  144. package/src/aws/ec2/internet-gateway.ts +557 -0
  145. package/src/aws/ec2/nat-gateway.ts +1123 -0
  146. package/src/aws/ec2/route-table-association.ts +747 -0
  147. package/src/aws/ec2/route-table.ts +452 -0
  148. package/src/aws/ec2/route.ts +447 -0
  149. package/src/aws/ec2/security-group-rule.ts +229 -0
  150. package/src/aws/ec2/security-group.ts +418 -0
  151. package/src/aws/ec2/subnet.ts +625 -0
  152. package/src/aws/ec2/utils.ts +102 -0
  153. package/src/aws/ec2/vpc.ts +969 -0
  154. package/src/aws/index.ts +9 -0
  155. package/src/aws/utils.ts +137 -0
  156. package/src/build-date.ts +8 -0
  157. package/src/cloudflare/astro.ts +4 -3
  158. package/src/cloudflare/bundle/externals.ts +36 -0
  159. package/src/cloudflare/bundle/internal-worker-bundle.ts +12 -3
  160. package/src/cloudflare/bundle/plugin-hot-reload.ts +32 -12
  161. package/src/cloudflare/bundle/plugin-hybrid-node-compat.ts +6 -1
  162. package/src/cloudflare/bundle/plugin-wasm.ts +26 -54
  163. package/src/cloudflare/bundle/validate-node-compat.ts +6 -5
  164. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  165. package/src/cloudflare/d1-database.ts +1 -1
  166. package/src/cloudflare/d1-migrations.ts +19 -18
  167. package/src/cloudflare/miniflare/miniflare-worker-options.ts +27 -13
  168. package/src/cloudflare/miniflare/miniflare.ts +2 -4
  169. package/src/cloudflare/miniflare/remote-binding-proxy.ts +5 -7
  170. package/src/cloudflare/react-router.ts +14 -6
  171. package/src/cloudflare/worker-bundle.ts +364 -0
  172. package/src/cloudflare/worker.ts +104 -241
  173. package/src/state/cloudflare-state-store.ts +1 -5
  174. package/src/state/dofs-state-store/store.ts +4 -4
  175. package/src/util/params.ts +33 -0
  176. package/src/util/timeout.ts +178 -0
  177. package/src/util/xml.ts +7 -0
  178. package/templates/nuxt/package.json +1 -1
  179. package/templates/nuxt/types/env.d.ts +11 -0
  180. package/templates/react-router/workers/app.ts +2 -2
  181. package/templates/rwsdk/.cursor/rules/alchemy_cloudflare.mdc +385 -0
  182. package/templates/rwsdk/wrangler.jsonc +10 -7
  183. package/templates/typescript/tsconfig.json +1 -2
  184. package/bin/alchemy.mjs +0 -51766
  185. package/lib/cloudflare/bundle/esbuild.d.ts +0 -16
  186. package/lib/cloudflare/bundle/esbuild.d.ts.map +0 -1
  187. package/lib/cloudflare/bundle/esbuild.js +0 -159
  188. package/lib/cloudflare/bundle/esbuild.js.map +0 -1
  189. package/lib/cloudflare/bundle/fs.d.ts +0 -18
  190. package/lib/cloudflare/bundle/fs.d.ts.map +0 -1
  191. package/lib/cloudflare/bundle/fs.js +0 -78
  192. package/lib/cloudflare/bundle/fs.js.map +0 -1
  193. package/lib/cloudflare/bundle/index.d.ts +0 -3
  194. package/lib/cloudflare/bundle/index.d.ts.map +0 -1
  195. package/lib/cloudflare/bundle/index.js +0 -2
  196. package/lib/cloudflare/bundle/index.js.map +0 -1
  197. package/lib/cloudflare/bundle/inline.d.ts +0 -12
  198. package/lib/cloudflare/bundle/inline.d.ts.map +0 -1
  199. package/lib/cloudflare/bundle/inline.js +0 -32
  200. package/lib/cloudflare/bundle/inline.js.map +0 -1
  201. package/lib/cloudflare/bundle/normalize.d.ts +0 -21
  202. package/lib/cloudflare/bundle/normalize.d.ts.map +0 -1
  203. package/lib/cloudflare/bundle/normalize.js +0 -49
  204. package/lib/cloudflare/bundle/normalize.js.map +0 -1
  205. package/lib/cloudflare/bundle/shared.d.ts +0 -30
  206. package/lib/cloudflare/bundle/shared.d.ts.map +0 -1
  207. package/lib/cloudflare/bundle/shared.js +0 -34
  208. package/lib/cloudflare/bundle/shared.js.map +0 -1
  209. package/src/cloudflare/bundle/esbuild.ts +0 -194
  210. package/src/cloudflare/bundle/fs.ts +0 -103
  211. package/src/cloudflare/bundle/index.ts +0 -6
  212. package/src/cloudflare/bundle/inline.ts +0 -42
  213. package/src/cloudflare/bundle/normalize.ts +0 -76
  214. package/src/cloudflare/bundle/shared.ts +0 -73
@@ -0,0 +1,625 @@
1
+ import type { AwsClient } from "aws4fetch";
2
+ import { XMLParser } from "fast-xml-parser";
3
+ import type { Context } from "../../context.ts";
4
+ import { Resource } from "../../resource.ts";
5
+ import { ignore } from "../../util/ignore.ts";
6
+ import { logger } from "../../util/logger.ts";
7
+ import {
8
+ mergeTimeoutConfig,
9
+ type TimeoutConfig,
10
+ waitForResourceState,
11
+ } from "../../util/timeout.ts";
12
+ import { retry } from "../retry.ts";
13
+ import { callEC2Api, createEC2Client } from "./utils.ts";
14
+ import type { Vpc } from "./vpc.ts";
15
+
16
+ /**
17
+ * Properties for creating or updating a Subnet
18
+ */
19
+ export interface SubnetProps {
20
+ /**
21
+ * The VPC to create the subnet in
22
+ */
23
+ vpc: Vpc | string;
24
+
25
+ /**
26
+ * The CIDR block for the subnet
27
+ */
28
+ cidrBlock: string;
29
+
30
+ /**
31
+ * The availability zone for the subnet
32
+ */
33
+ availabilityZone: string;
34
+
35
+ /**
36
+ * Whether instances launched in this subnet should be assigned a public IP address
37
+ * @default false
38
+ */
39
+ mapPublicIpOnLaunch?: boolean;
40
+
41
+ /**
42
+ * Tags to apply to the subnet
43
+ */
44
+ tags?: Record<string, string>;
45
+
46
+ /**
47
+ * Timeout configuration for subnet operations
48
+ * @default Subnet-specific sensible defaults (30 attempts, 1000ms delay)
49
+ */
50
+ timeout?: Partial<TimeoutConfig>;
51
+ }
52
+
53
+ /**
54
+ * Output returned after Subnet creation/update
55
+ */
56
+ export interface Subnet extends Resource<"aws::Subnet">, SubnetProps {
57
+ /**
58
+ * The ID of the subnet
59
+ */
60
+ subnetId: string;
61
+
62
+ /**
63
+ * The ID of the VPC the subnet belongs to
64
+ */
65
+ vpcId: string;
66
+
67
+ /**
68
+ * The current state of the subnet
69
+ */
70
+ state: "pending" | "available";
71
+
72
+ /**
73
+ * The number of available IP addresses in the subnet
74
+ */
75
+ availableIpAddressCount: number;
76
+
77
+ /**
78
+ * Whether this is the default subnet for the availability zone
79
+ */
80
+ defaultForAz: boolean;
81
+ }
82
+
83
+ /**
84
+ * Wait for subnet to be in available state
85
+ */
86
+ async function waitForSubnetAvailable(
87
+ client: AwsClient,
88
+ subnetId: string,
89
+ timeoutConfig: TimeoutConfig,
90
+ ): Promise<void> {
91
+ const checkFunction = async () => {
92
+ const response = await callEC2Api<DescribeSubnetsResponse>(
93
+ client,
94
+ "DescribeSubnets",
95
+ parseSubnetXmlResponse,
96
+ {
97
+ "SubnetId.1": subnetId,
98
+ },
99
+ );
100
+ return response.Subnets?.[0];
101
+ };
102
+
103
+ const isReady = (subnet: AwsSubnet | undefined) => {
104
+ return subnet?.State === "available";
105
+ };
106
+
107
+ await waitForResourceState(
108
+ checkFunction,
109
+ isReady,
110
+ timeoutConfig,
111
+ subnetId,
112
+ "Subnet",
113
+ "is now available",
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Wait for Subnet to be deleted
119
+ */
120
+ async function waitForSubnetDeleted(
121
+ client: AwsClient,
122
+ subnetId: string,
123
+ timeoutConfig: TimeoutConfig,
124
+ ): Promise<void> {
125
+ const checkFunction = async () => {
126
+ try {
127
+ const response = await callEC2Api<DescribeSubnetsResponse>(
128
+ client,
129
+ "DescribeSubnets",
130
+ parseSubnetXmlResponse,
131
+ {
132
+ "SubnetId.1": subnetId,
133
+ },
134
+ );
135
+ return response.Subnets?.[0];
136
+ } catch (error: any) {
137
+ // If Subnet is not found, it's been deleted - return undefined
138
+ if (
139
+ error.code === "InvalidSubnetID.NotFound" ||
140
+ error.code === "InvalidSubnet.NotFound"
141
+ ) {
142
+ return undefined;
143
+ }
144
+ throw error;
145
+ }
146
+ };
147
+
148
+ const isReady = (subnet: AwsSubnet | undefined) => {
149
+ // Subnet is deleted if it doesn't exist
150
+ return !subnet;
151
+ };
152
+
153
+ await waitForResourceState(
154
+ checkFunction,
155
+ isReady,
156
+ timeoutConfig,
157
+ subnetId,
158
+ "Subnet",
159
+ "be deleted",
160
+ );
161
+ }
162
+
163
+ /**
164
+ * AWS Subnet Resource
165
+ *
166
+ * Creates and manages subnets within a VPC. Subnets are used to segment
167
+ * your VPC into smaller networks for organizing resources.
168
+ *
169
+ * @example
170
+ * // Create a public subnet for web servers
171
+ * const publicSubnet = await Subnet("public-subnet", {
172
+ * vpc: myVpc,
173
+ * cidrBlock: "10.0.1.0/24",
174
+ * availabilityZone: process.env.AWS_REGION ? `${process.env.AWS_REGION}a` : "us-east-1a",
175
+ * mapPublicIpOnLaunch: true,
176
+ * tags: {
177
+ * Name: "public-subnet",
178
+ * Type: "public"
179
+ * }
180
+ * });
181
+ *
182
+ * @example
183
+ * // Create a private subnet for databases
184
+ * const privateSubnet = await Subnet("private-subnet", {
185
+ * vpc: myVpc,
186
+ * cidrBlock: "10.0.2.0/24",
187
+ * availabilityZone: "us-west-2b",
188
+ * mapPublicIpOnLaunch: false,
189
+ * tags: {
190
+ * Name: "private-subnet",
191
+ * Type: "private"
192
+ * }
193
+ * });
194
+ *
195
+ * @example
196
+ * // Create a subnet with custom timeout configuration
197
+ * const customSubnet = await Subnet("custom-subnet", {
198
+ * vpc: myVpc,
199
+ * cidrBlock: "10.0.3.0/24",
200
+ * availabilityZone: "us-west-2c",
201
+ * timeout: {
202
+ * maxAttempts: 50,
203
+ * delayMs: 500
204
+ * }
205
+ * });
206
+ */
207
+ export const Subnet = Resource(
208
+ "aws::Subnet",
209
+ async function (
210
+ this: Context<Subnet>,
211
+ _id: string,
212
+ props: SubnetProps,
213
+ ): Promise<Subnet> {
214
+ const client = await createEC2Client();
215
+ const vpcId = typeof props.vpc === "string" ? props.vpc : props.vpc.vpcId;
216
+
217
+ // Validate timeout config early, but use default for deletion if invalid
218
+ let timeoutConfig: TimeoutConfig;
219
+ try {
220
+ timeoutConfig = mergeTimeoutConfig(SUBNET_TIMEOUT, props.timeout);
221
+ } catch (error) {
222
+ if (this.phase === "delete") {
223
+ // Use default timeout for deletion to avoid blocking cleanup
224
+ timeoutConfig = SUBNET_TIMEOUT;
225
+ } else {
226
+ // Re-throw validation error during creation/update
227
+ throw error;
228
+ }
229
+ }
230
+
231
+ if (this.phase === "delete") {
232
+ if (this.output?.subnetId) {
233
+ logger.log(`🗑️ Deleting Subnet: ${this.output.subnetId}`);
234
+ await retry(async () => {
235
+ await ignore("InvalidSubnetID.NotFound", async () => {
236
+ await callEC2Api(client, "DeleteSubnet", parseSubnetXmlResponse, {
237
+ SubnetId: this.output!.subnetId,
238
+ });
239
+ });
240
+ });
241
+
242
+ // Wait for Subnet to be fully deleted to avoid dependency violations
243
+ logger.log(
244
+ ` Waiting for Subnet ${this.output.subnetId} to be fully deleted...`,
245
+ );
246
+ await waitForSubnetDeleted(client, this.output.subnetId, timeoutConfig);
247
+ logger.log(` ✅ Subnet ${this.output.subnetId} deletion completed`);
248
+ }
249
+ return this.destroy();
250
+ }
251
+
252
+ let subnet: AwsSubnet;
253
+
254
+ if (this.phase === "update" && this.output?.subnetId) {
255
+ // Get existing subnet
256
+ const response = await callEC2Api<DescribeSubnetsResponse>(
257
+ client,
258
+ "DescribeSubnets",
259
+ parseSubnetXmlResponse,
260
+ {
261
+ "SubnetId.1": this.output.subnetId,
262
+ },
263
+ );
264
+
265
+ if (!response.Subnets?.[0]) {
266
+ throw new Error(`Subnet ${this.output.subnetId} not found`);
267
+ }
268
+
269
+ subnet = response.Subnets[0];
270
+
271
+ // Update public IP assignment if it has changed
272
+ if (props.mapPublicIpOnLaunch !== undefined) {
273
+ await callEC2Api(
274
+ client,
275
+ "ModifySubnetAttribute",
276
+ parseSubnetXmlResponse,
277
+ {
278
+ SubnetId: subnet.SubnetId,
279
+ "MapPublicIpOnLaunch.Value": props.mapPublicIpOnLaunch.toString(),
280
+ },
281
+ );
282
+ }
283
+
284
+ // Update tags if provided
285
+ if (props.tags) {
286
+ const tagParams: Record<string, string> = {};
287
+ Object.entries(props.tags).forEach(([key, value], index) => {
288
+ tagParams[`Tag.${index + 1}.Key`] = key;
289
+ tagParams[`Tag.${index + 1}.Value`] = value;
290
+ });
291
+
292
+ await callEC2Api(client, "CreateTags", parseSubnetXmlResponse, {
293
+ "ResourceId.1": subnet.SubnetId,
294
+ ...tagParams,
295
+ });
296
+ }
297
+ } else {
298
+ // Create new subnet
299
+ const createSubnetParams: CreateSubnetParams = {
300
+ VpcId: vpcId,
301
+ CidrBlock: props.cidrBlock,
302
+ AvailabilityZone: props.availabilityZone,
303
+ };
304
+
305
+ // Add tags if specified
306
+ if (props.tags) {
307
+ createSubnetParams.TagSpecifications = [
308
+ {
309
+ ResourceType: "subnet",
310
+ Tags: Object.entries(props.tags).map(([key, value]) => ({
311
+ Key: key,
312
+ Value: value,
313
+ })),
314
+ },
315
+ ];
316
+ }
317
+
318
+ const createParams =
319
+ convertCreateSubnetParamsToAwsFormat(createSubnetParams);
320
+
321
+ const response = await callEC2Api<CreateSubnetResponse>(
322
+ client,
323
+ "CreateSubnet",
324
+ parseSubnetXmlResponse,
325
+ createParams,
326
+ );
327
+
328
+ if (!response.Subnet) {
329
+ throw new Error("Failed to create subnet");
330
+ }
331
+
332
+ subnet = response.Subnet;
333
+
334
+ // Set public IP assignment if specified
335
+ if (props.mapPublicIpOnLaunch !== undefined) {
336
+ await callEC2Api(
337
+ client,
338
+ "ModifySubnetAttribute",
339
+ parseSubnetXmlResponse,
340
+ {
341
+ SubnetId: subnet.SubnetId,
342
+ "MapPublicIpOnLaunch.Value": props.mapPublicIpOnLaunch.toString(),
343
+ },
344
+ );
345
+ }
346
+
347
+ // Wait for subnet to be available
348
+ await waitForSubnetAvailable(client, subnet.SubnetId, timeoutConfig);
349
+ }
350
+
351
+ return this({
352
+ subnetId: subnet.SubnetId,
353
+ vpcId: subnet.VpcId,
354
+ state: subnet.State as "pending" | "available",
355
+ availableIpAddressCount: subnet.AvailableIpAddressCount,
356
+ defaultForAz: subnet.DefaultForAz,
357
+ ...props,
358
+ vpc: vpcId,
359
+ });
360
+ },
361
+ );
362
+
363
+ /**
364
+ * Parse XML responses specifically for Subnet operations
365
+ */
366
+ function parseSubnetXmlResponse<
367
+ T extends
368
+ | CreateSubnetResponse
369
+ | DescribeSubnetsResponse
370
+ | { success: boolean },
371
+ >(xmlText: string): T {
372
+ const parser = new XMLParser({
373
+ ignoreAttributes: true,
374
+ parseAttributeValue: true,
375
+ parseTagValue: true,
376
+ trimValues: true,
377
+ });
378
+
379
+ const parsed = parser.parse(xmlText);
380
+ const result: Record<string, unknown> = {};
381
+
382
+ // Parse CreateSubnetResponse
383
+ if (parsed.CreateSubnetResponse) {
384
+ const subnet = parsed.CreateSubnetResponse.subnet as SubnetXmlResult;
385
+ if (subnet) {
386
+ result.Subnet = {
387
+ SubnetId: subnet.subnetId,
388
+ VpcId: subnet.vpcId,
389
+ State: subnet.state,
390
+ CidrBlock: subnet.cidrBlock,
391
+ AvailabilityZone: subnet.availabilityZone,
392
+ AvailableIpAddressCount: subnet.availableIpAddressCount,
393
+ DefaultForAz: subnet.defaultForAz,
394
+ MapPublicIpOnLaunch: subnet.mapPublicIpOnLaunch,
395
+ };
396
+ }
397
+ }
398
+
399
+ // Parse DescribeSubnetsResponse
400
+ if (parsed.DescribeSubnetsResponse) {
401
+ const subnetSet = parsed.DescribeSubnetsResponse.subnetSet;
402
+ if (subnetSet?.item) {
403
+ const subnets = Array.isArray(subnetSet.item)
404
+ ? subnetSet.item
405
+ : [subnetSet.item];
406
+ result.Subnets = subnets.map((subnet: SubnetXmlResult) => ({
407
+ SubnetId: subnet.subnetId,
408
+ VpcId: subnet.vpcId,
409
+ State: subnet.state,
410
+ CidrBlock: subnet.cidrBlock,
411
+ AvailabilityZone: subnet.availabilityZone,
412
+ AvailableIpAddressCount: subnet.availableIpAddressCount,
413
+ DefaultForAz: subnet.defaultForAz,
414
+ MapPublicIpOnLaunch: subnet.mapPublicIpOnLaunch,
415
+ }));
416
+ } else {
417
+ result.Subnets = [];
418
+ }
419
+ }
420
+
421
+ // Generic success responses
422
+ const successResponses = [
423
+ "ModifySubnetAttributeResponse",
424
+ "DeleteSubnetResponse",
425
+ ];
426
+
427
+ for (const responseType of successResponses) {
428
+ if (parsed[responseType]) {
429
+ result.success = true;
430
+ break;
431
+ }
432
+ }
433
+
434
+ return result as T;
435
+ }
436
+
437
+ /**
438
+ * Subnet timeout constants
439
+ */
440
+ export const SUBNET_TIMEOUT: TimeoutConfig = {
441
+ maxAttempts: 30,
442
+ delayMs: 1000, // 1 second - Subnets are quick
443
+ };
444
+
445
+ /**
446
+ * Comprehensive AWS Subnet API Parameter Types
447
+ */
448
+
449
+ // Common structures
450
+ interface TagSpecification {
451
+ ResourceType: string;
452
+ Tags: Array<{
453
+ Key: string;
454
+ Value: string;
455
+ }>;
456
+ }
457
+
458
+ // Subnet API Request Parameters
459
+ interface CreateSubnetParams {
460
+ VpcId: string;
461
+ CidrBlock?: string;
462
+ Ipv6CidrBlock?: string;
463
+ AvailabilityZone?: string;
464
+ AvailabilityZoneId?: string;
465
+ TagSpecifications?: TagSpecification[];
466
+ OutpostArn?: string;
467
+ CustomerOwnedIpv4Pool?: string;
468
+ MapPublicIpOnLaunch?: boolean;
469
+ MapCustomerOwnedIpOnLaunch?: boolean;
470
+ Ipv4IpamPoolId?: string;
471
+ Ipv4NetmaskLength?: number;
472
+ Ipv6IpamPoolId?: string;
473
+ Ipv6NetmaskLength?: number;
474
+ DryRun?: boolean;
475
+ }
476
+
477
+ // Subnet XML Response Types
478
+ interface SubnetXmlTag {
479
+ key: string;
480
+ value: string;
481
+ }
482
+
483
+ interface SubnetXmlResult {
484
+ subnetId: string;
485
+ state: "pending" | "available" | "unavailable";
486
+ vpcId: string;
487
+ cidrBlock: string;
488
+ ipv6CidrBlockAssociationSet?: Array<{
489
+ associationId: string;
490
+ ipv6CidrBlock: string;
491
+ ipv6CidrBlockState: {
492
+ state:
493
+ | "associating"
494
+ | "associated"
495
+ | "disassociating"
496
+ | "disassociated"
497
+ | "failing"
498
+ | "failed";
499
+ statusMessage?: string;
500
+ };
501
+ }>;
502
+ availableIpAddressCount: number;
503
+ availabilityZone: string;
504
+ availabilityZoneId?: string;
505
+ defaultForAz: boolean;
506
+ mapPublicIpOnLaunch: boolean;
507
+ mapCustomerOwnedIpOnLaunch?: boolean;
508
+ customerOwnedIpv4Pool?: string;
509
+ tags?: SubnetXmlTag[];
510
+ assignIpv6AddressOnCreation: boolean;
511
+ privateDnsNameOptionsOnLaunch?: {
512
+ hostnameType: "ip-name" | "resource-name";
513
+ enableResourceNameDnsARecord: boolean;
514
+ enableResourceNameDnsAAAARecord: boolean;
515
+ };
516
+ enableDns64?: boolean;
517
+ ipv6Native?: boolean;
518
+ outpostArn?: string;
519
+ ownerId?: string;
520
+ }
521
+
522
+ /**
523
+ * Helper functions to convert typed parameters to AWS API format
524
+ */
525
+ function convertCreateSubnetParamsToAwsFormat(
526
+ params: CreateSubnetParams,
527
+ ): Record<string, string> {
528
+ const awsParams: Record<string, string> = {
529
+ VpcId: params.VpcId,
530
+ };
531
+
532
+ if (params.CidrBlock) {
533
+ awsParams.CidrBlock = params.CidrBlock;
534
+ }
535
+
536
+ if (params.Ipv6CidrBlock) {
537
+ awsParams.Ipv6CidrBlock = params.Ipv6CidrBlock;
538
+ }
539
+
540
+ if (params.AvailabilityZone) {
541
+ awsParams.AvailabilityZone = params.AvailabilityZone;
542
+ }
543
+
544
+ if (params.AvailabilityZoneId) {
545
+ awsParams.AvailabilityZoneId = params.AvailabilityZoneId;
546
+ }
547
+
548
+ if (params.OutpostArn) {
549
+ awsParams.OutpostArn = params.OutpostArn;
550
+ }
551
+
552
+ if (params.CustomerOwnedIpv4Pool) {
553
+ awsParams.CustomerOwnedIpv4Pool = params.CustomerOwnedIpv4Pool;
554
+ }
555
+
556
+ if (params.MapPublicIpOnLaunch !== undefined) {
557
+ awsParams.MapPublicIpOnLaunch = params.MapPublicIpOnLaunch.toString();
558
+ }
559
+
560
+ if (params.MapCustomerOwnedIpOnLaunch !== undefined) {
561
+ awsParams.MapCustomerOwnedIpOnLaunch =
562
+ params.MapCustomerOwnedIpOnLaunch.toString();
563
+ }
564
+
565
+ if (params.Ipv4IpamPoolId) {
566
+ awsParams.Ipv4IpamPoolId = params.Ipv4IpamPoolId;
567
+ }
568
+
569
+ if (params.Ipv4NetmaskLength !== undefined) {
570
+ awsParams.Ipv4NetmaskLength = params.Ipv4NetmaskLength.toString();
571
+ }
572
+
573
+ if (params.Ipv6IpamPoolId) {
574
+ awsParams.Ipv6IpamPoolId = params.Ipv6IpamPoolId;
575
+ }
576
+
577
+ if (params.Ipv6NetmaskLength !== undefined) {
578
+ awsParams.Ipv6NetmaskLength = params.Ipv6NetmaskLength.toString();
579
+ }
580
+
581
+ if (params.DryRun !== undefined) {
582
+ awsParams.DryRun = params.DryRun.toString();
583
+ }
584
+
585
+ // Handle TagSpecifications
586
+ if (params.TagSpecifications) {
587
+ params.TagSpecifications.forEach((tagSpec, specIndex) => {
588
+ const specNum = specIndex + 1;
589
+ awsParams[`TagSpecification.${specNum}.ResourceType`] =
590
+ tagSpec.ResourceType;
591
+
592
+ tagSpec.Tags.forEach((tag, tagIndex) => {
593
+ const tagNum = tagIndex + 1;
594
+ awsParams[`TagSpecification.${specNum}.Tag.${tagNum}.Key`] = tag.Key;
595
+ awsParams[`TagSpecification.${specNum}.Tag.${tagNum}.Value`] =
596
+ tag.Value;
597
+ });
598
+ });
599
+ }
600
+
601
+ return awsParams;
602
+ }
603
+
604
+ /**
605
+ * AWS Subnet API response types
606
+ */
607
+ interface AwsSubnet {
608
+ SubnetId: string;
609
+ VpcId: string;
610
+ State: string;
611
+ CidrBlock: string;
612
+ AvailabilityZone: string;
613
+ AvailableIpAddressCount: number;
614
+ DefaultForAz: boolean;
615
+ MapPublicIpOnLaunch: boolean;
616
+ Tags?: Array<{ Key: string; Value: string }>;
617
+ }
618
+
619
+ interface CreateSubnetResponse {
620
+ Subnet: AwsSubnet;
621
+ }
622
+
623
+ interface DescribeSubnetsResponse {
624
+ Subnets: AwsSubnet[];
625
+ }
@@ -0,0 +1,102 @@
1
+ import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
2
+ import { AwsClient } from "aws4fetch";
3
+ import { logger } from "../../util/logger.ts";
4
+ import { flattenParams } from "../../util/params.ts";
5
+ import { getRegion } from "../utils.ts";
6
+
7
+ /**
8
+ * Shared EC2 utilities for all EC2 resources
9
+ */
10
+
11
+ /**
12
+ * Create an AWS EC2 client
13
+ */
14
+ export async function createEC2Client(): Promise<AwsClient> {
15
+ const credentials = await fromNodeProviderChain()();
16
+ const region = await getRegion();
17
+
18
+ return new AwsClient({
19
+ ...credentials,
20
+ service: "ec2",
21
+ region,
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Make an EC2 API call with custom response parser
27
+ */
28
+ export async function callEC2Api<T>(
29
+ client: AwsClient,
30
+ action: string,
31
+ responseParser: (xmlText: string) => T,
32
+ params: Record<string, any> = {},
33
+ ): Promise<T> {
34
+ // Try the API call, and retry once with fresh credentials on auth failure
35
+ for (let attempt = 1; attempt <= 2; attempt++) {
36
+ try {
37
+ const region = await getRegion();
38
+ const url = `https://ec2.${region}.amazonaws.com/`;
39
+
40
+ const body = new URLSearchParams({
41
+ Action: action,
42
+ Version: "2016-11-15",
43
+ ...flattenParams(params),
44
+ });
45
+
46
+ const signedRequest = await client.sign(url, {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/x-www-form-urlencoded",
50
+ },
51
+ body: body.toString(),
52
+ });
53
+
54
+ const response = await fetch(signedRequest);
55
+
56
+ if (!response.ok) {
57
+ const errorText = await response.text();
58
+
59
+ // Parse AWS error code from XML response
60
+ const errorCodeMatch = errorText.match(/<Code>([^<]+)<\/Code>/);
61
+ const errorCode = errorCodeMatch ? errorCodeMatch[1] : null;
62
+
63
+ // If this is an auth failure and we haven't retried yet, create a fresh client and retry
64
+ if (errorCode === "AuthFailure" && attempt === 1) {
65
+ logger.log(
66
+ `🔄 Auth failure detected, refreshing credentials and retrying ${action}...`,
67
+ );
68
+ // Create a fresh client with new credentials
69
+ client = await createEC2Client();
70
+ continue; // Retry with fresh client
71
+ }
72
+
73
+ const error = new Error(
74
+ `EC2 API error: ${response.status} ${response.statusText}\n${errorText}`,
75
+ );
76
+
77
+ // Set the error code for the ignore function
78
+ if (errorCode) {
79
+ (error as any).code = errorCode;
80
+ }
81
+
82
+ throw error;
83
+ }
84
+
85
+ const xmlText = await response.text();
86
+ const parsed = responseParser(xmlText);
87
+ return parsed;
88
+ } catch (error) {
89
+ // If it's not an auth failure or we've already retried, throw the error
90
+ if (attempt === 2 || !(error as any)?.message?.includes("AuthFailure")) {
91
+ throw error;
92
+ }
93
+ // Otherwise, continue to retry
94
+ }
95
+ }
96
+
97
+ // This should never be reached, but TypeScript needs it
98
+ throw new Error("Unexpected error in API call retry logic");
99
+ }
100
+
101
+ // Re-export getRegion for convenience
102
+ export { getRegion };