@stacksjs/ts-cloud 0.1.7 → 0.1.9

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 (77) hide show
  1. package/dist/aws/s3.d.ts +1 -1
  2. package/dist/bin/cli.js +223 -222
  3. package/dist/index.js +132 -132
  4. package/package.json +18 -16
  5. package/src/aws/acm.ts +768 -0
  6. package/src/aws/application-autoscaling.ts +845 -0
  7. package/src/aws/bedrock.ts +4074 -0
  8. package/src/aws/client.ts +891 -0
  9. package/src/aws/cloudformation.ts +896 -0
  10. package/src/aws/cloudfront.ts +1531 -0
  11. package/src/aws/cloudwatch-logs.ts +154 -0
  12. package/src/aws/comprehend.ts +839 -0
  13. package/src/aws/connect.ts +1056 -0
  14. package/src/aws/deploy-imap.ts +384 -0
  15. package/src/aws/dynamodb.ts +340 -0
  16. package/src/aws/ec2.ts +1385 -0
  17. package/src/aws/ecr.ts +621 -0
  18. package/src/aws/ecs.ts +615 -0
  19. package/src/aws/elasticache.ts +301 -0
  20. package/src/aws/elbv2.ts +942 -0
  21. package/src/aws/email.ts +928 -0
  22. package/src/aws/eventbridge.ts +248 -0
  23. package/src/aws/iam.ts +1689 -0
  24. package/src/aws/imap-server.ts +2100 -0
  25. package/src/aws/index.ts +213 -0
  26. package/src/aws/kendra.ts +1097 -0
  27. package/src/aws/lambda.ts +786 -0
  28. package/src/aws/opensearch.ts +158 -0
  29. package/src/aws/personalize.ts +977 -0
  30. package/src/aws/polly.ts +559 -0
  31. package/src/aws/rds.ts +888 -0
  32. package/src/aws/rekognition.ts +846 -0
  33. package/src/aws/route53-domains.ts +359 -0
  34. package/src/aws/route53.ts +1046 -0
  35. package/src/aws/s3.ts +2334 -0
  36. package/src/aws/scheduler.ts +571 -0
  37. package/src/aws/secrets-manager.ts +769 -0
  38. package/src/aws/ses.ts +1081 -0
  39. package/src/aws/setup-phone.ts +104 -0
  40. package/src/aws/setup-sms.ts +580 -0
  41. package/src/aws/sms.ts +1735 -0
  42. package/src/aws/smtp-server.ts +531 -0
  43. package/src/aws/sns.ts +758 -0
  44. package/src/aws/sqs.ts +382 -0
  45. package/src/aws/ssm.ts +807 -0
  46. package/src/aws/sts.ts +92 -0
  47. package/src/aws/support.ts +391 -0
  48. package/src/aws/test-imap.ts +86 -0
  49. package/src/aws/textract.ts +780 -0
  50. package/src/aws/transcribe.ts +108 -0
  51. package/src/aws/translate.ts +641 -0
  52. package/src/aws/voice.ts +1379 -0
  53. package/src/config.ts +35 -0
  54. package/src/deploy/index.ts +7 -0
  55. package/src/deploy/static-site-external-dns.ts +945 -0
  56. package/src/deploy/static-site.ts +1175 -0
  57. package/src/dns/cloudflare.ts +548 -0
  58. package/src/dns/godaddy.ts +412 -0
  59. package/src/dns/index.ts +205 -0
  60. package/src/dns/porkbun.ts +362 -0
  61. package/src/dns/route53-adapter.ts +414 -0
  62. package/src/dns/types.ts +119 -0
  63. package/src/dns/validator.ts +369 -0
  64. package/src/generators/index.ts +5 -0
  65. package/src/generators/infrastructure.ts +1660 -0
  66. package/src/index.ts +163 -0
  67. package/src/push/apns.ts +452 -0
  68. package/src/push/fcm.ts +506 -0
  69. package/src/push/index.ts +58 -0
  70. package/src/security/pre-deploy-scanner.ts +655 -0
  71. package/src/ssl/acme-client.ts +478 -0
  72. package/src/ssl/index.ts +7 -0
  73. package/src/ssl/letsencrypt.ts +747 -0
  74. package/src/types.ts +2 -0
  75. package/src/utils/cli.ts +398 -0
  76. package/src/validation/index.ts +5 -0
  77. package/src/validation/template.ts +405 -0
package/src/aws/ec2.ts ADDED
@@ -0,0 +1,1385 @@
1
+ /**
2
+ * AWS EC2 Client
3
+ * Manages EC2 instances, VPCs, security groups, and related resources using direct API calls
4
+ */
5
+
6
+ import { AWSClient } from './client'
7
+
8
+ export interface Instance {
9
+ InstanceId?: string
10
+ ImageId?: string
11
+ InstanceType?: string
12
+ State?: {
13
+ Code?: number
14
+ Name?: 'pending' | 'running' | 'shutting-down' | 'terminated' | 'stopping' | 'stopped'
15
+ }
16
+ PrivateIpAddress?: string
17
+ PublicIpAddress?: string
18
+ SubnetId?: string
19
+ VpcId?: string
20
+ SecurityGroups?: { GroupId?: string, GroupName?: string }[]
21
+ Tags?: { Key?: string, Value?: string }[]
22
+ LaunchTime?: string
23
+ Placement?: {
24
+ AvailabilityZone?: string
25
+ Tenancy?: string
26
+ }
27
+ Architecture?: string
28
+ RootDeviceType?: string
29
+ RootDeviceName?: string
30
+ BlockDeviceMappings?: {
31
+ DeviceName?: string
32
+ Ebs?: {
33
+ VolumeId?: string
34
+ Status?: string
35
+ AttachTime?: string
36
+ DeleteOnTermination?: boolean
37
+ }
38
+ }[]
39
+ IamInstanceProfile?: {
40
+ Arn?: string
41
+ Id?: string
42
+ }
43
+ }
44
+
45
+ export interface Vpc {
46
+ VpcId?: string
47
+ CidrBlock?: string
48
+ State?: 'pending' | 'available'
49
+ DhcpOptionsId?: string
50
+ InstanceTenancy?: string
51
+ IsDefault?: boolean
52
+ Tags?: { Key?: string, Value?: string }[]
53
+ }
54
+
55
+ export interface Subnet {
56
+ SubnetId?: string
57
+ VpcId?: string
58
+ CidrBlock?: string
59
+ AvailabilityZone?: string
60
+ AvailableIpAddressCount?: number
61
+ State?: 'pending' | 'available'
62
+ MapPublicIpOnLaunch?: boolean
63
+ Tags?: { Key?: string, Value?: string }[]
64
+ }
65
+
66
+ export interface SecurityGroup {
67
+ GroupId?: string
68
+ GroupName?: string
69
+ Description?: string
70
+ VpcId?: string
71
+ IpPermissions?: IpPermission[]
72
+ IpPermissionsEgress?: IpPermission[]
73
+ Tags?: { Key?: string, Value?: string }[]
74
+ }
75
+
76
+ export interface IpPermission {
77
+ IpProtocol?: string
78
+ FromPort?: number
79
+ ToPort?: number
80
+ IpRanges?: { CidrIp?: string, Description?: string }[]
81
+ Ipv6Ranges?: { CidrIpv6?: string, Description?: string }[]
82
+ UserIdGroupPairs?: { GroupId?: string, UserId?: string, Description?: string }[]
83
+ }
84
+
85
+ export interface InternetGateway {
86
+ InternetGatewayId?: string
87
+ Attachments?: { VpcId?: string, State?: string }[]
88
+ Tags?: { Key?: string, Value?: string }[]
89
+ }
90
+
91
+ export interface RouteTable {
92
+ RouteTableId?: string
93
+ VpcId?: string
94
+ Routes?: {
95
+ DestinationCidrBlock?: string
96
+ GatewayId?: string
97
+ NatGatewayId?: string
98
+ State?: string
99
+ }[]
100
+ Associations?: {
101
+ RouteTableAssociationId?: string
102
+ SubnetId?: string
103
+ Main?: boolean
104
+ }[]
105
+ Tags?: { Key?: string, Value?: string }[]
106
+ }
107
+
108
+ export interface Address {
109
+ PublicIp?: string
110
+ AllocationId?: string
111
+ AssociationId?: string
112
+ InstanceId?: string
113
+ NetworkInterfaceId?: string
114
+ PrivateIpAddress?: string
115
+ Domain?: 'vpc' | 'standard'
116
+ Tags?: { Key?: string, Value?: string }[]
117
+ }
118
+
119
+ export interface ConsoleOutput {
120
+ InstanceId?: string
121
+ Output?: string
122
+ Timestamp?: string
123
+ }
124
+
125
+ export interface InstanceStatus {
126
+ InstanceId?: string
127
+ InstanceState?: {
128
+ Code?: number
129
+ Name?: string
130
+ }
131
+ InstanceStatus?: {
132
+ Status?: string
133
+ Details?: { Name?: string, Status?: string }[]
134
+ }
135
+ SystemStatus?: {
136
+ Status?: string
137
+ Details?: { Name?: string, Status?: string }[]
138
+ }
139
+ }
140
+
141
+ /**
142
+ * EC2 client using direct API calls
143
+ */
144
+ export class EC2Client {
145
+ private client: AWSClient
146
+ private region: string
147
+
148
+ constructor(region: string = 'us-east-1') {
149
+ this.region = region
150
+ this.client = new AWSClient()
151
+ }
152
+
153
+ /**
154
+ * Describe EC2 instances
155
+ */
156
+ async describeInstances(options?: {
157
+ InstanceIds?: string[]
158
+ Filters?: { Name: string, Values: string[] }[]
159
+ MaxResults?: number
160
+ NextToken?: string
161
+ }): Promise<{
162
+ Reservations?: {
163
+ ReservationId?: string
164
+ Instances?: Instance[]
165
+ }[]
166
+ NextToken?: string
167
+ }> {
168
+ const params: Record<string, string> = {
169
+ Action: 'DescribeInstances',
170
+ Version: '2016-11-15',
171
+ }
172
+
173
+ if (options?.InstanceIds) {
174
+ options.InstanceIds.forEach((id, i) => {
175
+ params[`InstanceId.${i + 1}`] = id
176
+ })
177
+ }
178
+
179
+ if (options?.Filters) {
180
+ options.Filters.forEach((filter, i) => {
181
+ params[`Filter.${i + 1}.Name`] = filter.Name
182
+ filter.Values.forEach((val, j) => {
183
+ params[`Filter.${i + 1}.Value.${j + 1}`] = val
184
+ })
185
+ })
186
+ }
187
+
188
+ if (options?.MaxResults) {
189
+ params.MaxResults = String(options.MaxResults)
190
+ }
191
+
192
+ if (options?.NextToken) {
193
+ params.NextToken = options.NextToken
194
+ }
195
+
196
+ const result = await this.client.request({
197
+ service: 'ec2',
198
+ region: this.region,
199
+ method: 'POST',
200
+ path: '/',
201
+ headers: {
202
+ 'Content-Type': 'application/x-www-form-urlencoded',
203
+ },
204
+ body: new URLSearchParams(params).toString(),
205
+ })
206
+
207
+ // Handle EC2 XML response wrapper
208
+ const response = result.DescribeInstancesResponse || result
209
+
210
+ return {
211
+ Reservations: this.parseReservations(response.reservationSet?.item),
212
+ NextToken: response.nextToken,
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Get a single instance by ID
218
+ */
219
+ async getInstance(instanceId: string): Promise<Instance | undefined> {
220
+ const result = await this.describeInstances({ InstanceIds: [instanceId] })
221
+ return result.Reservations?.[0]?.Instances?.[0]
222
+ }
223
+
224
+ /**
225
+ * Get console output from an EC2 instance
226
+ */
227
+ async getConsoleOutput(instanceId: string, latest?: boolean): Promise<ConsoleOutput> {
228
+ const params: Record<string, string> = {
229
+ Action: 'GetConsoleOutput',
230
+ Version: '2016-11-15',
231
+ InstanceId: instanceId,
232
+ }
233
+
234
+ if (latest) {
235
+ params.Latest = 'true'
236
+ }
237
+
238
+ const result = await this.client.request({
239
+ service: 'ec2',
240
+ region: this.region,
241
+ method: 'POST',
242
+ path: '/',
243
+ headers: {
244
+ 'Content-Type': 'application/x-www-form-urlencoded',
245
+ },
246
+ body: new URLSearchParams(params).toString(),
247
+ })
248
+
249
+ // Handle EC2 XML response wrapper
250
+ const response = result.GetConsoleOutputResponse || result
251
+
252
+ return {
253
+ InstanceId: response.instanceId,
254
+ Output: response.output,
255
+ Timestamp: response.timestamp,
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Get console output decoded (convenience method)
261
+ */
262
+ async getConsoleOutputDecoded(instanceId: string, options?: {
263
+ latest?: boolean
264
+ tailLines?: number
265
+ }): Promise<string> {
266
+ const result = await this.getConsoleOutput(instanceId, options?.latest)
267
+
268
+ if (!result.Output) {
269
+ return 'No console output available yet'
270
+ }
271
+
272
+ // Decode base64
273
+ const decoded = Buffer.from(result.Output, 'base64').toString('utf-8')
274
+
275
+ if (options?.tailLines) {
276
+ const lines = decoded.split('\n')
277
+ return lines.slice(-options.tailLines).join('\n')
278
+ }
279
+
280
+ return decoded
281
+ }
282
+
283
+ /**
284
+ * Describe instance status
285
+ */
286
+ async describeInstanceStatus(options?: {
287
+ InstanceIds?: string[]
288
+ IncludeAllInstances?: boolean
289
+ Filters?: { Name: string, Values: string[] }[]
290
+ }): Promise<{
291
+ InstanceStatuses?: InstanceStatus[]
292
+ }> {
293
+ const params: Record<string, string> = {
294
+ Action: 'DescribeInstanceStatus',
295
+ Version: '2016-11-15',
296
+ }
297
+
298
+ if (options?.InstanceIds) {
299
+ options.InstanceIds.forEach((id, i) => {
300
+ params[`InstanceId.${i + 1}`] = id
301
+ })
302
+ }
303
+
304
+ if (options?.IncludeAllInstances) {
305
+ params.IncludeAllInstances = 'true'
306
+ }
307
+
308
+ if (options?.Filters) {
309
+ options.Filters.forEach((filter, i) => {
310
+ params[`Filter.${i + 1}.Name`] = filter.Name
311
+ filter.Values.forEach((val, j) => {
312
+ params[`Filter.${i + 1}.Value.${j + 1}`] = val
313
+ })
314
+ })
315
+ }
316
+
317
+ const result = await this.client.request({
318
+ service: 'ec2',
319
+ region: this.region,
320
+ method: 'POST',
321
+ path: '/',
322
+ headers: {
323
+ 'Content-Type': 'application/x-www-form-urlencoded',
324
+ },
325
+ body: new URLSearchParams(params).toString(),
326
+ })
327
+
328
+ return {
329
+ InstanceStatuses: this.parseArray(result.instanceStatusSet?.item).map((item: any) => ({
330
+ InstanceId: item.instanceId,
331
+ InstanceState: item.instanceState ? {
332
+ Code: Number.parseInt(item.instanceState.code),
333
+ Name: item.instanceState.name,
334
+ } : undefined,
335
+ InstanceStatus: item.instanceStatus ? {
336
+ Status: item.instanceStatus.status,
337
+ Details: this.parseArray(item.instanceStatus.details?.item).map((d: any) => ({
338
+ Name: d.name,
339
+ Status: d.status,
340
+ })),
341
+ } : undefined,
342
+ SystemStatus: item.systemStatus ? {
343
+ Status: item.systemStatus.status,
344
+ Details: this.parseArray(item.systemStatus.details?.item).map((d: any) => ({
345
+ Name: d.name,
346
+ Status: d.status,
347
+ })),
348
+ } : undefined,
349
+ })),
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Start instances
355
+ */
356
+ async startInstances(instanceIds: string[]): Promise<{
357
+ StartingInstances?: { InstanceId?: string, CurrentState?: { Name?: string }, PreviousState?: { Name?: string } }[]
358
+ }> {
359
+ const params: Record<string, string> = {
360
+ Action: 'StartInstances',
361
+ Version: '2016-11-15',
362
+ }
363
+
364
+ instanceIds.forEach((id, i) => {
365
+ params[`InstanceId.${i + 1}`] = id
366
+ })
367
+
368
+ const result = await this.client.request({
369
+ service: 'ec2',
370
+ region: this.region,
371
+ method: 'POST',
372
+ path: '/',
373
+ headers: {
374
+ 'Content-Type': 'application/x-www-form-urlencoded',
375
+ },
376
+ body: new URLSearchParams(params).toString(),
377
+ })
378
+
379
+ return {
380
+ StartingInstances: this.parseArray(result.instancesSet?.item).map((item: any) => ({
381
+ InstanceId: item.instanceId,
382
+ CurrentState: item.currentState ? { Name: item.currentState.name } : undefined,
383
+ PreviousState: item.previousState ? { Name: item.previousState.name } : undefined,
384
+ })),
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Stop instances
390
+ */
391
+ async stopInstances(instanceIds: string[], force?: boolean): Promise<{
392
+ StoppingInstances?: { InstanceId?: string, CurrentState?: { Name?: string }, PreviousState?: { Name?: string } }[]
393
+ }> {
394
+ const params: Record<string, string> = {
395
+ Action: 'StopInstances',
396
+ Version: '2016-11-15',
397
+ }
398
+
399
+ instanceIds.forEach((id, i) => {
400
+ params[`InstanceId.${i + 1}`] = id
401
+ })
402
+
403
+ if (force) {
404
+ params.Force = 'true'
405
+ }
406
+
407
+ const result = await this.client.request({
408
+ service: 'ec2',
409
+ region: this.region,
410
+ method: 'POST',
411
+ path: '/',
412
+ headers: {
413
+ 'Content-Type': 'application/x-www-form-urlencoded',
414
+ },
415
+ body: new URLSearchParams(params).toString(),
416
+ })
417
+
418
+ return {
419
+ StoppingInstances: this.parseArray(result.instancesSet?.item).map((item: any) => ({
420
+ InstanceId: item.instanceId,
421
+ CurrentState: item.currentState ? { Name: item.currentState.name } : undefined,
422
+ PreviousState: item.previousState ? { Name: item.previousState.name } : undefined,
423
+ })),
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Reboot instances
429
+ */
430
+ async rebootInstances(instanceIds: string[]): Promise<void> {
431
+ const params: Record<string, string> = {
432
+ Action: 'RebootInstances',
433
+ Version: '2016-11-15',
434
+ }
435
+
436
+ instanceIds.forEach((id, i) => {
437
+ params[`InstanceId.${i + 1}`] = id
438
+ })
439
+
440
+ await this.client.request({
441
+ service: 'ec2',
442
+ region: this.region,
443
+ method: 'POST',
444
+ path: '/',
445
+ headers: {
446
+ 'Content-Type': 'application/x-www-form-urlencoded',
447
+ },
448
+ body: new URLSearchParams(params).toString(),
449
+ })
450
+ }
451
+
452
+ /**
453
+ * Terminate instances
454
+ */
455
+ async terminateInstances(instanceIds: string[]): Promise<{
456
+ TerminatingInstances?: { InstanceId?: string, CurrentState?: { Name?: string }, PreviousState?: { Name?: string } }[]
457
+ }> {
458
+ const params: Record<string, string> = {
459
+ Action: 'TerminateInstances',
460
+ Version: '2016-11-15',
461
+ }
462
+
463
+ instanceIds.forEach((id, i) => {
464
+ params[`InstanceId.${i + 1}`] = id
465
+ })
466
+
467
+ const result = await this.client.request({
468
+ service: 'ec2',
469
+ region: this.region,
470
+ method: 'POST',
471
+ path: '/',
472
+ headers: {
473
+ 'Content-Type': 'application/x-www-form-urlencoded',
474
+ },
475
+ body: new URLSearchParams(params).toString(),
476
+ })
477
+
478
+ return {
479
+ TerminatingInstances: this.parseArray(result.instancesSet?.item).map((item: any) => ({
480
+ InstanceId: item.instanceId,
481
+ CurrentState: item.currentState ? { Name: item.currentState.name } : undefined,
482
+ PreviousState: item.previousState ? { Name: item.previousState.name } : undefined,
483
+ })),
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Describe VPCs
489
+ */
490
+ async describeVpcs(options?: {
491
+ VpcIds?: string[]
492
+ Filters?: { Name: string, Values: string[] }[]
493
+ }): Promise<{
494
+ Vpcs?: Vpc[]
495
+ }> {
496
+ const params: Record<string, string> = {
497
+ Action: 'DescribeVpcs',
498
+ Version: '2016-11-15',
499
+ }
500
+
501
+ if (options?.VpcIds) {
502
+ options.VpcIds.forEach((id, i) => {
503
+ params[`VpcId.${i + 1}`] = id
504
+ })
505
+ }
506
+
507
+ if (options?.Filters) {
508
+ options.Filters.forEach((filter, i) => {
509
+ params[`Filter.${i + 1}.Name`] = filter.Name
510
+ filter.Values.forEach((val, j) => {
511
+ params[`Filter.${i + 1}.Value.${j + 1}`] = val
512
+ })
513
+ })
514
+ }
515
+
516
+ const result = await this.client.request({
517
+ service: 'ec2',
518
+ region: this.region,
519
+ method: 'POST',
520
+ path: '/',
521
+ headers: {
522
+ 'Content-Type': 'application/x-www-form-urlencoded',
523
+ },
524
+ body: new URLSearchParams(params).toString(),
525
+ })
526
+
527
+ return {
528
+ Vpcs: this.parseArray(result.vpcSet?.item).map((item: any) => ({
529
+ VpcId: item.vpcId,
530
+ CidrBlock: item.cidrBlock,
531
+ State: item.state,
532
+ DhcpOptionsId: item.dhcpOptionsId,
533
+ InstanceTenancy: item.instanceTenancy,
534
+ IsDefault: item.isDefault === 'true',
535
+ Tags: this.parseTags(item.tagSet?.item),
536
+ })),
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Describe Subnets
542
+ */
543
+ async describeSubnets(options?: {
544
+ SubnetIds?: string[]
545
+ Filters?: { Name: string, Values: string[] }[]
546
+ }): Promise<{
547
+ Subnets?: Subnet[]
548
+ }> {
549
+ const params: Record<string, string> = {
550
+ Action: 'DescribeSubnets',
551
+ Version: '2016-11-15',
552
+ }
553
+
554
+ if (options?.SubnetIds) {
555
+ options.SubnetIds.forEach((id, i) => {
556
+ params[`SubnetId.${i + 1}`] = id
557
+ })
558
+ }
559
+
560
+ if (options?.Filters) {
561
+ options.Filters.forEach((filter, i) => {
562
+ params[`Filter.${i + 1}.Name`] = filter.Name
563
+ filter.Values.forEach((val, j) => {
564
+ params[`Filter.${i + 1}.Value.${j + 1}`] = val
565
+ })
566
+ })
567
+ }
568
+
569
+ const result = await this.client.request({
570
+ service: 'ec2',
571
+ region: this.region,
572
+ method: 'POST',
573
+ path: '/',
574
+ headers: {
575
+ 'Content-Type': 'application/x-www-form-urlencoded',
576
+ },
577
+ body: new URLSearchParams(params).toString(),
578
+ })
579
+
580
+ return {
581
+ Subnets: this.parseArray(result.subnetSet?.item).map((item: any) => ({
582
+ SubnetId: item.subnetId,
583
+ VpcId: item.vpcId,
584
+ CidrBlock: item.cidrBlock,
585
+ AvailabilityZone: item.availabilityZone,
586
+ AvailableIpAddressCount: Number.parseInt(item.availableIpAddressCount),
587
+ State: item.state,
588
+ MapPublicIpOnLaunch: item.mapPublicIpOnLaunch === 'true',
589
+ Tags: this.parseTags(item.tagSet?.item),
590
+ })),
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Describe Security Groups
596
+ */
597
+ async describeSecurityGroups(options?: {
598
+ GroupIds?: string[]
599
+ GroupNames?: string[]
600
+ Filters?: { Name: string, Values: string[] }[]
601
+ }): Promise<{
602
+ SecurityGroups?: SecurityGroup[]
603
+ }> {
604
+ const params: Record<string, string> = {
605
+ Action: 'DescribeSecurityGroups',
606
+ Version: '2016-11-15',
607
+ }
608
+
609
+ if (options?.GroupIds) {
610
+ options.GroupIds.forEach((id, i) => {
611
+ params[`GroupId.${i + 1}`] = id
612
+ })
613
+ }
614
+
615
+ if (options?.GroupNames) {
616
+ options.GroupNames.forEach((name, i) => {
617
+ params[`GroupName.${i + 1}`] = name
618
+ })
619
+ }
620
+
621
+ if (options?.Filters) {
622
+ options.Filters.forEach((filter, i) => {
623
+ params[`Filter.${i + 1}.Name`] = filter.Name
624
+ filter.Values.forEach((val, j) => {
625
+ params[`Filter.${i + 1}.Value.${j + 1}`] = val
626
+ })
627
+ })
628
+ }
629
+
630
+ const result = await this.client.request({
631
+ service: 'ec2',
632
+ region: this.region,
633
+ method: 'POST',
634
+ path: '/',
635
+ headers: {
636
+ 'Content-Type': 'application/x-www-form-urlencoded',
637
+ },
638
+ body: new URLSearchParams(params).toString(),
639
+ })
640
+
641
+ return {
642
+ SecurityGroups: this.parseArray(result.securityGroupInfo?.item).map((item: any) => ({
643
+ GroupId: item.groupId,
644
+ GroupName: item.groupName,
645
+ Description: item.groupDescription,
646
+ VpcId: item.vpcId,
647
+ IpPermissions: this.parseIpPermissions(item.ipPermissions?.item),
648
+ IpPermissionsEgress: this.parseIpPermissions(item.ipPermissionsEgress?.item),
649
+ Tags: this.parseTags(item.tagSet?.item),
650
+ })),
651
+ }
652
+ }
653
+
654
+ /**
655
+ * Describe Internet Gateways
656
+ */
657
+ async describeInternetGateways(options?: {
658
+ InternetGatewayIds?: string[]
659
+ Filters?: { Name: string, Values: string[] }[]
660
+ }): Promise<{
661
+ InternetGateways?: InternetGateway[]
662
+ }> {
663
+ const params: Record<string, string> = {
664
+ Action: 'DescribeInternetGateways',
665
+ Version: '2016-11-15',
666
+ }
667
+
668
+ if (options?.InternetGatewayIds) {
669
+ options.InternetGatewayIds.forEach((id, i) => {
670
+ params[`InternetGatewayId.${i + 1}`] = id
671
+ })
672
+ }
673
+
674
+ if (options?.Filters) {
675
+ options.Filters.forEach((filter, i) => {
676
+ params[`Filter.${i + 1}.Name`] = filter.Name
677
+ filter.Values.forEach((val, j) => {
678
+ params[`Filter.${i + 1}.Value.${j + 1}`] = val
679
+ })
680
+ })
681
+ }
682
+
683
+ const result = await this.client.request({
684
+ service: 'ec2',
685
+ region: this.region,
686
+ method: 'POST',
687
+ path: '/',
688
+ headers: {
689
+ 'Content-Type': 'application/x-www-form-urlencoded',
690
+ },
691
+ body: new URLSearchParams(params).toString(),
692
+ })
693
+
694
+ return {
695
+ InternetGateways: this.parseArray(result.internetGatewaySet?.item).map((item: any) => ({
696
+ InternetGatewayId: item.internetGatewayId,
697
+ Attachments: this.parseArray(item.attachmentSet?.item).map((a: any) => ({
698
+ VpcId: a.vpcId,
699
+ State: a.state,
700
+ })),
701
+ Tags: this.parseTags(item.tagSet?.item),
702
+ })),
703
+ }
704
+ }
705
+
706
+ /**
707
+ * Describe Elastic IPs (Addresses)
708
+ */
709
+ async describeAddresses(options?: {
710
+ AllocationIds?: string[]
711
+ PublicIps?: string[]
712
+ Filters?: { Name: string, Values: string[] }[]
713
+ }): Promise<{
714
+ Addresses?: Address[]
715
+ }> {
716
+ const params: Record<string, string> = {
717
+ Action: 'DescribeAddresses',
718
+ Version: '2016-11-15',
719
+ }
720
+
721
+ if (options?.AllocationIds) {
722
+ options.AllocationIds.forEach((id, i) => {
723
+ params[`AllocationId.${i + 1}`] = id
724
+ })
725
+ }
726
+
727
+ if (options?.PublicIps) {
728
+ options.PublicIps.forEach((ip, i) => {
729
+ params[`PublicIp.${i + 1}`] = ip
730
+ })
731
+ }
732
+
733
+ if (options?.Filters) {
734
+ options.Filters.forEach((filter, i) => {
735
+ params[`Filter.${i + 1}.Name`] = filter.Name
736
+ filter.Values.forEach((val, j) => {
737
+ params[`Filter.${i + 1}.Value.${j + 1}`] = val
738
+ })
739
+ })
740
+ }
741
+
742
+ const result = await this.client.request({
743
+ service: 'ec2',
744
+ region: this.region,
745
+ method: 'POST',
746
+ path: '/',
747
+ headers: {
748
+ 'Content-Type': 'application/x-www-form-urlencoded',
749
+ },
750
+ body: new URLSearchParams(params).toString(),
751
+ })
752
+
753
+ return {
754
+ Addresses: this.parseArray(result.addressesSet?.item).map((item: any) => ({
755
+ PublicIp: item.publicIp,
756
+ AllocationId: item.allocationId,
757
+ AssociationId: item.associationId,
758
+ InstanceId: item.instanceId,
759
+ NetworkInterfaceId: item.networkInterfaceId,
760
+ PrivateIpAddress: item.privateIpAddress,
761
+ Domain: item.domain,
762
+ Tags: this.parseTags(item.tagSet?.item),
763
+ })),
764
+ }
765
+ }
766
+
767
+ /**
768
+ * Allocate Elastic IP
769
+ */
770
+ async allocateAddress(options?: {
771
+ Domain?: 'vpc' | 'standard'
772
+ TagSpecifications?: { ResourceType: string, Tags: { Key: string, Value: string }[] }[]
773
+ }): Promise<{
774
+ AllocationId?: string
775
+ PublicIp?: string
776
+ Domain?: string
777
+ }> {
778
+ const params: Record<string, string> = {
779
+ Action: 'AllocateAddress',
780
+ Version: '2016-11-15',
781
+ }
782
+
783
+ if (options?.Domain) {
784
+ params.Domain = options.Domain
785
+ }
786
+
787
+ if (options?.TagSpecifications) {
788
+ options.TagSpecifications.forEach((spec, i) => {
789
+ params[`TagSpecification.${i + 1}.ResourceType`] = spec.ResourceType
790
+ spec.Tags.forEach((tag, j) => {
791
+ params[`TagSpecification.${i + 1}.Tag.${j + 1}.Key`] = tag.Key
792
+ params[`TagSpecification.${i + 1}.Tag.${j + 1}.Value`] = tag.Value
793
+ })
794
+ })
795
+ }
796
+
797
+ const result = await this.client.request({
798
+ service: 'ec2',
799
+ region: this.region,
800
+ method: 'POST',
801
+ path: '/',
802
+ headers: {
803
+ 'Content-Type': 'application/x-www-form-urlencoded',
804
+ },
805
+ body: new URLSearchParams(params).toString(),
806
+ })
807
+
808
+ return {
809
+ AllocationId: result.allocationId,
810
+ PublicIp: result.publicIp,
811
+ Domain: result.domain,
812
+ }
813
+ }
814
+
815
+ /**
816
+ * Associate Elastic IP with instance
817
+ */
818
+ async associateAddress(options: {
819
+ AllocationId?: string
820
+ PublicIp?: string
821
+ InstanceId?: string
822
+ NetworkInterfaceId?: string
823
+ PrivateIpAddress?: string
824
+ AllowReassociation?: boolean
825
+ }): Promise<{
826
+ AssociationId?: string
827
+ }> {
828
+ const params: Record<string, string> = {
829
+ Action: 'AssociateAddress',
830
+ Version: '2016-11-15',
831
+ }
832
+
833
+ if (options.AllocationId) {
834
+ params.AllocationId = options.AllocationId
835
+ }
836
+
837
+ if (options.PublicIp) {
838
+ params.PublicIp = options.PublicIp
839
+ }
840
+
841
+ if (options.InstanceId) {
842
+ params.InstanceId = options.InstanceId
843
+ }
844
+
845
+ if (options.NetworkInterfaceId) {
846
+ params.NetworkInterfaceId = options.NetworkInterfaceId
847
+ }
848
+
849
+ if (options.PrivateIpAddress) {
850
+ params.PrivateIpAddress = options.PrivateIpAddress
851
+ }
852
+
853
+ if (options.AllowReassociation) {
854
+ params.AllowReassociation = 'true'
855
+ }
856
+
857
+ const result = await this.client.request({
858
+ service: 'ec2',
859
+ region: this.region,
860
+ method: 'POST',
861
+ path: '/',
862
+ headers: {
863
+ 'Content-Type': 'application/x-www-form-urlencoded',
864
+ },
865
+ body: new URLSearchParams(params).toString(),
866
+ })
867
+
868
+ return {
869
+ AssociationId: result.associationId,
870
+ }
871
+ }
872
+
873
+ /**
874
+ * Create tags for resources
875
+ */
876
+ async createTags(options: {
877
+ Resources: string[]
878
+ Tags: { Key: string, Value: string }[]
879
+ }): Promise<void> {
880
+ const params: Record<string, string> = {
881
+ Action: 'CreateTags',
882
+ Version: '2016-11-15',
883
+ }
884
+
885
+ options.Resources.forEach((id, i) => {
886
+ params[`ResourceId.${i + 1}`] = id
887
+ })
888
+
889
+ options.Tags.forEach((tag, i) => {
890
+ params[`Tag.${i + 1}.Key`] = tag.Key
891
+ params[`Tag.${i + 1}.Value`] = tag.Value
892
+ })
893
+
894
+ await this.client.request({
895
+ service: 'ec2',
896
+ region: this.region,
897
+ method: 'POST',
898
+ path: '/',
899
+ headers: {
900
+ 'Content-Type': 'application/x-www-form-urlencoded',
901
+ },
902
+ body: new URLSearchParams(params).toString(),
903
+ })
904
+ }
905
+
906
+ /**
907
+ * Wait for instance to reach a specific state
908
+ */
909
+ async waitForInstanceState(
910
+ instanceId: string,
911
+ targetState: 'running' | 'stopped' | 'terminated',
912
+ options?: {
913
+ maxWaitMs?: number
914
+ pollIntervalMs?: number
915
+ },
916
+ ): Promise<Instance | undefined> {
917
+ const maxWait = options?.maxWaitMs || 300000 // 5 minutes
918
+ const pollInterval = options?.pollIntervalMs || 5000 // 5 seconds
919
+ const startTime = Date.now()
920
+
921
+ while (Date.now() - startTime < maxWait) {
922
+ const instance = await this.getInstance(instanceId)
923
+
924
+ if (instance?.State?.Name === targetState) {
925
+ return instance
926
+ }
927
+
928
+ await new Promise(resolve => setTimeout(resolve, pollInterval))
929
+ }
930
+
931
+ return undefined
932
+ }
933
+
934
+ /**
935
+ * Create a VPC
936
+ */
937
+ async createVpc(options: {
938
+ CidrBlock: string
939
+ InstanceTenancy?: string
940
+ TagSpecifications?: { ResourceType: string, Tags: { Key: string, Value: string }[] }[]
941
+ }): Promise<{
942
+ Vpc?: Vpc
943
+ }> {
944
+ const params: Record<string, string> = {
945
+ Action: 'CreateVpc',
946
+ Version: '2016-11-15',
947
+ CidrBlock: options.CidrBlock,
948
+ }
949
+
950
+ if (options.InstanceTenancy) {
951
+ params.InstanceTenancy = options.InstanceTenancy
952
+ }
953
+
954
+ if (options.TagSpecifications) {
955
+ options.TagSpecifications.forEach((spec, i) => {
956
+ params[`TagSpecification.${i + 1}.ResourceType`] = spec.ResourceType
957
+ spec.Tags.forEach((tag, j) => {
958
+ params[`TagSpecification.${i + 1}.Tag.${j + 1}.Key`] = tag.Key
959
+ params[`TagSpecification.${i + 1}.Tag.${j + 1}.Value`] = tag.Value
960
+ })
961
+ })
962
+ }
963
+
964
+ const result = await this.client.request({
965
+ service: 'ec2',
966
+ region: this.region,
967
+ method: 'POST',
968
+ path: '/',
969
+ headers: {
970
+ 'Content-Type': 'application/x-www-form-urlencoded',
971
+ },
972
+ body: new URLSearchParams(params).toString(),
973
+ })
974
+
975
+ const response = result.CreateVpcResponse || result
976
+ const vpc = response.vpc
977
+
978
+ return {
979
+ Vpc: vpc ? {
980
+ VpcId: vpc.vpcId,
981
+ CidrBlock: vpc.cidrBlock,
982
+ State: vpc.state,
983
+ DhcpOptionsId: vpc.dhcpOptionsId,
984
+ InstanceTenancy: vpc.instanceTenancy,
985
+ IsDefault: vpc.isDefault === 'true',
986
+ Tags: this.parseTags(vpc.tagSet?.item),
987
+ } : undefined,
988
+ }
989
+ }
990
+
991
+ /**
992
+ * Create a Subnet
993
+ */
994
+ async createSubnet(options: {
995
+ VpcId: string
996
+ CidrBlock: string
997
+ AvailabilityZone?: string
998
+ TagSpecifications?: { ResourceType: string, Tags: { Key: string, Value: string }[] }[]
999
+ }): Promise<{
1000
+ Subnet?: Subnet
1001
+ }> {
1002
+ const params: Record<string, string> = {
1003
+ Action: 'CreateSubnet',
1004
+ Version: '2016-11-15',
1005
+ VpcId: options.VpcId,
1006
+ CidrBlock: options.CidrBlock,
1007
+ }
1008
+
1009
+ if (options.AvailabilityZone) {
1010
+ params.AvailabilityZone = options.AvailabilityZone
1011
+ }
1012
+
1013
+ if (options.TagSpecifications) {
1014
+ options.TagSpecifications.forEach((spec, i) => {
1015
+ params[`TagSpecification.${i + 1}.ResourceType`] = spec.ResourceType
1016
+ spec.Tags.forEach((tag, j) => {
1017
+ params[`TagSpecification.${i + 1}.Tag.${j + 1}.Key`] = tag.Key
1018
+ params[`TagSpecification.${i + 1}.Tag.${j + 1}.Value`] = tag.Value
1019
+ })
1020
+ })
1021
+ }
1022
+
1023
+ const result = await this.client.request({
1024
+ service: 'ec2',
1025
+ region: this.region,
1026
+ method: 'POST',
1027
+ path: '/',
1028
+ headers: {
1029
+ 'Content-Type': 'application/x-www-form-urlencoded',
1030
+ },
1031
+ body: new URLSearchParams(params).toString(),
1032
+ })
1033
+
1034
+ const response = result.CreateSubnetResponse || result
1035
+ const subnet = response.subnet
1036
+
1037
+ return {
1038
+ Subnet: subnet ? {
1039
+ SubnetId: subnet.subnetId,
1040
+ VpcId: subnet.vpcId,
1041
+ CidrBlock: subnet.cidrBlock,
1042
+ AvailabilityZone: subnet.availabilityZone,
1043
+ AvailableIpAddressCount: subnet.availableIpAddressCount ? Number.parseInt(subnet.availableIpAddressCount) : undefined,
1044
+ State: subnet.state,
1045
+ MapPublicIpOnLaunch: subnet.mapPublicIpOnLaunch === 'true',
1046
+ Tags: this.parseTags(subnet.tagSet?.item),
1047
+ } : undefined,
1048
+ }
1049
+ }
1050
+
1051
+ /**
1052
+ * Modify a Subnet attribute
1053
+ */
1054
+ async modifySubnetAttribute(options: {
1055
+ SubnetId: string
1056
+ MapPublicIpOnLaunch?: { Value: boolean }
1057
+ }): Promise<void> {
1058
+ const params: Record<string, string> = {
1059
+ Action: 'ModifySubnetAttribute',
1060
+ Version: '2016-11-15',
1061
+ SubnetId: options.SubnetId,
1062
+ }
1063
+
1064
+ if (options.MapPublicIpOnLaunch !== undefined) {
1065
+ params['MapPublicIpOnLaunch.Value'] = String(options.MapPublicIpOnLaunch.Value)
1066
+ }
1067
+
1068
+ await this.client.request({
1069
+ service: 'ec2',
1070
+ region: this.region,
1071
+ method: 'POST',
1072
+ path: '/',
1073
+ headers: {
1074
+ 'Content-Type': 'application/x-www-form-urlencoded',
1075
+ },
1076
+ body: new URLSearchParams(params).toString(),
1077
+ })
1078
+ }
1079
+
1080
+ /**
1081
+ * Create a Security Group
1082
+ */
1083
+ async createSecurityGroup(options: {
1084
+ GroupName: string
1085
+ Description: string
1086
+ VpcId?: string
1087
+ TagSpecifications?: { ResourceType: string, Tags: { Key: string, Value: string }[] }[]
1088
+ }): Promise<{
1089
+ GroupId?: string
1090
+ }> {
1091
+ const params: Record<string, string> = {
1092
+ Action: 'CreateSecurityGroup',
1093
+ Version: '2016-11-15',
1094
+ GroupName: options.GroupName,
1095
+ GroupDescription: options.Description,
1096
+ }
1097
+
1098
+ if (options.VpcId) {
1099
+ params.VpcId = options.VpcId
1100
+ }
1101
+
1102
+ if (options.TagSpecifications) {
1103
+ options.TagSpecifications.forEach((spec, i) => {
1104
+ params[`TagSpecification.${i + 1}.ResourceType`] = spec.ResourceType
1105
+ spec.Tags.forEach((tag, j) => {
1106
+ params[`TagSpecification.${i + 1}.Tag.${j + 1}.Key`] = tag.Key
1107
+ params[`TagSpecification.${i + 1}.Tag.${j + 1}.Value`] = tag.Value
1108
+ })
1109
+ })
1110
+ }
1111
+
1112
+ const result = await this.client.request({
1113
+ service: 'ec2',
1114
+ region: this.region,
1115
+ method: 'POST',
1116
+ path: '/',
1117
+ headers: {
1118
+ 'Content-Type': 'application/x-www-form-urlencoded',
1119
+ },
1120
+ body: new URLSearchParams(params).toString(),
1121
+ })
1122
+
1123
+ const response = result.CreateSecurityGroupResponse || result
1124
+
1125
+ return {
1126
+ GroupId: response.groupId,
1127
+ }
1128
+ }
1129
+
1130
+ /**
1131
+ * Authorize Security Group Ingress (add inbound rule)
1132
+ */
1133
+ async authorizeSecurityGroupIngress(options: {
1134
+ GroupId: string
1135
+ IpPermissions: IpPermission[]
1136
+ }): Promise<void> {
1137
+ const params: Record<string, string> = {
1138
+ Action: 'AuthorizeSecurityGroupIngress',
1139
+ Version: '2016-11-15',
1140
+ GroupId: options.GroupId,
1141
+ }
1142
+
1143
+ this.encodeIpPermissions(params, options.IpPermissions)
1144
+
1145
+ await this.client.request({
1146
+ service: 'ec2',
1147
+ region: this.region,
1148
+ method: 'POST',
1149
+ path: '/',
1150
+ headers: {
1151
+ 'Content-Type': 'application/x-www-form-urlencoded',
1152
+ },
1153
+ body: new URLSearchParams(params).toString(),
1154
+ })
1155
+ }
1156
+
1157
+ /**
1158
+ * Authorize Security Group Egress (add outbound rule)
1159
+ */
1160
+ async authorizeSecurityGroupEgress(options: {
1161
+ GroupId: string
1162
+ IpPermissions: IpPermission[]
1163
+ }): Promise<void> {
1164
+ const params: Record<string, string> = {
1165
+ Action: 'AuthorizeSecurityGroupEgress',
1166
+ Version: '2016-11-15',
1167
+ GroupId: options.GroupId,
1168
+ }
1169
+
1170
+ this.encodeIpPermissions(params, options.IpPermissions)
1171
+
1172
+ await this.client.request({
1173
+ service: 'ec2',
1174
+ region: this.region,
1175
+ method: 'POST',
1176
+ path: '/',
1177
+ headers: {
1178
+ 'Content-Type': 'application/x-www-form-urlencoded',
1179
+ },
1180
+ body: new URLSearchParams(params).toString(),
1181
+ })
1182
+ }
1183
+
1184
+ /**
1185
+ * Describe Route Tables
1186
+ */
1187
+ async describeRouteTables(options?: {
1188
+ RouteTableIds?: string[]
1189
+ Filters?: { Name: string, Values: string[] }[]
1190
+ }): Promise<{
1191
+ RouteTables?: RouteTable[]
1192
+ }> {
1193
+ const params: Record<string, string> = {
1194
+ Action: 'DescribeRouteTables',
1195
+ Version: '2016-11-15',
1196
+ }
1197
+
1198
+ if (options?.RouteTableIds) {
1199
+ options.RouteTableIds.forEach((id, i) => {
1200
+ params[`RouteTableId.${i + 1}`] = id
1201
+ })
1202
+ }
1203
+
1204
+ if (options?.Filters) {
1205
+ options.Filters.forEach((filter, i) => {
1206
+ params[`Filter.${i + 1}.Name`] = filter.Name
1207
+ filter.Values.forEach((val, j) => {
1208
+ params[`Filter.${i + 1}.Value.${j + 1}`] = val
1209
+ })
1210
+ })
1211
+ }
1212
+
1213
+ const result = await this.client.request({
1214
+ service: 'ec2',
1215
+ region: this.region,
1216
+ method: 'POST',
1217
+ path: '/',
1218
+ headers: {
1219
+ 'Content-Type': 'application/x-www-form-urlencoded',
1220
+ },
1221
+ body: new URLSearchParams(params).toString(),
1222
+ })
1223
+
1224
+ const response = result.DescribeRouteTablesResponse || result
1225
+
1226
+ return {
1227
+ RouteTables: this.parseArray(response.routeTableSet?.item).map((item: any) => ({
1228
+ RouteTableId: item.routeTableId,
1229
+ VpcId: item.vpcId,
1230
+ Routes: this.parseArray(item.routeSet?.item).map((r: any) => ({
1231
+ DestinationCidrBlock: r.destinationCidrBlock,
1232
+ GatewayId: r.gatewayId,
1233
+ NatGatewayId: r.natGatewayId,
1234
+ State: r.state,
1235
+ })),
1236
+ Associations: this.parseArray(item.associationSet?.item).map((a: any) => ({
1237
+ RouteTableAssociationId: a.routeTableAssociationId,
1238
+ SubnetId: a.subnetId,
1239
+ Main: a.main === 'true' || a.main === true,
1240
+ })),
1241
+ Tags: this.parseTags(item.tagSet?.item),
1242
+ })),
1243
+ }
1244
+ }
1245
+
1246
+ /**
1247
+ * Encode IpPermissions into query parameters for security group rules
1248
+ */
1249
+ private encodeIpPermissions(params: Record<string, string>, permissions: IpPermission[]): void {
1250
+ permissions.forEach((perm, i) => {
1251
+ const prefix = `IpPermissions.${i + 1}`
1252
+
1253
+ if (perm.IpProtocol !== undefined) {
1254
+ params[`${prefix}.IpProtocol`] = perm.IpProtocol
1255
+ }
1256
+ if (perm.FromPort !== undefined) {
1257
+ params[`${prefix}.FromPort`] = String(perm.FromPort)
1258
+ }
1259
+ if (perm.ToPort !== undefined) {
1260
+ params[`${prefix}.ToPort`] = String(perm.ToPort)
1261
+ }
1262
+
1263
+ if (perm.IpRanges) {
1264
+ perm.IpRanges.forEach((range, j) => {
1265
+ if (range.CidrIp) {
1266
+ params[`${prefix}.IpRanges.${j + 1}.CidrIp`] = range.CidrIp
1267
+ }
1268
+ if (range.Description) {
1269
+ params[`${prefix}.IpRanges.${j + 1}.Description`] = range.Description
1270
+ }
1271
+ })
1272
+ }
1273
+
1274
+ if (perm.Ipv6Ranges) {
1275
+ perm.Ipv6Ranges.forEach((range, j) => {
1276
+ if (range.CidrIpv6) {
1277
+ params[`${prefix}.Ipv6Ranges.${j + 1}.CidrIpv6`] = range.CidrIpv6
1278
+ }
1279
+ if (range.Description) {
1280
+ params[`${prefix}.Ipv6Ranges.${j + 1}.Description`] = range.Description
1281
+ }
1282
+ })
1283
+ }
1284
+
1285
+ if (perm.UserIdGroupPairs) {
1286
+ perm.UserIdGroupPairs.forEach((pair, j) => {
1287
+ if (pair.GroupId) {
1288
+ params[`${prefix}.Groups.${j + 1}.GroupId`] = pair.GroupId
1289
+ }
1290
+ if (pair.UserId) {
1291
+ params[`${prefix}.Groups.${j + 1}.UserId`] = pair.UserId
1292
+ }
1293
+ if (pair.Description) {
1294
+ params[`${prefix}.Groups.${j + 1}.Description`] = pair.Description
1295
+ }
1296
+ })
1297
+ }
1298
+ })
1299
+ }
1300
+
1301
+ // Helper methods for parsing EC2 XML responses
1302
+
1303
+ private parseArray(item: any): any[] {
1304
+ if (!item)
1305
+ return []
1306
+ return Array.isArray(item) ? item : [item]
1307
+ }
1308
+
1309
+ private parseTags(item: any): { Key?: string, Value?: string }[] {
1310
+ return this.parseArray(item).map((t: any) => ({
1311
+ Key: t.key,
1312
+ Value: t.value,
1313
+ }))
1314
+ }
1315
+
1316
+ private parseReservations(item: any): { ReservationId?: string, Instances?: Instance[] }[] {
1317
+ return this.parseArray(item).map((r: any) => ({
1318
+ ReservationId: r.reservationId,
1319
+ Instances: this.parseInstances(r.instancesSet?.item || r.instancesSet),
1320
+ }))
1321
+ }
1322
+
1323
+ private parseInstances(item: any): Instance[] {
1324
+ return this.parseArray(item).map((i: any) => ({
1325
+ InstanceId: i.instanceId,
1326
+ ImageId: i.imageId,
1327
+ InstanceType: i.instanceType,
1328
+ State: i.instanceState ? {
1329
+ Code: Number.parseInt(i.instanceState.code),
1330
+ Name: i.instanceState.name,
1331
+ } : undefined,
1332
+ PrivateIpAddress: i.privateIpAddress,
1333
+ PublicIpAddress: i.ipAddress,
1334
+ SubnetId: i.subnetId,
1335
+ VpcId: i.vpcId,
1336
+ SecurityGroups: this.parseArray(i.groupSet?.item).map((g: any) => ({
1337
+ GroupId: g.groupId,
1338
+ GroupName: g.groupName,
1339
+ })),
1340
+ Tags: this.parseTags(i.tagSet?.item),
1341
+ LaunchTime: i.launchTime,
1342
+ Placement: i.placement ? {
1343
+ AvailabilityZone: i.placement.availabilityZone,
1344
+ Tenancy: i.placement.tenancy,
1345
+ } : undefined,
1346
+ Architecture: i.architecture,
1347
+ RootDeviceType: i.rootDeviceType,
1348
+ RootDeviceName: i.rootDeviceName,
1349
+ BlockDeviceMappings: this.parseArray(i.blockDeviceMapping?.item).map((b: any) => ({
1350
+ DeviceName: b.deviceName,
1351
+ Ebs: b.ebs ? {
1352
+ VolumeId: b.ebs.volumeId,
1353
+ Status: b.ebs.status,
1354
+ AttachTime: b.ebs.attachTime,
1355
+ DeleteOnTermination: b.ebs.deleteOnTermination === 'true',
1356
+ } : undefined,
1357
+ })),
1358
+ IamInstanceProfile: i.iamInstanceProfile ? {
1359
+ Arn: i.iamInstanceProfile.arn,
1360
+ Id: i.iamInstanceProfile.id,
1361
+ } : undefined,
1362
+ }))
1363
+ }
1364
+
1365
+ private parseIpPermissions(item: any): IpPermission[] {
1366
+ return this.parseArray(item).map((p: any) => ({
1367
+ IpProtocol: p.ipProtocol,
1368
+ FromPort: p.fromPort ? Number.parseInt(p.fromPort) : undefined,
1369
+ ToPort: p.toPort ? Number.parseInt(p.toPort) : undefined,
1370
+ IpRanges: this.parseArray(p.ipRanges?.item).map((r: any) => ({
1371
+ CidrIp: r.cidrIp,
1372
+ Description: r.description,
1373
+ })),
1374
+ Ipv6Ranges: this.parseArray(p.ipv6Ranges?.item).map((r: any) => ({
1375
+ CidrIpv6: r.cidrIpv6,
1376
+ Description: r.description,
1377
+ })),
1378
+ UserIdGroupPairs: this.parseArray(p.groups?.item).map((g: any) => ({
1379
+ GroupId: g.groupId,
1380
+ UserId: g.userId,
1381
+ Description: g.description,
1382
+ })),
1383
+ }))
1384
+ }
1385
+ }