@stacksjs/ts-cloud 0.1.3 → 0.1.6

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 (187) hide show
  1. package/README.md +98 -13
  2. package/dist/aws/acm.d.ts +129 -0
  3. package/dist/aws/application-autoscaling.d.ts +282 -0
  4. package/dist/aws/bedrock.d.ts +2292 -0
  5. package/dist/aws/client.d.ts +79 -0
  6. package/dist/aws/cloudformation.d.ts +105 -0
  7. package/dist/aws/cloudfront.d.ts +265 -0
  8. package/dist/aws/cloudwatch-logs.d.ts +48 -0
  9. package/dist/aws/comprehend.d.ts +505 -0
  10. package/dist/aws/connect.d.ts +377 -0
  11. package/dist/aws/deploy-imap.d.ts +14 -0
  12. package/dist/aws/dynamodb.d.ts +176 -0
  13. package/dist/aws/ec2.d.ts +272 -0
  14. package/dist/aws/ecr.d.ts +149 -0
  15. package/dist/aws/ecs.d.ts +162 -0
  16. package/dist/aws/elasticache.d.ts +71 -0
  17. package/dist/aws/elbv2.d.ts +248 -0
  18. package/dist/aws/email.d.ts +175 -0
  19. package/dist/aws/eventbridge.d.ts +142 -0
  20. package/dist/aws/iam.d.ts +638 -0
  21. package/dist/aws/imap-server.d.ts +119 -0
  22. package/{src/aws/index.ts → dist/aws/index.d.ts} +62 -83
  23. package/{src/aws/kendra.ts → dist/aws/kendra.d.ts} +71 -386
  24. package/dist/aws/lambda.d.ts +232 -0
  25. package/dist/aws/opensearch.d.ts +87 -0
  26. package/dist/aws/personalize.d.ts +516 -0
  27. package/dist/aws/polly.d.ts +214 -0
  28. package/dist/aws/rds.d.ts +240 -0
  29. package/dist/aws/rekognition.d.ts +543 -0
  30. package/dist/aws/route53-domains.d.ts +113 -0
  31. package/dist/aws/route53.d.ts +215 -0
  32. package/dist/aws/s3.d.ts +212 -0
  33. package/dist/aws/scheduler.d.ts +140 -0
  34. package/dist/aws/secrets-manager.d.ts +170 -0
  35. package/dist/aws/ses.d.ts +288 -0
  36. package/dist/aws/setup-phone.d.ts +0 -0
  37. package/dist/aws/setup-sms.d.ts +115 -0
  38. package/dist/aws/sms.d.ts +304 -0
  39. package/dist/aws/smtp-server.d.ts +61 -0
  40. package/dist/aws/sns.d.ts +117 -0
  41. package/dist/aws/sqs.d.ts +65 -0
  42. package/dist/aws/ssm.d.ts +179 -0
  43. package/dist/aws/sts.d.ts +15 -0
  44. package/dist/aws/support.d.ts +104 -0
  45. package/dist/aws/test-imap.d.ts +0 -0
  46. package/dist/aws/textract.d.ts +403 -0
  47. package/dist/aws/transcribe.d.ts +60 -0
  48. package/dist/aws/translate.d.ts +358 -0
  49. package/dist/aws/voice.d.ts +219 -0
  50. package/dist/bin/cli.js +1724 -0
  51. package/dist/config.d.ts +7 -0
  52. package/dist/deploy/index.d.ts +2 -0
  53. package/dist/deploy/static-site-external-dns.d.ts +51 -0
  54. package/dist/deploy/static-site.d.ts +71 -0
  55. package/dist/dns/cloudflare.d.ts +52 -0
  56. package/dist/dns/godaddy.d.ts +38 -0
  57. package/dist/dns/index.d.ts +45 -0
  58. package/dist/dns/porkbun.d.ts +18 -0
  59. package/dist/dns/route53-adapter.d.ts +38 -0
  60. package/{src/dns/types.ts → dist/dns/types.d.ts} +26 -63
  61. package/dist/dns/validator.d.ts +78 -0
  62. package/dist/generators/index.d.ts +1 -0
  63. package/dist/generators/infrastructure.d.ts +30 -0
  64. package/{src/index.ts → dist/index.d.ts} +70 -93
  65. package/dist/index.js +7881 -0
  66. package/dist/push/apns.d.ts +60 -0
  67. package/dist/push/fcm.d.ts +117 -0
  68. package/dist/push/index.d.ts +14 -0
  69. package/dist/security/pre-deploy-scanner.d.ts +69 -0
  70. package/dist/ssl/acme-client.d.ts +67 -0
  71. package/dist/ssl/index.d.ts +2 -0
  72. package/dist/ssl/letsencrypt.d.ts +48 -0
  73. package/dist/types.d.ts +1 -0
  74. package/dist/utils/cli.d.ts +123 -0
  75. package/dist/validation/index.d.ts +1 -0
  76. package/dist/validation/template.d.ts +23 -0
  77. package/package.json +8 -8
  78. package/bin/cli.ts +0 -133
  79. package/bin/commands/analytics.ts +0 -328
  80. package/bin/commands/api.ts +0 -379
  81. package/bin/commands/assets.ts +0 -221
  82. package/bin/commands/audit.ts +0 -501
  83. package/bin/commands/backup.ts +0 -682
  84. package/bin/commands/cache.ts +0 -294
  85. package/bin/commands/cdn.ts +0 -281
  86. package/bin/commands/config.ts +0 -202
  87. package/bin/commands/container.ts +0 -105
  88. package/bin/commands/cost.ts +0 -208
  89. package/bin/commands/database.ts +0 -401
  90. package/bin/commands/deploy.ts +0 -674
  91. package/bin/commands/domain.ts +0 -397
  92. package/bin/commands/email.ts +0 -423
  93. package/bin/commands/environment.ts +0 -285
  94. package/bin/commands/events.ts +0 -424
  95. package/bin/commands/firewall.ts +0 -145
  96. package/bin/commands/function.ts +0 -116
  97. package/bin/commands/generate.ts +0 -280
  98. package/bin/commands/git.ts +0 -139
  99. package/bin/commands/iam.ts +0 -464
  100. package/bin/commands/index.ts +0 -48
  101. package/bin/commands/init.ts +0 -120
  102. package/bin/commands/logs.ts +0 -148
  103. package/bin/commands/network.ts +0 -579
  104. package/bin/commands/notify.ts +0 -489
  105. package/bin/commands/queue.ts +0 -407
  106. package/bin/commands/scheduler.ts +0 -370
  107. package/bin/commands/secrets.ts +0 -54
  108. package/bin/commands/server.ts +0 -629
  109. package/bin/commands/shared.ts +0 -97
  110. package/bin/commands/ssl.ts +0 -138
  111. package/bin/commands/stack.ts +0 -325
  112. package/bin/commands/status.ts +0 -385
  113. package/bin/commands/storage.ts +0 -450
  114. package/bin/commands/team.ts +0 -96
  115. package/bin/commands/tunnel.ts +0 -489
  116. package/bin/commands/utils.ts +0 -202
  117. package/build.ts +0 -15
  118. package/cloud +0 -2
  119. package/src/aws/acm.ts +0 -768
  120. package/src/aws/application-autoscaling.ts +0 -845
  121. package/src/aws/bedrock.ts +0 -4074
  122. package/src/aws/client.ts +0 -878
  123. package/src/aws/cloudformation.ts +0 -896
  124. package/src/aws/cloudfront.ts +0 -1531
  125. package/src/aws/cloudwatch-logs.ts +0 -154
  126. package/src/aws/comprehend.ts +0 -839
  127. package/src/aws/connect.ts +0 -1056
  128. package/src/aws/deploy-imap.ts +0 -384
  129. package/src/aws/dynamodb.ts +0 -340
  130. package/src/aws/ec2.ts +0 -1385
  131. package/src/aws/ecr.ts +0 -621
  132. package/src/aws/ecs.ts +0 -615
  133. package/src/aws/elasticache.ts +0 -301
  134. package/src/aws/elbv2.ts +0 -942
  135. package/src/aws/email.ts +0 -928
  136. package/src/aws/eventbridge.ts +0 -248
  137. package/src/aws/iam.ts +0 -1689
  138. package/src/aws/imap-server.ts +0 -2100
  139. package/src/aws/lambda.ts +0 -786
  140. package/src/aws/opensearch.ts +0 -158
  141. package/src/aws/personalize.ts +0 -977
  142. package/src/aws/polly.ts +0 -559
  143. package/src/aws/rds.ts +0 -888
  144. package/src/aws/rekognition.ts +0 -846
  145. package/src/aws/route53-domains.ts +0 -359
  146. package/src/aws/route53.ts +0 -1046
  147. package/src/aws/s3.ts +0 -2318
  148. package/src/aws/scheduler.ts +0 -571
  149. package/src/aws/secrets-manager.ts +0 -769
  150. package/src/aws/ses.ts +0 -1081
  151. package/src/aws/setup-phone.ts +0 -104
  152. package/src/aws/setup-sms.ts +0 -580
  153. package/src/aws/sms.ts +0 -1735
  154. package/src/aws/smtp-server.ts +0 -531
  155. package/src/aws/sns.ts +0 -758
  156. package/src/aws/sqs.ts +0 -382
  157. package/src/aws/ssm.ts +0 -807
  158. package/src/aws/sts.ts +0 -92
  159. package/src/aws/support.ts +0 -391
  160. package/src/aws/test-imap.ts +0 -86
  161. package/src/aws/textract.ts +0 -780
  162. package/src/aws/transcribe.ts +0 -108
  163. package/src/aws/translate.ts +0 -641
  164. package/src/aws/voice.ts +0 -1379
  165. package/src/config.ts +0 -35
  166. package/src/deploy/index.ts +0 -7
  167. package/src/deploy/static-site-external-dns.ts +0 -906
  168. package/src/deploy/static-site.ts +0 -1125
  169. package/src/dns/godaddy.ts +0 -412
  170. package/src/dns/index.ts +0 -183
  171. package/src/dns/porkbun.ts +0 -362
  172. package/src/dns/route53-adapter.ts +0 -414
  173. package/src/dns/validator.ts +0 -369
  174. package/src/generators/index.ts +0 -5
  175. package/src/generators/infrastructure.ts +0 -1660
  176. package/src/push/apns.ts +0 -452
  177. package/src/push/fcm.ts +0 -506
  178. package/src/push/index.ts +0 -58
  179. package/src/ssl/acme-client.ts +0 -478
  180. package/src/ssl/index.ts +0 -7
  181. package/src/ssl/letsencrypt.ts +0 -747
  182. package/src/types.ts +0 -2
  183. package/src/utils/cli.ts +0 -398
  184. package/src/validation/index.ts +0 -5
  185. package/src/validation/template.ts +0 -405
  186. package/test/index.test.ts +0 -128
  187. package/tsconfig.json +0 -18
@@ -1,906 +0,0 @@
1
- /**
2
- * Static Site Deployment with External DNS Provider Support
3
- * Deploys static sites to AWS (S3 + CloudFront + ACM) with DNS managed by external providers (Porkbun, GoDaddy, etc.)
4
- */
5
-
6
- import { CloudFormationClient } from '../aws/cloudformation'
7
- import { S3Client } from '../aws/s3'
8
- import { CloudFrontClient } from '../aws/cloudfront'
9
- import { ACMClient } from '../aws/acm'
10
- import type { DnsProvider, DnsProviderConfig } from '../dns/types'
11
- import { createDnsProvider } from '../dns'
12
- import { UnifiedDnsValidator } from '../dns/validator'
13
-
14
- export interface ExternalDnsStaticSiteConfig {
15
- /** Site name used for resource naming */
16
- siteName: string
17
- /** AWS region for S3 bucket */
18
- region?: string
19
- /** Custom domain (e.g., bunpress.org) */
20
- domain: string
21
- /** S3 bucket name (auto-generated if not provided) */
22
- bucket?: string
23
- /** ACM certificate ARN (auto-created if not provided) */
24
- certificateArn?: string
25
- /** CloudFormation stack name */
26
- stackName?: string
27
- /** Default root object */
28
- defaultRootObject?: string
29
- /** Error document */
30
- errorDocument?: string
31
- /** Cache control for assets */
32
- cacheControl?: string
33
- /** Tags to apply to resources */
34
- tags?: Record<string, string>
35
- /** DNS provider configuration */
36
- dnsProvider: DnsProviderConfig
37
- }
38
-
39
- export interface ExternalDnsDeployResult {
40
- success: boolean
41
- stackId?: string
42
- stackName: string
43
- bucket: string
44
- distributionId?: string
45
- distributionDomain?: string
46
- domain?: string
47
- certificateArn?: string
48
- message: string
49
- filesUploaded?: number
50
- }
51
-
52
- /**
53
- * Generate CloudFormation template for static site infrastructure (without Route53)
54
- */
55
- export function generateExternalDnsStaticSiteTemplate(config: {
56
- bucketName: string
57
- domain?: string
58
- aliases?: string[]
59
- certificateArn?: string
60
- defaultRootObject?: string
61
- errorDocument?: string
62
- }): object {
63
- const {
64
- bucketName,
65
- domain,
66
- aliases,
67
- certificateArn,
68
- defaultRootObject = 'index.html',
69
- errorDocument = '404.html',
70
- } = config
71
-
72
- const resources: Record<string, any> = {}
73
- const outputs: Record<string, any> = {}
74
-
75
- // S3 Bucket
76
- resources.S3Bucket = {
77
- Type: 'AWS::S3::Bucket',
78
- Properties: {
79
- BucketName: bucketName,
80
- PublicAccessBlockConfiguration: {
81
- BlockPublicAcls: true,
82
- BlockPublicPolicy: false,
83
- IgnorePublicAcls: true,
84
- RestrictPublicBuckets: false,
85
- },
86
- WebsiteConfiguration: {
87
- IndexDocument: defaultRootObject,
88
- ErrorDocument: errorDocument,
89
- },
90
- },
91
- }
92
-
93
- outputs.BucketName = {
94
- Description: 'S3 Bucket Name',
95
- Value: { Ref: 'S3Bucket' },
96
- }
97
-
98
- outputs.BucketArn = {
99
- Description: 'S3 Bucket ARN',
100
- Value: { 'Fn::GetAtt': ['S3Bucket', 'Arn'] },
101
- }
102
-
103
- // Origin Access Control
104
- resources.CloudFrontOAC = {
105
- Type: 'AWS::CloudFront::OriginAccessControl',
106
- Properties: {
107
- OriginAccessControlConfig: {
108
- Name: `OAC-${bucketName}`,
109
- Description: `OAC for ${bucketName}`,
110
- OriginAccessControlOriginType: 's3',
111
- SigningBehavior: 'always',
112
- SigningProtocol: 'sigv4',
113
- },
114
- },
115
- }
116
-
117
- // CloudFront Function for URL rewriting
118
- resources.UrlRewriteFunction = {
119
- Type: 'AWS::CloudFront::Function',
120
- Properties: {
121
- Name: { 'Fn::Sub': '${AWS::StackName}-url-rewrite' },
122
- AutoPublish: true,
123
- FunctionConfig: {
124
- Comment: 'Append .html extension to URLs without extensions',
125
- Runtime: 'cloudfront-js-2.0',
126
- },
127
- FunctionCode: `function handler(event) {
128
- var request = event.request;
129
- var uri = request.uri;
130
-
131
- // If URI ends with /, serve index.html
132
- if (uri.endsWith('/')) {
133
- request.uri = uri + 'index.html';
134
- }
135
- // If URI doesn't have an extension, append .html
136
- else if (!uri.includes('.')) {
137
- request.uri = uri + '.html';
138
- }
139
-
140
- return request;
141
- }`,
142
- },
143
- }
144
-
145
- // CloudFront Distribution
146
- const distributionConfig: any = {
147
- Enabled: true,
148
- DefaultRootObject: defaultRootObject,
149
- HttpVersion: 'http2and3',
150
- IPV6Enabled: true,
151
- PriceClass: 'PriceClass_100',
152
- Origins: [
153
- {
154
- Id: `S3-${bucketName}`,
155
- DomainName: { 'Fn::GetAtt': ['S3Bucket', 'RegionalDomainName'] },
156
- S3OriginConfig: {
157
- OriginAccessIdentity: '',
158
- },
159
- OriginAccessControlId: { 'Fn::GetAtt': ['CloudFrontOAC', 'Id'] },
160
- },
161
- ],
162
- DefaultCacheBehavior: {
163
- TargetOriginId: `S3-${bucketName}`,
164
- ViewerProtocolPolicy: 'redirect-to-https',
165
- AllowedMethods: ['GET', 'HEAD'],
166
- CachedMethods: ['GET', 'HEAD'],
167
- Compress: true,
168
- CachePolicyId: '658327ea-f89d-4fab-a63d-7e88639e58f6', // Managed-CachingOptimized
169
- FunctionAssociations: [
170
- {
171
- EventType: 'viewer-request',
172
- FunctionARN: { 'Fn::GetAtt': ['UrlRewriteFunction', 'FunctionARN'] },
173
- },
174
- ],
175
- },
176
- CustomErrorResponses: [
177
- {
178
- ErrorCode: 403,
179
- ResponseCode: 200,
180
- ResponsePagePath: `/${defaultRootObject}`,
181
- ErrorCachingMinTTL: 300,
182
- },
183
- {
184
- ErrorCode: 404,
185
- ResponseCode: 404,
186
- ResponsePagePath: `/${errorDocument}`,
187
- ErrorCachingMinTTL: 300,
188
- },
189
- ],
190
- }
191
-
192
- // Add custom domain configuration if provided
193
- if (domain && certificateArn) {
194
- // Use provided aliases or default to just the domain
195
- distributionConfig.Aliases = aliases && aliases.length > 0 ? aliases : [domain]
196
- distributionConfig.ViewerCertificate = {
197
- AcmCertificateArn: certificateArn,
198
- SslSupportMethod: 'sni-only',
199
- MinimumProtocolVersion: 'TLSv1.2_2021',
200
- }
201
- }
202
- else {
203
- distributionConfig.ViewerCertificate = {
204
- CloudFrontDefaultCertificate: true,
205
- }
206
- }
207
-
208
- resources.CloudFrontDistribution = {
209
- Type: 'AWS::CloudFront::Distribution',
210
- DependsOn: ['S3Bucket', 'CloudFrontOAC', 'UrlRewriteFunction'],
211
- Properties: {
212
- DistributionConfig: distributionConfig,
213
- },
214
- }
215
-
216
- outputs.DistributionId = {
217
- Description: 'CloudFront Distribution ID',
218
- Value: { Ref: 'CloudFrontDistribution' },
219
- }
220
-
221
- outputs.DistributionDomain = {
222
- Description: 'CloudFront Distribution Domain',
223
- Value: { 'Fn::GetAtt': ['CloudFrontDistribution', 'DomainName'] },
224
- }
225
-
226
- // S3 Bucket Policy for CloudFront OAC
227
- resources.S3BucketPolicy = {
228
- Type: 'AWS::S3::BucketPolicy',
229
- DependsOn: ['S3Bucket', 'CloudFrontDistribution'],
230
- Properties: {
231
- Bucket: { Ref: 'S3Bucket' },
232
- PolicyDocument: {
233
- Version: '2012-10-17',
234
- Statement: [
235
- {
236
- Sid: 'AllowCloudFrontServicePrincipal',
237
- Effect: 'Allow',
238
- Principal: {
239
- Service: 'cloudfront.amazonaws.com',
240
- },
241
- Action: 's3:GetObject',
242
- Resource: { 'Fn::Sub': 'arn:aws:s3:::${S3Bucket}/*' },
243
- Condition: {
244
- StringEquals: {
245
- 'AWS:SourceArn': {
246
- 'Fn::Sub': 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}',
247
- },
248
- },
249
- },
250
- },
251
- ],
252
- },
253
- },
254
- }
255
-
256
- // NOTE: No Route53 records - DNS will be managed by external provider
257
-
258
- outputs.SiteUrl = {
259
- Description: 'Site URL',
260
- Value: domain ? `https://${domain}` : { 'Fn::Sub': 'https://${CloudFrontDistribution.DomainName}' },
261
- }
262
-
263
- return {
264
- AWSTemplateFormatVersion: '2010-09-09',
265
- Description: `Static site infrastructure for ${domain || bucketName} (External DNS)`,
266
- Resources: resources,
267
- Outputs: outputs,
268
- }
269
- }
270
-
271
- /**
272
- * Deploy a static site to AWS with external DNS provider
273
- */
274
- export async function deployStaticSiteWithExternalDns(
275
- config: ExternalDnsStaticSiteConfig,
276
- ): Promise<ExternalDnsDeployResult> {
277
- const region = config.region || 'us-east-1'
278
- const cfRegion = 'us-east-1' // CloudFormation for global resources must be in us-east-1
279
- const domain = config.domain
280
-
281
- // Generate bucket name if not provided
282
- const bucket = config.bucket || domain.replace(/\./g, '-')
283
-
284
- // Generate stack name
285
- const stackName = config.stackName || `${config.siteName}-static-site`
286
-
287
- // Initialize clients
288
- const cf = new CloudFormationClient(cfRegion)
289
- const acm = new ACMClient('us-east-1') // ACM certs for CloudFront must be in us-east-1
290
-
291
- // Create DNS provider
292
- const dnsProvider: DnsProvider = createDnsProvider(config.dnsProvider)
293
-
294
- // Verify DNS provider can manage this domain
295
- console.log(`Verifying DNS provider can manage ${domain}...`)
296
- const canManage = await dnsProvider.canManageDomain(domain)
297
- if (!canManage) {
298
- return {
299
- success: false,
300
- stackName,
301
- bucket,
302
- message: `DNS provider '${dnsProvider.name}' cannot manage domain ${domain}. Please check your API credentials and domain ownership.`,
303
- }
304
- }
305
- console.log(`DNS provider '${dnsProvider.name}' verified for ${domain}`)
306
-
307
- let certificateArn = config.certificateArn
308
-
309
- // Determine if we're dealing with an apex domain (need www SAN)
310
- const domainParts = domain.split('.')
311
- const isApexDomain = domainParts.length === 2
312
- const wwwDomain = isApexDomain ? `www.${domain}` : undefined
313
-
314
- // Auto-create SSL certificate if not provided
315
- if (!certificateArn) {
316
- console.log(`Checking for existing SSL certificate for ${domain}...`)
317
-
318
- // Check for existing certificate that covers both apex and www
319
- const existingCert = await acm.findCertificateByDomain(domain)
320
- let existingCertCoversWww = false
321
-
322
- if (existingCert && existingCert.Status === 'ISSUED') {
323
- // Check if the existing cert also covers www
324
- if (wwwDomain && existingCert.SubjectAlternativeNames) {
325
- existingCertCoversWww = existingCert.SubjectAlternativeNames.includes(wwwDomain)
326
- || existingCert.SubjectAlternativeNames.some(san => san === `*.${domain}`)
327
- }
328
- else {
329
- existingCertCoversWww = true // Not apex domain, no www needed
330
- }
331
-
332
- if (existingCertCoversWww) {
333
- certificateArn = existingCert.CertificateArn
334
- console.log(`Found existing certificate with www coverage: ${certificateArn}`)
335
- }
336
- else {
337
- console.log(`Existing certificate doesn't cover ${wwwDomain}, requesting new one...`)
338
- }
339
- }
340
-
341
- if (!certificateArn) {
342
- // Request and validate new certificate using external DNS provider
343
- // Always include www for apex domains
344
- console.log(`Requesting new SSL certificate for ${domain}${wwwDomain ? ` (including ${wwwDomain})` : ''}...`)
345
- const validator = new UnifiedDnsValidator(dnsProvider, 'us-east-1')
346
-
347
- const certResult = await validator.findOrCreateCertificate({
348
- domainName: domain,
349
- subjectAlternativeNames: wwwDomain ? [wwwDomain] : undefined,
350
- waitForValidation: true,
351
- maxWaitMinutes: 10,
352
- })
353
-
354
- if (certResult.status !== 'issued') {
355
- return {
356
- success: false,
357
- stackName,
358
- bucket,
359
- message: `SSL certificate validation failed. Status: ${certResult.status}`,
360
- }
361
- }
362
-
363
- certificateArn = certResult.certificateArn
364
- console.log(`Certificate issued: ${certificateArn}`)
365
- }
366
- }
367
-
368
- // Check if stack already exists
369
- let stackExists = false
370
- let existingBucketName: string | undefined
371
- try {
372
- const existingStacks = await cf.describeStacks({ stackName })
373
- if (existingStacks.Stacks.length > 0) {
374
- const stack = existingStacks.Stacks[0]
375
- const stackStatus = stack.StackStatus
376
-
377
- if (stackStatus === 'DELETE_IN_PROGRESS') {
378
- console.log('Previous stack is still being deleted, waiting...')
379
- await cf.waitForStack(stackName, 'stack-delete-complete')
380
- stackExists = false
381
- }
382
- else if (stackStatus === 'DELETE_COMPLETE') {
383
- stackExists = false
384
- }
385
- else {
386
- stackExists = true
387
- const outputs = stack.Outputs || []
388
- existingBucketName = outputs.find(o => o.OutputKey === 'BucketName')?.OutputValue
389
- }
390
- }
391
- }
392
- catch (err: any) {
393
- if (err.message?.includes('does not exist') || err.code === 'ValidationError') {
394
- stackExists = false
395
- }
396
- else {
397
- throw err
398
- }
399
- }
400
-
401
- // Determine final bucket name
402
- let finalBucket = existingBucketName || bucket
403
- if (!stackExists) {
404
- const s3 = new S3Client(region)
405
- const cloudfront = new CloudFrontClient()
406
-
407
- // Check if S3 bucket exists (orphaned from previous deployment)
408
- try {
409
- const headResult = await s3.headBucket(bucket)
410
- if (headResult.exists) {
411
- console.log(`Found orphaned S3 bucket ${bucket}, cleaning up...`)
412
- try {
413
- const cleanupPromise = s3.emptyBucket(bucket).then(() => s3.deleteBucket(bucket))
414
- const timeoutPromise = new Promise<never>((_, reject) =>
415
- setTimeout(() => reject(new Error('Bucket cleanup timeout')), 30000),
416
- )
417
- await Promise.race([cleanupPromise, timeoutPromise])
418
- console.log(`Deleted orphaned S3 bucket ${bucket}`)
419
- }
420
- catch (cleanupErr: any) {
421
- console.log(`Note: Could not clean up S3 bucket: ${cleanupErr.message}`)
422
- const suffix = Date.now().toString(36)
423
- finalBucket = `${bucket}-${suffix}`
424
- console.log(`Using alternative bucket name: ${finalBucket}`)
425
- }
426
- }
427
- }
428
- catch {
429
- // Bucket doesn't exist, good
430
- }
431
-
432
- // Check for existing CloudFront distributions with our domain
433
- if (domain) {
434
- try {
435
- console.log(`Checking for existing CloudFront distributions with alias ${domain}...`)
436
- const distributions = await cloudfront.listDistributions()
437
- for (const dist of distributions) {
438
- let aliases: string[] = []
439
- if (dist.Aliases?.Items) {
440
- if (Array.isArray(dist.Aliases.Items)) {
441
- aliases = dist.Aliases.Items
442
- }
443
- else if (typeof dist.Aliases.Items === 'object') {
444
- const cname = (dist.Aliases.Items as any).CNAME
445
- if (typeof cname === 'string') {
446
- aliases = [cname]
447
- }
448
- else if (Array.isArray(cname)) {
449
- aliases = cname
450
- }
451
- }
452
- }
453
- if (aliases.includes(domain)) {
454
- console.log(`Found existing CloudFront distribution ${dist.Id} with alias ${domain}`)
455
- console.log(`Reusing existing infrastructure for updates...`)
456
-
457
- // Get the origin bucket from the distribution
458
- const distConfig = await cloudfront.getDistributionConfig(dist.Id!)
459
- const originsData = distConfig.DistributionConfig?.Origins?.Items
460
- let originBucket: string | undefined
461
-
462
- if (originsData) {
463
- // Handle AWS XML-to-JSON format: single item is { Origin: {...} }, multiple is { Origin: [...] } or [...]
464
- let originList: any[] = []
465
- if (Array.isArray(originsData)) {
466
- originList = originsData
467
- }
468
- else if (originsData.Origin) {
469
- originList = Array.isArray(originsData.Origin) ? originsData.Origin : [originsData.Origin]
470
- }
471
- else {
472
- originList = [originsData]
473
- }
474
-
475
- for (const origin of originList) {
476
- const domainName = origin.DomainName || ''
477
- // Extract bucket name from S3 domain (e.g., "bucket-name.s3.us-east-1.amazonaws.com")
478
- const s3Match = domainName.match(/^([^.]+)\.s3[\.-]/)
479
- if (s3Match) {
480
- originBucket = s3Match[1]
481
- break
482
- }
483
- }
484
- }
485
-
486
- if (originBucket) {
487
- console.log(`Using existing S3 bucket: ${originBucket}`)
488
- // Return success with existing infrastructure info - skip CloudFormation
489
- return {
490
- success: true,
491
- stackName: `existing-${dist.Id}`,
492
- bucket: originBucket,
493
- distributionId: dist.Id,
494
- distributionDomain: dist.DomainName,
495
- domain,
496
- certificateArn,
497
- message: 'Using existing CloudFront distribution',
498
- _existingInfrastructure: true, // Flag to indicate we're reusing infrastructure
499
- } as ExternalDnsDeployResult & { _existingInfrastructure: boolean }
500
- }
501
- }
502
- }
503
- }
504
- catch {
505
- // No distributions or error listing them
506
- }
507
- }
508
- }
509
-
510
- // Generate CloudFormation template (without Route53)
511
- // Build aliases list (include www for apex domains)
512
- const aliases = wwwDomain ? [domain, wwwDomain] : [domain]
513
-
514
- const template = generateExternalDnsStaticSiteTemplate({
515
- bucketName: finalBucket,
516
- domain,
517
- aliases,
518
- certificateArn,
519
- defaultRootObject: config.defaultRootObject,
520
- errorDocument: config.errorDocument,
521
- })
522
-
523
- // Build tags
524
- const tags = Object.entries(config.tags || {}).map(([Key, Value]) => ({ Key, Value }))
525
- tags.push({ Key: 'ManagedBy', Value: 'ts-cloud' })
526
- tags.push({ Key: 'Application', Value: config.siteName })
527
- tags.push({ Key: 'DnsProvider', Value: dnsProvider.name })
528
-
529
- // Create or update stack
530
- let stackId: string
531
- let isUpdate = false
532
-
533
- if (stackExists) {
534
- isUpdate = true
535
- console.log(`Updating CloudFormation stack: ${stackName}`)
536
- try {
537
- const result = await cf.updateStack({
538
- stackName,
539
- templateBody: JSON.stringify(template),
540
- capabilities: ['CAPABILITY_IAM'],
541
- tags,
542
- })
543
- stackId = result.StackId
544
- console.log(`Update initiated, stack ID: ${stackId}`)
545
- }
546
- catch (err: any) {
547
- if (err.message?.includes('No updates are to be performed')) {
548
- const stacks = await cf.describeStacks({ stackName })
549
- stackId = stacks.Stacks[0].StackId
550
- const outputs = stacks.Stacks[0]?.Outputs || []
551
- const getOutput = (key: string) => outputs.find(o => o.OutputKey === key)?.OutputValue
552
-
553
- // Still need to ensure DNS records exist
554
- const distributionDomain = getOutput('DistributionDomain')
555
- if (distributionDomain) {
556
- await ensureDnsRecords(dnsProvider, domain, distributionDomain)
557
- }
558
-
559
- return {
560
- success: true,
561
- stackId,
562
- stackName,
563
- bucket: getOutput('BucketName') || finalBucket,
564
- distributionId: getOutput('DistributionId'),
565
- distributionDomain,
566
- domain,
567
- certificateArn,
568
- message: 'Static site infrastructure is already up to date',
569
- }
570
- }
571
- else {
572
- throw err
573
- }
574
- }
575
- }
576
- else {
577
- console.log(`Creating CloudFormation stack: ${stackName}`)
578
- console.log(`Bucket name: ${finalBucket}`)
579
- console.log(`Domain: ${domain}`)
580
- console.log(`Certificate ARN: ${certificateArn}`)
581
- const result = await cf.createStack({
582
- stackName,
583
- templateBody: JSON.stringify(template),
584
- capabilities: ['CAPABILITY_IAM'],
585
- tags,
586
- onFailure: 'DELETE',
587
- })
588
- stackId = result.StackId
589
- console.log(`Create initiated, stack ID: ${stackId}`)
590
- }
591
-
592
- // Wait for stack to complete
593
- console.log(`Waiting for stack to reach ${isUpdate ? 'stack-update-complete' : 'stack-create-complete'}...`)
594
- try {
595
- await cf.waitForStack(stackName, isUpdate ? 'stack-update-complete' : 'stack-create-complete')
596
- console.log('Stack operation completed successfully!')
597
- }
598
- catch (err: any) {
599
- // Check for CloudFront account verification error
600
- if (err.message?.includes('must be verified') || err.message?.includes('Access denied for operation')) {
601
- console.log('CloudFront account verification required - checking for existing infrastructure...')
602
-
603
- // Try to find an existing CloudFront distribution we can use
604
- try {
605
- const cloudfront = new CloudFrontClient()
606
- const distributions = await cloudfront.listDistributions()
607
-
608
- for (const dist of distributions) {
609
- let aliases: string[] = []
610
- if (dist.Aliases?.Items) {
611
- if (Array.isArray(dist.Aliases.Items)) {
612
- aliases = dist.Aliases.Items
613
- }
614
- else if (typeof dist.Aliases.Items === 'object') {
615
- const cname = (dist.Aliases.Items as any).CNAME
616
- if (typeof cname === 'string') {
617
- aliases = [cname]
618
- }
619
- else if (Array.isArray(cname)) {
620
- aliases = cname
621
- }
622
- }
623
- }
624
-
625
- if (aliases.includes(domain)) {
626
- console.log(`Found existing CloudFront distribution ${dist.Id} with alias ${domain}`)
627
- console.log(`Using existing infrastructure despite account verification requirement...`)
628
-
629
- // Get the origin bucket from the distribution
630
- const distConfig = await cloudfront.getDistributionConfig(dist.Id!)
631
- const originsData = distConfig.DistributionConfig?.Origins?.Items
632
- let originBucket: string | undefined
633
-
634
- if (originsData) {
635
- let originList: any[] = []
636
- if (Array.isArray(originsData)) {
637
- originList = originsData
638
- }
639
- else if (originsData.Origin) {
640
- originList = Array.isArray(originsData.Origin) ? originsData.Origin : [originsData.Origin]
641
- }
642
- else {
643
- originList = [originsData]
644
- }
645
-
646
- for (const origin of originList) {
647
- const domainName = origin.DomainName || ''
648
- const s3Match = domainName.match(/^([^.]+)\.s3[\.-]/)
649
- if (s3Match) {
650
- originBucket = s3Match[1]
651
- break
652
- }
653
- }
654
- }
655
-
656
- if (originBucket) {
657
- console.log(`Using existing S3 bucket: ${originBucket}`)
658
-
659
- // Also ensure DNS records exist
660
- const distributionDomain = dist.DomainName
661
- if (distributionDomain) {
662
- await ensureDnsRecords(dnsProvider, domain, distributionDomain)
663
- }
664
-
665
- return {
666
- success: true,
667
- stackName: `existing-${dist.Id}`,
668
- bucket: originBucket,
669
- distributionId: dist.Id,
670
- distributionDomain: dist.DomainName,
671
- domain,
672
- certificateArn,
673
- message: 'Using existing CloudFront distribution (account verification pending for new distributions)',
674
- }
675
- }
676
- }
677
- }
678
- }
679
- catch {
680
- // Couldn't find existing infrastructure
681
- }
682
-
683
- // No existing infrastructure found, show verification message
684
- const verificationMessage = `
685
- ┌─────────────────────────────────────────────────────────────────────────────┐
686
- │ AWS ACCOUNT VERIFICATION REQUIRED │
687
- ├─────────────────────────────────────────────────────────────────────────────┤
688
- │ Your AWS account must be verified before you can create CloudFront │
689
- │ distributions. This is a one-time requirement for new AWS accounts. │
690
- │ │
691
- │ To verify your account: │
692
- │ │
693
- │ 1. Go to: https://console.aws.amazon.com/support/home#/ │
694
- │ 2. Click "Create case" │
695
- │ 3. Select "Service limit increase" │
696
- │ 4. For Service: Select "CloudFront" │
697
- │ 5. For Request: "Please verify my account for CloudFront access" │
698
- │ 6. Submit the case │
699
- │ │
700
- │ Verification typically takes 1-2 business days. │
701
- │ After verification, re-run: bunx bunpress deploy │
702
- └─────────────────────────────────────────────────────────────────────────────┘`
703
- console.log(verificationMessage)
704
- return {
705
- success: false,
706
- stackId,
707
- stackName,
708
- bucket: finalBucket,
709
- message: 'CloudFront account verification required. Please contact AWS Support.',
710
- }
711
- }
712
-
713
- return {
714
- success: false,
715
- stackId,
716
- stackName,
717
- bucket: finalBucket,
718
- message: `Stack deployment failed: ${err.message}`,
719
- }
720
- }
721
-
722
- // Get stack outputs
723
- const stacks = await cf.describeStacks({ stackName })
724
- const outputs = stacks.Stacks[0]?.Outputs || []
725
- const getOutput = (key: string) => outputs.find(o => o.OutputKey === key)?.OutputValue
726
-
727
- const distributionDomain = getOutput('DistributionDomain')
728
-
729
- // Create DNS records using external DNS provider
730
- if (distributionDomain) {
731
- console.log(`Creating DNS records via ${dnsProvider.name}...`)
732
- await ensureDnsRecords(dnsProvider, domain, distributionDomain)
733
- }
734
-
735
- return {
736
- success: true,
737
- stackId,
738
- stackName,
739
- bucket: getOutput('BucketName') || finalBucket,
740
- distributionId: getOutput('DistributionId'),
741
- distributionDomain,
742
- domain,
743
- certificateArn,
744
- message: 'Static site infrastructure deployed successfully with external DNS',
745
- }
746
- }
747
-
748
- /**
749
- * Create or update DNS records pointing to CloudFront
750
- */
751
- async function ensureDnsRecords(
752
- dnsProvider: DnsProvider,
753
- domain: string,
754
- cloudfrontDomain: string,
755
- ): Promise<void> {
756
- // Create CNAME record pointing to CloudFront
757
- // Note: For apex domains (e.g., bunpress.org), some DNS providers support ALIAS/ANAME records
758
- // Porkbun supports ALIAS records for apex domains
759
-
760
- // Check if this is an apex domain (no subdomain)
761
- const parts = domain.split('.')
762
- const isApexDomain = parts.length === 2
763
-
764
- if (isApexDomain) {
765
- // For apex domains, create an ALIAS record (Porkbun supports this)
766
- // Porkbun uses 'ALIAS' type for apex domain CNAME-like behavior
767
- console.log(`Creating ALIAS record for apex domain ${domain} -> ${cloudfrontDomain}`)
768
-
769
- const result = await dnsProvider.upsertRecord(domain, {
770
- name: domain,
771
- type: 'ALIAS' as any, // Porkbun-specific type for apex domains
772
- content: cloudfrontDomain,
773
- ttl: 600,
774
- })
775
-
776
- if (!result.success) {
777
- // Fallback to A record using CloudFront IPs (not recommended but works)
778
- console.log(`ALIAS record failed, trying CNAME with @ subdomain...`)
779
- const cnameResult = await dnsProvider.upsertRecord(domain, {
780
- name: domain,
781
- type: 'CNAME',
782
- content: cloudfrontDomain,
783
- ttl: 600,
784
- })
785
-
786
- if (!cnameResult.success) {
787
- console.warn(`Warning: Could not create DNS record: ${cnameResult.message}`)
788
- console.warn(`Please manually create a CNAME or ALIAS record:`)
789
- console.warn(` ${domain} -> ${cloudfrontDomain}`)
790
- }
791
- else {
792
- console.log(`Created CNAME record: ${domain} -> ${cloudfrontDomain}`)
793
- }
794
- }
795
- else {
796
- console.log(`Created ALIAS record: ${domain} -> ${cloudfrontDomain}`)
797
- }
798
-
799
- // Also create CNAME for www subdomain
800
- const wwwDomain = `www.${domain}`
801
- console.log(`Creating CNAME record for ${wwwDomain} -> ${cloudfrontDomain}`)
802
- const wwwResult = await dnsProvider.upsertRecord(domain, {
803
- name: wwwDomain,
804
- type: 'CNAME',
805
- content: cloudfrontDomain,
806
- ttl: 600,
807
- })
808
-
809
- if (!wwwResult.success) {
810
- console.warn(`Warning: Could not create www DNS record: ${wwwResult.message}`)
811
- console.warn(`Please manually create a CNAME record:`)
812
- console.warn(` ${wwwDomain} -> ${cloudfrontDomain}`)
813
- }
814
- else {
815
- console.log(`Created CNAME record: ${wwwDomain} -> ${cloudfrontDomain}`)
816
- }
817
- }
818
- else {
819
- // For subdomains, use standard CNAME
820
- console.log(`Creating CNAME record for ${domain} -> ${cloudfrontDomain}`)
821
-
822
- const result = await dnsProvider.upsertRecord(domain, {
823
- name: domain,
824
- type: 'CNAME',
825
- content: cloudfrontDomain,
826
- ttl: 600,
827
- })
828
-
829
- if (!result.success) {
830
- console.warn(`Warning: Could not create DNS record: ${result.message}`)
831
- console.warn(`Please manually create a CNAME record:`)
832
- console.warn(` ${domain} -> ${cloudfrontDomain}`)
833
- }
834
- else {
835
- console.log(`Created CNAME record: ${domain} -> ${cloudfrontDomain}`)
836
- }
837
- }
838
- }
839
-
840
- /**
841
- * Full deployment with external DNS: infrastructure + files + cache invalidation
842
- */
843
- export async function deployStaticSiteWithExternalDnsFull(config: ExternalDnsStaticSiteConfig & {
844
- sourceDir: string
845
- cleanBucket?: boolean
846
- onProgress?: (stage: string, detail?: string) => void
847
- }): Promise<ExternalDnsDeployResult> {
848
- const { sourceDir, cleanBucket = true, onProgress, ...siteConfig } = config
849
-
850
- // Step 1: Deploy infrastructure
851
- onProgress?.('infrastructure', 'Deploying CloudFormation stack...')
852
- const infraResult = await deployStaticSiteWithExternalDns(siteConfig)
853
-
854
- if (!infraResult.success) {
855
- return infraResult
856
- }
857
-
858
- // Step 2: Clean bucket before upload
859
- if (cleanBucket) {
860
- onProgress?.('clean', 'Cleaning old files from S3...')
861
- try {
862
- const s3 = new S3Client(siteConfig.region || 'us-east-1')
863
- await s3.emptyBucket(infraResult.bucket)
864
- }
865
- catch (err: any) {
866
- console.log(`Note: Could not clean bucket: ${err.message}`)
867
- }
868
- }
869
-
870
- // Step 3: Upload files
871
- onProgress?.('upload', 'Uploading files to S3...')
872
- const { uploadStaticFiles } = await import('./static-site')
873
- const uploadResult = await uploadStaticFiles({
874
- sourceDir,
875
- bucket: infraResult.bucket,
876
- region: siteConfig.region || 'us-east-1',
877
- cacheControl: siteConfig.cacheControl,
878
- onProgress: (uploaded, total, file) => {
879
- onProgress?.('upload', `${uploaded}/${total}: ${file}`)
880
- },
881
- })
882
-
883
- if (uploadResult.errors.length > 0) {
884
- return {
885
- ...infraResult,
886
- success: false,
887
- message: `Upload errors: ${uploadResult.errors.join(', ')}`,
888
- filesUploaded: uploadResult.uploaded,
889
- }
890
- }
891
-
892
- // Step 4: Invalidate cache
893
- if (infraResult.distributionId) {
894
- onProgress?.('invalidate', 'Invalidating CloudFront cache...')
895
- const { invalidateCache } = await import('./static-site')
896
- await invalidateCache(infraResult.distributionId)
897
- }
898
-
899
- onProgress?.('complete', 'Deployment complete!')
900
-
901
- return {
902
- ...infraResult,
903
- filesUploaded: uploadResult.uploaded,
904
- message: `Deployed ${uploadResult.uploaded} files successfully with external DNS`,
905
- }
906
- }