@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
@@ -0,0 +1,1046 @@
1
+ /**
2
+ * Route53 Client - DNS management without AWS SDK
3
+ * Uses direct AWS API calls with Signature V4
4
+ */
5
+
6
+ import { AWSClient } from './client'
7
+
8
+ export interface HostedZone {
9
+ Id: string
10
+ Name: string
11
+ CallerReference?: string
12
+ Config?: {
13
+ Comment?: string
14
+ PrivateZone?: boolean
15
+ }
16
+ ResourceRecordSetCount?: number
17
+ }
18
+
19
+ export interface ResourceRecordSet {
20
+ Name: string
21
+ Type: string
22
+ TTL?: number
23
+ ResourceRecords?: { Value: string }[]
24
+ AliasTarget?: {
25
+ HostedZoneId: string
26
+ DNSName: string
27
+ EvaluateTargetHealth: boolean
28
+ }
29
+ SetIdentifier?: string
30
+ Weight?: number
31
+ Region?: string
32
+ GeoLocation?: {
33
+ ContinentCode?: string
34
+ CountryCode?: string
35
+ SubdivisionCode?: string
36
+ }
37
+ Failover?: 'PRIMARY' | 'SECONDARY'
38
+ HealthCheckId?: string
39
+ }
40
+
41
+ export interface DelegationSet {
42
+ Id?: string
43
+ CallerReference?: string
44
+ NameServers: string[]
45
+ }
46
+
47
+ export interface CreateHostedZoneResult {
48
+ HostedZone: HostedZone
49
+ ChangeInfo: {
50
+ Id: string
51
+ Status: string
52
+ SubmittedAt: string
53
+ }
54
+ DelegationSet: DelegationSet
55
+ Location: string
56
+ }
57
+
58
+ export interface ListHostedZonesResult {
59
+ HostedZones: HostedZone[]
60
+ IsTruncated: boolean
61
+ MaxItems: string
62
+ Marker?: string
63
+ NextMarker?: string
64
+ }
65
+
66
+ export interface GetHostedZoneResult {
67
+ HostedZone: HostedZone
68
+ DelegationSet: DelegationSet
69
+ VPCs?: { VPCId: string, VPCRegion: string }[]
70
+ }
71
+
72
+ export interface ListResourceRecordSetsResult {
73
+ ResourceRecordSets: ResourceRecordSet[]
74
+ IsTruncated: boolean
75
+ MaxItems: string
76
+ NextRecordName?: string
77
+ NextRecordType?: string
78
+ NextRecordIdentifier?: string
79
+ }
80
+
81
+ export interface ChangeResourceRecordSetsResult {
82
+ ChangeInfo: {
83
+ Id: string
84
+ Status: string
85
+ SubmittedAt: string
86
+ Comment?: string
87
+ }
88
+ }
89
+
90
+ export interface Change {
91
+ Action: 'CREATE' | 'DELETE' | 'UPSERT'
92
+ ResourceRecordSet: ResourceRecordSet
93
+ }
94
+
95
+ export interface ChangeBatch {
96
+ Comment?: string
97
+ Changes: Change[]
98
+ }
99
+
100
+ /**
101
+ * Route53 Client for DNS management
102
+ */
103
+ export class Route53Client {
104
+ private client: AWSClient
105
+ private region: string
106
+
107
+ constructor(region: string = 'us-east-1') {
108
+ this.region = region
109
+ this.client = new AWSClient()
110
+ }
111
+
112
+ /**
113
+ * Create a new hosted zone
114
+ */
115
+ async createHostedZone(params: {
116
+ Name: string
117
+ CallerReference?: string
118
+ HostedZoneConfig?: {
119
+ Comment?: string
120
+ PrivateZone?: boolean
121
+ }
122
+ VPC?: {
123
+ VPCRegion: string
124
+ VPCId: string
125
+ }
126
+ DelegationSetId?: string
127
+ }): Promise<CreateHostedZoneResult> {
128
+ const callerReference = params.CallerReference || `${Date.now()}-${Math.random().toString(36).slice(2)}`
129
+
130
+ // Build XML request body
131
+ let xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
132
+ <CreateHostedZoneRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
133
+ <Name>${params.Name}</Name>
134
+ <CallerReference>${callerReference}</CallerReference>`
135
+
136
+ if (params.HostedZoneConfig) {
137
+ xmlBody += `
138
+ <HostedZoneConfig>`
139
+ if (params.HostedZoneConfig.Comment) {
140
+ xmlBody += `
141
+ <Comment>${params.HostedZoneConfig.Comment}</Comment>`
142
+ }
143
+ if (params.HostedZoneConfig.PrivateZone !== undefined) {
144
+ xmlBody += `
145
+ <PrivateZone>${params.HostedZoneConfig.PrivateZone}</PrivateZone>`
146
+ }
147
+ xmlBody += `
148
+ </HostedZoneConfig>`
149
+ }
150
+
151
+ if (params.VPC) {
152
+ xmlBody += `
153
+ <VPC>
154
+ <VPCRegion>${params.VPC.VPCRegion}</VPCRegion>
155
+ <VPCId>${params.VPC.VPCId}</VPCId>
156
+ </VPC>`
157
+ }
158
+
159
+ if (params.DelegationSetId) {
160
+ xmlBody += `
161
+ <DelegationSetId>${params.DelegationSetId}</DelegationSetId>`
162
+ }
163
+
164
+ xmlBody += `
165
+ </CreateHostedZoneRequest>`
166
+
167
+ const result = await this.client.request({
168
+ service: 'route53',
169
+ region: this.region,
170
+ method: 'POST',
171
+ path: '/2013-04-01/hostedzone',
172
+ headers: {
173
+ 'content-type': 'application/xml',
174
+ },
175
+ body: xmlBody,
176
+ })
177
+
178
+ return this.parseCreateHostedZoneResponse(result)
179
+ }
180
+
181
+ /**
182
+ * List hosted zones
183
+ */
184
+ async listHostedZones(params?: {
185
+ Marker?: string
186
+ MaxItems?: string
187
+ }): Promise<ListHostedZonesResult> {
188
+ const queryParams: Record<string, string> = {}
189
+
190
+ if (params?.Marker) queryParams.marker = params.Marker
191
+ if (params?.MaxItems) queryParams.maxitems = params.MaxItems
192
+
193
+ const result = await this.client.request({
194
+ service: 'route53',
195
+ region: this.region,
196
+ method: 'GET',
197
+ path: '/2013-04-01/hostedzone',
198
+ queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
199
+ })
200
+
201
+ return this.parseListHostedZonesResponse(result)
202
+ }
203
+
204
+ /**
205
+ * List hosted zones by name (more efficient for finding specific zone)
206
+ */
207
+ async listHostedZonesByName(params?: {
208
+ DNSName?: string
209
+ HostedZoneId?: string
210
+ MaxItems?: string
211
+ }): Promise<ListHostedZonesResult> {
212
+ const queryParams: Record<string, string> = {}
213
+
214
+ if (params?.DNSName) queryParams.dnsname = params.DNSName
215
+ if (params?.HostedZoneId) queryParams.hostedzoneid = params.HostedZoneId
216
+ if (params?.MaxItems) queryParams.maxitems = params.MaxItems
217
+
218
+ const result = await this.client.request({
219
+ service: 'route53',
220
+ region: this.region,
221
+ method: 'GET',
222
+ path: '/2013-04-01/hostedzonesbyname',
223
+ queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
224
+ })
225
+
226
+ return this.parseListHostedZonesResponse(result)
227
+ }
228
+
229
+ /**
230
+ * Get a hosted zone
231
+ */
232
+ async getHostedZone(params: {
233
+ Id: string
234
+ }): Promise<GetHostedZoneResult> {
235
+ // Strip /hostedzone/ prefix if present
236
+ const hostedZoneId = params.Id.replace('/hostedzone/', '')
237
+
238
+ const result = await this.client.request({
239
+ service: 'route53',
240
+ region: this.region,
241
+ method: 'GET',
242
+ path: `/2013-04-01/hostedzone/${hostedZoneId}`,
243
+ })
244
+
245
+ return this.parseGetHostedZoneResponse(result)
246
+ }
247
+
248
+ /**
249
+ * Delete a hosted zone
250
+ */
251
+ async deleteHostedZone(params: {
252
+ Id: string
253
+ }): Promise<void> {
254
+ // Strip /hostedzone/ prefix if present
255
+ const hostedZoneId = params.Id.replace('/hostedzone/', '')
256
+
257
+ await this.client.request({
258
+ service: 'route53',
259
+ region: this.region,
260
+ method: 'DELETE',
261
+ path: `/2013-04-01/hostedzone/${hostedZoneId}`,
262
+ })
263
+ }
264
+
265
+ /**
266
+ * List resource record sets in a hosted zone
267
+ */
268
+ async listResourceRecordSets(params: {
269
+ HostedZoneId: string
270
+ StartRecordName?: string
271
+ StartRecordType?: string
272
+ StartRecordIdentifier?: string
273
+ MaxItems?: string
274
+ }): Promise<ListResourceRecordSetsResult> {
275
+ // Strip /hostedzone/ prefix if present
276
+ const hostedZoneId = params.HostedZoneId.replace('/hostedzone/', '')
277
+
278
+ const queryParams: Record<string, string> = {}
279
+
280
+ if (params.StartRecordName) queryParams.name = params.StartRecordName
281
+ if (params.StartRecordType) queryParams.type = params.StartRecordType
282
+ if (params.StartRecordIdentifier) queryParams.identifier = params.StartRecordIdentifier
283
+ if (params.MaxItems) queryParams.maxitems = params.MaxItems
284
+
285
+ const result = await this.client.request({
286
+ service: 'route53',
287
+ region: this.region,
288
+ method: 'GET',
289
+ path: `/2013-04-01/hostedzone/${hostedZoneId}/rrset`,
290
+ queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
291
+ })
292
+
293
+ return this.parseListResourceRecordSetsResponse(result)
294
+ }
295
+
296
+ /**
297
+ * Change resource record sets (create, delete, or upsert)
298
+ */
299
+ async changeResourceRecordSets(params: {
300
+ HostedZoneId: string
301
+ ChangeBatch: ChangeBatch
302
+ }): Promise<ChangeResourceRecordSetsResult> {
303
+ // Strip /hostedzone/ prefix if present
304
+ const hostedZoneId = params.HostedZoneId.replace('/hostedzone/', '')
305
+
306
+ // Build XML request body
307
+ let xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
308
+ <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
309
+ <ChangeBatch>`
310
+
311
+ if (params.ChangeBatch.Comment) {
312
+ xmlBody += `
313
+ <Comment>${this.escapeXml(params.ChangeBatch.Comment)}</Comment>`
314
+ }
315
+
316
+ xmlBody += `
317
+ <Changes>`
318
+
319
+ for (const change of params.ChangeBatch.Changes) {
320
+ xmlBody += `
321
+ <Change>
322
+ <Action>${change.Action}</Action>
323
+ <ResourceRecordSet>
324
+ <Name>${change.ResourceRecordSet.Name}</Name>
325
+ <Type>${change.ResourceRecordSet.Type}</Type>`
326
+
327
+ if (change.ResourceRecordSet.TTL !== undefined) {
328
+ xmlBody += `
329
+ <TTL>${change.ResourceRecordSet.TTL}</TTL>`
330
+ }
331
+
332
+ if (change.ResourceRecordSet.SetIdentifier) {
333
+ xmlBody += `
334
+ <SetIdentifier>${change.ResourceRecordSet.SetIdentifier}</SetIdentifier>`
335
+ }
336
+
337
+ if (change.ResourceRecordSet.Weight !== undefined) {
338
+ xmlBody += `
339
+ <Weight>${change.ResourceRecordSet.Weight}</Weight>`
340
+ }
341
+
342
+ if (change.ResourceRecordSet.Region) {
343
+ xmlBody += `
344
+ <Region>${change.ResourceRecordSet.Region}</Region>`
345
+ }
346
+
347
+ if (change.ResourceRecordSet.Failover) {
348
+ xmlBody += `
349
+ <Failover>${change.ResourceRecordSet.Failover}</Failover>`
350
+ }
351
+
352
+ if (change.ResourceRecordSet.HealthCheckId) {
353
+ xmlBody += `
354
+ <HealthCheckId>${change.ResourceRecordSet.HealthCheckId}</HealthCheckId>`
355
+ }
356
+
357
+ if (change.ResourceRecordSet.ResourceRecords && change.ResourceRecordSet.ResourceRecords.length > 0) {
358
+ xmlBody += `
359
+ <ResourceRecords>`
360
+ for (const record of change.ResourceRecordSet.ResourceRecords) {
361
+ xmlBody += `
362
+ <ResourceRecord>
363
+ <Value>${this.escapeXml(record.Value)}</Value>
364
+ </ResourceRecord>`
365
+ }
366
+ xmlBody += `
367
+ </ResourceRecords>`
368
+ }
369
+
370
+ if (change.ResourceRecordSet.AliasTarget) {
371
+ xmlBody += `
372
+ <AliasTarget>
373
+ <HostedZoneId>${change.ResourceRecordSet.AliasTarget.HostedZoneId}</HostedZoneId>
374
+ <DNSName>${change.ResourceRecordSet.AliasTarget.DNSName}</DNSName>
375
+ <EvaluateTargetHealth>${change.ResourceRecordSet.AliasTarget.EvaluateTargetHealth}</EvaluateTargetHealth>
376
+ </AliasTarget>`
377
+ }
378
+
379
+ if (change.ResourceRecordSet.GeoLocation) {
380
+ xmlBody += `
381
+ <GeoLocation>`
382
+ if (change.ResourceRecordSet.GeoLocation.ContinentCode) {
383
+ xmlBody += `
384
+ <ContinentCode>${change.ResourceRecordSet.GeoLocation.ContinentCode}</ContinentCode>`
385
+ }
386
+ if (change.ResourceRecordSet.GeoLocation.CountryCode) {
387
+ xmlBody += `
388
+ <CountryCode>${change.ResourceRecordSet.GeoLocation.CountryCode}</CountryCode>`
389
+ }
390
+ if (change.ResourceRecordSet.GeoLocation.SubdivisionCode) {
391
+ xmlBody += `
392
+ <SubdivisionCode>${change.ResourceRecordSet.GeoLocation.SubdivisionCode}</SubdivisionCode>`
393
+ }
394
+ xmlBody += `
395
+ </GeoLocation>`
396
+ }
397
+
398
+ xmlBody += `
399
+ </ResourceRecordSet>
400
+ </Change>`
401
+ }
402
+
403
+ xmlBody += `
404
+ </Changes>
405
+ </ChangeBatch>
406
+ </ChangeResourceRecordSetsRequest>`
407
+
408
+ const result = await this.client.request({
409
+ service: 'route53',
410
+ region: this.region,
411
+ method: 'POST',
412
+ path: `/2013-04-01/hostedzone/${hostedZoneId}/rrset`,
413
+ headers: {
414
+ 'content-type': 'application/xml',
415
+ },
416
+ body: xmlBody,
417
+ })
418
+
419
+ return this.parseChangeResourceRecordSetsResponse(result)
420
+ }
421
+
422
+ /**
423
+ * Escape XML special characters
424
+ */
425
+ private escapeXml(str: string): string {
426
+ return str
427
+ .replace(/&/g, '&amp;')
428
+ .replace(/</g, '&lt;')
429
+ .replace(/>/g, '&gt;')
430
+ .replace(/"/g, '&quot;')
431
+ .replace(/'/g, '&apos;')
432
+ }
433
+
434
+ /**
435
+ * Parse CreateHostedZone response
436
+ */
437
+ private parseCreateHostedZoneResponse(result: any): CreateHostedZoneResult {
438
+ const response = result.CreateHostedZoneResponse || result
439
+
440
+ return {
441
+ HostedZone: this.parseHostedZone(response.HostedZone),
442
+ ChangeInfo: {
443
+ Id: response.ChangeInfo?.Id || '',
444
+ Status: response.ChangeInfo?.Status || '',
445
+ SubmittedAt: response.ChangeInfo?.SubmittedAt || '',
446
+ },
447
+ DelegationSet: this.parseDelegationSet(response.DelegationSet),
448
+ Location: response.Location || '',
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Parse ListHostedZones response
454
+ */
455
+ private parseListHostedZonesResponse(result: any): ListHostedZonesResult {
456
+ const response = result.ListHostedZonesResponse || result.ListHostedZonesByNameResponse || result
457
+
458
+ let hostedZones = response.HostedZones?.HostedZone || response.HostedZones || []
459
+
460
+ // Ensure it's an array
461
+ if (!Array.isArray(hostedZones)) {
462
+ hostedZones = hostedZones ? [hostedZones] : []
463
+ }
464
+
465
+ return {
466
+ HostedZones: hostedZones.map((hz: any) => this.parseHostedZone(hz)),
467
+ IsTruncated: response.IsTruncated === 'true' || response.IsTruncated === true,
468
+ MaxItems: response.MaxItems || '100',
469
+ Marker: response.Marker,
470
+ NextMarker: response.NextMarker,
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Parse GetHostedZone response
476
+ */
477
+ private parseGetHostedZoneResponse(result: any): GetHostedZoneResult {
478
+ const response = result.GetHostedZoneResponse || result
479
+
480
+ return {
481
+ HostedZone: this.parseHostedZone(response.HostedZone),
482
+ DelegationSet: this.parseDelegationSet(response.DelegationSet),
483
+ VPCs: response.VPCs?.VPC ? (Array.isArray(response.VPCs.VPC) ? response.VPCs.VPC : [response.VPCs.VPC]) : undefined,
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Parse ListResourceRecordSets response
489
+ */
490
+ private parseListResourceRecordSetsResponse(result: any): ListResourceRecordSetsResult {
491
+ const response = result.ListResourceRecordSetsResponse || result
492
+
493
+ let recordSets = response.ResourceRecordSets?.ResourceRecordSet || response.ResourceRecordSets || []
494
+
495
+ // Ensure it's an array
496
+ if (!Array.isArray(recordSets)) {
497
+ recordSets = recordSets ? [recordSets] : []
498
+ }
499
+
500
+ return {
501
+ ResourceRecordSets: recordSets.map((rs: any) => this.parseResourceRecordSet(rs)),
502
+ IsTruncated: response.IsTruncated === 'true' || response.IsTruncated === true,
503
+ MaxItems: response.MaxItems || '100',
504
+ NextRecordName: response.NextRecordName,
505
+ NextRecordType: response.NextRecordType,
506
+ NextRecordIdentifier: response.NextRecordIdentifier,
507
+ }
508
+ }
509
+
510
+ /**
511
+ * Parse ChangeResourceRecordSets response
512
+ */
513
+ private parseChangeResourceRecordSetsResponse(result: any): ChangeResourceRecordSetsResult {
514
+ const response = result.ChangeResourceRecordSetsResponse || result
515
+
516
+ return {
517
+ ChangeInfo: {
518
+ Id: response.ChangeInfo?.Id || '',
519
+ Status: response.ChangeInfo?.Status || '',
520
+ SubmittedAt: response.ChangeInfo?.SubmittedAt || '',
521
+ Comment: response.ChangeInfo?.Comment,
522
+ },
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Parse a hosted zone object
528
+ */
529
+ private parseHostedZone(hz: any): HostedZone {
530
+ if (!hz) return { Id: '', Name: '' }
531
+
532
+ return {
533
+ Id: hz.Id || '',
534
+ Name: hz.Name || '',
535
+ CallerReference: hz.CallerReference,
536
+ Config: hz.Config ? {
537
+ Comment: hz.Config.Comment,
538
+ PrivateZone: hz.Config.PrivateZone === 'true' || hz.Config.PrivateZone === true,
539
+ } : undefined,
540
+ ResourceRecordSetCount: hz.ResourceRecordSetCount ? Number(hz.ResourceRecordSetCount) : undefined,
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Parse a delegation set object
546
+ */
547
+ private parseDelegationSet(ds: any): DelegationSet {
548
+ if (!ds) return { NameServers: [] }
549
+
550
+ let nameServers = ds.NameServers?.NameServer || ds.NameServers || []
551
+
552
+ // Ensure it's an array
553
+ if (!Array.isArray(nameServers)) {
554
+ nameServers = nameServers ? [nameServers] : []
555
+ }
556
+
557
+ return {
558
+ Id: ds.Id,
559
+ CallerReference: ds.CallerReference,
560
+ NameServers: nameServers,
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Parse a resource record set object
566
+ */
567
+ private parseResourceRecordSet(rs: any): ResourceRecordSet {
568
+ if (!rs) return { Name: '', Type: '' }
569
+
570
+ let resourceRecords = rs.ResourceRecords?.ResourceRecord || rs.ResourceRecords || []
571
+
572
+ // Ensure it's an array
573
+ if (!Array.isArray(resourceRecords)) {
574
+ resourceRecords = resourceRecords ? [resourceRecords] : []
575
+ }
576
+
577
+ return {
578
+ Name: rs.Name || '',
579
+ Type: rs.Type || '',
580
+ TTL: rs.TTL ? Number(rs.TTL) : undefined,
581
+ ResourceRecords: resourceRecords.map((rr: any) => ({
582
+ Value: rr.Value || rr,
583
+ })),
584
+ AliasTarget: rs.AliasTarget ? {
585
+ HostedZoneId: rs.AliasTarget.HostedZoneId,
586
+ DNSName: rs.AliasTarget.DNSName,
587
+ EvaluateTargetHealth: rs.AliasTarget.EvaluateTargetHealth === 'true' || rs.AliasTarget.EvaluateTargetHealth === true,
588
+ } : undefined,
589
+ SetIdentifier: rs.SetIdentifier,
590
+ Weight: rs.Weight ? Number(rs.Weight) : undefined,
591
+ Region: rs.Region,
592
+ GeoLocation: rs.GeoLocation,
593
+ Failover: rs.Failover,
594
+ HealthCheckId: rs.HealthCheckId,
595
+ }
596
+ }
597
+
598
+ // Helper methods for common operations
599
+
600
+ /**
601
+ * Find hosted zone by domain name
602
+ */
603
+ async findHostedZoneByName(domainName: string): Promise<HostedZone | null> {
604
+ // Ensure domain ends with a dot
605
+ const normalizedDomain = domainName.endsWith('.') ? domainName : `${domainName}.`
606
+
607
+ const result = await this.listHostedZonesByName({ DNSName: normalizedDomain })
608
+ const zone = result.HostedZones.find(z => z.Name === normalizedDomain)
609
+
610
+ return zone || null
611
+ }
612
+
613
+ /**
614
+ * Create an A record
615
+ */
616
+ async createARecord(params: {
617
+ HostedZoneId: string
618
+ Name: string
619
+ Value: string | string[]
620
+ TTL?: number
621
+ }): Promise<ChangeResourceRecordSetsResult> {
622
+ const values = Array.isArray(params.Value) ? params.Value : [params.Value]
623
+
624
+ return this.changeResourceRecordSets({
625
+ HostedZoneId: params.HostedZoneId,
626
+ ChangeBatch: {
627
+ Changes: [{
628
+ Action: 'UPSERT',
629
+ ResourceRecordSet: {
630
+ Name: params.Name,
631
+ Type: 'A',
632
+ TTL: params.TTL || 300,
633
+ ResourceRecords: values.map(v => ({ Value: v })),
634
+ },
635
+ }],
636
+ },
637
+ })
638
+ }
639
+
640
+ /**
641
+ * Create a CNAME record
642
+ */
643
+ async createCnameRecord(params: {
644
+ HostedZoneId: string
645
+ Name: string
646
+ Value: string
647
+ TTL?: number
648
+ }): Promise<ChangeResourceRecordSetsResult> {
649
+ return this.changeResourceRecordSets({
650
+ HostedZoneId: params.HostedZoneId,
651
+ ChangeBatch: {
652
+ Changes: [{
653
+ Action: 'UPSERT',
654
+ ResourceRecordSet: {
655
+ Name: params.Name,
656
+ Type: 'CNAME',
657
+ TTL: params.TTL || 300,
658
+ ResourceRecords: [{ Value: params.Value }],
659
+ },
660
+ }],
661
+ },
662
+ })
663
+ }
664
+
665
+ /**
666
+ * Create an alias record (for CloudFront, ALB, etc.)
667
+ */
668
+ async createAliasRecord(params: {
669
+ HostedZoneId: string
670
+ Name: string
671
+ TargetHostedZoneId: string
672
+ TargetDNSName: string
673
+ EvaluateTargetHealth?: boolean
674
+ Type?: 'A' | 'AAAA'
675
+ }): Promise<ChangeResourceRecordSetsResult> {
676
+ return this.changeResourceRecordSets({
677
+ HostedZoneId: params.HostedZoneId,
678
+ ChangeBatch: {
679
+ Changes: [{
680
+ Action: 'UPSERT',
681
+ ResourceRecordSet: {
682
+ Name: params.Name,
683
+ Type: params.Type || 'A',
684
+ AliasTarget: {
685
+ HostedZoneId: params.TargetHostedZoneId,
686
+ DNSName: params.TargetDNSName,
687
+ EvaluateTargetHealth: params.EvaluateTargetHealth ?? false,
688
+ },
689
+ },
690
+ }],
691
+ },
692
+ })
693
+ }
694
+
695
+ /**
696
+ * Create a TXT record
697
+ */
698
+ async createTxtRecord(params: {
699
+ HostedZoneId: string
700
+ Name: string
701
+ Value: string | string[]
702
+ TTL?: number
703
+ }): Promise<ChangeResourceRecordSetsResult> {
704
+ const values = Array.isArray(params.Value) ? params.Value : [params.Value]
705
+ // TXT records need to be quoted
706
+ const quotedValues = values.map((v) => {
707
+ if (!v.startsWith('"')) {
708
+ return `"${v}"`
709
+ }
710
+ return v
711
+ })
712
+
713
+ return this.changeResourceRecordSets({
714
+ HostedZoneId: params.HostedZoneId,
715
+ ChangeBatch: {
716
+ Changes: [{
717
+ Action: 'UPSERT',
718
+ ResourceRecordSet: {
719
+ Name: params.Name,
720
+ Type: 'TXT',
721
+ TTL: params.TTL || 300,
722
+ ResourceRecords: quotedValues.map(v => ({ Value: v })),
723
+ },
724
+ }],
725
+ },
726
+ })
727
+ }
728
+
729
+ /**
730
+ * Create an MX record
731
+ */
732
+ async createMxRecord(params: {
733
+ HostedZoneId: string
734
+ Name: string
735
+ Values: Array<{ priority: number, mailServer: string }>
736
+ TTL?: number
737
+ }): Promise<ChangeResourceRecordSetsResult> {
738
+ return this.changeResourceRecordSets({
739
+ HostedZoneId: params.HostedZoneId,
740
+ ChangeBatch: {
741
+ Changes: [{
742
+ Action: 'UPSERT',
743
+ ResourceRecordSet: {
744
+ Name: params.Name,
745
+ Type: 'MX',
746
+ TTL: params.TTL || 300,
747
+ ResourceRecords: params.Values.map(v => ({
748
+ Value: `${v.priority} ${v.mailServer}`,
749
+ })),
750
+ },
751
+ }],
752
+ },
753
+ })
754
+ }
755
+
756
+ /**
757
+ * Delete a record
758
+ */
759
+ async deleteRecord(params: {
760
+ HostedZoneId: string
761
+ RecordSet: ResourceRecordSet
762
+ }): Promise<ChangeResourceRecordSetsResult> {
763
+ return this.changeResourceRecordSets({
764
+ HostedZoneId: params.HostedZoneId,
765
+ ChangeBatch: {
766
+ Changes: [{
767
+ Action: 'DELETE',
768
+ ResourceRecordSet: params.RecordSet,
769
+ }],
770
+ },
771
+ })
772
+ }
773
+
774
+ /**
775
+ * Wait for a change to become INSYNC
776
+ */
777
+ async waitForChange(changeId: string, maxAttempts = 60, delayMs = 5000): Promise<boolean> {
778
+ const id = changeId.replace('/change/', '')
779
+
780
+ for (let i = 0; i < maxAttempts; i++) {
781
+ const result = await this.client.request({
782
+ service: 'route53',
783
+ region: this.region,
784
+ method: 'GET',
785
+ path: `/2013-04-01/change/${id}`,
786
+ })
787
+
788
+ const status = result.GetChangeResponse?.ChangeInfo?.Status || result.ChangeInfo?.Status
789
+ if (status === 'INSYNC') {
790
+ return true
791
+ }
792
+
793
+ await new Promise(resolve => setTimeout(resolve, delayMs))
794
+ }
795
+
796
+ return false
797
+ }
798
+
799
+ /**
800
+ * Find or create a hosted zone for a domain
801
+ * Automatically creates the zone if it doesn't exist
802
+ */
803
+ async findOrCreateHostedZone(params: {
804
+ domainName: string
805
+ comment?: string
806
+ privateZone?: boolean
807
+ vpc?: {
808
+ VPCRegion: string
809
+ VPCId: string
810
+ }
811
+ }): Promise<{
812
+ hostedZone: HostedZone
813
+ nameServers: string[]
814
+ isNew: boolean
815
+ }> {
816
+ // Normalize domain (ensure it ends with a dot)
817
+ const normalizedDomain = params.domainName.endsWith('.')
818
+ ? params.domainName
819
+ : `${params.domainName}.`
820
+
821
+ // First, try to find existing hosted zone
822
+ const existing = await this.findHostedZoneByName(normalizedDomain)
823
+
824
+ if (existing) {
825
+ // Get the delegation set for name servers
826
+ const zoneDetails = await this.getHostedZone({ Id: existing.Id })
827
+ return {
828
+ hostedZone: existing,
829
+ nameServers: zoneDetails.DelegationSet.NameServers,
830
+ isNew: false,
831
+ }
832
+ }
833
+
834
+ // Create new hosted zone
835
+ const result = await this.createHostedZone({
836
+ Name: normalizedDomain,
837
+ HostedZoneConfig: {
838
+ Comment: params.comment || `Hosted zone for ${params.domainName}`,
839
+ PrivateZone: params.privateZone,
840
+ },
841
+ VPC: params.vpc,
842
+ })
843
+
844
+ return {
845
+ hostedZone: result.HostedZone,
846
+ nameServers: result.DelegationSet.NameServers,
847
+ isNew: true,
848
+ }
849
+ }
850
+
851
+ /**
852
+ * Get the root domain from a subdomain
853
+ * e.g., "api.example.com" -> "example.com"
854
+ */
855
+ static getRootDomain(domain: string): string {
856
+ const parts = domain.replace(/\.$/, '').split('.')
857
+ if (parts.length <= 2) {
858
+ return domain
859
+ }
860
+ // Return last two parts (handles most TLDs)
861
+ return parts.slice(-2).join('.')
862
+ }
863
+
864
+ /**
865
+ * Find the hosted zone for a domain or its parent domain
866
+ * Useful when you have a subdomain and need to find the zone
867
+ */
868
+ async findHostedZoneForDomain(domain: string): Promise<HostedZone | null> {
869
+ const normalizedDomain = domain.replace(/\.$/, '')
870
+ const parts = normalizedDomain.split('.')
871
+
872
+ // Try from most specific to least specific
873
+ for (let i = 0; i < parts.length - 1; i++) {
874
+ const testDomain = parts.slice(i).join('.')
875
+ const zone = await this.findHostedZoneByName(testDomain)
876
+ if (zone) {
877
+ return zone
878
+ }
879
+ }
880
+
881
+ return null
882
+ }
883
+
884
+ /**
885
+ * Ensure a hosted zone exists for a domain, creating it if necessary
886
+ * Returns the hosted zone ID suitable for use in CloudFormation
887
+ */
888
+ async ensureHostedZone(params: {
889
+ domainName: string
890
+ comment?: string
891
+ }): Promise<{
892
+ hostedZoneId: string
893
+ nameServers: string[]
894
+ isNew: boolean
895
+ action: 'found' | 'created'
896
+ }> {
897
+ const result = await this.findOrCreateHostedZone({
898
+ domainName: params.domainName,
899
+ comment: params.comment,
900
+ })
901
+
902
+ // Strip /hostedzone/ prefix for compatibility
903
+ const hostedZoneId = result.hostedZone.Id.replace('/hostedzone/', '')
904
+
905
+ return {
906
+ hostedZoneId,
907
+ nameServers: result.nameServers,
908
+ isNew: result.isNew,
909
+ action: result.isNew ? 'created' : 'found',
910
+ }
911
+ }
912
+
913
+ /**
914
+ * Setup DNS for a domain with automatic hosted zone creation
915
+ * Creates the hosted zone if needed and returns setup information
916
+ */
917
+ async setupDomainDns(params: {
918
+ domain: string
919
+ createIfNotExists?: boolean
920
+ }): Promise<{
921
+ success: boolean
922
+ hostedZoneId: string | null
923
+ nameServers: string[]
924
+ isNew: boolean
925
+ message: string
926
+ }> {
927
+ const { domain, createIfNotExists = true } = params
928
+
929
+ // Try to find existing zone
930
+ const existing = await this.findHostedZoneByName(domain)
931
+
932
+ if (existing) {
933
+ const zoneDetails = await this.getHostedZone({ Id: existing.Id })
934
+ return {
935
+ success: true,
936
+ hostedZoneId: existing.Id.replace('/hostedzone/', ''),
937
+ nameServers: zoneDetails.DelegationSet.NameServers,
938
+ isNew: false,
939
+ message: `Found existing hosted zone for ${domain}`,
940
+ }
941
+ }
942
+
943
+ if (!createIfNotExists) {
944
+ return {
945
+ success: false,
946
+ hostedZoneId: null,
947
+ nameServers: [],
948
+ isNew: false,
949
+ message: `No hosted zone found for ${domain} and createIfNotExists is false`,
950
+ }
951
+ }
952
+
953
+ // Create the hosted zone
954
+ const result = await this.createHostedZone({
955
+ Name: domain,
956
+ HostedZoneConfig: {
957
+ Comment: `Created automatically by ts-cloud for ${domain}`,
958
+ },
959
+ })
960
+
961
+ return {
962
+ success: true,
963
+ hostedZoneId: result.HostedZone.Id.replace('/hostedzone/', ''),
964
+ nameServers: result.DelegationSet.NameServers,
965
+ isNew: true,
966
+ message: `Created new hosted zone for ${domain}. Please update your domain registrar with these name servers: ${result.DelegationSet.NameServers.join(', ')}`,
967
+ }
968
+ }
969
+
970
+ /**
971
+ * CloudFront hosted zone ID (global)
972
+ */
973
+ static readonly CloudFrontHostedZoneId = 'Z2FDTNDATAQYW2'
974
+
975
+ /**
976
+ * S3 website hosting hosted zone IDs by region
977
+ */
978
+ static readonly S3WebsiteHostedZoneIds: Record<string, string> = {
979
+ 'us-east-1': 'Z3AQBSTGFYJSTF',
980
+ 'us-east-2': 'Z2O1EMRO9K5GLX',
981
+ 'us-west-1': 'Z2F56UZL2M1ACD',
982
+ 'us-west-2': 'Z3BJ6K6RIION7M',
983
+ 'ap-east-1': 'ZNB98KWMFR0R6',
984
+ 'ap-south-1': 'Z11RGJOFQNVJUP',
985
+ 'ap-northeast-1': 'Z2M4EHUR26P7ZW',
986
+ 'ap-northeast-2': 'Z3W03O7B5YMIYP',
987
+ 'ap-northeast-3': 'Z2YQB5RD63NC85',
988
+ 'ap-southeast-1': 'Z3O0J2DXBE1FTB',
989
+ 'ap-southeast-2': 'Z1WCIGYICN2BYD',
990
+ 'ca-central-1': 'Z1QDHH18159H29',
991
+ 'eu-central-1': 'Z21DNDUVLTQW6Q',
992
+ 'eu-west-1': 'Z1BKCTXD74EZPE',
993
+ 'eu-west-2': 'Z3GKZC51ZF0DB4',
994
+ 'eu-west-3': 'Z3R1K369G5AVDG',
995
+ 'eu-north-1': 'Z3BAZG2TWCNX0D',
996
+ 'sa-east-1': 'Z7KQH4QJS55SO',
997
+ }
998
+
999
+ /**
1000
+ * ALB hosted zone IDs by region
1001
+ */
1002
+ static readonly ALBHostedZoneIds: Record<string, string> = {
1003
+ 'us-east-1': 'Z35SXDOTRQ7X7K',
1004
+ 'us-east-2': 'Z3AADJGX6KTTL2',
1005
+ 'us-west-1': 'Z368ELLRRE2KJ0',
1006
+ 'us-west-2': 'Z1H1FL5HABSF5',
1007
+ 'ap-east-1': 'Z3DQVH9N71FHZ0',
1008
+ 'ap-south-1': 'ZP97RAFLXTNZK',
1009
+ 'ap-northeast-1': 'Z14GRHDCWA56QT',
1010
+ 'ap-northeast-2': 'ZWKZPGTI48KDX',
1011
+ 'ap-northeast-3': 'Z5LXEBD8Y73MNV',
1012
+ 'ap-southeast-1': 'Z1LMS91P8CMLE5',
1013
+ 'ap-southeast-2': 'Z1GM3OXH4ZPM65',
1014
+ 'ca-central-1': 'ZQSVJUPU6J1EY',
1015
+ 'eu-central-1': 'Z215JYRZR1TBD5',
1016
+ 'eu-west-1': 'Z32O12XQLNTSW2',
1017
+ 'eu-west-2': 'ZHURV8PSTC4K8',
1018
+ 'eu-west-3': 'Z3Q77PNBQS71R4',
1019
+ 'eu-north-1': 'Z23TAZ6LKFMNIO',
1020
+ 'sa-east-1': 'Z2P70J7HTTTPLU',
1021
+ }
1022
+
1023
+ /**
1024
+ * API Gateway hosted zone IDs by region
1025
+ */
1026
+ static readonly APIGatewayHostedZoneIds: Record<string, string> = {
1027
+ 'us-east-1': 'Z1UJRXOUMOOFQ8',
1028
+ 'us-east-2': 'ZOJJZC49E0EPZ',
1029
+ 'us-west-1': 'Z2MUQ32089INYE',
1030
+ 'us-west-2': 'Z2OJLYMUO9EFXC',
1031
+ 'ap-east-1': 'Z3FD1VL90ND7K5',
1032
+ 'ap-south-1': 'Z3VO1THU9YC4UR',
1033
+ 'ap-northeast-1': 'Z1YSHQZHG15GKL',
1034
+ 'ap-northeast-2': 'Z20JF4UZKIW1U8',
1035
+ 'ap-northeast-3': 'Z2YQB5RD63NC85',
1036
+ 'ap-southeast-1': 'ZL327KTPIQFUL',
1037
+ 'ap-southeast-2': 'Z2RPCDW04V8134',
1038
+ 'ca-central-1': 'Z19DQILCV0OWEC',
1039
+ 'eu-central-1': 'Z1U9ULNL0V5AJ3',
1040
+ 'eu-west-1': 'ZLY8HYME6SFDD',
1041
+ 'eu-west-2': 'ZJ5UAJN8Y3Z2Q',
1042
+ 'eu-west-3': 'Z3KY65QIEKYHQQ',
1043
+ 'eu-north-1': 'Z3UWIKFBOOGXPP',
1044
+ 'sa-east-1': 'ZCMLWB8V5SYIT',
1045
+ }
1046
+ }