@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
package/src/aws/acm.ts ADDED
@@ -0,0 +1,768 @@
1
+ /**
2
+ * ACM (AWS Certificate Manager) Client
3
+ * For requesting and managing SSL/TLS certificates
4
+ */
5
+
6
+ import { AWSClient } from './client'
7
+
8
+ export interface CertificateDetail {
9
+ CertificateArn: string
10
+ DomainName: string
11
+ SubjectAlternativeNames?: string[]
12
+ Status: 'PENDING_VALIDATION' | 'ISSUED' | 'INACTIVE' | 'EXPIRED' | 'VALIDATION_TIMED_OUT' | 'REVOKED' | 'FAILED'
13
+ Type?: 'IMPORTED' | 'AMAZON_ISSUED' | 'PRIVATE'
14
+ DomainValidationOptions?: {
15
+ DomainName: string
16
+ ValidationDomain?: string
17
+ ValidationStatus?: 'PENDING_VALIDATION' | 'SUCCESS' | 'FAILED'
18
+ ResourceRecord?: {
19
+ Name: string
20
+ Type: string
21
+ Value: string
22
+ }
23
+ ValidationMethod?: 'EMAIL' | 'DNS'
24
+ }[]
25
+ CreatedAt?: string
26
+ IssuedAt?: string
27
+ NotBefore?: string
28
+ NotAfter?: string
29
+ }
30
+
31
+ export class ACMClient {
32
+ private client: AWSClient
33
+ private region: string
34
+
35
+ constructor(region: string = 'us-east-1') {
36
+ this.client = new AWSClient()
37
+ this.region = region
38
+ }
39
+
40
+ /**
41
+ * Request a new certificate
42
+ */
43
+ async requestCertificate(params: {
44
+ DomainName: string
45
+ SubjectAlternativeNames?: string[]
46
+ ValidationMethod?: 'EMAIL' | 'DNS'
47
+ }): Promise<{
48
+ CertificateArn: string
49
+ }> {
50
+ const requestBody: Record<string, any> = {
51
+ DomainName: params.DomainName,
52
+ ValidationMethod: params.ValidationMethod || 'DNS',
53
+ }
54
+
55
+ if (params.SubjectAlternativeNames) {
56
+ requestBody.SubjectAlternativeNames = params.SubjectAlternativeNames
57
+ }
58
+
59
+ const result = await this.client.request({
60
+ service: 'acm',
61
+ region: this.region,
62
+ method: 'POST',
63
+ path: '/',
64
+ headers: {
65
+ 'content-type': 'application/x-amz-json-1.1',
66
+ 'x-amz-target': 'CertificateManager.RequestCertificate',
67
+ },
68
+ body: JSON.stringify(requestBody),
69
+ })
70
+
71
+ return {
72
+ CertificateArn: result.CertificateArn || '',
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Describe a certificate to get its details and validation options
78
+ */
79
+ async describeCertificate(params: {
80
+ CertificateArn: string
81
+ }): Promise<CertificateDetail> {
82
+ const result = await this.client.request({
83
+ service: 'acm',
84
+ region: this.region,
85
+ method: 'POST',
86
+ path: '/',
87
+ headers: {
88
+ 'content-type': 'application/x-amz-json-1.1',
89
+ 'x-amz-target': 'CertificateManager.DescribeCertificate',
90
+ },
91
+ body: JSON.stringify({
92
+ CertificateArn: params.CertificateArn,
93
+ }),
94
+ })
95
+
96
+ const cert = result.Certificate || {}
97
+ return {
98
+ CertificateArn: cert.CertificateArn || '',
99
+ DomainName: cert.DomainName || '',
100
+ SubjectAlternativeNames: cert.SubjectAlternativeNames,
101
+ Status: cert.Status || 'PENDING_VALIDATION',
102
+ Type: cert.Type,
103
+ DomainValidationOptions: cert.DomainValidationOptions?.map((opt: any) => ({
104
+ DomainName: opt.DomainName,
105
+ ValidationDomain: opt.ValidationDomain,
106
+ ValidationStatus: opt.ValidationStatus,
107
+ ResourceRecord: opt.ResourceRecord ? {
108
+ Name: opt.ResourceRecord.Name,
109
+ Type: opt.ResourceRecord.Type,
110
+ Value: opt.ResourceRecord.Value,
111
+ } : undefined,
112
+ ValidationMethod: opt.ValidationMethod,
113
+ })),
114
+ CreatedAt: cert.CreatedAt,
115
+ IssuedAt: cert.IssuedAt,
116
+ NotBefore: cert.NotBefore,
117
+ NotAfter: cert.NotAfter,
118
+ }
119
+ }
120
+
121
+ /**
122
+ * List certificates
123
+ */
124
+ async listCertificates(params?: {
125
+ CertificateStatuses?: ('PENDING_VALIDATION' | 'ISSUED' | 'INACTIVE' | 'EXPIRED' | 'VALIDATION_TIMED_OUT' | 'REVOKED' | 'FAILED')[]
126
+ MaxItems?: number
127
+ NextToken?: string
128
+ }): Promise<{
129
+ CertificateSummaryList: { CertificateArn: string, DomainName: string }[]
130
+ NextToken?: string
131
+ }> {
132
+ const requestBody: Record<string, any> = {}
133
+
134
+ if (params?.CertificateStatuses) {
135
+ requestBody.CertificateStatuses = params.CertificateStatuses
136
+ }
137
+ if (params?.MaxItems) {
138
+ requestBody.MaxItems = params.MaxItems
139
+ }
140
+ if (params?.NextToken) {
141
+ requestBody.NextToken = params.NextToken
142
+ }
143
+
144
+ const result = await this.client.request({
145
+ service: 'acm',
146
+ region: this.region,
147
+ method: 'POST',
148
+ path: '/',
149
+ headers: {
150
+ 'content-type': 'application/x-amz-json-1.1',
151
+ 'x-amz-target': 'CertificateManager.ListCertificates',
152
+ },
153
+ body: JSON.stringify(requestBody),
154
+ })
155
+
156
+ return {
157
+ CertificateSummaryList: (result.CertificateSummaryList || []).map((cert: any) => ({
158
+ CertificateArn: cert.CertificateArn,
159
+ DomainName: cert.DomainName,
160
+ })),
161
+ NextToken: result.NextToken,
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Delete a certificate
167
+ */
168
+ async deleteCertificate(params: {
169
+ CertificateArn: string
170
+ }): Promise<void> {
171
+ await this.client.request({
172
+ service: 'acm',
173
+ region: this.region,
174
+ method: 'POST',
175
+ path: '/',
176
+ headers: {
177
+ 'content-type': 'application/x-amz-json-1.1',
178
+ 'x-amz-target': 'CertificateManager.DeleteCertificate',
179
+ },
180
+ body: JSON.stringify({
181
+ CertificateArn: params.CertificateArn,
182
+ }),
183
+ })
184
+ }
185
+
186
+ /**
187
+ * Get certificate tags
188
+ */
189
+ async listTagsForCertificate(params: {
190
+ CertificateArn: string
191
+ }): Promise<{ Tags: Array<{ Key: string, Value?: string }> }> {
192
+ const result = await this.client.request({
193
+ service: 'acm',
194
+ region: this.region,
195
+ method: 'POST',
196
+ path: '/',
197
+ headers: {
198
+ 'content-type': 'application/x-amz-json-1.1',
199
+ 'x-amz-target': 'CertificateManager.ListTagsForCertificate',
200
+ },
201
+ body: JSON.stringify({
202
+ CertificateArn: params.CertificateArn,
203
+ }),
204
+ })
205
+
206
+ return {
207
+ Tags: result.Tags || [],
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Add tags to a certificate
213
+ */
214
+ async addTagsToCertificate(params: {
215
+ CertificateArn: string
216
+ Tags: Array<{ Key: string, Value?: string }>
217
+ }): Promise<void> {
218
+ await this.client.request({
219
+ service: 'acm',
220
+ region: this.region,
221
+ method: 'POST',
222
+ path: '/',
223
+ headers: {
224
+ 'content-type': 'application/x-amz-json-1.1',
225
+ 'x-amz-target': 'CertificateManager.AddTagsToCertificate',
226
+ },
227
+ body: JSON.stringify({
228
+ CertificateArn: params.CertificateArn,
229
+ Tags: params.Tags,
230
+ }),
231
+ })
232
+ }
233
+
234
+ /**
235
+ * Resend validation email
236
+ */
237
+ async resendValidationEmail(params: {
238
+ CertificateArn: string
239
+ Domain: string
240
+ ValidationDomain: string
241
+ }): Promise<void> {
242
+ await this.client.request({
243
+ service: 'acm',
244
+ region: this.region,
245
+ method: 'POST',
246
+ path: '/',
247
+ headers: {
248
+ 'content-type': 'application/x-amz-json-1.1',
249
+ 'x-amz-target': 'CertificateManager.ResendValidationEmail',
250
+ },
251
+ body: JSON.stringify({
252
+ CertificateArn: params.CertificateArn,
253
+ Domain: params.Domain,
254
+ ValidationDomain: params.ValidationDomain,
255
+ }),
256
+ })
257
+ }
258
+
259
+ // Helper methods
260
+
261
+ /**
262
+ * Find certificate by domain name
263
+ */
264
+ async findCertificateByDomain(domainName: string): Promise<CertificateDetail | null> {
265
+ // List all issued certificates
266
+ const result = await this.listCertificates({
267
+ CertificateStatuses: ['ISSUED'],
268
+ })
269
+
270
+ // Find certificate matching domain
271
+ const summary = result.CertificateSummaryList.find(c =>
272
+ c.DomainName === domainName ||
273
+ c.DomainName === `*.${domainName.split('.').slice(1).join('.')}`,
274
+ )
275
+
276
+ if (!summary) {
277
+ return null
278
+ }
279
+
280
+ // Get full details
281
+ return this.describeCertificate({ CertificateArn: summary.CertificateArn })
282
+ }
283
+
284
+ /**
285
+ * Wait for certificate to be issued
286
+ */
287
+ async waitForCertificateValidation(
288
+ certificateArn: string,
289
+ maxAttempts = 60,
290
+ delayMs = 30000,
291
+ ): Promise<CertificateDetail | null> {
292
+ for (let i = 0; i < maxAttempts; i++) {
293
+ const cert = await this.describeCertificate({ CertificateArn: certificateArn })
294
+
295
+ if (cert.Status === 'ISSUED') {
296
+ return cert
297
+ }
298
+
299
+ if (cert.Status === 'FAILED' || cert.Status === 'VALIDATION_TIMED_OUT') {
300
+ return null
301
+ }
302
+
303
+ await new Promise(resolve => setTimeout(resolve, delayMs))
304
+ }
305
+
306
+ return null
307
+ }
308
+
309
+ /**
310
+ * Get DNS validation records for a certificate
311
+ */
312
+ async getDnsValidationRecords(certificateArn: string): Promise<Array<{
313
+ domainName: string
314
+ recordName: string
315
+ recordType: string
316
+ recordValue: string
317
+ }>> {
318
+ const cert = await this.describeCertificate({ CertificateArn: certificateArn })
319
+
320
+ if (!cert.DomainValidationOptions) {
321
+ return []
322
+ }
323
+
324
+ return cert.DomainValidationOptions
325
+ .filter(opt => opt.ResourceRecord && opt.ValidationMethod === 'DNS')
326
+ .map(opt => ({
327
+ domainName: opt.DomainName,
328
+ recordName: opt.ResourceRecord!.Name,
329
+ recordType: opt.ResourceRecord!.Type,
330
+ recordValue: opt.ResourceRecord!.Value,
331
+ }))
332
+ }
333
+
334
+ /**
335
+ * Request certificate for a domain with common SANs
336
+ * Automatically includes www and wildcard
337
+ */
338
+ async requestCertificateWithSans(params: {
339
+ DomainName: string
340
+ IncludeWww?: boolean
341
+ IncludeWildcard?: boolean
342
+ AdditionalSans?: string[]
343
+ }): Promise<{ CertificateArn: string }> {
344
+ const sans = new Set<string>()
345
+
346
+ // Always include the main domain
347
+ sans.add(params.DomainName)
348
+
349
+ // Add www subdomain
350
+ if (params.IncludeWww !== false) {
351
+ sans.add(`www.${params.DomainName}`)
352
+ }
353
+
354
+ // Add wildcard
355
+ if (params.IncludeWildcard) {
356
+ sans.add(`*.${params.DomainName}`)
357
+ }
358
+
359
+ // Add additional SANs
360
+ if (params.AdditionalSans) {
361
+ for (const san of params.AdditionalSans) {
362
+ sans.add(san)
363
+ }
364
+ }
365
+
366
+ return this.requestCertificate({
367
+ DomainName: params.DomainName,
368
+ SubjectAlternativeNames: Array.from(sans),
369
+ ValidationMethod: 'DNS',
370
+ })
371
+ }
372
+
373
+ /**
374
+ * Check if certificate is valid for a given domain
375
+ */
376
+ async isCertificateValidForDomain(
377
+ certificateArn: string,
378
+ domainName: string,
379
+ ): Promise<boolean> {
380
+ const cert = await this.describeCertificate({ CertificateArn: certificateArn })
381
+
382
+ if (cert.Status !== 'ISSUED') {
383
+ return false
384
+ }
385
+
386
+ // Check if domain matches
387
+ if (cert.DomainName === domainName) {
388
+ return true
389
+ }
390
+
391
+ // Check wildcard match
392
+ if (cert.DomainName?.startsWith('*.')) {
393
+ const baseDomain = cert.DomainName.slice(2)
394
+ const domainParts = domainName.split('.')
395
+ const baseParts = baseDomain.split('.')
396
+
397
+ if (domainParts.slice(-baseParts.length).join('.') === baseDomain) {
398
+ return true
399
+ }
400
+ }
401
+
402
+ // Check SANs
403
+ if (cert.SubjectAlternativeNames) {
404
+ for (const san of cert.SubjectAlternativeNames) {
405
+ if (san === domainName) {
406
+ return true
407
+ }
408
+
409
+ if (san.startsWith('*.')) {
410
+ const baseDomain = san.slice(2)
411
+ const domainParts = domainName.split('.')
412
+ const baseParts = baseDomain.split('.')
413
+
414
+ if (domainParts.slice(-baseParts.length).join('.') === baseDomain) {
415
+ return true
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ return false
422
+ }
423
+ }
424
+
425
+ import { Route53Client } from './route53'
426
+ import type { DnsProvider, DnsProviderConfig } from '../dns/types'
427
+ import { createDnsProvider } from '../dns'
428
+
429
+ /**
430
+ * Helper class for ACM DNS validation with Route53 integration
431
+ * @deprecated Use UnifiedDnsValidator from 'ts-cloud/dns' for multi-provider support (Route53, Porkbun, GoDaddy)
432
+ */
433
+ export class ACMDnsValidator {
434
+ private acm: ACMClient
435
+ private route53: Route53Client
436
+ private dnsProvider?: DnsProvider
437
+
438
+ /**
439
+ * Create ACM DNS validator
440
+ * @param region - AWS region for ACM (default: us-east-1)
441
+ * @param dnsProviderConfig - Optional external DNS provider config (Porkbun, GoDaddy)
442
+ */
443
+ constructor(region: string = 'us-east-1', dnsProviderConfig?: DnsProviderConfig) {
444
+ this.acm = new ACMClient(region)
445
+ this.route53 = new Route53Client()
446
+
447
+ // Initialize external DNS provider if config provided
448
+ if (dnsProviderConfig && dnsProviderConfig.provider !== 'route53') {
449
+ this.dnsProvider = createDnsProvider(dnsProviderConfig)
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Request certificate and automatically create DNS validation records
455
+ * @param params.domainName - Primary domain name for the certificate
456
+ * @param params.hostedZoneId - Route53 hosted zone ID (required if no external DNS provider configured)
457
+ * @param params.subjectAlternativeNames - Additional domain names (SANs)
458
+ * @param params.waitForValidation - Wait for certificate to be issued
459
+ * @param params.maxWaitMinutes - Maximum wait time in minutes
460
+ */
461
+ async requestAndValidate(params: {
462
+ domainName: string
463
+ hostedZoneId?: string
464
+ subjectAlternativeNames?: string[]
465
+ waitForValidation?: boolean
466
+ maxWaitMinutes?: number
467
+ }): Promise<{
468
+ certificateArn: string
469
+ validationRecords: Array<{
470
+ domainName: string
471
+ recordName: string
472
+ recordValue: string
473
+ }>
474
+ }> {
475
+ const {
476
+ domainName,
477
+ hostedZoneId,
478
+ subjectAlternativeNames = [],
479
+ waitForValidation = false,
480
+ maxWaitMinutes = 30,
481
+ } = params
482
+
483
+ // Validate that we have a DNS provider
484
+ if (!this.dnsProvider && !hostedZoneId) {
485
+ throw new Error('Either hostedZoneId or external DNS provider configuration is required')
486
+ }
487
+
488
+ // Request certificate
489
+ const { CertificateArn } = await this.acm.requestCertificate({
490
+ DomainName: domainName,
491
+ SubjectAlternativeNames: subjectAlternativeNames.length > 0
492
+ ? [domainName, ...subjectAlternativeNames]
493
+ : undefined,
494
+ ValidationMethod: 'DNS',
495
+ })
496
+
497
+ // Wait for DNS validation options to be available
498
+ await this.waitForValidationOptions(CertificateArn)
499
+
500
+ // Get validation records
501
+ const validationRecords = await this.acm.getDnsValidationRecords(CertificateArn)
502
+
503
+ // Create DNS records using the appropriate provider
504
+ if (this.dnsProvider) {
505
+ // Use external DNS provider (Porkbun, GoDaddy, etc.)
506
+ for (const record of validationRecords) {
507
+ const result = await this.dnsProvider.upsertRecord(domainName, {
508
+ name: record.recordName,
509
+ type: record.recordType as any,
510
+ content: record.recordValue,
511
+ ttl: 300,
512
+ })
513
+
514
+ if (!result.success) {
515
+ console.warn(`Failed to create validation record for ${record.domainName}: ${result.message}`)
516
+ }
517
+ }
518
+ }
519
+ else if (hostedZoneId) {
520
+ // Use Route53
521
+ for (const record of validationRecords) {
522
+ await this.route53.changeResourceRecordSets({
523
+ HostedZoneId: hostedZoneId,
524
+ ChangeBatch: {
525
+ Comment: `ACM DNS validation for ${record.domainName}`,
526
+ Changes: [{
527
+ Action: 'UPSERT',
528
+ ResourceRecordSet: {
529
+ Name: record.recordName,
530
+ Type: record.recordType as any,
531
+ TTL: 300,
532
+ ResourceRecords: [{ Value: record.recordValue }],
533
+ },
534
+ }],
535
+ },
536
+ })
537
+ }
538
+ }
539
+
540
+ // Wait for validation if requested
541
+ if (waitForValidation) {
542
+ const cert = await this.acm.waitForCertificateValidation(
543
+ CertificateArn,
544
+ maxWaitMinutes * 2, // attempts (every 30 seconds)
545
+ 30000, // 30 seconds between checks
546
+ )
547
+
548
+ if (!cert) {
549
+ throw new Error(`Certificate validation timed out after ${maxWaitMinutes} minutes`)
550
+ }
551
+ }
552
+
553
+ return {
554
+ certificateArn: CertificateArn,
555
+ validationRecords,
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Wait for validation options to become available
561
+ */
562
+ private async waitForValidationOptions(certificateArn: string, maxAttempts = 30): Promise<void> {
563
+ for (let i = 0; i < maxAttempts; i++) {
564
+ const cert = await this.acm.describeCertificate({ CertificateArn: certificateArn })
565
+
566
+ if (cert.DomainValidationOptions &&
567
+ cert.DomainValidationOptions.length > 0 &&
568
+ cert.DomainValidationOptions[0].ResourceRecord) {
569
+ return
570
+ }
571
+
572
+ await new Promise(resolve => setTimeout(resolve, 2000))
573
+ }
574
+
575
+ throw new Error('Timeout waiting for DNS validation options')
576
+ }
577
+
578
+ /**
579
+ * Create validation records for an existing certificate
580
+ * Uses external DNS provider if configured, otherwise Route53
581
+ */
582
+ async createValidationRecords(params: {
583
+ certificateArn: string
584
+ hostedZoneId?: string
585
+ domain?: string
586
+ }): Promise<Array<{
587
+ domainName: string
588
+ recordName: string
589
+ recordValue: string
590
+ changeId?: string
591
+ }>> {
592
+ const { certificateArn, hostedZoneId, domain } = params
593
+
594
+ // Validate DNS provider availability
595
+ if (!this.dnsProvider && !hostedZoneId) {
596
+ throw new Error('Either hostedZoneId or external DNS provider configuration is required')
597
+ }
598
+
599
+ // Get validation records
600
+ const validationRecords = await this.acm.getDnsValidationRecords(certificateArn)
601
+ const results: Array<{
602
+ domainName: string
603
+ recordName: string
604
+ recordValue: string
605
+ changeId?: string
606
+ }> = []
607
+
608
+ if (this.dnsProvider) {
609
+ // Use external DNS provider
610
+ const targetDomain = domain || validationRecords[0]?.domainName
611
+ for (const record of validationRecords) {
612
+ const result = await this.dnsProvider.upsertRecord(targetDomain, {
613
+ name: record.recordName,
614
+ type: record.recordType as any,
615
+ content: record.recordValue,
616
+ ttl: 300,
617
+ })
618
+
619
+ results.push({
620
+ ...record,
621
+ changeId: result.success ? result.id : undefined,
622
+ })
623
+ }
624
+ }
625
+ else if (hostedZoneId) {
626
+ // Use Route53
627
+ for (const record of validationRecords) {
628
+ const result = await this.route53.changeResourceRecordSets({
629
+ HostedZoneId: hostedZoneId,
630
+ ChangeBatch: {
631
+ Comment: `ACM DNS validation for ${record.domainName}`,
632
+ Changes: [{
633
+ Action: 'UPSERT',
634
+ ResourceRecordSet: {
635
+ Name: record.recordName,
636
+ Type: record.recordType as any,
637
+ TTL: 300,
638
+ ResourceRecords: [{ Value: record.recordValue }],
639
+ },
640
+ }],
641
+ },
642
+ })
643
+
644
+ results.push({
645
+ ...record,
646
+ changeId: result.ChangeInfo?.Id,
647
+ })
648
+ }
649
+ }
650
+
651
+ return results
652
+ }
653
+
654
+ /**
655
+ * Delete validation records after certificate is issued
656
+ * Uses external DNS provider if configured, otherwise Route53
657
+ */
658
+ async deleteValidationRecords(params: {
659
+ certificateArn: string
660
+ hostedZoneId?: string
661
+ domain?: string
662
+ }): Promise<void> {
663
+ const { certificateArn, hostedZoneId, domain } = params
664
+
665
+ // Get validation records
666
+ const validationRecords = await this.acm.getDnsValidationRecords(certificateArn)
667
+
668
+ if (this.dnsProvider) {
669
+ // Use external DNS provider
670
+ const targetDomain = domain || validationRecords[0]?.domainName
671
+ for (const record of validationRecords) {
672
+ try {
673
+ await this.dnsProvider.deleteRecord(targetDomain, {
674
+ name: record.recordName,
675
+ type: record.recordType as any,
676
+ content: record.recordValue,
677
+ })
678
+ }
679
+ catch {
680
+ // Ignore errors if record doesn't exist
681
+ }
682
+ }
683
+ }
684
+ else if (hostedZoneId) {
685
+ // Use Route53
686
+ for (const record of validationRecords) {
687
+ try {
688
+ await this.route53.changeResourceRecordSets({
689
+ HostedZoneId: hostedZoneId,
690
+ ChangeBatch: {
691
+ Comment: `Cleanup ACM DNS validation for ${record.domainName}`,
692
+ Changes: [{
693
+ Action: 'DELETE',
694
+ ResourceRecordSet: {
695
+ Name: record.recordName,
696
+ Type: record.recordType as any,
697
+ TTL: 300,
698
+ ResourceRecords: [{ Value: record.recordValue }],
699
+ },
700
+ }],
701
+ },
702
+ })
703
+ }
704
+ catch {
705
+ // Ignore errors if record doesn't exist
706
+ }
707
+ }
708
+ }
709
+ }
710
+
711
+ /**
712
+ * Find or create a certificate for a domain
713
+ * Uses external DNS provider if configured, otherwise Route53
714
+ */
715
+ async findOrCreateCertificate(params: {
716
+ domainName: string
717
+ hostedZoneId?: string
718
+ subjectAlternativeNames?: string[]
719
+ waitForValidation?: boolean
720
+ }): Promise<{
721
+ certificateArn: string
722
+ isNew: boolean
723
+ }> {
724
+ const { domainName, hostedZoneId, subjectAlternativeNames, waitForValidation = true } = params
725
+
726
+ // Validate DNS provider availability
727
+ if (!this.dnsProvider && !hostedZoneId) {
728
+ throw new Error('Either hostedZoneId or external DNS provider configuration is required')
729
+ }
730
+
731
+ // Try to find existing certificate
732
+ const existing = await this.acm.findCertificateByDomain(domainName)
733
+
734
+ if (existing && existing.Status === 'ISSUED') {
735
+ return {
736
+ certificateArn: existing.CertificateArn,
737
+ isNew: false,
738
+ }
739
+ }
740
+
741
+ // Request new certificate
742
+ const { certificateArn } = await this.requestAndValidate({
743
+ domainName,
744
+ hostedZoneId,
745
+ subjectAlternativeNames,
746
+ waitForValidation,
747
+ })
748
+
749
+ return {
750
+ certificateArn,
751
+ isNew: true,
752
+ }
753
+ }
754
+
755
+ /**
756
+ * Check if using external DNS provider
757
+ */
758
+ hasExternalDnsProvider(): boolean {
759
+ return this.dnsProvider !== undefined
760
+ }
761
+
762
+ /**
763
+ * Get the DNS provider name if using external provider
764
+ */
765
+ getDnsProviderName(): string {
766
+ return this.dnsProvider?.name || 'route53'
767
+ }
768
+ }