@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.
- package/dist/bin/cli.js +1 -1
- package/package.json +18 -16
- package/src/aws/acm.ts +768 -0
- package/src/aws/application-autoscaling.ts +845 -0
- package/src/aws/bedrock.ts +4074 -0
- package/src/aws/client.ts +891 -0
- package/src/aws/cloudformation.ts +896 -0
- package/src/aws/cloudfront.ts +1531 -0
- package/src/aws/cloudwatch-logs.ts +154 -0
- package/src/aws/comprehend.ts +839 -0
- package/src/aws/connect.ts +1056 -0
- package/src/aws/deploy-imap.ts +384 -0
- package/src/aws/dynamodb.ts +340 -0
- package/src/aws/ec2.ts +1385 -0
- package/src/aws/ecr.ts +621 -0
- package/src/aws/ecs.ts +615 -0
- package/src/aws/elasticache.ts +301 -0
- package/src/aws/elbv2.ts +942 -0
- package/src/aws/email.ts +928 -0
- package/src/aws/eventbridge.ts +248 -0
- package/src/aws/iam.ts +1689 -0
- package/src/aws/imap-server.ts +2100 -0
- package/src/aws/index.ts +213 -0
- package/src/aws/kendra.ts +1097 -0
- package/src/aws/lambda.ts +786 -0
- package/src/aws/opensearch.ts +158 -0
- package/src/aws/personalize.ts +977 -0
- package/src/aws/polly.ts +559 -0
- package/src/aws/rds.ts +888 -0
- package/src/aws/rekognition.ts +846 -0
- package/src/aws/route53-domains.ts +359 -0
- package/src/aws/route53.ts +1046 -0
- package/src/aws/s3.ts +2334 -0
- package/src/aws/scheduler.ts +571 -0
- package/src/aws/secrets-manager.ts +769 -0
- package/src/aws/ses.ts +1081 -0
- package/src/aws/setup-phone.ts +104 -0
- package/src/aws/setup-sms.ts +580 -0
- package/src/aws/sms.ts +1735 -0
- package/src/aws/smtp-server.ts +531 -0
- package/src/aws/sns.ts +758 -0
- package/src/aws/sqs.ts +382 -0
- package/src/aws/ssm.ts +807 -0
- package/src/aws/sts.ts +92 -0
- package/src/aws/support.ts +391 -0
- package/src/aws/test-imap.ts +86 -0
- package/src/aws/textract.ts +780 -0
- package/src/aws/transcribe.ts +108 -0
- package/src/aws/translate.ts +641 -0
- package/src/aws/voice.ts +1379 -0
- package/src/config.ts +35 -0
- package/src/deploy/index.ts +7 -0
- package/src/deploy/static-site-external-dns.ts +945 -0
- package/src/deploy/static-site.ts +1175 -0
- package/src/dns/cloudflare.ts +548 -0
- package/src/dns/godaddy.ts +412 -0
- package/src/dns/index.ts +205 -0
- package/src/dns/porkbun.ts +362 -0
- package/src/dns/route53-adapter.ts +414 -0
- package/src/dns/types.ts +119 -0
- package/src/dns/validator.ts +369 -0
- package/src/generators/index.ts +5 -0
- package/src/generators/infrastructure.ts +1660 -0
- package/src/index.ts +163 -0
- package/src/push/apns.ts +452 -0
- package/src/push/fcm.ts +506 -0
- package/src/push/index.ts +58 -0
- package/src/security/pre-deploy-scanner.ts +655 -0
- package/src/ssl/acme-client.ts +478 -0
- package/src/ssl/index.ts +7 -0
- package/src/ssl/letsencrypt.ts +747 -0
- package/src/types.ts +2 -0
- package/src/utils/cli.ts +398 -0
- package/src/validation/index.ts +5 -0
- 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
|
+
}
|