@stacksjs/ts-cloud 0.1.8 → 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 (75) hide show
  1. package/dist/bin/cli.js +1 -1
  2. package/package.json +18 -16
  3. package/src/aws/acm.ts +768 -0
  4. package/src/aws/application-autoscaling.ts +845 -0
  5. package/src/aws/bedrock.ts +4074 -0
  6. package/src/aws/client.ts +891 -0
  7. package/src/aws/cloudformation.ts +896 -0
  8. package/src/aws/cloudfront.ts +1531 -0
  9. package/src/aws/cloudwatch-logs.ts +154 -0
  10. package/src/aws/comprehend.ts +839 -0
  11. package/src/aws/connect.ts +1056 -0
  12. package/src/aws/deploy-imap.ts +384 -0
  13. package/src/aws/dynamodb.ts +340 -0
  14. package/src/aws/ec2.ts +1385 -0
  15. package/src/aws/ecr.ts +621 -0
  16. package/src/aws/ecs.ts +615 -0
  17. package/src/aws/elasticache.ts +301 -0
  18. package/src/aws/elbv2.ts +942 -0
  19. package/src/aws/email.ts +928 -0
  20. package/src/aws/eventbridge.ts +248 -0
  21. package/src/aws/iam.ts +1689 -0
  22. package/src/aws/imap-server.ts +2100 -0
  23. package/src/aws/index.ts +213 -0
  24. package/src/aws/kendra.ts +1097 -0
  25. package/src/aws/lambda.ts +786 -0
  26. package/src/aws/opensearch.ts +158 -0
  27. package/src/aws/personalize.ts +977 -0
  28. package/src/aws/polly.ts +559 -0
  29. package/src/aws/rds.ts +888 -0
  30. package/src/aws/rekognition.ts +846 -0
  31. package/src/aws/route53-domains.ts +359 -0
  32. package/src/aws/route53.ts +1046 -0
  33. package/src/aws/s3.ts +2334 -0
  34. package/src/aws/scheduler.ts +571 -0
  35. package/src/aws/secrets-manager.ts +769 -0
  36. package/src/aws/ses.ts +1081 -0
  37. package/src/aws/setup-phone.ts +104 -0
  38. package/src/aws/setup-sms.ts +580 -0
  39. package/src/aws/sms.ts +1735 -0
  40. package/src/aws/smtp-server.ts +531 -0
  41. package/src/aws/sns.ts +758 -0
  42. package/src/aws/sqs.ts +382 -0
  43. package/src/aws/ssm.ts +807 -0
  44. package/src/aws/sts.ts +92 -0
  45. package/src/aws/support.ts +391 -0
  46. package/src/aws/test-imap.ts +86 -0
  47. package/src/aws/textract.ts +780 -0
  48. package/src/aws/transcribe.ts +108 -0
  49. package/src/aws/translate.ts +641 -0
  50. package/src/aws/voice.ts +1379 -0
  51. package/src/config.ts +35 -0
  52. package/src/deploy/index.ts +7 -0
  53. package/src/deploy/static-site-external-dns.ts +945 -0
  54. package/src/deploy/static-site.ts +1175 -0
  55. package/src/dns/cloudflare.ts +548 -0
  56. package/src/dns/godaddy.ts +412 -0
  57. package/src/dns/index.ts +205 -0
  58. package/src/dns/porkbun.ts +362 -0
  59. package/src/dns/route53-adapter.ts +414 -0
  60. package/src/dns/types.ts +119 -0
  61. package/src/dns/validator.ts +369 -0
  62. package/src/generators/index.ts +5 -0
  63. package/src/generators/infrastructure.ts +1660 -0
  64. package/src/index.ts +163 -0
  65. package/src/push/apns.ts +452 -0
  66. package/src/push/fcm.ts +506 -0
  67. package/src/push/index.ts +58 -0
  68. package/src/security/pre-deploy-scanner.ts +655 -0
  69. package/src/ssl/acme-client.ts +478 -0
  70. package/src/ssl/index.ts +7 -0
  71. package/src/ssl/letsencrypt.ts +747 -0
  72. package/src/types.ts +2 -0
  73. package/src/utils/cli.ts +398 -0
  74. package/src/validation/index.ts +5 -0
  75. package/src/validation/template.ts +405 -0
@@ -0,0 +1,942 @@
1
+ /**
2
+ * AWS Elastic Load Balancing V2 (ELBv2) Operations
3
+ * Direct API calls without AWS CLI dependency
4
+ *
5
+ * Supports Application Load Balancers (ALB), Network Load Balancers (NLB),
6
+ * and Gateway Load Balancers (GWLB)
7
+ */
8
+
9
+ import { AWSClient } from './client'
10
+
11
+ export interface LoadBalancer {
12
+ LoadBalancerArn?: string
13
+ DNSName?: string
14
+ CanonicalHostedZoneId?: string
15
+ CreatedTime?: string
16
+ LoadBalancerName?: string
17
+ Scheme?: 'internet-facing' | 'internal'
18
+ VpcId?: string
19
+ State?: {
20
+ Code?: 'active' | 'provisioning' | 'active_impaired' | 'failed'
21
+ Reason?: string
22
+ }
23
+ Type?: 'application' | 'network' | 'gateway'
24
+ AvailabilityZones?: AvailabilityZone[]
25
+ SecurityGroups?: string[]
26
+ IpAddressType?: 'ipv4' | 'dualstack'
27
+ }
28
+
29
+ export interface AvailabilityZone {
30
+ ZoneName?: string
31
+ SubnetId?: string
32
+ OutpostId?: string
33
+ LoadBalancerAddresses?: LoadBalancerAddress[]
34
+ }
35
+
36
+ export interface LoadBalancerAddress {
37
+ IpAddress?: string
38
+ AllocationId?: string
39
+ PrivateIPv4Address?: string
40
+ IPv6Address?: string
41
+ }
42
+
43
+ export interface TargetGroup {
44
+ TargetGroupArn?: string
45
+ TargetGroupName?: string
46
+ Protocol?: string
47
+ Port?: number
48
+ VpcId?: string
49
+ HealthCheckProtocol?: string
50
+ HealthCheckPort?: string
51
+ HealthCheckEnabled?: boolean
52
+ HealthCheckIntervalSeconds?: number
53
+ HealthCheckTimeoutSeconds?: number
54
+ HealthyThresholdCount?: number
55
+ UnhealthyThresholdCount?: number
56
+ HealthCheckPath?: string
57
+ Matcher?: {
58
+ HttpCode?: string
59
+ GrpcCode?: string
60
+ }
61
+ LoadBalancerArns?: string[]
62
+ TargetType?: 'instance' | 'ip' | 'lambda' | 'alb'
63
+ ProtocolVersion?: string
64
+ IpAddressType?: 'ipv4' | 'ipv6'
65
+ }
66
+
67
+ export interface Listener {
68
+ ListenerArn?: string
69
+ LoadBalancerArn?: string
70
+ Port?: number
71
+ Protocol?: string
72
+ Certificates?: Certificate[]
73
+ SslPolicy?: string
74
+ DefaultActions?: Action[]
75
+ AlpnPolicy?: string[]
76
+ }
77
+
78
+ export interface Certificate {
79
+ CertificateArn?: string
80
+ IsDefault?: boolean
81
+ }
82
+
83
+ export interface Action {
84
+ Type?: 'forward' | 'redirect' | 'fixed-response' | 'authenticate-oidc' | 'authenticate-cognito'
85
+ TargetGroupArn?: string
86
+ Order?: number
87
+ RedirectConfig?: {
88
+ Protocol?: string
89
+ Port?: string
90
+ Host?: string
91
+ Path?: string
92
+ Query?: string
93
+ StatusCode?: 'HTTP_301' | 'HTTP_302'
94
+ }
95
+ FixedResponseConfig?: {
96
+ MessageBody?: string
97
+ StatusCode?: string
98
+ ContentType?: string
99
+ }
100
+ ForwardConfig?: {
101
+ TargetGroups?: Array<{
102
+ TargetGroupArn?: string
103
+ Weight?: number
104
+ }>
105
+ TargetGroupStickinessConfig?: {
106
+ Enabled?: boolean
107
+ DurationSeconds?: number
108
+ }
109
+ }
110
+ }
111
+
112
+ export interface Rule {
113
+ RuleArn?: string
114
+ Priority?: string
115
+ Conditions?: Condition[]
116
+ Actions?: Action[]
117
+ IsDefault?: boolean
118
+ }
119
+
120
+ export interface Condition {
121
+ Field?: string
122
+ Values?: string[]
123
+ HostHeaderConfig?: { Values?: string[] }
124
+ PathPatternConfig?: { Values?: string[] }
125
+ HttpHeaderConfig?: { HttpHeaderName?: string, Values?: string[] }
126
+ QueryStringConfig?: { Values?: Array<{ Key?: string, Value?: string }> }
127
+ HttpRequestMethodConfig?: { Values?: string[] }
128
+ SourceIpConfig?: { Values?: string[] }
129
+ }
130
+
131
+ export interface TargetHealthDescription {
132
+ Target?: {
133
+ Id?: string
134
+ Port?: number
135
+ AvailabilityZone?: string
136
+ }
137
+ HealthCheckPort?: string
138
+ TargetHealth?: {
139
+ State?: 'initial' | 'healthy' | 'unhealthy' | 'unused' | 'draining' | 'unavailable'
140
+ Reason?: string
141
+ Description?: string
142
+ }
143
+ }
144
+
145
+ /**
146
+ * ELBv2 client for managing Application, Network, and Gateway Load Balancers
147
+ */
148
+ export class ELBv2Client {
149
+ private client: AWSClient
150
+ private region: string
151
+
152
+ constructor(region: string = 'us-east-1') {
153
+ this.region = region
154
+ this.client = new AWSClient()
155
+ }
156
+
157
+ /**
158
+ * Describe load balancers
159
+ */
160
+ async describeLoadBalancers(options?: {
161
+ LoadBalancerArns?: string[]
162
+ Names?: string[]
163
+ Marker?: string
164
+ PageSize?: number
165
+ }): Promise<{ LoadBalancers?: LoadBalancer[], NextMarker?: string }> {
166
+ const params: Record<string, any> = {}
167
+
168
+ if (options?.LoadBalancerArns) {
169
+ options.LoadBalancerArns.forEach((arn, index) => {
170
+ params[`LoadBalancerArns.member.${index + 1}`] = arn
171
+ })
172
+ }
173
+
174
+ if (options?.Names) {
175
+ options.Names.forEach((name, index) => {
176
+ params[`Names.member.${index + 1}`] = name
177
+ })
178
+ }
179
+
180
+ if (options?.Marker) {
181
+ params.Marker = options.Marker
182
+ }
183
+
184
+ if (options?.PageSize) {
185
+ params.PageSize = options.PageSize
186
+ }
187
+
188
+ const result = await this.client.request({
189
+ service: 'elasticloadbalancing',
190
+ region: this.region,
191
+ method: 'POST',
192
+ path: '/',
193
+ headers: {
194
+ 'Content-Type': 'application/x-www-form-urlencoded',
195
+ },
196
+ body: this.buildFormBody('DescribeLoadBalancers', params),
197
+ })
198
+
199
+ return this.normalizeResult(result, 'DescribeLoadBalancersResult')
200
+ }
201
+
202
+ /**
203
+ * Describe target groups
204
+ */
205
+ async describeTargetGroups(options?: {
206
+ LoadBalancerArn?: string
207
+ TargetGroupArns?: string[]
208
+ Names?: string[]
209
+ Marker?: string
210
+ PageSize?: number
211
+ }): Promise<{ TargetGroups?: TargetGroup[], NextMarker?: string }> {
212
+ const params: Record<string, any> = {}
213
+
214
+ if (options?.LoadBalancerArn) {
215
+ params.LoadBalancerArn = options.LoadBalancerArn
216
+ }
217
+
218
+ if (options?.TargetGroupArns) {
219
+ options.TargetGroupArns.forEach((arn, index) => {
220
+ params[`TargetGroupArns.member.${index + 1}`] = arn
221
+ })
222
+ }
223
+
224
+ if (options?.Names) {
225
+ options.Names.forEach((name, index) => {
226
+ params[`Names.member.${index + 1}`] = name
227
+ })
228
+ }
229
+
230
+ if (options?.Marker) {
231
+ params.Marker = options.Marker
232
+ }
233
+
234
+ if (options?.PageSize) {
235
+ params.PageSize = options.PageSize
236
+ }
237
+
238
+ const result = await this.client.request({
239
+ service: 'elasticloadbalancing',
240
+ region: this.region,
241
+ method: 'POST',
242
+ path: '/',
243
+ headers: {
244
+ 'Content-Type': 'application/x-www-form-urlencoded',
245
+ },
246
+ body: this.buildFormBody('DescribeTargetGroups', params),
247
+ })
248
+
249
+ return this.normalizeResult(result, 'DescribeTargetGroupsResult')
250
+ }
251
+
252
+ /**
253
+ * Describe target health
254
+ */
255
+ async describeTargetHealth(options: {
256
+ TargetGroupArn: string
257
+ Targets?: Array<{ Id: string, Port?: number, AvailabilityZone?: string }>
258
+ }): Promise<{ TargetHealthDescriptions?: TargetHealthDescription[] }> {
259
+ const params: Record<string, any> = {
260
+ TargetGroupArn: options.TargetGroupArn,
261
+ }
262
+
263
+ if (options.Targets) {
264
+ options.Targets.forEach((target, index) => {
265
+ params[`Targets.member.${index + 1}.Id`] = target.Id
266
+ if (target.Port) {
267
+ params[`Targets.member.${index + 1}.Port`] = target.Port
268
+ }
269
+ if (target.AvailabilityZone) {
270
+ params[`Targets.member.${index + 1}.AvailabilityZone`] = target.AvailabilityZone
271
+ }
272
+ })
273
+ }
274
+
275
+ const result = await this.client.request({
276
+ service: 'elasticloadbalancing',
277
+ region: this.region,
278
+ method: 'POST',
279
+ path: '/',
280
+ headers: {
281
+ 'Content-Type': 'application/x-www-form-urlencoded',
282
+ },
283
+ body: this.buildFormBody('DescribeTargetHealth', params),
284
+ })
285
+
286
+ return this.normalizeResult(result, 'DescribeTargetHealthResult')
287
+ }
288
+
289
+ /**
290
+ * Describe listeners
291
+ */
292
+ async describeListeners(options?: {
293
+ LoadBalancerArn?: string
294
+ ListenerArns?: string[]
295
+ Marker?: string
296
+ PageSize?: number
297
+ }): Promise<{ Listeners?: Listener[], NextMarker?: string }> {
298
+ const params: Record<string, any> = {}
299
+
300
+ if (options?.LoadBalancerArn) {
301
+ params.LoadBalancerArn = options.LoadBalancerArn
302
+ }
303
+
304
+ if (options?.ListenerArns) {
305
+ options.ListenerArns.forEach((arn, index) => {
306
+ params[`ListenerArns.member.${index + 1}`] = arn
307
+ })
308
+ }
309
+
310
+ if (options?.Marker) {
311
+ params.Marker = options.Marker
312
+ }
313
+
314
+ if (options?.PageSize) {
315
+ params.PageSize = options.PageSize
316
+ }
317
+
318
+ const result = await this.client.request({
319
+ service: 'elasticloadbalancing',
320
+ region: this.region,
321
+ method: 'POST',
322
+ path: '/',
323
+ headers: {
324
+ 'Content-Type': 'application/x-www-form-urlencoded',
325
+ },
326
+ body: this.buildFormBody('DescribeListeners', params),
327
+ })
328
+
329
+ return this.normalizeResult(result, 'DescribeListenersResult')
330
+ }
331
+
332
+ /**
333
+ * Describe rules for a listener
334
+ */
335
+ async describeRules(options?: {
336
+ ListenerArn?: string
337
+ RuleArns?: string[]
338
+ Marker?: string
339
+ PageSize?: number
340
+ }): Promise<{ Rules?: Rule[], NextMarker?: string }> {
341
+ const params: Record<string, any> = {}
342
+
343
+ if (options?.ListenerArn) {
344
+ params.ListenerArn = options.ListenerArn
345
+ }
346
+
347
+ if (options?.RuleArns) {
348
+ options.RuleArns.forEach((arn, index) => {
349
+ params[`RuleArns.member.${index + 1}`] = arn
350
+ })
351
+ }
352
+
353
+ if (options?.Marker) {
354
+ params.Marker = options.Marker
355
+ }
356
+
357
+ if (options?.PageSize) {
358
+ params.PageSize = options.PageSize
359
+ }
360
+
361
+ const result = await this.client.request({
362
+ service: 'elasticloadbalancing',
363
+ region: this.region,
364
+ method: 'POST',
365
+ path: '/',
366
+ headers: {
367
+ 'Content-Type': 'application/x-www-form-urlencoded',
368
+ },
369
+ body: this.buildFormBody('DescribeRules', params),
370
+ })
371
+
372
+ return this.normalizeResult(result, 'DescribeRulesResult')
373
+ }
374
+
375
+ /**
376
+ * Describe load balancer attributes
377
+ */
378
+ async describeLoadBalancerAttributes(loadBalancerArn: string): Promise<{ Attributes?: Array<{ Key: string, Value: string }> }> {
379
+ const params = {
380
+ LoadBalancerArn: loadBalancerArn,
381
+ }
382
+
383
+ const result = await this.client.request({
384
+ service: 'elasticloadbalancing',
385
+ region: this.region,
386
+ method: 'POST',
387
+ path: '/',
388
+ headers: {
389
+ 'Content-Type': 'application/x-www-form-urlencoded',
390
+ },
391
+ body: this.buildFormBody('DescribeLoadBalancerAttributes', params),
392
+ })
393
+
394
+ return this.normalizeResult(result, 'DescribeLoadBalancerAttributesResult')
395
+ }
396
+
397
+ /**
398
+ * Describe target group attributes
399
+ */
400
+ async describeTargetGroupAttributes(targetGroupArn: string): Promise<{ Attributes?: Array<{ Key: string, Value: string }> }> {
401
+ const params = {
402
+ TargetGroupArn: targetGroupArn,
403
+ }
404
+
405
+ const result = await this.client.request({
406
+ service: 'elasticloadbalancing',
407
+ region: this.region,
408
+ method: 'POST',
409
+ path: '/',
410
+ headers: {
411
+ 'Content-Type': 'application/x-www-form-urlencoded',
412
+ },
413
+ body: this.buildFormBody('DescribeTargetGroupAttributes', params),
414
+ })
415
+
416
+ return this.normalizeResult(result, 'DescribeTargetGroupAttributesResult')
417
+ }
418
+
419
+ /**
420
+ * Create a load balancer
421
+ */
422
+ async createLoadBalancer(options: {
423
+ Name: string
424
+ Subnets?: string[]
425
+ SubnetMappings?: Array<{
426
+ SubnetId: string
427
+ AllocationId?: string
428
+ PrivateIPv4Address?: string
429
+ IPv6Address?: string
430
+ }>
431
+ SecurityGroups?: string[]
432
+ Scheme?: 'internet-facing' | 'internal'
433
+ Type?: 'application' | 'network' | 'gateway'
434
+ IpAddressType?: 'ipv4' | 'dualstack'
435
+ Tags?: Array<{ Key: string, Value: string }>
436
+ }): Promise<{ LoadBalancers?: LoadBalancer[] }> {
437
+ const params: Record<string, any> = {
438
+ Name: options.Name,
439
+ }
440
+
441
+ if (options.Subnets) {
442
+ options.Subnets.forEach((subnet, index) => {
443
+ params[`Subnets.member.${index + 1}`] = subnet
444
+ })
445
+ }
446
+
447
+ if (options.SubnetMappings) {
448
+ options.SubnetMappings.forEach((mapping, index) => {
449
+ params[`SubnetMappings.member.${index + 1}.SubnetId`] = mapping.SubnetId
450
+ if (mapping.AllocationId) {
451
+ params[`SubnetMappings.member.${index + 1}.AllocationId`] = mapping.AllocationId
452
+ }
453
+ if (mapping.PrivateIPv4Address) {
454
+ params[`SubnetMappings.member.${index + 1}.PrivateIPv4Address`] = mapping.PrivateIPv4Address
455
+ }
456
+ if (mapping.IPv6Address) {
457
+ params[`SubnetMappings.member.${index + 1}.IPv6Address`] = mapping.IPv6Address
458
+ }
459
+ })
460
+ }
461
+
462
+ if (options.SecurityGroups) {
463
+ options.SecurityGroups.forEach((sg, index) => {
464
+ params[`SecurityGroups.member.${index + 1}`] = sg
465
+ })
466
+ }
467
+
468
+ if (options.Scheme) {
469
+ params.Scheme = options.Scheme
470
+ }
471
+
472
+ if (options.Type) {
473
+ params.Type = options.Type
474
+ }
475
+
476
+ if (options.IpAddressType) {
477
+ params.IpAddressType = options.IpAddressType
478
+ }
479
+
480
+ if (options.Tags) {
481
+ options.Tags.forEach((tag, index) => {
482
+ params[`Tags.member.${index + 1}.Key`] = tag.Key
483
+ params[`Tags.member.${index + 1}.Value`] = tag.Value
484
+ })
485
+ }
486
+
487
+ const result = await this.client.request({
488
+ service: 'elasticloadbalancing',
489
+ region: this.region,
490
+ method: 'POST',
491
+ path: '/',
492
+ headers: {
493
+ 'Content-Type': 'application/x-www-form-urlencoded',
494
+ },
495
+ body: this.buildFormBody('CreateLoadBalancer', params),
496
+ })
497
+
498
+ return this.normalizeResult(result, 'CreateLoadBalancerResult')
499
+ }
500
+
501
+ /**
502
+ * Delete a load balancer
503
+ */
504
+ async deleteLoadBalancer(loadBalancerArn: string): Promise<void> {
505
+ const params = {
506
+ LoadBalancerArn: loadBalancerArn,
507
+ }
508
+
509
+ await this.client.request({
510
+ service: 'elasticloadbalancing',
511
+ region: this.region,
512
+ method: 'POST',
513
+ path: '/',
514
+ headers: {
515
+ 'Content-Type': 'application/x-www-form-urlencoded',
516
+ },
517
+ body: this.buildFormBody('DeleteLoadBalancer', params),
518
+ })
519
+ }
520
+
521
+ /**
522
+ * Create a target group
523
+ */
524
+ async createTargetGroup(options: {
525
+ Name: string
526
+ Protocol?: string
527
+ ProtocolVersion?: string
528
+ Port?: number
529
+ VpcId?: string
530
+ HealthCheckProtocol?: string
531
+ HealthCheckPort?: string
532
+ HealthCheckEnabled?: boolean
533
+ HealthCheckPath?: string
534
+ HealthCheckIntervalSeconds?: number
535
+ HealthCheckTimeoutSeconds?: number
536
+ HealthyThresholdCount?: number
537
+ UnhealthyThresholdCount?: number
538
+ Matcher?: { HttpCode?: string, GrpcCode?: string }
539
+ TargetType?: 'instance' | 'ip' | 'lambda' | 'alb'
540
+ Tags?: Array<{ Key: string, Value: string }>
541
+ IpAddressType?: 'ipv4' | 'ipv6'
542
+ }): Promise<{ TargetGroups?: TargetGroup[] }> {
543
+ const params: Record<string, any> = {
544
+ Name: options.Name,
545
+ }
546
+
547
+ if (options.Protocol) params.Protocol = options.Protocol
548
+ if (options.ProtocolVersion) params.ProtocolVersion = options.ProtocolVersion
549
+ if (options.Port) params.Port = options.Port
550
+ if (options.VpcId) params.VpcId = options.VpcId
551
+ if (options.HealthCheckProtocol) params.HealthCheckProtocol = options.HealthCheckProtocol
552
+ if (options.HealthCheckPort) params.HealthCheckPort = options.HealthCheckPort
553
+ if (options.HealthCheckEnabled !== undefined) params.HealthCheckEnabled = options.HealthCheckEnabled
554
+ if (options.HealthCheckPath) params.HealthCheckPath = options.HealthCheckPath
555
+ if (options.HealthCheckIntervalSeconds) params.HealthCheckIntervalSeconds = options.HealthCheckIntervalSeconds
556
+ if (options.HealthCheckTimeoutSeconds) params.HealthCheckTimeoutSeconds = options.HealthCheckTimeoutSeconds
557
+ if (options.HealthyThresholdCount) params.HealthyThresholdCount = options.HealthyThresholdCount
558
+ if (options.UnhealthyThresholdCount) params.UnhealthyThresholdCount = options.UnhealthyThresholdCount
559
+ if (options.TargetType) params.TargetType = options.TargetType
560
+ if (options.IpAddressType) params.IpAddressType = options.IpAddressType
561
+
562
+ if (options.Matcher) {
563
+ if (options.Matcher.HttpCode) params['Matcher.HttpCode'] = options.Matcher.HttpCode
564
+ if (options.Matcher.GrpcCode) params['Matcher.GrpcCode'] = options.Matcher.GrpcCode
565
+ }
566
+
567
+ if (options.Tags) {
568
+ options.Tags.forEach((tag, index) => {
569
+ params[`Tags.member.${index + 1}.Key`] = tag.Key
570
+ params[`Tags.member.${index + 1}.Value`] = tag.Value
571
+ })
572
+ }
573
+
574
+ const result = await this.client.request({
575
+ service: 'elasticloadbalancing',
576
+ region: this.region,
577
+ method: 'POST',
578
+ path: '/',
579
+ headers: {
580
+ 'Content-Type': 'application/x-www-form-urlencoded',
581
+ },
582
+ body: this.buildFormBody('CreateTargetGroup', params),
583
+ })
584
+
585
+ return this.normalizeResult(result, 'CreateTargetGroupResult')
586
+ }
587
+
588
+ /**
589
+ * Delete a target group
590
+ */
591
+ async deleteTargetGroup(targetGroupArn: string): Promise<void> {
592
+ const params = {
593
+ TargetGroupArn: targetGroupArn,
594
+ }
595
+
596
+ await this.client.request({
597
+ service: 'elasticloadbalancing',
598
+ region: this.region,
599
+ method: 'POST',
600
+ path: '/',
601
+ headers: {
602
+ 'Content-Type': 'application/x-www-form-urlencoded',
603
+ },
604
+ body: this.buildFormBody('DeleteTargetGroup', params),
605
+ })
606
+ }
607
+
608
+ /**
609
+ * Register targets with a target group
610
+ */
611
+ async registerTargets(options: {
612
+ TargetGroupArn: string
613
+ Targets: Array<{ Id: string, Port?: number, AvailabilityZone?: string }>
614
+ }): Promise<void> {
615
+ const params: Record<string, any> = {
616
+ TargetGroupArn: options.TargetGroupArn,
617
+ }
618
+
619
+ options.Targets.forEach((target, index) => {
620
+ params[`Targets.member.${index + 1}.Id`] = target.Id
621
+ if (target.Port) {
622
+ params[`Targets.member.${index + 1}.Port`] = target.Port
623
+ }
624
+ if (target.AvailabilityZone) {
625
+ params[`Targets.member.${index + 1}.AvailabilityZone`] = target.AvailabilityZone
626
+ }
627
+ })
628
+
629
+ await this.client.request({
630
+ service: 'elasticloadbalancing',
631
+ region: this.region,
632
+ method: 'POST',
633
+ path: '/',
634
+ headers: {
635
+ 'Content-Type': 'application/x-www-form-urlencoded',
636
+ },
637
+ body: this.buildFormBody('RegisterTargets', params),
638
+ })
639
+ }
640
+
641
+ /**
642
+ * Deregister targets from a target group
643
+ */
644
+ async deregisterTargets(options: {
645
+ TargetGroupArn: string
646
+ Targets: Array<{ Id: string, Port?: number, AvailabilityZone?: string }>
647
+ }): Promise<void> {
648
+ const params: Record<string, any> = {
649
+ TargetGroupArn: options.TargetGroupArn,
650
+ }
651
+
652
+ options.Targets.forEach((target, index) => {
653
+ params[`Targets.member.${index + 1}.Id`] = target.Id
654
+ if (target.Port) {
655
+ params[`Targets.member.${index + 1}.Port`] = target.Port
656
+ }
657
+ if (target.AvailabilityZone) {
658
+ params[`Targets.member.${index + 1}.AvailabilityZone`] = target.AvailabilityZone
659
+ }
660
+ })
661
+
662
+ await this.client.request({
663
+ service: 'elasticloadbalancing',
664
+ region: this.region,
665
+ method: 'POST',
666
+ path: '/',
667
+ headers: {
668
+ 'Content-Type': 'application/x-www-form-urlencoded',
669
+ },
670
+ body: this.buildFormBody('DeregisterTargets', params),
671
+ })
672
+ }
673
+
674
+ /**
675
+ * Create a listener
676
+ */
677
+ async createListener(options: {
678
+ LoadBalancerArn: string
679
+ Protocol?: string
680
+ Port: number
681
+ SslPolicy?: string
682
+ Certificates?: Array<{ CertificateArn: string }>
683
+ DefaultActions: Array<{
684
+ Type: 'forward' | 'redirect' | 'fixed-response'
685
+ TargetGroupArn?: string
686
+ Order?: number
687
+ RedirectConfig?: {
688
+ Protocol?: string
689
+ Port?: string
690
+ Host?: string
691
+ Path?: string
692
+ Query?: string
693
+ StatusCode: 'HTTP_301' | 'HTTP_302'
694
+ }
695
+ FixedResponseConfig?: {
696
+ MessageBody?: string
697
+ StatusCode: string
698
+ ContentType?: string
699
+ }
700
+ }>
701
+ AlpnPolicy?: string[]
702
+ Tags?: Array<{ Key: string, Value: string }>
703
+ }): Promise<{ Listeners?: Listener[] }> {
704
+ const params: Record<string, any> = {
705
+ LoadBalancerArn: options.LoadBalancerArn,
706
+ Port: options.Port,
707
+ }
708
+
709
+ if (options.Protocol) params.Protocol = options.Protocol
710
+ if (options.SslPolicy) params.SslPolicy = options.SslPolicy
711
+
712
+ if (options.Certificates) {
713
+ options.Certificates.forEach((cert, index) => {
714
+ params[`Certificates.member.${index + 1}.CertificateArn`] = cert.CertificateArn
715
+ })
716
+ }
717
+
718
+ options.DefaultActions.forEach((action, index) => {
719
+ params[`DefaultActions.member.${index + 1}.Type`] = action.Type
720
+ if (action.TargetGroupArn) {
721
+ params[`DefaultActions.member.${index + 1}.TargetGroupArn`] = action.TargetGroupArn
722
+ }
723
+ if (action.Order !== undefined) {
724
+ params[`DefaultActions.member.${index + 1}.Order`] = action.Order
725
+ }
726
+ if (action.RedirectConfig) {
727
+ const rc = action.RedirectConfig
728
+ if (rc.Protocol) params[`DefaultActions.member.${index + 1}.RedirectConfig.Protocol`] = rc.Protocol
729
+ if (rc.Port) params[`DefaultActions.member.${index + 1}.RedirectConfig.Port`] = rc.Port
730
+ if (rc.Host) params[`DefaultActions.member.${index + 1}.RedirectConfig.Host`] = rc.Host
731
+ if (rc.Path) params[`DefaultActions.member.${index + 1}.RedirectConfig.Path`] = rc.Path
732
+ if (rc.Query) params[`DefaultActions.member.${index + 1}.RedirectConfig.Query`] = rc.Query
733
+ params[`DefaultActions.member.${index + 1}.RedirectConfig.StatusCode`] = rc.StatusCode
734
+ }
735
+ if (action.FixedResponseConfig) {
736
+ const fr = action.FixedResponseConfig
737
+ if (fr.MessageBody) params[`DefaultActions.member.${index + 1}.FixedResponseConfig.MessageBody`] = fr.MessageBody
738
+ params[`DefaultActions.member.${index + 1}.FixedResponseConfig.StatusCode`] = fr.StatusCode
739
+ if (fr.ContentType) params[`DefaultActions.member.${index + 1}.FixedResponseConfig.ContentType`] = fr.ContentType
740
+ }
741
+ })
742
+
743
+ if (options.AlpnPolicy) {
744
+ options.AlpnPolicy.forEach((policy, index) => {
745
+ params[`AlpnPolicy.member.${index + 1}`] = policy
746
+ })
747
+ }
748
+
749
+ if (options.Tags) {
750
+ options.Tags.forEach((tag, index) => {
751
+ params[`Tags.member.${index + 1}.Key`] = tag.Key
752
+ params[`Tags.member.${index + 1}.Value`] = tag.Value
753
+ })
754
+ }
755
+
756
+ const result = await this.client.request({
757
+ service: 'elasticloadbalancing',
758
+ region: this.region,
759
+ method: 'POST',
760
+ path: '/',
761
+ headers: {
762
+ 'Content-Type': 'application/x-www-form-urlencoded',
763
+ },
764
+ body: this.buildFormBody('CreateListener', params),
765
+ })
766
+
767
+ return this.normalizeResult(result, 'CreateListenerResult')
768
+ }
769
+
770
+ /**
771
+ * Delete a listener
772
+ */
773
+ async deleteListener(listenerArn: string): Promise<void> {
774
+ const params = {
775
+ ListenerArn: listenerArn,
776
+ }
777
+
778
+ await this.client.request({
779
+ service: 'elasticloadbalancing',
780
+ region: this.region,
781
+ method: 'POST',
782
+ path: '/',
783
+ headers: {
784
+ 'Content-Type': 'application/x-www-form-urlencoded',
785
+ },
786
+ body: this.buildFormBody('DeleteListener', params),
787
+ })
788
+ }
789
+
790
+ /**
791
+ * Modify listener
792
+ */
793
+ async modifyListener(options: {
794
+ ListenerArn: string
795
+ Port?: number
796
+ Protocol?: string
797
+ SslPolicy?: string
798
+ Certificates?: Array<{ CertificateArn: string }>
799
+ DefaultActions?: Action[]
800
+ AlpnPolicy?: string[]
801
+ }): Promise<{ Listeners?: Listener[] }> {
802
+ const params: Record<string, any> = {
803
+ ListenerArn: options.ListenerArn,
804
+ }
805
+
806
+ if (options.Port) params.Port = options.Port
807
+ if (options.Protocol) params.Protocol = options.Protocol
808
+ if (options.SslPolicy) params.SslPolicy = options.SslPolicy
809
+
810
+ if (options.Certificates) {
811
+ options.Certificates.forEach((cert, index) => {
812
+ params[`Certificates.member.${index + 1}.CertificateArn`] = cert.CertificateArn
813
+ })
814
+ }
815
+
816
+ if (options.DefaultActions) {
817
+ options.DefaultActions.forEach((action, index) => {
818
+ if (action.Type) params[`DefaultActions.member.${index + 1}.Type`] = action.Type
819
+ if (action.TargetGroupArn) params[`DefaultActions.member.${index + 1}.TargetGroupArn`] = action.TargetGroupArn
820
+ if (action.Order !== undefined) params[`DefaultActions.member.${index + 1}.Order`] = action.Order
821
+ })
822
+ }
823
+
824
+ if (options.AlpnPolicy) {
825
+ options.AlpnPolicy.forEach((policy, index) => {
826
+ params[`AlpnPolicy.member.${index + 1}`] = policy
827
+ })
828
+ }
829
+
830
+ const result = await this.client.request({
831
+ service: 'elasticloadbalancing',
832
+ region: this.region,
833
+ method: 'POST',
834
+ path: '/',
835
+ headers: {
836
+ 'Content-Type': 'application/x-www-form-urlencoded',
837
+ },
838
+ body: this.buildFormBody('ModifyListener', params),
839
+ })
840
+
841
+ return this.normalizeResult(result, 'ModifyListenerResult')
842
+ }
843
+
844
+ /**
845
+ * Build form URL encoded body for ELBv2 API
846
+ */
847
+ private buildFormBody(action: string, params: Record<string, any>): string {
848
+ const formParams: Record<string, string> = {
849
+ Action: action,
850
+ Version: '2015-12-01',
851
+ }
852
+
853
+ // Flatten params
854
+ for (const [key, value] of Object.entries(params)) {
855
+ if (value !== undefined && value !== null) {
856
+ formParams[key] = String(value)
857
+ }
858
+ }
859
+
860
+ return Object.entries(formParams)
861
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
862
+ .join('&')
863
+ }
864
+
865
+ /**
866
+ * Normalize the parsed XML result from AWS API
867
+ * The client parses XML to JSON, so we need to extract the result
868
+ */
869
+ private normalizeResult(parsed: any, resultKey: string): any {
870
+ // The AWS response is wrapped like: { DescribeLoadBalancersResponse: { DescribeLoadBalancersResult: {...} } }
871
+ // fast-xml-parser returns: { DescribeLoadBalancersResult: {...}, ResponseMetadata: {...} }
872
+
873
+ // Try direct access first
874
+ if (parsed && parsed[resultKey]) {
875
+ return this.normalizeArrays(parsed[resultKey])
876
+ }
877
+
878
+ // Try accessing through response wrapper
879
+ const responseKey = resultKey.replace('Result', 'Response')
880
+ if (parsed && parsed[responseKey] && parsed[responseKey][resultKey]) {
881
+ return this.normalizeArrays(parsed[responseKey][resultKey])
882
+ }
883
+
884
+ // Return parsed as-is if no wrapper found
885
+ return this.normalizeArrays(parsed)
886
+ }
887
+
888
+ /**
889
+ * Normalize arrays in the response
890
+ * AWS XML parsing sometimes returns single items as objects instead of arrays
891
+ */
892
+ private normalizeArrays(obj: any): any {
893
+ if (!obj || typeof obj !== 'object') {
894
+ return obj
895
+ }
896
+
897
+ // Handle arrays
898
+ if (Array.isArray(obj)) {
899
+ return obj.map(item => this.normalizeArrays(item))
900
+ }
901
+
902
+ const result: any = {}
903
+ for (const [key, value] of Object.entries(obj)) {
904
+ // Known array fields that should always be arrays
905
+ const arrayFields = [
906
+ 'LoadBalancers', 'TargetGroups', 'Listeners', 'Rules',
907
+ 'TargetHealthDescriptions', 'Attributes', 'SecurityGroups',
908
+ 'AvailabilityZones', 'Certificates', 'DefaultActions',
909
+ 'Conditions', 'Actions', 'member'
910
+ ]
911
+
912
+ if (key === 'member') {
913
+ // AWS returns arrays as { member: [...] } or { member: {...} }
914
+ if (Array.isArray(value)) {
915
+ return value.map(item => this.normalizeArrays(item))
916
+ }
917
+ return [this.normalizeArrays(value)]
918
+ }
919
+
920
+ if (arrayFields.includes(key)) {
921
+ if (value && typeof value === 'object' && 'member' in (value as any)) {
922
+ const memberValue = (value as any).member
923
+ result[key] = Array.isArray(memberValue)
924
+ ? memberValue.map((item: any) => this.normalizeArrays(item))
925
+ : [this.normalizeArrays(memberValue)]
926
+ } else if (Array.isArray(value)) {
927
+ result[key] = value.map(item => this.normalizeArrays(item))
928
+ } else if (value) {
929
+ result[key] = [this.normalizeArrays(value)]
930
+ } else {
931
+ result[key] = []
932
+ }
933
+ } else if (typeof value === 'object') {
934
+ result[key] = this.normalizeArrays(value)
935
+ } else {
936
+ result[key] = value
937
+ }
938
+ }
939
+
940
+ return result
941
+ }
942
+ }