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,447 @@
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 type { InternetGateway } from "./internet-gateway.ts";
14
+ import type { NatGateway } from "./nat-gateway.ts";
15
+ import type { RouteTable } from "./route-table.ts";
16
+ import { callEC2Api, createEC2Client } from "./utils.ts";
17
+
18
+ /**
19
+ * Properties for creating or updating a Route
20
+ */
21
+ export interface RouteProps {
22
+ /**
23
+ * The route table to add the route to
24
+ */
25
+ routeTable: RouteTable | string;
26
+
27
+ /**
28
+ * The destination CIDR block for the route
29
+ * @example "0.0.0.0/0" for default route
30
+ */
31
+ destinationCidrBlock: string;
32
+
33
+ /**
34
+ * The target for the route (one of the following)
35
+ */
36
+ target:
37
+ | { internetGateway: InternetGateway | string }
38
+ | { natGateway: NatGateway | string }
39
+ | { instanceId: string }
40
+ | { networkInterfaceId: string }
41
+ | { vpcPeeringConnectionId: string }
42
+ | { transitGatewayId: string };
43
+
44
+ /**
45
+ * Timeout configuration for Route operations
46
+ * @default Route-specific sensible defaults (30 attempts, 1000ms delay)
47
+ */
48
+ timeout?: Partial<TimeoutConfig>;
49
+ }
50
+
51
+ /**
52
+ * Output returned after Route creation/update
53
+ */
54
+ export interface Route extends Resource<"aws::Route">, RouteProps {
55
+ /**
56
+ * The ID of the route table
57
+ */
58
+ routeTableId: string;
59
+
60
+ /**
61
+ * The current state of the route
62
+ */
63
+ state: "active" | "blackhole";
64
+
65
+ /**
66
+ * The origin of the route
67
+ */
68
+ origin: "CreateRouteTable" | "CreateRoute" | "EnableVgwRoutePropagation";
69
+ }
70
+
71
+ /**
72
+ * AWS Route Resource
73
+ *
74
+ * Creates and manages individual routes within route tables, directing traffic
75
+ * to various targets like Internet Gateways, NAT Gateways, or instances.
76
+ *
77
+ * @example
78
+ * // Create a default route to Internet Gateway
79
+ * const publicRoute = await Route("public-default-route", {
80
+ * routeTable: publicRouteTable,
81
+ * destinationCidrBlock: "0.0.0.0/0",
82
+ * target: { internetGateway: mainIgw }
83
+ * });
84
+ *
85
+ * @example
86
+ * // Create a default route to NAT Gateway for private subnets
87
+ * const privateRoute = await Route("private-default-route", {
88
+ * routeTable: privateRouteTable,
89
+ * destinationCidrBlock: "0.0.0.0/0",
90
+ * target: { natGateway: mainNat }
91
+ * });
92
+ *
93
+ * @example
94
+ * // Create a route to a specific instance
95
+ * const instanceRoute = await Route("instance-route", {
96
+ * routeTable: "rtb-12345678",
97
+ * destinationCidrBlock: "10.1.0.0/16",
98
+ * target: { instanceId: "i-12345678" }
99
+ * });
100
+ *
101
+ * @example
102
+ * // Create a route to a network interface
103
+ * const eniRoute = await Route("eni-route", {
104
+ * routeTable: mainRouteTable,
105
+ * destinationCidrBlock: "192.168.1.0/24",
106
+ * target: { networkInterfaceId: "eni-12345678" }
107
+ * });
108
+ *
109
+ * @example
110
+ * // Create a route to a VPC peering connection
111
+ * const peeringRoute = await Route("peering-route", {
112
+ * routeTable: mainRouteTable,
113
+ * destinationCidrBlock: "10.2.0.0/16",
114
+ * target: { vpcPeeringConnectionId: "pcx-12345678" }
115
+ * });
116
+ *
117
+ * @example
118
+ * // Create a route to a Transit Gateway
119
+ * const transitRoute = await Route("transit-route", {
120
+ * routeTable: mainRouteTable,
121
+ * destinationCidrBlock: "10.3.0.0/16",
122
+ * target: { transitGatewayId: "tgw-12345678" }
123
+ * });
124
+ *
125
+ * @example
126
+ * // Create a route with custom timeout configuration
127
+ * const customRoute = await Route("custom-route", {
128
+ * routeTable: mainRouteTable,
129
+ * destinationCidrBlock: "172.16.0.0/16",
130
+ * target: { internetGateway: "igw-12345678" },
131
+ * timeout: {
132
+ * maxAttempts: 60,
133
+ * delayMs: 2000
134
+ * }
135
+ * });
136
+ */
137
+ export const Route = Resource(
138
+ "aws::Route",
139
+ async function (
140
+ this: Context<Route>,
141
+ _id: string,
142
+ props: RouteProps,
143
+ ): Promise<Route> {
144
+ const client = await createEC2Client();
145
+ const _timeoutConfig = mergeTimeoutConfig(ROUTE_TIMEOUT, props.timeout);
146
+ const routeTableId =
147
+ typeof props.routeTable === "string"
148
+ ? props.routeTable
149
+ : props.routeTable.routeTableId;
150
+
151
+ if (this.phase === "delete") {
152
+ if (this.output?.routeTableId) {
153
+ logger.log(
154
+ `🗑️ Deleting Route: ${this.output.destinationCidrBlock} from Route Table ${this.output.routeTableId}`,
155
+ );
156
+ await retry(async () => {
157
+ await ignore("InvalidRoute.NotFound", async () => {
158
+ await callEC2Api(client, "DeleteRoute", parseRouteXmlResponse, {
159
+ RouteTableId: this.output!.routeTableId,
160
+ DestinationCidrBlock: this.output!.destinationCidrBlock,
161
+ });
162
+ });
163
+ });
164
+
165
+ // Wait for route to be fully deleted
166
+ logger.log(
167
+ ` Waiting for Route ${this.output.destinationCidrBlock} to be fully deleted...`,
168
+ );
169
+ await waitForRouteDeleted(
170
+ client,
171
+ this.output.routeTableId,
172
+ this.output.destinationCidrBlock,
173
+ _timeoutConfig,
174
+ );
175
+ logger.log(
176
+ ` ✅ Route ${this.output.destinationCidrBlock} deletion completed`,
177
+ );
178
+ }
179
+ return this.destroy();
180
+ }
181
+
182
+ // Prepare route parameters
183
+ const routeParams: Record<string, string> = {
184
+ RouteTableId: routeTableId,
185
+ DestinationCidrBlock: props.destinationCidrBlock,
186
+ };
187
+
188
+ // Set the target based on the type
189
+ if ("internetGateway" in props.target) {
190
+ const igwId =
191
+ typeof props.target.internetGateway === "string"
192
+ ? props.target.internetGateway
193
+ : props.target.internetGateway.internetGatewayId;
194
+ routeParams.GatewayId = igwId;
195
+ } else if ("natGateway" in props.target) {
196
+ const natId =
197
+ typeof props.target.natGateway === "string"
198
+ ? props.target.natGateway
199
+ : props.target.natGateway.natGatewayId;
200
+ routeParams.NatGatewayId = natId;
201
+ } else if ("instanceId" in props.target) {
202
+ routeParams.InstanceId = props.target.instanceId;
203
+ } else if ("networkInterfaceId" in props.target) {
204
+ routeParams.NetworkInterfaceId = props.target.networkInterfaceId;
205
+ } else if ("vpcPeeringConnectionId" in props.target) {
206
+ routeParams.VpcPeeringConnectionId = props.target.vpcPeeringConnectionId;
207
+ } else if ("transitGatewayId" in props.target) {
208
+ routeParams.TransitGatewayId = props.target.transitGatewayId;
209
+ }
210
+
211
+ if (this.phase === "update" && this.output?.routeTableId) {
212
+ // Check if route exists
213
+ const response = await callEC2Api<DescribeRouteTablesResponse>(
214
+ client,
215
+ "DescribeRouteTables",
216
+ parseRouteXmlResponse,
217
+ {
218
+ "RouteTableId.1": routeTableId,
219
+ },
220
+ );
221
+
222
+ const routeTable = response.RouteTables?.[0];
223
+ const existingRoute = routeTable?.Routes?.find(
224
+ (r) => r.DestinationCidrBlock === props.destinationCidrBlock,
225
+ );
226
+
227
+ if (existingRoute) {
228
+ // Replace existing route
229
+ await callEC2Api(
230
+ client,
231
+ "ReplaceRoute",
232
+ parseRouteXmlResponse,
233
+ routeParams,
234
+ );
235
+ } else {
236
+ // Create new route
237
+ await callEC2Api(
238
+ client,
239
+ "CreateRoute",
240
+ parseRouteXmlResponse,
241
+ routeParams,
242
+ );
243
+ }
244
+ } else {
245
+ // Create new route
246
+ await callEC2Api(
247
+ client,
248
+ "CreateRoute",
249
+ parseRouteXmlResponse,
250
+ routeParams,
251
+ );
252
+ }
253
+
254
+ // Wait a moment for the route to be fully created
255
+ await new Promise((resolve) => setTimeout(resolve, 2000));
256
+
257
+ // Get the created/updated route to return current state
258
+ const response = await callEC2Api<DescribeRouteTablesResponse>(
259
+ client,
260
+ "DescribeRouteTables",
261
+ parseRouteXmlResponse,
262
+ {
263
+ "RouteTableId.1": routeTableId,
264
+ },
265
+ );
266
+
267
+ const routeTable = response.RouteTables?.[0];
268
+ const route = routeTable?.Routes?.find(
269
+ (r) => r.DestinationCidrBlock === props.destinationCidrBlock,
270
+ );
271
+
272
+ if (!route) {
273
+ throw new Error("Failed to find created route");
274
+ }
275
+
276
+ return this({
277
+ routeTableId,
278
+ state: route.State as "active" | "blackhole",
279
+ origin: route.Origin as
280
+ | "CreateRouteTable"
281
+ | "CreateRoute"
282
+ | "EnableVgwRoutePropagation",
283
+ ...props,
284
+ routeTable: routeTableId,
285
+ });
286
+ },
287
+ );
288
+
289
+ /**
290
+ * Wait for Route to be deleted
291
+ */
292
+ async function waitForRouteDeleted(
293
+ client: AwsClient,
294
+ routeTableId: string,
295
+ destinationCidrBlock: string,
296
+ timeoutConfig: TimeoutConfig,
297
+ ): Promise<void> {
298
+ const checkFunction = async () => {
299
+ try {
300
+ const response = await callEC2Api<DescribeRouteTablesResponse>(
301
+ client,
302
+ "DescribeRouteTables",
303
+ parseRouteXmlResponse,
304
+ {
305
+ "RouteTableId.1": routeTableId,
306
+ },
307
+ );
308
+
309
+ const routeTable = response.RouteTables?.[0];
310
+ const route = routeTable?.Routes?.find(
311
+ (r) => r.DestinationCidrBlock === destinationCidrBlock,
312
+ );
313
+
314
+ return route;
315
+ } catch (error: any) {
316
+ // If route table is not found, route is definitely deleted
317
+ if (
318
+ error.code === "InvalidRouteTableID.NotFound" ||
319
+ error.code === "InvalidRouteTable.NotFound"
320
+ ) {
321
+ return undefined;
322
+ }
323
+ throw error;
324
+ }
325
+ };
326
+
327
+ const isReady = (route: any) => {
328
+ // Route is deleted if it doesn't exist
329
+ return !route;
330
+ };
331
+
332
+ await waitForResourceState(
333
+ checkFunction,
334
+ isReady,
335
+ timeoutConfig,
336
+ `${routeTableId}/${destinationCidrBlock}`,
337
+ "Route",
338
+ "be deleted",
339
+ );
340
+ }
341
+
342
+ /**
343
+ * Route timeout constants
344
+ */
345
+ export const ROUTE_TIMEOUT: TimeoutConfig = {
346
+ maxAttempts: 30,
347
+ delayMs: 1000, // 1 second - Routes are fast resources
348
+ };
349
+
350
+ /**
351
+ * AWS Route API response types
352
+ */
353
+ interface DescribeRouteTablesResponse {
354
+ RouteTables: Array<{
355
+ RouteTableId: string;
356
+ VpcId: string;
357
+ Routes?: Array<{
358
+ DestinationCidrBlock?: string;
359
+ GatewayId?: string;
360
+ InstanceId?: string;
361
+ NatGatewayId?: string;
362
+ State: string;
363
+ Origin: string;
364
+ }>;
365
+ }>;
366
+ }
367
+
368
+ /**
369
+ * Raw XML parsing interfaces for type safety
370
+ */
371
+ interface RawRouteTableXmlItem {
372
+ routeTableId: string;
373
+ vpcId: string;
374
+ routeSet?: {
375
+ item?: Array<RawRouteXmlItem> | RawRouteXmlItem;
376
+ };
377
+ }
378
+
379
+ interface RawRouteXmlItem {
380
+ destinationCidrBlock?: string;
381
+ gatewayId?: string;
382
+ instanceId?: string;
383
+ networkInterfaceId?: string;
384
+ vpcPeeringConnectionId?: string;
385
+ natGatewayId?: string;
386
+ transitGatewayId?: string;
387
+ state: string;
388
+ origin: string;
389
+ }
390
+
391
+ /**
392
+ * Parse XML responses specifically for Route operations
393
+ */
394
+ function parseRouteXmlResponse<T>(xmlText: string): T {
395
+ const parser = new XMLParser({
396
+ ignoreAttributes: true,
397
+ parseAttributeValue: true,
398
+ parseTagValue: true,
399
+ trimValues: true,
400
+ });
401
+
402
+ const parsed = parser.parse(xmlText);
403
+ const result: Record<string, unknown> = {};
404
+
405
+ // Parse DescribeRouteTablesResponse
406
+ if (parsed.DescribeRouteTablesResponse) {
407
+ const routeTableSet = parsed.DescribeRouteTablesResponse.routeTableSet;
408
+ if (routeTableSet?.item) {
409
+ const routeTables = Array.isArray(routeTableSet.item)
410
+ ? routeTableSet.item
411
+ : [routeTableSet.item];
412
+ result.RouteTables = routeTables.map((rt: RawRouteTableXmlItem) => ({
413
+ RouteTableId: rt.routeTableId,
414
+ VpcId: rt.vpcId,
415
+ Routes: rt.routeSet?.item
416
+ ? (Array.isArray(rt.routeSet.item)
417
+ ? rt.routeSet.item
418
+ : [rt.routeSet.item]
419
+ ).map((route: RawRouteXmlItem) => ({
420
+ DestinationCidrBlock: route.destinationCidrBlock,
421
+ GatewayId: route.gatewayId,
422
+ InstanceId: route.instanceId,
423
+ NetworkInterfaceId: route.networkInterfaceId,
424
+ VpcPeeringConnectionId: route.vpcPeeringConnectionId,
425
+ NatGatewayId: route.natGatewayId,
426
+ TransitGatewayId: route.transitGatewayId,
427
+ State: route.state,
428
+ Origin: route.origin,
429
+ }))
430
+ : [],
431
+ }));
432
+ } else {
433
+ result.RouteTables = [];
434
+ }
435
+ }
436
+
437
+ // Handle success responses
438
+ if (
439
+ parsed.CreateRouteResponse ||
440
+ parsed.ReplaceRouteResponse ||
441
+ parsed.DeleteRouteResponse
442
+ ) {
443
+ result.success = true;
444
+ }
445
+
446
+ return result as T;
447
+ }
@@ -0,0 +1,229 @@
1
+ import { XMLParser } from "fast-xml-parser";
2
+ import type { Context } from "../../context.ts";
3
+ import { Resource } from "../../resource.ts";
4
+ import { ignore } from "../../util/ignore.ts";
5
+ import type { SecurityGroup } from "./security-group.ts";
6
+ import { callEC2Api, createEC2Client } from "./utils.ts";
7
+
8
+ /**
9
+ * Properties for creating or updating a Security Group Rule
10
+ */
11
+ export interface SecurityGroupRuleProps {
12
+ /**
13
+ * The security group to apply the rule to.
14
+ */
15
+ securityGroup: SecurityGroup | string;
16
+
17
+ /**
18
+ * The type of rule, either 'ingress' (inbound) or 'egress' (outbound).
19
+ */
20
+ type: "ingress" | "egress";
21
+
22
+ /**
23
+ * The IP protocol name (tcp, udp, icmp, icmpv6) or number.
24
+ * Use '-1' to specify all protocols.
25
+ */
26
+ protocol: string;
27
+
28
+ /**
29
+ * The start of the port range for the rule.
30
+ * For ICMP, this is the ICMP type number. A value of -1 indicates all types.
31
+ */
32
+ fromPort: number;
33
+
34
+ /**
35
+ * The end of the port range for the rule.
36
+ * For ICMP, this is the ICMP code. A value of -1 indicates all codes.
37
+ */
38
+ toPort: number;
39
+
40
+ /**
41
+ * The IPv4 CIDR ranges to allow.
42
+ */
43
+ cidrBlocks?: string[];
44
+
45
+ /**
46
+ * The source security group IDs to allow.
47
+ */
48
+ sourceSecurityGroups?: (SecurityGroup | string)[];
49
+
50
+ /**
51
+ * A description for the rule.
52
+ */
53
+ description?: string;
54
+ }
55
+
56
+ /**
57
+ * Output returned after Security Group Rule creation/update
58
+ */
59
+ export interface SecurityGroupRule
60
+ extends Resource<"aws::SecurityGroupRule">,
61
+ SecurityGroupRuleProps {
62
+ /**
63
+ * A unique identifier for the rule resource.
64
+ */
65
+ ruleId: string;
66
+ }
67
+
68
+ /**
69
+ * AWS Security Group Rule Resource
70
+ *
71
+ * Manages a single ingress or egress rule for a security group.
72
+ *
73
+ * @example
74
+ * // A rule to allow HTTP traffic from anywhere
75
+ * const httpRule = await SecurityGroupRule("web-sg-http-rule", {
76
+ * securityGroup: webSecurityGroup,
77
+ * type: "ingress",
78
+ * protocol: "tcp",
79
+ * fromPort: 80,
80
+ * toPort: 80,
81
+ * cidrBlocks: ["0.0.0.0/0"],
82
+ * description: "Allow HTTP access from anywhere"
83
+ * });
84
+ *
85
+ * @example
86
+ * // A rule to allow a database security group to receive traffic
87
+ * // from a web server security group on the MySQL port.
88
+ * const dbAccessRule = await SecurityGroupRule("db-access-from-web", {
89
+ * securityGroup: dbSecurityGroup,
90
+ * type: "ingress",
91
+ * protocol: "tcp",
92
+ * fromPort: 3306,
93
+ * toPort: 3306,
94
+ * sourceSecurityGroups: [webSecurityGroup],
95
+ * description: "Allow MySQL access from web servers"
96
+ * });
97
+ */
98
+ export const SecurityGroupRule = Resource(
99
+ "aws::SecurityGroupRule",
100
+ async function (
101
+ this: Context<SecurityGroupRule>,
102
+ _id: string,
103
+ props: SecurityGroupRuleProps,
104
+ ): Promise<SecurityGroupRule> {
105
+ const client = await createEC2Client();
106
+ const groupId =
107
+ typeof props.securityGroup === "string"
108
+ ? props.securityGroup
109
+ : props.securityGroup.groupId;
110
+
111
+ // A deterministic ID for the rule based on its properties
112
+ const ruleId = `${groupId}-${props.type}-${props.protocol}-${
113
+ props.fromPort
114
+ }-${props.toPort}-${(props.cidrBlocks || []).join("-")}-${(
115
+ props.sourceSecurityGroups || []
116
+ )
117
+ .map((sg) => (typeof sg === "string" ? sg : sg.groupId))
118
+ .join("-")}`;
119
+
120
+ if (this.phase === "delete") {
121
+ const revokeParams = convertRuleToApiParams(props, groupId);
122
+ const action =
123
+ props.type === "ingress"
124
+ ? "RevokeSecurityGroupIngress"
125
+ : "RevokeSecurityGroupEgress";
126
+
127
+ // Ignore errors if the rule doesn't exist, as it's already deleted.
128
+ await ignore("InvalidPermission.NotFound", async () => {
129
+ await callEC2Api(
130
+ client,
131
+ action,
132
+ parseSecurityGroupRuleXmlResponse,
133
+ revokeParams,
134
+ );
135
+ });
136
+
137
+ return this.destroy();
138
+ }
139
+
140
+ // For both create and update, we authorize the rule.
141
+ // The resource framework handles replacement by calling delete then create.
142
+ // The AuthorizeSecurityGroup* API calls are idempotent, so calling this on
143
+ // an existing rule will not cause an error.
144
+ const authorizeParams = convertRuleToApiParams(props, groupId);
145
+ const action =
146
+ props.type === "ingress"
147
+ ? "AuthorizeSecurityGroupIngress"
148
+ : "AuthorizeSecurityGroupEgress";
149
+
150
+ // There is no "pending" state for a security group rule. The API call
151
+ // is synchronous, and success is determined by the response.
152
+ await callEC2Api(
153
+ client,
154
+ action,
155
+ parseSecurityGroupRuleXmlResponse,
156
+ authorizeParams,
157
+ );
158
+
159
+ return this({
160
+ ruleId,
161
+ ...props,
162
+ });
163
+ },
164
+ );
165
+
166
+ // --- Helper Functions and Interfaces ---
167
+ /**
168
+ * Parse XML responses for Security Group Rule operations
169
+ */
170
+ function parseSecurityGroupRuleXmlResponse<T>(xmlText: string): T {
171
+ const parser = new XMLParser({
172
+ ignoreAttributes: true,
173
+ parseAttributeValue: true,
174
+ parseTagValue: true,
175
+ });
176
+ const parsed = parser.parse(xmlText);
177
+ const result: Record<string, unknown> = {};
178
+
179
+ if (
180
+ parsed.AuthorizeSecurityGroupIngressResponse ||
181
+ parsed.AuthorizeSecurityGroupEgressResponse ||
182
+ parsed.RevokeSecurityGroupIngressResponse ||
183
+ parsed.RevokeSecurityGroupEgressResponse
184
+ ) {
185
+ result.success = parsed.return === "true" || parsed.return === true;
186
+ }
187
+
188
+ return result as T;
189
+ }
190
+
191
+ /**
192
+ * Convert rule properties to the flat format required by the AWS API.
193
+ */
194
+ function convertRuleToApiParams(
195
+ props: SecurityGroupRuleProps,
196
+ groupId: string,
197
+ ): Record<string, string> {
198
+ const params: Record<string, string> = {
199
+ GroupId: groupId,
200
+ "IpPermissions.1.IpProtocol": props.protocol,
201
+ "IpPermissions.1.FromPort": props.fromPort.toString(),
202
+ "IpPermissions.1.ToPort": props.toPort.toString(),
203
+ };
204
+
205
+ const prefix = "IpPermissions.1";
206
+
207
+ if (props.cidrBlocks && props.cidrBlocks.length > 0) {
208
+ props.cidrBlocks.forEach((cidr, index) => {
209
+ const rangePrefix = `${prefix}.IpRanges.${index + 1}`;
210
+ params[`${rangePrefix}.CidrIp`] = cidr;
211
+ if (props.description) {
212
+ params[`${rangePrefix}.Description`] = props.description;
213
+ }
214
+ });
215
+ }
216
+
217
+ if (props.sourceSecurityGroups && props.sourceSecurityGroups.length > 0) {
218
+ props.sourceSecurityGroups.forEach((sg, index) => {
219
+ const groupPrefix = `${prefix}.UserIdGroupPairs.${index + 1}`;
220
+ params[`${groupPrefix}.GroupId`] =
221
+ typeof sg === "string" ? sg : sg.groupId;
222
+ if (props.description) {
223
+ params[`${groupPrefix}.Description`] = props.description;
224
+ }
225
+ });
226
+ }
227
+
228
+ return params;
229
+ }