@stacksjs/ts-cloud 0.9.2 → 0.9.4

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 (191) hide show
  1. package/dist/aws/index.js +1 -269
  2. package/dist/bin/chunk-00kg80qg.js +1 -0
  3. package/dist/bin/chunk-09nwf2gy.js +34 -0
  4. package/dist/bin/chunk-1k5kszjp.js +111 -0
  5. package/dist/bin/chunk-21e0mavm.js +1 -0
  6. package/dist/bin/chunk-271gk1n2.js +6 -0
  7. package/dist/bin/chunk-2empps47.js +2 -0
  8. package/dist/bin/chunk-38pq971s.js +2 -0
  9. package/dist/bin/chunk-3gykfyq4.js +37 -0
  10. package/dist/bin/chunk-3vqfexc4.js +70 -0
  11. package/dist/bin/chunk-4vrqm9h0.js +2 -0
  12. package/dist/bin/chunk-598yf4bs.js +1 -0
  13. package/dist/bin/chunk-5ectjq0b.js +1 -0
  14. package/dist/bin/chunk-5erkxpt7.js +512 -0
  15. package/dist/bin/chunk-5mrtzemj.js +1 -0
  16. package/dist/bin/chunk-5n9fmabb.js +1 -0
  17. package/dist/bin/chunk-9knpg30y.js +1 -0
  18. package/dist/bin/chunk-9pvsff8g.js +1 -0
  19. package/dist/bin/chunk-ax9mr6wm.js +44 -0
  20. package/dist/bin/chunk-axhnkn5g.js +2 -0
  21. package/dist/bin/chunk-c1vcdgvr.js +1 -0
  22. package/dist/bin/chunk-ctd2vg9w.js +2 -0
  23. package/dist/bin/chunk-dbgffky8.js +1 -0
  24. package/dist/bin/chunk-ddk4rt4j.js +6 -0
  25. package/dist/bin/chunk-dwr30vbv.js +1 -0
  26. package/dist/bin/chunk-e0rkrq70.js +1 -0
  27. package/dist/bin/chunk-e3xrwh9z.js +1 -0
  28. package/dist/bin/chunk-etrb03mt.js +1507 -0
  29. package/dist/bin/chunk-fhc6ntvd.js +2 -0
  30. package/dist/bin/chunk-fpm59y25.js +6 -0
  31. package/dist/bin/chunk-fvbxvq6b.js +1 -0
  32. package/dist/bin/chunk-fy8j3y8p.js +92 -0
  33. package/dist/bin/chunk-fz21b57w.js +1 -0
  34. package/dist/bin/chunk-gavvf0pg.js +141 -0
  35. package/dist/bin/chunk-ggn7ax8p.js +4 -0
  36. package/dist/bin/chunk-gpe2prkm.js +1 -0
  37. package/dist/bin/chunk-h5pa5men.js +2 -0
  38. package/dist/bin/chunk-hm08xdxf.js +2 -0
  39. package/dist/bin/chunk-hzgz13nf.js +1 -0
  40. package/dist/bin/chunk-j703btc5.js +50 -0
  41. package/dist/bin/chunk-k9t9wvms.js +1 -0
  42. package/dist/bin/chunk-m669wtgr.js +1 -0
  43. package/dist/bin/chunk-m7dgskt1.js +1 -0
  44. package/dist/bin/chunk-mh0kdzqp.js +2 -0
  45. package/dist/bin/chunk-mr0atd2s.js +2 -0
  46. package/dist/bin/chunk-mz0wbbsj.js +3 -0
  47. package/dist/bin/chunk-ngcyftm2.js +1 -0
  48. package/dist/bin/chunk-p66ptpmr.js +2 -0
  49. package/dist/bin/chunk-qk9qgepa.js +1 -0
  50. package/dist/bin/chunk-rxcvf10f.js +7364 -0
  51. package/dist/bin/chunk-rytk89az.js +60 -0
  52. package/dist/bin/chunk-tga3hhwk.js +6 -0
  53. package/dist/bin/chunk-tx0tqq2k.js +8 -0
  54. package/dist/bin/chunk-vek8a0w6.js +1 -0
  55. package/dist/bin/chunk-vm3q5gf2.js +3 -0
  56. package/dist/bin/chunk-y7s5r8y6.js +3 -0
  57. package/dist/bin/chunk-yv9sth30.js +1 -0
  58. package/dist/bin/chunk-zmegvjp8.js +581 -0
  59. package/dist/bin/cli.js +417 -10597
  60. package/dist/bin/dashboard-server.js +1 -6998
  61. package/dist/chunk-0ccta6z6.js +6 -0
  62. package/dist/chunk-0k1cs6pp.js +1 -0
  63. package/dist/chunk-1egjn203.js +704 -0
  64. package/dist/chunk-2wx29qga.js +5 -0
  65. package/dist/chunk-5m5rqmv7.js +1 -0
  66. package/dist/chunk-8ea5022g.js +114 -0
  67. package/dist/chunk-8xqn3c0w.js +2 -0
  68. package/dist/chunk-b4esxhmw.js +2 -0
  69. package/dist/chunk-b8e5wzwn.js +44 -0
  70. package/dist/chunk-bggt4n6e.js +2 -0
  71. package/dist/chunk-cwkg37rs.js +37 -0
  72. package/dist/chunk-dskvqt67.js +7355 -0
  73. package/dist/chunk-gahsdmxf.js +1617 -0
  74. package/dist/chunk-gnv3r2jg.js +1 -0
  75. package/dist/chunk-hg3c48cp.js +34 -0
  76. package/dist/chunk-m5azeva3.js +2 -0
  77. package/dist/chunk-r3qf56b3.js +92 -0
  78. package/dist/chunk-sdqa9ghv.js +141 -0
  79. package/dist/chunk-svrvvcm7.js +1 -0
  80. package/dist/chunk-v0p3hpc3.js +60 -0
  81. package/dist/chunk-wdzjbecg.js +50 -0
  82. package/dist/chunk-ww8k3g7j.js +1 -0
  83. package/dist/chunk-xmqfc62w.js +1 -0
  84. package/dist/chunk-xrebj679.js +435 -0
  85. package/dist/chunk-xt3s24vr.js +2 -0
  86. package/dist/chunk-y21zk0pn.js +4 -0
  87. package/dist/deploy/attach-credentials.d.ts +57 -0
  88. package/dist/deploy/index.js +1 -168
  89. package/dist/deploy/site-ports.d.ts +151 -0
  90. package/dist/deploy/site-target.d.ts +24 -1
  91. package/dist/dns/index.js +1 -41
  92. package/dist/drivers/index.js +1 -154
  93. package/dist/index.js +100 -6129
  94. package/dist/protection/index.js +1 -89
  95. package/dist/push/index.js +1 -509
  96. package/dist/spend/index.js +1 -173
  97. package/dist/ui/404.html +16 -548
  98. package/dist/ui/_stx/chunk.5ddb5087.js +90 -0
  99. package/dist/ui/_stx/chunk.9fd39d5b.js +86 -0
  100. package/dist/ui/_stx/chunk.d5301f79.js +119 -0
  101. package/dist/ui/_stx/crosswind.222ed683.css +1 -0
  102. package/dist/ui/_stx/crosswind.616c25f3.css +1 -0
  103. package/dist/ui/_stx/crosswind.80baa894.css +320 -0
  104. package/dist/ui/_stx/crosswind.906ee17c.css +1 -0
  105. package/dist/ui/_stx/crosswind.996ed49e.css +1 -0
  106. package/dist/ui/_stx/crosswind.b7a99a5c.css +1 -0
  107. package/dist/ui/_stx/router.fa5bc4dc.js +4 -0
  108. package/dist/ui/_stx/runtime.c696d17a.js +1 -0
  109. package/dist/ui/_stx/style.448ab192.css +1 -0
  110. package/dist/ui/accept-invitation.html +2 -1133
  111. package/dist/ui/access-denied.html +7 -1130
  112. package/dist/ui/account/automation.html +12 -1134
  113. package/dist/ui/account/security.html +13 -1136
  114. package/dist/ui/applications/compose.html +12 -1134
  115. package/dist/ui/applications/new.html +13 -1137
  116. package/dist/ui/data/backups.html +91 -1136
  117. package/dist/ui/data/services.html +89 -1136
  118. package/dist/ui/data/volumes.html +61 -1136
  119. package/dist/ui/index.html +60 -1136
  120. package/dist/ui/infrastructure/topology.html +128 -1135
  121. package/dist/ui/integrations.html +13 -1138
  122. package/dist/ui/operations/alerts.html +59 -1136
  123. package/dist/ui/operations/configuration.html +13 -1135
  124. package/dist/ui/operations/jobs.html +87 -1136
  125. package/dist/ui/operations/maintenance.html +12 -1134
  126. package/dist/ui/operations/observability.html +93 -1136
  127. package/dist/ui/operations/previews.html +12 -1134
  128. package/dist/ui/operations/queue.html +12 -1134
  129. package/dist/ui/operations/regions.html +12 -1134
  130. package/dist/ui/operations/releases.html +12 -1134
  131. package/dist/ui/operations/spend.html +65 -1136
  132. package/dist/ui/operations/workloads.html +115 -1136
  133. package/dist/ui/robots.txt +2 -0
  134. package/dist/ui/security.html +24 -1136
  135. package/dist/ui/server/actions.html +67 -1138
  136. package/dist/ui/server/activity.html +7 -1130
  137. package/dist/ui/server/capacity.html +12 -1134
  138. package/dist/ui/server/database.html +166 -1135
  139. package/dist/ui/server/deployments.html +60 -1136
  140. package/dist/ui/server/diagnostics.html +7 -1130
  141. package/dist/ui/server/firewall.html +286 -1212
  142. package/dist/ui/server/fleet.html +45 -1136
  143. package/dist/ui/server/logs.html +34 -1135
  144. package/dist/ui/server/metrics.html +124 -1140
  145. package/dist/ui/server/security.html +7 -1130
  146. package/dist/ui/server/services.html +46 -1133
  147. package/dist/ui/server/sites.html +287 -1137
  148. package/dist/ui/server/ssh-keys.html +62 -1135
  149. package/dist/ui/server/team.html +155 -1136
  150. package/dist/ui/server/terminal.html +97 -1183
  151. package/dist/ui/serverless/alarms.html +62 -1135
  152. package/dist/ui/serverless/assets.html +47 -1132
  153. package/dist/ui/serverless/cost.html +7 -1130
  154. package/dist/ui/serverless/data.html +46 -1135
  155. package/dist/ui/serverless/deployments.html +47 -1132
  156. package/dist/ui/serverless/firewall.html +7 -1130
  157. package/dist/ui/serverless/functions.html +50 -1136
  158. package/dist/ui/serverless/logs.html +34 -1135
  159. package/dist/ui/serverless/metrics.html +7 -1130
  160. package/dist/ui/serverless/queues.html +58 -1135
  161. package/dist/ui/serverless/secrets.html +62 -1136
  162. package/dist/ui/serverless/traces.html +12 -1134
  163. package/dist/ui/serverless.html +64 -1137
  164. package/dist/ui-src/pages/server/firewall.stx +14 -14
  165. package/package.json +3 -3
  166. package/dist/chunk-01d86gt1.js +0 -1324
  167. package/dist/chunk-050r7ycy.js +0 -46516
  168. package/dist/chunk-29rse4p1.js +0 -12203
  169. package/dist/chunk-32e7ya18.js +0 -642
  170. package/dist/chunk-4cjrg98a.js +0 -597
  171. package/dist/chunk-50jpda9q.js +0 -371
  172. package/dist/chunk-6a1t7fnq.js +0 -12121
  173. package/dist/chunk-728jvrr0.js +0 -1701
  174. package/dist/chunk-7m60qnc8.js +0 -1762
  175. package/dist/chunk-aa2dkn7b.js +0 -205
  176. package/dist/chunk-d48vtdj4.js +0 -433
  177. package/dist/chunk-f55nkpvb.js +0 -8
  178. package/dist/chunk-fgk8cqew.js +0 -152
  179. package/dist/chunk-he9a874b.js +0 -769
  180. package/dist/chunk-hg7rhrgd.js +0 -7274
  181. package/dist/chunk-k2yyatfw.js +0 -10
  182. package/dist/chunk-k4px0bnv.js +0 -23
  183. package/dist/chunk-nsgqsmrh.js +0 -1779
  184. package/dist/chunk-qr803fye.js +0 -2122
  185. package/dist/chunk-rds0sy87.js +0 -1771
  186. package/dist/chunk-rxgchb08.js +0 -42033
  187. package/dist/chunk-ttz2ra5j.js +0 -12
  188. package/dist/chunk-v0bahtg2.js +0 -4
  189. package/dist/chunk-va2yd85b.js +0 -200
  190. package/dist/chunk-x9s0cqfx.js +0 -13
  191. package/dist/chunk-zqtpg06c.js +0 -1752
@@ -1,1701 +0,0 @@
1
- import {
2
- CloudFrontClient
3
- } from "./chunk-01d86gt1.js";
4
- import {
5
- ACMClient,
6
- ACMDnsValidator,
7
- Route53Provider,
8
- UnifiedDnsValidator,
9
- createDnsProvider
10
- } from "./chunk-qr803fye.js";
11
- import {
12
- Route53Client
13
- } from "./chunk-32e7ya18.js";
14
- import {
15
- CloudFormationClient
16
- } from "./chunk-4cjrg98a.js";
17
- import {
18
- S3Client
19
- } from "./chunk-rds0sy87.js";
20
- import {
21
- __require
22
- } from "./chunk-v0bahtg2.js";
23
-
24
- // src/deploy/static-site-external-dns.ts
25
- function generateExternalDnsStaticSiteTemplate(config) {
26
- const {
27
- bucketName,
28
- domain,
29
- aliases,
30
- certificateArn,
31
- defaultRootObject = "index.html",
32
- errorDocument = "404.html",
33
- passthroughUrls = false,
34
- singlePageApp = false,
35
- dynamicApp = false,
36
- computeOriginDomain,
37
- computeOriginPort = 3008,
38
- computeOriginId = "app-compute",
39
- retainOnStackDelete = false
40
- } = config;
41
- const retainPolicy = retainOnStackDelete ? { DeletionPolicy: "Retain", UpdateReplacePolicy: "Retain" } : {};
42
- const useComputeOrigin = dynamicApp && !!computeOriginDomain;
43
- const defaultAllowedMethods = useComputeOrigin ? ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"] : ["GET", "HEAD"];
44
- const defaultCachedMethods = useComputeOrigin ? ["GET", "HEAD"] : ["GET", "HEAD"];
45
- const resources = {};
46
- const outputs = {};
47
- resources.S3Bucket = {
48
- Type: "AWS::S3::Bucket",
49
- ...retainPolicy,
50
- Properties: {
51
- BucketName: bucketName,
52
- PublicAccessBlockConfiguration: {
53
- BlockPublicAcls: true,
54
- BlockPublicPolicy: false,
55
- IgnorePublicAcls: true,
56
- RestrictPublicBuckets: false
57
- },
58
- WebsiteConfiguration: {
59
- IndexDocument: defaultRootObject,
60
- ErrorDocument: errorDocument
61
- }
62
- }
63
- };
64
- outputs.BucketName = {
65
- Description: "S3 Bucket Name",
66
- Value: { Ref: "S3Bucket" }
67
- };
68
- outputs.BucketArn = {
69
- Description: "S3 Bucket ARN",
70
- Value: { "Fn::GetAtt": ["S3Bucket", "Arn"] }
71
- };
72
- resources.CloudFrontOAC = {
73
- Type: "AWS::CloudFront::OriginAccessControl",
74
- ...retainPolicy,
75
- Properties: {
76
- OriginAccessControlConfig: {
77
- Name: `OAC-${bucketName}`,
78
- Description: `OAC for ${bucketName}`,
79
- OriginAccessControlOriginType: "s3",
80
- SigningBehavior: "always",
81
- SigningProtocol: "sigv4"
82
- }
83
- }
84
- };
85
- let installRootRedirectLogicalId;
86
- if (passthroughUrls && useComputeOrigin) {
87
- installRootRedirectLogicalId = "InstallRootRedirectFunction";
88
- resources[installRootRedirectLogicalId] = {
89
- Type: "AWS::CloudFront::Function",
90
- Properties: {
91
- Name: { "Fn::Sub": "${AWS::StackName}-install-root-redirect" },
92
- AutoPublish: true,
93
- FunctionConfig: {
94
- Comment: "Redirect curl pantry.dev | bash (GET /) to /install.sh on S3",
95
- Runtime: "cloudfront-js-2.0"
96
- },
97
- FunctionCode: `function handler(event) {
98
- var request = event.request;
99
- if (request.uri === '/' || request.uri === '') {
100
- return {
101
- statusCode: 302,
102
- statusDescription: 'Found',
103
- headers: {
104
- location: { value: 'https://' + request.headers.host.value + '/install.sh' }
105
- }
106
- };
107
- }
108
- return request;
109
- }`
110
- }
111
- };
112
- }
113
- if (!passthroughUrls) {
114
- resources.UrlRewriteFunction = {
115
- Type: "AWS::CloudFront::Function",
116
- Properties: {
117
- Name: { "Fn::Sub": "${AWS::StackName}-url-rewrite" },
118
- AutoPublish: true,
119
- FunctionConfig: {
120
- Comment: "Append .html extension to URLs without extensions",
121
- Runtime: "cloudfront-js-2.0"
122
- },
123
- FunctionCode: `function handler(event) {
124
- const request = event.request;
125
- var uri = request.uri;
126
-
127
- // If URI ends with /, serve index.html
128
- if (uri.endsWith('/')) {
129
- request.uri = uri + 'index.html';
130
- }
131
- // If URI doesn't have an extension, append .html
132
- else if (!uri.includes('.')) {
133
- request.uri = uri + '.html';
134
- }
135
-
136
- return request;
137
- }`
138
- }
139
- };
140
- }
141
- const s3Origin = {
142
- Id: `S3-${bucketName}`,
143
- DomainName: { "Fn::GetAtt": ["S3Bucket", "RegionalDomainName"] },
144
- S3OriginConfig: {
145
- OriginAccessIdentity: ""
146
- },
147
- OriginAccessControlId: { "Fn::GetAtt": ["CloudFrontOAC", "Id"] }
148
- };
149
- const computeOrigin = useComputeOrigin ? {
150
- Id: computeOriginId,
151
- DomainName: computeOriginDomain,
152
- CustomOriginConfig: {
153
- HTTPPort: computeOriginPort,
154
- HTTPSPort: 443,
155
- OriginProtocolPolicy: "http-only",
156
- OriginSSLProtocols: ["TLSv1.2"]
157
- }
158
- } : null;
159
- const defaultTargetOriginId = useComputeOrigin ? computeOriginId : `S3-${bucketName}`;
160
- const distributionConfig = {
161
- Enabled: true,
162
- DefaultRootObject: defaultRootObject,
163
- HttpVersion: "http2and3",
164
- IPV6Enabled: true,
165
- PriceClass: "PriceClass_100",
166
- Origins: computeOrigin ? [computeOrigin, s3Origin] : [s3Origin],
167
- DefaultCacheBehavior: {
168
- TargetOriginId: defaultTargetOriginId,
169
- ViewerProtocolPolicy: "redirect-to-https",
170
- AllowedMethods: defaultAllowedMethods,
171
- CachedMethods: defaultCachedMethods,
172
- Compress: true,
173
- ...useComputeOrigin ? {
174
- CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
175
- OriginRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac",
176
- ...passthroughUrls && installRootRedirectLogicalId ? {
177
- FunctionAssociations: [
178
- {
179
- EventType: "viewer-request",
180
- FunctionARN: { "Fn::GetAtt": [installRootRedirectLogicalId, "FunctionARN"] }
181
- }
182
- ]
183
- } : {}
184
- } : {
185
- CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
186
- ...!passthroughUrls && {
187
- FunctionAssociations: [
188
- {
189
- EventType: "viewer-request",
190
- FunctionARN: { "Fn::GetAtt": ["UrlRewriteFunction", "FunctionARN"] }
191
- }
192
- ]
193
- }
194
- }
195
- },
196
- ...useComputeOrigin && passthroughUrls ? {
197
- CacheBehaviors: [
198
- {
199
- PathPattern: "/install.sh",
200
- TargetOriginId: `S3-${bucketName}`,
201
- ViewerProtocolPolicy: "redirect-to-https",
202
- AllowedMethods: ["GET", "HEAD", "OPTIONS"],
203
- CachedMethods: ["GET", "HEAD", "OPTIONS"],
204
- Compress: true,
205
- CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6"
206
- }
207
- ]
208
- } : {},
209
- CustomErrorResponses: singlePageApp ? [
210
- {
211
- ErrorCode: 403,
212
- ResponseCode: 200,
213
- ResponsePagePath: `/${defaultRootObject}`,
214
- ErrorCachingMinTTL: 300
215
- },
216
- {
217
- ErrorCode: 404,
218
- ResponseCode: 200,
219
- ResponsePagePath: `/${defaultRootObject}`,
220
- ErrorCachingMinTTL: 300
221
- }
222
- ] : [
223
- {
224
- ErrorCode: 403,
225
- ResponseCode: 404,
226
- ResponsePagePath: `/${errorDocument}`,
227
- ErrorCachingMinTTL: 300
228
- },
229
- {
230
- ErrorCode: 404,
231
- ResponseCode: 404,
232
- ResponsePagePath: `/${errorDocument}`,
233
- ErrorCachingMinTTL: 300
234
- }
235
- ]
236
- };
237
- if (domain && certificateArn) {
238
- distributionConfig.Aliases = aliases && aliases.length > 0 ? aliases : [domain];
239
- distributionConfig.ViewerCertificate = {
240
- AcmCertificateArn: certificateArn,
241
- SslSupportMethod: "sni-only",
242
- MinimumProtocolVersion: "TLSv1.2_2021"
243
- };
244
- } else {
245
- distributionConfig.ViewerCertificate = {
246
- CloudFrontDefaultCertificate: true
247
- };
248
- }
249
- resources.CloudFrontDistribution = {
250
- Type: "AWS::CloudFront::Distribution",
251
- ...retainPolicy,
252
- DependsOn: [
253
- "S3Bucket",
254
- "CloudFrontOAC",
255
- ...passthroughUrls && useComputeOrigin && installRootRedirectLogicalId ? [installRootRedirectLogicalId] : [],
256
- ...!passthroughUrls ? ["UrlRewriteFunction"] : []
257
- ],
258
- Properties: {
259
- DistributionConfig: distributionConfig
260
- }
261
- };
262
- outputs.DistributionId = {
263
- Description: "CloudFront Distribution ID",
264
- Value: { Ref: "CloudFrontDistribution" }
265
- };
266
- outputs.DistributionDomain = {
267
- Description: "CloudFront Distribution Domain",
268
- Value: { "Fn::GetAtt": ["CloudFrontDistribution", "DomainName"] }
269
- };
270
- resources.S3BucketPolicy = {
271
- Type: "AWS::S3::BucketPolicy",
272
- ...retainPolicy,
273
- DependsOn: ["S3Bucket", "CloudFrontDistribution"],
274
- Properties: {
275
- Bucket: { Ref: "S3Bucket" },
276
- PolicyDocument: {
277
- Version: "2012-10-17",
278
- Statement: [
279
- {
280
- Sid: "AllowCloudFrontServicePrincipal",
281
- Effect: "Allow",
282
- Principal: {
283
- Service: "cloudfront.amazonaws.com"
284
- },
285
- Action: "s3:GetObject",
286
- Resource: { "Fn::Sub": "arn:aws:s3:::${S3Bucket}/*" },
287
- Condition: {
288
- StringEquals: {
289
- "AWS:SourceArn": {
290
- "Fn::Sub": "arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}"
291
- }
292
- }
293
- }
294
- }
295
- ]
296
- }
297
- }
298
- };
299
- outputs.SiteUrl = {
300
- Description: "Site URL",
301
- Value: domain ? `https://${domain}` : { "Fn::Sub": "https://${CloudFrontDistribution.DomainName}" }
302
- };
303
- return {
304
- AWSTemplateFormatVersion: "2010-09-09",
305
- Description: `Static site infrastructure for ${domain || bucketName} (External DNS)`,
306
- Resources: resources,
307
- Outputs: outputs
308
- };
309
- }
310
- async function deployStaticSiteWithExternalDns(config) {
311
- const region = config.region || "us-east-1";
312
- const cfRegion = "us-east-1";
313
- const domain = config.domain;
314
- const bucket = config.bucket || domain.replace(/\./g, "-");
315
- const stackName = config.stackName || `${config.siteName}-static-site`;
316
- const cf = new CloudFormationClient(cfRegion);
317
- const acm = new ACMClient("us-east-1");
318
- let dnsProvider = null;
319
- if (!config.skipDnsVerification) {
320
- dnsProvider = createDnsProvider(config.dnsProvider);
321
- console.log(`Verifying DNS provider can manage ${domain}...`);
322
- const verify = await dnsProvider.listRecords(domain);
323
- if (!verify.success) {
324
- const reason = verify.message || "unknown error — check API credentials and domain ownership";
325
- return {
326
- success: false,
327
- stackName,
328
- bucket,
329
- message: `DNS provider '${dnsProvider.name}' cannot manage domain ${domain}: ${reason}`
330
- };
331
- }
332
- console.log(`DNS provider '${dnsProvider.name}' verified for ${domain}`);
333
- } else {
334
- console.log(`Skipping DNS verification for ${domain}`);
335
- }
336
- let certificateArn = config.certificateArn;
337
- const domainParts = domain.split(".");
338
- const isApexDomain = domainParts.length === 2;
339
- const wwwDomain = isApexDomain ? `www.${domain}` : undefined;
340
- if (!certificateArn) {
341
- console.log(`Checking for existing SSL certificate for ${domain}...`);
342
- const existingCert = await acm.findCertificateByDomain(domain);
343
- let existingCertCoversWww = false;
344
- if (existingCert && existingCert.Status === "ISSUED") {
345
- if (wwwDomain && existingCert.SubjectAlternativeNames) {
346
- existingCertCoversWww = existingCert.SubjectAlternativeNames.includes(wwwDomain) || existingCert.SubjectAlternativeNames.some((san) => san === `*.${domain}`);
347
- } else {
348
- existingCertCoversWww = true;
349
- }
350
- if (existingCertCoversWww) {
351
- certificateArn = existingCert.CertificateArn;
352
- console.log(`Found existing certificate with www coverage: ${certificateArn}`);
353
- } else {
354
- console.log(`Existing certificate doesn't cover ${wwwDomain}, requesting new one...`);
355
- }
356
- }
357
- if (!certificateArn) {
358
- if (!dnsProvider) {
359
- return {
360
- success: false,
361
- stackName,
362
- bucket,
363
- message: `No DNS provider available to validate SSL certificate for ${domain}. Provide a certificateArn or enable DNS verification.`
364
- };
365
- }
366
- console.log(`Requesting new SSL certificate for ${domain}${wwwDomain ? ` (including ${wwwDomain})` : ""}...`);
367
- const validator = new UnifiedDnsValidator(dnsProvider, "us-east-1");
368
- const certResult = await validator.findOrCreateCertificate({
369
- domainName: domain,
370
- subjectAlternativeNames: wwwDomain ? [wwwDomain] : undefined,
371
- waitForValidation: true,
372
- maxWaitMinutes: 10
373
- });
374
- if (certResult.status !== "issued") {
375
- return {
376
- success: false,
377
- stackName,
378
- bucket,
379
- message: `SSL certificate validation failed. Status: ${certResult.status}`
380
- };
381
- }
382
- certificateArn = certResult.certificateArn;
383
- console.log(`Certificate issued: ${certificateArn}`);
384
- }
385
- }
386
- let stackExists = false;
387
- let existingBucketName;
388
- try {
389
- const existingStacks = await cf.describeStacks({ stackName });
390
- if (existingStacks.Stacks.length > 0) {
391
- const stack = existingStacks.Stacks[0];
392
- const stackStatus = stack.StackStatus;
393
- if (stackStatus === "DELETE_IN_PROGRESS") {
394
- console.log("Previous stack is still being deleted, waiting...");
395
- await cf.waitForStack(stackName, "stack-delete-complete");
396
- stackExists = false;
397
- } else if (stackStatus === "DELETE_COMPLETE") {
398
- stackExists = false;
399
- } else {
400
- stackExists = true;
401
- const outputs2 = stack.Outputs || [];
402
- existingBucketName = outputs2.find((o) => o.OutputKey === "BucketName")?.OutputValue;
403
- }
404
- }
405
- } catch (err) {
406
- if (err.message?.includes("does not exist") || err.code === "ValidationError") {
407
- stackExists = false;
408
- } else {
409
- throw err;
410
- }
411
- }
412
- let finalBucket = existingBucketName || bucket;
413
- if (!stackExists) {
414
- const s3 = new S3Client(region);
415
- const cloudfront = new CloudFrontClient;
416
- let hasExistingDistribution = false;
417
- if (domain) {
418
- try {
419
- console.log(`Checking for existing CloudFront distributions with alias ${domain}...`);
420
- const distributions = await cloudfront.listDistributions();
421
- for (const dist of distributions) {
422
- let aliases2 = [];
423
- if (dist.Aliases?.Items) {
424
- if (Array.isArray(dist.Aliases.Items)) {
425
- aliases2 = dist.Aliases.Items;
426
- } else if (typeof dist.Aliases.Items === "object") {
427
- const cname = dist.Aliases.Items.CNAME;
428
- if (typeof cname === "string") {
429
- aliases2 = [cname];
430
- } else if (Array.isArray(cname)) {
431
- aliases2 = cname;
432
- }
433
- }
434
- }
435
- if (aliases2.includes(domain)) {
436
- hasExistingDistribution = true;
437
- console.log(`Found existing CloudFront distribution ${dist.Id} with alias ${domain}`);
438
- console.log(`Reusing existing infrastructure for updates...`);
439
- const distConfig = await cloudfront.getDistributionConfig(dist.Id);
440
- const originsData = distConfig.DistributionConfig?.Origins?.Items;
441
- let originBucket;
442
- if (originsData) {
443
- let originList = [];
444
- if (Array.isArray(originsData)) {
445
- originList = originsData;
446
- } else if (originsData.Origin) {
447
- originList = Array.isArray(originsData.Origin) ? originsData.Origin : [originsData.Origin];
448
- } else {
449
- originList = [originsData];
450
- }
451
- for (const origin of originList) {
452
- const domainName = origin.DomainName || "";
453
- const s3Match = domainName.match(/^([^.]+)\.s3[\.-]/);
454
- if (s3Match) {
455
- originBucket = s3Match[1];
456
- break;
457
- }
458
- }
459
- }
460
- if (originBucket) {
461
- console.log(`Using existing S3 bucket: ${originBucket}`);
462
- return {
463
- success: true,
464
- stackName: `existing-${dist.Id}`,
465
- bucket: originBucket,
466
- distributionId: dist.Id,
467
- distributionDomain: dist.DomainName,
468
- domain,
469
- certificateArn,
470
- message: "Using existing CloudFront distribution",
471
- _existingInfrastructure: true
472
- };
473
- }
474
- }
475
- }
476
- } catch {}
477
- }
478
- if (!hasExistingDistribution) {
479
- try {
480
- const headResult = await s3.headBucket(bucket);
481
- if (headResult.exists) {
482
- console.log(`Found orphaned S3 bucket ${bucket}, cleaning up...`);
483
- try {
484
- const cleanupPromise = s3.emptyBucket(bucket).then(() => s3.deleteBucket(bucket));
485
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Bucket cleanup timeout")), 30000));
486
- await Promise.race([cleanupPromise, timeoutPromise]);
487
- console.log(`Deleted orphaned S3 bucket ${bucket}`);
488
- } catch (cleanupErr) {
489
- console.log(`Note: Could not clean up S3 bucket: ${cleanupErr.message}`);
490
- const suffix = Date.now().toString(36);
491
- finalBucket = `${bucket}-${suffix}`;
492
- console.log(`Using alternative bucket name: ${finalBucket}`);
493
- }
494
- }
495
- } catch {}
496
- }
497
- }
498
- const aliases = wwwDomain ? [domain, wwwDomain] : [domain];
499
- const template = generateExternalDnsStaticSiteTemplate({
500
- bucketName: finalBucket,
501
- domain,
502
- aliases,
503
- certificateArn,
504
- defaultRootObject: config.defaultRootObject,
505
- errorDocument: config.errorDocument,
506
- passthroughUrls: config.passthroughUrls,
507
- singlePageApp: config.singlePageApp,
508
- dynamicApp: config.dynamicApp,
509
- computeOriginDomain: config.computeOriginDomain,
510
- computeOriginPort: config.computeOriginPort,
511
- computeOriginId: config.computeOriginId
512
- });
513
- const tags = Object.entries(config.tags || {}).map(([Key, Value]) => ({ Key, Value }));
514
- tags.push({ Key: "ManagedBy", Value: "ts-cloud" });
515
- tags.push({ Key: "Application", Value: config.siteName });
516
- if (dnsProvider) {
517
- tags.push({ Key: "DnsProvider", Value: dnsProvider.name });
518
- }
519
- let stackId;
520
- let isUpdate = false;
521
- if (stackExists) {
522
- isUpdate = true;
523
- console.log(`Updating CloudFormation stack: ${stackName}`);
524
- try {
525
- const result = await cf.updateStack({
526
- stackName,
527
- templateBody: JSON.stringify(template),
528
- capabilities: ["CAPABILITY_IAM"],
529
- tags
530
- });
531
- stackId = result.StackId;
532
- console.log(`Update initiated, stack ID: ${stackId}`);
533
- } catch (err) {
534
- if (err.message?.includes("No updates are to be performed")) {
535
- const stacks2 = await cf.describeStacks({ stackName });
536
- stackId = stacks2.Stacks[0].StackId;
537
- const outputs2 = stacks2.Stacks[0]?.Outputs || [];
538
- const getOutput2 = (key) => outputs2.find((o) => o.OutputKey === key)?.OutputValue;
539
- const distributionDomain2 = getOutput2("DistributionDomain");
540
- if (distributionDomain2) {
541
- await ensureDnsRecords(dnsProvider, domain, distributionDomain2);
542
- }
543
- return {
544
- success: true,
545
- stackId,
546
- stackName,
547
- bucket: getOutput2("BucketName") || finalBucket,
548
- distributionId: getOutput2("DistributionId"),
549
- distributionDomain: distributionDomain2,
550
- domain,
551
- certificateArn,
552
- message: "Static site infrastructure is already up to date"
553
- };
554
- } else {
555
- throw err;
556
- }
557
- }
558
- } else {
559
- console.log(`Creating CloudFormation stack: ${stackName}`);
560
- console.log(`Bucket name: ${finalBucket}`);
561
- console.log(`Domain: ${domain}`);
562
- console.log(`Certificate ARN: ${certificateArn}`);
563
- const result = await cf.createStack({
564
- stackName,
565
- templateBody: JSON.stringify(template),
566
- capabilities: ["CAPABILITY_IAM"],
567
- tags,
568
- onFailure: "DELETE"
569
- });
570
- stackId = result.StackId;
571
- console.log(`Create initiated, stack ID: ${stackId}`);
572
- }
573
- console.log(`Waiting for stack to reach ${isUpdate ? "stack-update-complete" : "stack-create-complete"}...`);
574
- try {
575
- await cf.waitForStack(stackName, isUpdate ? "stack-update-complete" : "stack-create-complete");
576
- console.log("Stack operation completed successfully!");
577
- } catch (err) {
578
- if (err.message?.includes("must be verified") || err.message?.includes("Access denied for operation")) {
579
- console.log("CloudFront account verification required - checking for existing infrastructure...");
580
- try {
581
- const cloudfront = new CloudFrontClient;
582
- const distributions = await cloudfront.listDistributions();
583
- for (const dist of distributions) {
584
- let aliases2 = [];
585
- if (dist.Aliases?.Items) {
586
- if (Array.isArray(dist.Aliases.Items)) {
587
- aliases2 = dist.Aliases.Items;
588
- } else if (typeof dist.Aliases.Items === "object") {
589
- const cname = dist.Aliases.Items.CNAME;
590
- if (typeof cname === "string") {
591
- aliases2 = [cname];
592
- } else if (Array.isArray(cname)) {
593
- aliases2 = cname;
594
- }
595
- }
596
- }
597
- if (aliases2.includes(domain)) {
598
- console.log(`Found existing CloudFront distribution ${dist.Id} with alias ${domain}`);
599
- console.log(`Using existing infrastructure despite account verification requirement...`);
600
- const distConfig = await cloudfront.getDistributionConfig(dist.Id);
601
- const originsData = distConfig.DistributionConfig?.Origins?.Items;
602
- let originBucket;
603
- if (originsData) {
604
- let originList = [];
605
- if (Array.isArray(originsData)) {
606
- originList = originsData;
607
- } else if (originsData.Origin) {
608
- originList = Array.isArray(originsData.Origin) ? originsData.Origin : [originsData.Origin];
609
- } else {
610
- originList = [originsData];
611
- }
612
- for (const origin of originList) {
613
- const domainName = origin.DomainName || "";
614
- const s3Match = domainName.match(/^([^.]+)\.s3[\.-]/);
615
- if (s3Match) {
616
- originBucket = s3Match[1];
617
- break;
618
- }
619
- }
620
- }
621
- if (originBucket) {
622
- console.log(`Using existing S3 bucket: ${originBucket}`);
623
- const distributionDomain2 = dist.DomainName;
624
- if (distributionDomain2) {
625
- await ensureDnsRecords(dnsProvider, domain, distributionDomain2);
626
- }
627
- return {
628
- success: true,
629
- stackName: `existing-${dist.Id}`,
630
- bucket: originBucket,
631
- distributionId: dist.Id,
632
- distributionDomain: dist.DomainName,
633
- domain,
634
- certificateArn,
635
- message: "Using existing CloudFront distribution (account verification pending for new distributions)"
636
- };
637
- }
638
- }
639
- }
640
- } catch {}
641
- const verificationMessage = `
642
- ┌─────────────────────────────────────────────────────────────────────────────┐
643
- │ AWS ACCOUNT VERIFICATION REQUIRED │
644
- ├─────────────────────────────────────────────────────────────────────────────┤
645
- │ Your AWS account must be verified before you can create CloudFront │
646
- │ distributions. This is a one-time requirement for new AWS accounts. │
647
- │ │
648
- │ To verify your account: │
649
- │ │
650
- │ 1. Go to: https://console.aws.amazon.com/support/home#/ │
651
- │ 2. Click "Create case" │
652
- │ 3. Select "Service limit increase" │
653
- │ 4. For Service: Select "CloudFront" │
654
- │ 5. For Request: "Please verify my account for CloudFront access" │
655
- │ 6. Submit the case │
656
- │ │
657
- │ Verification typically takes 1-2 business days. │
658
- │ After verification, re-run: bunx bunpress deploy │
659
- └─────────────────────────────────────────────────────────────────────────────┘`;
660
- console.log(verificationMessage);
661
- return {
662
- success: false,
663
- stackId,
664
- stackName,
665
- bucket: finalBucket,
666
- message: "CloudFront account verification required. Please contact AWS Support."
667
- };
668
- }
669
- return {
670
- success: false,
671
- stackId,
672
- stackName,
673
- bucket: finalBucket,
674
- message: `Stack deployment failed: ${err.message}`
675
- };
676
- }
677
- const stacks = await cf.describeStacks({ stackName });
678
- const outputs = stacks.Stacks[0]?.Outputs || [];
679
- const getOutput = (key) => outputs.find((o) => o.OutputKey === key)?.OutputValue;
680
- const distributionDomain = getOutput("DistributionDomain");
681
- if (distributionDomain && dnsProvider) {
682
- console.log(`Creating DNS records via ${dnsProvider.name}...`);
683
- await ensureDnsRecords(dnsProvider, domain, distributionDomain);
684
- } else if (distributionDomain && !dnsProvider) {
685
- console.log(`Skipping DNS record creation (DNS verification was skipped)`);
686
- }
687
- return {
688
- success: true,
689
- stackId,
690
- stackName,
691
- bucket: getOutput("BucketName") || finalBucket,
692
- distributionId: getOutput("DistributionId"),
693
- distributionDomain,
694
- domain,
695
- certificateArn,
696
- message: "Static site infrastructure deployed successfully with external DNS"
697
- };
698
- }
699
- async function ensureDnsRecords(dnsProvider, domain, cloudfrontDomain) {
700
- if (!dnsProvider) {
701
- console.log(`Skipping DNS record creation (DNS verification was skipped)`);
702
- return;
703
- }
704
- const parts = domain.split(".");
705
- const isApexDomain = parts.length === 2;
706
- if (isApexDomain) {
707
- if (dnsProvider instanceof Route53Provider) {
708
- console.log(`Creating Route53 alias record for apex domain ${domain} -> ${cloudfrontDomain}`);
709
- const result = await dnsProvider.createCloudFrontAlias({
710
- domain,
711
- name: domain,
712
- cloudFrontDomainName: cloudfrontDomain
713
- });
714
- if (!result.success) {
715
- console.warn(`Warning: Could not create Route53 alias record: ${result.message}`);
716
- console.warn(`Please manually create an A-record alias in Route53:`);
717
- console.warn(` ${domain} -> ${cloudfrontDomain}`);
718
- } else {
719
- console.log(`Created Route53 alias record: ${domain} -> ${cloudfrontDomain}`);
720
- }
721
- const wwwDomain = `www.${domain}`;
722
- console.log(`Creating Route53 alias record for ${wwwDomain} -> ${cloudfrontDomain}`);
723
- const wwwResult = await dnsProvider.createCloudFrontAlias({
724
- domain,
725
- name: wwwDomain,
726
- cloudFrontDomainName: cloudfrontDomain
727
- });
728
- if (!wwwResult.success) {
729
- console.warn(`Warning: Could not create www Route53 alias: ${wwwResult.message}`);
730
- console.warn(`Please manually create an A-record alias in Route53:`);
731
- console.warn(` ${wwwDomain} -> ${cloudfrontDomain}`);
732
- } else {
733
- console.log(`Created Route53 alias record: ${wwwDomain} -> ${cloudfrontDomain}`);
734
- }
735
- } else {
736
- try {
737
- const existingA = await dnsProvider.listRecords(domain, "A");
738
- if (existingA.success) {
739
- for (const rec of existingA.records) {
740
- const recSub = rec.name.replace(/\.$/, "");
741
- if (recSub === domain || recSub === "") {
742
- console.log(`Removing existing root A record (${rec.content}) to make room for ALIAS...`);
743
- await dnsProvider.deleteRecord(domain, {
744
- name: domain,
745
- type: "A",
746
- content: rec.content
747
- });
748
- }
749
- }
750
- }
751
- } catch (err) {
752
- console.log(`Note: Could not check for conflicting A records: ${err instanceof Error ? err.message : err}`);
753
- }
754
- console.log(`Creating ALIAS record for apex domain ${domain} -> ${cloudfrontDomain}`);
755
- const result = await dnsProvider.upsertRecord(domain, {
756
- name: domain,
757
- type: "ALIAS",
758
- content: cloudfrontDomain,
759
- ttl: 600
760
- });
761
- if (!result.success) {
762
- console.warn(`Warning: Could not create ALIAS record: ${result.message}`);
763
- console.warn(`Please manually create an ALIAS record in your DNS provider:`);
764
- console.warn(` ${domain} -> ${cloudfrontDomain}`);
765
- } else {
766
- console.log(`Created ALIAS record: ${domain} -> ${cloudfrontDomain}`);
767
- }
768
- const wwwDomain = `www.${domain}`;
769
- console.log(`Creating CNAME record for ${wwwDomain} -> ${cloudfrontDomain}`);
770
- const wwwResult = await dnsProvider.upsertRecord(domain, {
771
- name: wwwDomain,
772
- type: "CNAME",
773
- content: cloudfrontDomain,
774
- ttl: 600
775
- });
776
- if (!wwwResult.success) {
777
- console.warn(`Warning: Could not create www DNS record: ${wwwResult.message}`);
778
- console.warn(`Please manually create a CNAME record:`);
779
- console.warn(` ${wwwDomain} -> ${cloudfrontDomain}`);
780
- } else {
781
- console.log(`Created CNAME record: ${wwwDomain} -> ${cloudfrontDomain}`);
782
- }
783
- }
784
- } else {
785
- console.log(`Creating CNAME record for ${domain} -> ${cloudfrontDomain}`);
786
- const result = await dnsProvider.upsertRecord(domain, {
787
- name: domain,
788
- type: "CNAME",
789
- content: cloudfrontDomain,
790
- ttl: 600
791
- });
792
- if (!result.success) {
793
- console.warn(`Warning: Could not create DNS record: ${result.message}`);
794
- console.warn(`Please manually create a CNAME record:`);
795
- console.warn(` ${domain} -> ${cloudfrontDomain}`);
796
- } else {
797
- console.log(`Created CNAME record: ${domain} -> ${cloudfrontDomain}`);
798
- }
799
- }
800
- }
801
- async function deployStaticSiteWithExternalDnsFull(config) {
802
- const { sourceDir, cleanBucket = false, onProgress, ...siteConfig } = config;
803
- onProgress?.("infrastructure", "Deploying CloudFormation stack...");
804
- const infraResult = await deployStaticSiteWithExternalDns(siteConfig);
805
- if (!infraResult.success) {
806
- return infraResult;
807
- }
808
- if (cleanBucket) {
809
- onProgress?.("clean", "Cleaning old files from S3...");
810
- try {
811
- const s3 = new S3Client(siteConfig.region || "us-east-1");
812
- await s3.emptyBucket(infraResult.bucket);
813
- } catch (err) {
814
- console.log(`Note: Could not clean bucket: ${err.message}`);
815
- }
816
- }
817
- onProgress?.("upload", "Uploading files to S3...");
818
- const { uploadStaticFiles } = await import("./chunk-k4px0bnv.js");
819
- const uploadResult = await uploadStaticFiles({
820
- sourceDir,
821
- bucket: infraResult.bucket,
822
- region: siteConfig.region || "us-east-1",
823
- cacheControl: siteConfig.cacheControl,
824
- onProgress: (uploaded, total, file) => {
825
- onProgress?.("upload", `${uploaded}/${total}: ${file}`);
826
- }
827
- });
828
- if (uploadResult.errors.length > 0) {
829
- return {
830
- ...infraResult,
831
- success: false,
832
- message: `Upload errors: ${uploadResult.errors.join(", ")}`,
833
- filesUploaded: uploadResult.uploaded
834
- };
835
- }
836
- if (infraResult.distributionId && uploadResult.uploaded > 0) {
837
- onProgress?.("invalidate", "Invalidating CloudFront cache...");
838
- const { invalidateCache } = await import("./chunk-k4px0bnv.js");
839
- await invalidateCache(infraResult.distributionId);
840
- }
841
- onProgress?.("complete", "Deployment complete!");
842
- const message = uploadResult.skipped > 0 ? `Deployed ${uploadResult.uploaded} files (${uploadResult.skipped} unchanged) with external DNS` : `Deployed ${uploadResult.uploaded} files successfully with external DNS`;
843
- return {
844
- ...infraResult,
845
- filesUploaded: uploadResult.uploaded,
846
- filesSkipped: uploadResult.skipped,
847
- message
848
- };
849
- }
850
-
851
- // src/deploy/static-site.ts
852
- function generateStaticSiteTemplate(config) {
853
- const {
854
- bucketName,
855
- domain,
856
- certificateArn,
857
- hostedZoneId,
858
- defaultRootObject = "index.html",
859
- errorDocument = "404.html"
860
- } = config;
861
- const resources = {};
862
- const outputs = {};
863
- resources.S3Bucket = {
864
- Type: "AWS::S3::Bucket",
865
- Properties: {
866
- BucketName: bucketName,
867
- PublicAccessBlockConfiguration: {
868
- BlockPublicAcls: true,
869
- BlockPublicPolicy: false,
870
- IgnorePublicAcls: true,
871
- RestrictPublicBuckets: false
872
- },
873
- WebsiteConfiguration: {
874
- IndexDocument: defaultRootObject,
875
- ErrorDocument: errorDocument
876
- }
877
- }
878
- };
879
- outputs.BucketName = {
880
- Description: "S3 Bucket Name",
881
- Value: { Ref: "S3Bucket" }
882
- };
883
- outputs.BucketArn = {
884
- Description: "S3 Bucket ARN",
885
- Value: { "Fn::GetAtt": ["S3Bucket", "Arn"] }
886
- };
887
- resources.CloudFrontOAC = {
888
- Type: "AWS::CloudFront::OriginAccessControl",
889
- Properties: {
890
- OriginAccessControlConfig: {
891
- Name: `OAC-${bucketName}`,
892
- Description: `OAC for ${bucketName}`,
893
- OriginAccessControlOriginType: "s3",
894
- SigningBehavior: "always",
895
- SigningProtocol: "sigv4"
896
- }
897
- }
898
- };
899
- resources.UrlRewriteFunction = {
900
- Type: "AWS::CloudFront::Function",
901
- Properties: {
902
- Name: { "Fn::Sub": "${AWS::StackName}-url-rewrite" },
903
- AutoPublish: true,
904
- FunctionConfig: {
905
- Comment: "Append .html extension to URLs without extensions",
906
- Runtime: "cloudfront-js-2.0"
907
- },
908
- FunctionCode: `function handler(event) {
909
- const request = event.request;
910
- var uri = request.uri;
911
-
912
- // If URI ends with /, serve index.html
913
- if (uri.endsWith('/')) {
914
- request.uri = uri + 'index.html';
915
- }
916
- // If URI doesn't have an extension, append .html
917
- else if (!uri.includes('.')) {
918
- request.uri = uri + '.html';
919
- }
920
-
921
- return request;
922
- }`
923
- }
924
- };
925
- const distributionConfig = {
926
- Enabled: true,
927
- DefaultRootObject: defaultRootObject,
928
- HttpVersion: "http2and3",
929
- IPV6Enabled: true,
930
- PriceClass: "PriceClass_100",
931
- Origins: [
932
- {
933
- Id: `S3-${bucketName}`,
934
- DomainName: { "Fn::GetAtt": ["S3Bucket", "RegionalDomainName"] },
935
- S3OriginConfig: {
936
- OriginAccessIdentity: ""
937
- },
938
- OriginAccessControlId: { "Fn::GetAtt": ["CloudFrontOAC", "Id"] }
939
- }
940
- ],
941
- DefaultCacheBehavior: {
942
- TargetOriginId: `S3-${bucketName}`,
943
- ViewerProtocolPolicy: "redirect-to-https",
944
- AllowedMethods: ["GET", "HEAD"],
945
- CachedMethods: ["GET", "HEAD"],
946
- Compress: true,
947
- CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
948
- FunctionAssociations: [
949
- {
950
- EventType: "viewer-request",
951
- FunctionARN: { "Fn::GetAtt": ["UrlRewriteFunction", "FunctionARN"] }
952
- }
953
- ]
954
- },
955
- CustomErrorResponses: [
956
- {
957
- ErrorCode: 403,
958
- ResponseCode: 200,
959
- ResponsePagePath: `/${defaultRootObject}`,
960
- ErrorCachingMinTTL: 300
961
- },
962
- {
963
- ErrorCode: 404,
964
- ResponseCode: 404,
965
- ResponsePagePath: `/${errorDocument}`,
966
- ErrorCachingMinTTL: 300
967
- }
968
- ]
969
- };
970
- if (domain && certificateArn) {
971
- distributionConfig.Aliases = [domain];
972
- distributionConfig.ViewerCertificate = {
973
- AcmCertificateArn: certificateArn,
974
- SslSupportMethod: "sni-only",
975
- MinimumProtocolVersion: "TLSv1.2_2021"
976
- };
977
- } else {
978
- distributionConfig.ViewerCertificate = {
979
- CloudFrontDefaultCertificate: true
980
- };
981
- }
982
- resources.CloudFrontDistribution = {
983
- Type: "AWS::CloudFront::Distribution",
984
- DependsOn: ["S3Bucket", "CloudFrontOAC", "UrlRewriteFunction"],
985
- Properties: {
986
- DistributionConfig: distributionConfig
987
- }
988
- };
989
- outputs.DistributionId = {
990
- Description: "CloudFront Distribution ID",
991
- Value: { Ref: "CloudFrontDistribution" }
992
- };
993
- outputs.DistributionDomain = {
994
- Description: "CloudFront Distribution Domain",
995
- Value: { "Fn::GetAtt": ["CloudFrontDistribution", "DomainName"] }
996
- };
997
- resources.S3BucketPolicy = {
998
- Type: "AWS::S3::BucketPolicy",
999
- DependsOn: ["S3Bucket", "CloudFrontDistribution"],
1000
- Properties: {
1001
- Bucket: { Ref: "S3Bucket" },
1002
- PolicyDocument: {
1003
- Version: "2012-10-17",
1004
- Statement: [
1005
- {
1006
- Sid: "AllowCloudFrontServicePrincipal",
1007
- Effect: "Allow",
1008
- Principal: {
1009
- Service: "cloudfront.amazonaws.com"
1010
- },
1011
- Action: "s3:GetObject",
1012
- Resource: { "Fn::Sub": "arn:aws:s3:::${S3Bucket}/*" },
1013
- Condition: {
1014
- StringEquals: {
1015
- "AWS:SourceArn": {
1016
- "Fn::Sub": "arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}"
1017
- }
1018
- }
1019
- }
1020
- }
1021
- ]
1022
- }
1023
- }
1024
- };
1025
- if (domain && hostedZoneId) {
1026
- resources.DNSRecord = {
1027
- Type: "AWS::Route53::RecordSet",
1028
- DependsOn: "CloudFrontDistribution",
1029
- Properties: {
1030
- HostedZoneId: hostedZoneId,
1031
- Name: domain,
1032
- Type: "A",
1033
- AliasTarget: {
1034
- DNSName: { "Fn::GetAtt": ["CloudFrontDistribution", "DomainName"] },
1035
- HostedZoneId: "Z2FDTNDATAQYW2",
1036
- EvaluateTargetHealth: false
1037
- }
1038
- }
1039
- };
1040
- resources.DNSRecordIPv6 = {
1041
- Type: "AWS::Route53::RecordSet",
1042
- DependsOn: "CloudFrontDistribution",
1043
- Properties: {
1044
- HostedZoneId: hostedZoneId,
1045
- Name: domain,
1046
- Type: "AAAA",
1047
- AliasTarget: {
1048
- DNSName: { "Fn::GetAtt": ["CloudFrontDistribution", "DomainName"] },
1049
- HostedZoneId: "Z2FDTNDATAQYW2",
1050
- EvaluateTargetHealth: false
1051
- }
1052
- }
1053
- };
1054
- outputs.SiteUrl = {
1055
- Description: "Site URL",
1056
- Value: { "Fn::Sub": "https://${DNSRecord}" }
1057
- };
1058
- }
1059
- return {
1060
- AWSTemplateFormatVersion: "2010-09-09",
1061
- Description: `Static site infrastructure for ${domain || bucketName}`,
1062
- Resources: resources,
1063
- Outputs: outputs
1064
- };
1065
- }
1066
- async function deployStaticSite(config) {
1067
- if (config.dnsProvider && config.dnsProvider.provider !== "route53") {
1068
- const domain2 = config.domain || (config.subdomain && config.baseDomain ? `${config.subdomain}.${config.baseDomain}` : undefined);
1069
- if (!domain2) {
1070
- return {
1071
- success: false,
1072
- stackName: config.stackName || `${config.siteName}-static-site`,
1073
- bucket: config.bucket || `${config.siteName}-${Date.now()}`,
1074
- message: "Domain is required when using external DNS provider"
1075
- };
1076
- }
1077
- return deployStaticSiteWithExternalDns({
1078
- siteName: config.siteName,
1079
- region: config.region,
1080
- domain: domain2,
1081
- bucket: config.bucket,
1082
- certificateArn: config.certificateArn,
1083
- stackName: config.stackName,
1084
- defaultRootObject: config.defaultRootObject,
1085
- errorDocument: config.errorDocument,
1086
- cacheControl: config.cacheControl,
1087
- tags: config.tags,
1088
- dnsProvider: config.dnsProvider
1089
- });
1090
- }
1091
- const region = config.region || "us-east-1";
1092
- const cfRegion = "us-east-1";
1093
- let domain;
1094
- if (config.domain) {
1095
- domain = config.domain;
1096
- } else if (config.subdomain && config.baseDomain) {
1097
- domain = `${config.subdomain}.${config.baseDomain}`;
1098
- }
1099
- const bucket = config.bucket || (domain ? domain.replace(/\./g, "-") : `${config.siteName}-${Date.now()}`);
1100
- const stackName = config.stackName || `${config.siteName}-static-site`;
1101
- const cf = new CloudFormationClient(cfRegion);
1102
- const route53 = new Route53Client;
1103
- const acm = new ACMClient("us-east-1");
1104
- const acmValidator = new ACMDnsValidator("us-east-1");
1105
- let hostedZoneId = config.hostedZoneId;
1106
- let certificateArn = config.certificateArn;
1107
- if (domain && !hostedZoneId) {
1108
- const zone = await route53.findHostedZoneForDomain(domain);
1109
- if (zone) {
1110
- hostedZoneId = zone.Id.replace("/hostedzone/", "");
1111
- } else {
1112
- return {
1113
- success: false,
1114
- stackName,
1115
- bucket,
1116
- message: `No Route53 hosted zone found for ${config.baseDomain || domain}. Please create one first.`
1117
- };
1118
- }
1119
- }
1120
- if (domain && !certificateArn && hostedZoneId) {
1121
- const existingCert = await acm.findCertificateByDomain(domain);
1122
- if (existingCert && existingCert.Status === "ISSUED") {
1123
- certificateArn = existingCert.CertificateArn;
1124
- } else {
1125
- const certResult = await acmValidator.requestAndValidate({
1126
- domainName: domain,
1127
- hostedZoneId,
1128
- waitForValidation: true,
1129
- maxWaitMinutes: 10
1130
- });
1131
- certificateArn = certResult.certificateArn;
1132
- }
1133
- }
1134
- let stackExists = false;
1135
- let existingBucketName;
1136
- try {
1137
- const existingStacks = await cf.describeStacks({ stackName });
1138
- if (existingStacks.Stacks.length > 0) {
1139
- const stack = existingStacks.Stacks[0];
1140
- const stackStatus = stack.StackStatus;
1141
- if (stackStatus === "DELETE_IN_PROGRESS") {
1142
- console.log("Previous stack is still being deleted, waiting...");
1143
- await cf.waitForStack(stackName, "stack-delete-complete");
1144
- stackExists = false;
1145
- } else if (stackStatus === "DELETE_COMPLETE") {
1146
- stackExists = false;
1147
- } else {
1148
- stackExists = true;
1149
- const outputs2 = stack.Outputs || [];
1150
- existingBucketName = outputs2.find((o) => o.OutputKey === "BucketName")?.OutputValue;
1151
- }
1152
- }
1153
- } catch (err) {
1154
- if (err.message?.includes("does not exist") || err.code === "ValidationError") {
1155
- stackExists = false;
1156
- } else {
1157
- throw err;
1158
- }
1159
- }
1160
- let finalBucket = existingBucketName || bucket;
1161
- if (!stackExists) {
1162
- const s3 = new S3Client(region);
1163
- const cloudfront = new CloudFrontClient;
1164
- let hasExistingDistribution = false;
1165
- if (domain) {
1166
- try {
1167
- console.log(`Checking for existing CloudFront distribution for ${domain}...`);
1168
- const distributions = await cloudfront.listDistributions();
1169
- for (const dist of distributions) {
1170
- let aliases = [];
1171
- if (dist.Aliases?.Items) {
1172
- if (Array.isArray(dist.Aliases.Items)) {
1173
- aliases = dist.Aliases.Items;
1174
- } else if (typeof dist.Aliases.Items === "object") {
1175
- const cname = dist.Aliases.Items.CNAME;
1176
- if (typeof cname === "string") {
1177
- aliases = [cname];
1178
- } else if (Array.isArray(cname)) {
1179
- aliases = cname;
1180
- }
1181
- }
1182
- }
1183
- if (aliases.includes(domain)) {
1184
- hasExistingDistribution = true;
1185
- console.log(`Found existing CloudFront distribution ${dist.Id} for ${domain}`);
1186
- const distConfig = await cloudfront.getDistributionConfig(dist.Id);
1187
- const originsData = distConfig.DistributionConfig?.Origins?.Items;
1188
- let originBucket;
1189
- if (originsData) {
1190
- let originList = [];
1191
- if (Array.isArray(originsData)) {
1192
- originList = originsData;
1193
- } else if (originsData.Origin) {
1194
- originList = Array.isArray(originsData.Origin) ? originsData.Origin : [originsData.Origin];
1195
- } else {
1196
- originList = [originsData];
1197
- }
1198
- for (const origin of originList) {
1199
- const domainName = origin.DomainName || "";
1200
- const s3Match = domainName.match(/^([^.]+)\.s3[\.-]/);
1201
- if (s3Match) {
1202
- originBucket = s3Match[1];
1203
- break;
1204
- }
1205
- }
1206
- }
1207
- if (originBucket) {
1208
- const expectedBucketPrefix = domain.replace(/\./g, "-");
1209
- if (!originBucket.startsWith(expectedBucketPrefix) && !originBucket.includes(config.siteName)) {
1210
- console.log(`Warning: Found distribution with mismatched bucket ${originBucket}, skipping...`);
1211
- continue;
1212
- }
1213
- console.log(`Using existing S3 bucket: ${originBucket}`);
1214
- if (hostedZoneId && dist.DomainName) {
1215
- try {
1216
- console.log(`Ensuring Route53 records exist for ${domain}...`);
1217
- await route53.createAliasRecord({
1218
- HostedZoneId: hostedZoneId,
1219
- Name: domain,
1220
- Type: "A",
1221
- TargetHostedZoneId: Route53Client.CloudFrontHostedZoneId,
1222
- TargetDNSName: dist.DomainName,
1223
- EvaluateTargetHealth: false
1224
- });
1225
- await route53.createAliasRecord({
1226
- HostedZoneId: hostedZoneId,
1227
- Name: domain,
1228
- Type: "AAAA",
1229
- TargetHostedZoneId: Route53Client.CloudFrontHostedZoneId,
1230
- TargetDNSName: dist.DomainName,
1231
- EvaluateTargetHealth: false
1232
- });
1233
- console.log(`Route53 records ensured for ${domain}`);
1234
- } catch (dnsErr) {
1235
- console.log(`Note: Could not update Route53 records: ${dnsErr.message}`);
1236
- }
1237
- }
1238
- return {
1239
- success: true,
1240
- stackName: `existing-${dist.Id}`,
1241
- bucket: originBucket,
1242
- distributionId: dist.Id,
1243
- distributionDomain: dist.DomainName,
1244
- domain,
1245
- certificateArn,
1246
- message: "Using existing CloudFront distribution"
1247
- };
1248
- }
1249
- }
1250
- }
1251
- } catch {}
1252
- if (hostedZoneId) {
1253
- try {
1254
- const recordsResult = await route53.listResourceRecordSets({ HostedZoneId: hostedZoneId });
1255
- const records = recordsResult.ResourceRecordSets || [];
1256
- for (const record of records) {
1257
- if (record.Name === `${domain}.` && (record.Type === "A" || record.Type === "AAAA")) {
1258
- if (record.AliasTarget) {
1259
- console.log(`Found orphaned Route53 ${record.Type} record for ${domain}, cleaning up...`);
1260
- try {
1261
- await route53.deleteRecord({
1262
- HostedZoneId: hostedZoneId,
1263
- RecordSet: record
1264
- });
1265
- console.log(`Deleted orphaned Route53 ${record.Type} record for ${domain}`);
1266
- } catch (recordErr) {
1267
- console.log(`Note: Could not delete Route53 record: ${recordErr.message}`);
1268
- }
1269
- }
1270
- }
1271
- }
1272
- } catch {}
1273
- }
1274
- }
1275
- if (!hasExistingDistribution) {
1276
- try {
1277
- const headResult = await s3.headBucket(bucket);
1278
- if (headResult.exists) {
1279
- console.log(`Found orphaned S3 bucket ${bucket}, cleaning up...`);
1280
- try {
1281
- const cleanupPromise = s3.emptyBucket(bucket).then(() => s3.deleteBucket(bucket));
1282
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Bucket cleanup timeout")), 30000));
1283
- await Promise.race([cleanupPromise, timeoutPromise]);
1284
- console.log(`Deleted orphaned S3 bucket ${bucket}`);
1285
- } catch (cleanupErr) {
1286
- console.log(`Note: Could not clean up S3 bucket: ${cleanupErr.message}`);
1287
- const suffix = Date.now().toString(36);
1288
- finalBucket = `${bucket}-${suffix}`;
1289
- console.log(`Using alternative bucket name: ${finalBucket}`);
1290
- }
1291
- }
1292
- } catch {}
1293
- }
1294
- }
1295
- const template = generateStaticSiteTemplate({
1296
- bucketName: finalBucket,
1297
- domain,
1298
- certificateArn,
1299
- hostedZoneId,
1300
- defaultRootObject: config.defaultRootObject,
1301
- errorDocument: config.errorDocument
1302
- });
1303
- const tags = Object.entries(config.tags || {}).map(([Key, Value]) => ({ Key, Value }));
1304
- tags.push({ Key: "ManagedBy", Value: "ts-cloud" });
1305
- tags.push({ Key: "Application", Value: config.siteName });
1306
- let stackId;
1307
- let isUpdate = false;
1308
- if (stackExists) {
1309
- isUpdate = true;
1310
- console.log(`Updating CloudFormation stack: ${stackName}`);
1311
- console.log(`Using existing bucket: ${finalBucket}`);
1312
- console.log(`Domain: ${domain || "not specified"}`);
1313
- console.log(`Certificate ARN: ${certificateArn || "not specified"}`);
1314
- try {
1315
- const result = await cf.updateStack({
1316
- stackName,
1317
- templateBody: JSON.stringify(template),
1318
- capabilities: ["CAPABILITY_IAM"],
1319
- tags
1320
- });
1321
- stackId = result.StackId;
1322
- console.log(`Update initiated, stack ID: ${stackId}`);
1323
- } catch (err) {
1324
- if (err.message?.includes("No updates are to be performed")) {
1325
- const stacks2 = await cf.describeStacks({ stackName });
1326
- stackId = stacks2.Stacks[0].StackId;
1327
- const outputs2 = stacks2.Stacks[0]?.Outputs || [];
1328
- const getOutput2 = (key) => outputs2.find((o) => o.OutputKey === key)?.OutputValue;
1329
- return {
1330
- success: true,
1331
- stackId,
1332
- stackName,
1333
- bucket: getOutput2("BucketName") || finalBucket,
1334
- distributionId: getOutput2("DistributionId"),
1335
- distributionDomain: getOutput2("DistributionDomain"),
1336
- domain,
1337
- certificateArn,
1338
- message: "Static site infrastructure is already up to date"
1339
- };
1340
- } else {
1341
- throw err;
1342
- }
1343
- }
1344
- } else {
1345
- console.log(`Creating CloudFormation stack: ${stackName}`);
1346
- console.log(`Bucket name: ${finalBucket}`);
1347
- console.log(`Domain: ${domain || "not specified"}`);
1348
- console.log(`Certificate ARN: ${certificateArn || "not specified"}`);
1349
- console.log("Stack does not exist, creating...");
1350
- const result = await cf.createStack({
1351
- stackName,
1352
- templateBody: JSON.stringify(template),
1353
- capabilities: ["CAPABILITY_IAM"],
1354
- tags,
1355
- onFailure: "DELETE"
1356
- });
1357
- stackId = result.StackId;
1358
- console.log(`Create initiated, stack ID: ${stackId}`);
1359
- }
1360
- console.log(`Waiting for stack to reach ${isUpdate ? "stack-update-complete" : "stack-create-complete"}...`);
1361
- try {
1362
- await cf.waitForStack(stackName, isUpdate ? "stack-update-complete" : "stack-create-complete");
1363
- console.log("Stack operation completed successfully!");
1364
- } catch (err) {
1365
- if (err.message?.includes("must be verified") || err.message?.includes("Access denied for operation") || err.message?.includes("failed")) {
1366
- console.log("CloudFormation deployment failed, trying direct API creation...");
1367
- const cloudfront = new CloudFrontClient;
1368
- if (domain) {
1369
- try {
1370
- const distributions = await cloudfront.listDistributions();
1371
- for (const dist of distributions) {
1372
- let aliases = [];
1373
- if (dist.Aliases?.Items) {
1374
- if (Array.isArray(dist.Aliases.Items)) {
1375
- aliases = dist.Aliases.Items;
1376
- } else if (typeof dist.Aliases.Items === "object") {
1377
- const cname = dist.Aliases.Items.CNAME;
1378
- if (typeof cname === "string") {
1379
- aliases = [cname];
1380
- } else if (Array.isArray(cname)) {
1381
- aliases = cname;
1382
- }
1383
- }
1384
- }
1385
- if (aliases.includes(domain)) {
1386
- console.log(`Found existing CloudFront distribution ${dist.Id} with alias ${domain}`);
1387
- const distConfig = await cloudfront.getDistributionConfig(dist.Id);
1388
- const originsData = distConfig.DistributionConfig?.Origins?.Items;
1389
- let originBucket;
1390
- if (originsData) {
1391
- let originList = [];
1392
- if (Array.isArray(originsData)) {
1393
- originList = originsData;
1394
- } else if (originsData.Origin) {
1395
- originList = Array.isArray(originsData.Origin) ? originsData.Origin : [originsData.Origin];
1396
- } else {
1397
- originList = [originsData];
1398
- }
1399
- for (const origin of originList) {
1400
- const domainName = origin.DomainName || "";
1401
- const s3Match = domainName.match(/^([^.]+)\.s3[\.-]/);
1402
- if (s3Match) {
1403
- originBucket = s3Match[1];
1404
- break;
1405
- }
1406
- }
1407
- }
1408
- if (originBucket) {
1409
- console.log(`Using existing S3 bucket: ${originBucket}`);
1410
- return {
1411
- success: true,
1412
- stackName: `existing-${dist.Id}`,
1413
- bucket: originBucket,
1414
- distributionId: dist.Id,
1415
- distributionDomain: dist.DomainName,
1416
- domain,
1417
- certificateArn,
1418
- message: "Using existing CloudFront distribution (account verification pending for new distributions)"
1419
- };
1420
- }
1421
- }
1422
- }
1423
- } catch {}
1424
- }
1425
- console.log("No existing infrastructure found, creating via direct API calls...");
1426
- try {
1427
- const s3Direct = new S3Client(region);
1428
- const bucketExists = await s3Direct.headBucket(finalBucket);
1429
- if (bucketExists.exists) {
1430
- console.log(`Using existing S3 bucket: ${finalBucket}`);
1431
- } else {
1432
- console.log(`Creating S3 bucket: ${finalBucket}...`);
1433
- await s3Direct.createBucket(finalBucket);
1434
- }
1435
- await s3Direct.putBucketWebsite(finalBucket, {
1436
- IndexDocument: config.defaultRootObject || "index.html",
1437
- ErrorDocument: config.errorDocument || "404.html"
1438
- });
1439
- await s3Direct.putPublicAccessBlock(finalBucket, {
1440
- BlockPublicAcls: true,
1441
- IgnorePublicAcls: true,
1442
- BlockPublicPolicy: false,
1443
- RestrictPublicBuckets: false
1444
- });
1445
- console.log(`S3 bucket ${finalBucket} configured`);
1446
- const oacName = `OAC-${finalBucket}`;
1447
- console.log(`Creating Origin Access Control: ${oacName}...`);
1448
- const oac = await cloudfront.findOrCreateOriginAccessControl(oacName);
1449
- console.log(`Origin Access Control ${oac.Id} ready`);
1450
- console.log(`Creating CloudFront distribution...`);
1451
- const distResult = await cloudfront.createDistributionForS3({
1452
- bucketName: finalBucket,
1453
- bucketRegion: region,
1454
- originAccessControlId: oac.Id,
1455
- aliases: domain ? [domain] : [],
1456
- certificateArn,
1457
- defaultRootObject: config.defaultRootObject || "index.html",
1458
- comment: `Distribution for ${domain || finalBucket}`
1459
- });
1460
- console.log(`CloudFront distribution ${distResult.Id} created`);
1461
- console.log(`Updating S3 bucket policy...`);
1462
- const bucketPolicy = CloudFrontClient.getS3BucketPolicyForCloudFront(finalBucket, distResult.ARN);
1463
- await s3Direct.putBucketPolicy(finalBucket, bucketPolicy);
1464
- console.log(`S3 bucket policy updated`);
1465
- if (domain && hostedZoneId) {
1466
- console.log(`Creating Route53 records for ${domain}...`);
1467
- try {
1468
- await route53.createAliasRecord({
1469
- HostedZoneId: hostedZoneId,
1470
- Name: domain,
1471
- Type: "A",
1472
- TargetHostedZoneId: Route53Client.CloudFrontHostedZoneId,
1473
- TargetDNSName: distResult.DomainName,
1474
- EvaluateTargetHealth: false
1475
- });
1476
- await route53.createAliasRecord({
1477
- HostedZoneId: hostedZoneId,
1478
- Name: domain,
1479
- Type: "AAAA",
1480
- TargetHostedZoneId: Route53Client.CloudFrontHostedZoneId,
1481
- TargetDNSName: distResult.DomainName,
1482
- EvaluateTargetHealth: false
1483
- });
1484
- console.log(`Route53 records created for ${domain}`);
1485
- } catch (dnsErr) {
1486
- console.log(`Note: Could not create Route53 records: ${dnsErr.message}`);
1487
- }
1488
- }
1489
- return {
1490
- success: true,
1491
- stackName: `direct-${distResult.Id}`,
1492
- bucket: finalBucket,
1493
- distributionId: distResult.Id,
1494
- distributionDomain: distResult.DomainName,
1495
- domain,
1496
- certificateArn,
1497
- message: "Static site infrastructure created via direct API calls"
1498
- };
1499
- } catch (directErr) {
1500
- console.log(`Direct API creation failed: ${directErr.message}`);
1501
- return {
1502
- success: false,
1503
- stackId,
1504
- stackName,
1505
- bucket: finalBucket,
1506
- message: `Deployment failed: ${directErr.message}`
1507
- };
1508
- }
1509
- }
1510
- return {
1511
- success: false,
1512
- stackId,
1513
- stackName,
1514
- bucket: finalBucket,
1515
- message: `Stack deployment failed: ${err.message}`
1516
- };
1517
- }
1518
- const stacks = await cf.describeStacks({ stackName });
1519
- const outputs = stacks.Stacks[0]?.Outputs || [];
1520
- const getOutput = (key) => outputs.find((o) => o.OutputKey === key)?.OutputValue;
1521
- return {
1522
- success: true,
1523
- stackId,
1524
- stackName,
1525
- bucket: getOutput("BucketName") || finalBucket,
1526
- distributionId: getOutput("DistributionId"),
1527
- distributionDomain: getOutput("DistributionDomain"),
1528
- domain,
1529
- certificateArn,
1530
- message: "Static site infrastructure deployed successfully"
1531
- };
1532
- }
1533
- async function uploadStaticFiles(options) {
1534
- const { sourceDir, bucket, region, cacheControl = "max-age=31536000, public", onProgress } = options;
1535
- const s3 = new S3Client(region);
1536
- const { readdir } = await import("node:fs/promises");
1537
- const { join, relative } = await import("node:path");
1538
- const { createHash } = await import("node:crypto");
1539
- async function listFiles(dir) {
1540
- const files2 = [];
1541
- const entries = await readdir(dir, { withFileTypes: true });
1542
- for (const entry of entries) {
1543
- const fullPath = join(dir, entry.name);
1544
- if (entry.isDirectory()) {
1545
- files2.push(...await listFiles(fullPath));
1546
- } else {
1547
- files2.push(fullPath);
1548
- }
1549
- }
1550
- return files2;
1551
- }
1552
- function getContentType(filePath) {
1553
- const ext = filePath.split(".").pop()?.toLowerCase();
1554
- const types = {
1555
- html: "text/html; charset=utf-8",
1556
- css: "text/css; charset=utf-8",
1557
- js: "application/javascript; charset=utf-8",
1558
- json: "application/json; charset=utf-8",
1559
- png: "image/png",
1560
- jpg: "image/jpeg",
1561
- jpeg: "image/jpeg",
1562
- gif: "image/gif",
1563
- svg: "image/svg+xml",
1564
- ico: "image/x-icon",
1565
- webp: "image/webp",
1566
- woff: "font/woff",
1567
- woff2: "font/woff2",
1568
- ttf: "font/ttf",
1569
- xml: "application/xml",
1570
- txt: "text/plain; charset=utf-8",
1571
- sh: "text/plain; charset=utf-8",
1572
- bash: "text/plain; charset=utf-8",
1573
- ps1: "text/plain; charset=utf-8"
1574
- };
1575
- return types[ext || ""] || "application/octet-stream";
1576
- }
1577
- function computeMD5(content) {
1578
- return createHash("md5").update(content).digest("hex");
1579
- }
1580
- async function getExistingETags() {
1581
- const etagMap = new Map;
1582
- let continuationToken;
1583
- do {
1584
- const result = await s3.listObjects({
1585
- bucket,
1586
- maxKeys: 1000,
1587
- continuationToken
1588
- });
1589
- for (const obj of result.objects) {
1590
- const etag = obj.ETag?.replace(/"/g, "") || "";
1591
- etagMap.set(obj.Key, etag);
1592
- }
1593
- continuationToken = result.nextContinuationToken;
1594
- } while (continuationToken);
1595
- return etagMap;
1596
- }
1597
- const files = await listFiles(sourceDir);
1598
- const errors = [];
1599
- let uploaded = 0;
1600
- let skipped = 0;
1601
- const existingETags = await getExistingETags();
1602
- for (const file of files) {
1603
- const key = relative(sourceDir, file);
1604
- const contentType = getContentType(file);
1605
- const fileCacheControl = file.endsWith(".html") ? "max-age=3600, public" : cacheControl;
1606
- try {
1607
- const content = Buffer.from(await Bun.file(file).arrayBuffer());
1608
- const localMD5 = computeMD5(content);
1609
- const existingETag = existingETags.get(key);
1610
- if (existingETag && existingETag === localMD5) {
1611
- skipped++;
1612
- onProgress?.(uploaded + skipped, files.length, key);
1613
- continue;
1614
- }
1615
- await s3.putObject({
1616
- bucket,
1617
- key,
1618
- body: content,
1619
- contentType,
1620
- cacheControl: fileCacheControl
1621
- });
1622
- uploaded++;
1623
- onProgress?.(uploaded + skipped, files.length, key);
1624
- } catch (err) {
1625
- errors.push(`Failed to upload ${key}: ${err.message}`);
1626
- }
1627
- }
1628
- return { uploaded, skipped, errors };
1629
- }
1630
- async function invalidateCache(distributionId) {
1631
- const cloudfront = new CloudFrontClient;
1632
- const result = await cloudfront.invalidateAll(distributionId);
1633
- return { invalidationId: result.Id };
1634
- }
1635
- async function deleteStaticSite(stackName, region = "us-east-1") {
1636
- const cf = new CloudFormationClient(region);
1637
- try {
1638
- const stacks = await cf.describeStacks({ stackName });
1639
- const outputs = stacks.Stacks[0]?.Outputs || [];
1640
- const bucketName = outputs.find((o) => o.OutputKey === "BucketName")?.OutputValue;
1641
- if (bucketName) {
1642
- const s3 = new S3Client(region);
1643
- await s3.emptyBucket(bucketName);
1644
- }
1645
- } catch {}
1646
- await cf.deleteStack(stackName);
1647
- const result = await cf.waitForStackComplete(stackName, 60, 1e4);
1648
- return {
1649
- success: result.success || result.status === "DELETE_COMPLETE",
1650
- message: result.success ? "Static site deleted successfully" : `Deletion failed: ${result.status}`
1651
- };
1652
- }
1653
- async function deployStaticSiteFull(config) {
1654
- const { sourceDir, cleanBucket = false, onProgress, ...siteConfig } = config;
1655
- onProgress?.("infrastructure", "Deploying CloudFormation stack...");
1656
- const infraResult = await deployStaticSite(siteConfig);
1657
- if (!infraResult.success) {
1658
- return infraResult;
1659
- }
1660
- if (cleanBucket) {
1661
- onProgress?.("clean", "Cleaning old files from S3...");
1662
- try {
1663
- const s3 = new S3Client(siteConfig.region || "us-east-1");
1664
- await s3.emptyBucket(infraResult.bucket);
1665
- } catch (err) {
1666
- console.log(`Note: Could not clean bucket: ${err.message}`);
1667
- }
1668
- }
1669
- onProgress?.("upload", "Uploading files to S3...");
1670
- const uploadResult = await uploadStaticFiles({
1671
- sourceDir,
1672
- bucket: infraResult.bucket,
1673
- region: siteConfig.region || "us-east-1",
1674
- cacheControl: siteConfig.cacheControl,
1675
- onProgress: (uploaded, total, file) => {
1676
- onProgress?.("upload", `${uploaded}/${total}: ${file}`);
1677
- }
1678
- });
1679
- if (uploadResult.errors.length > 0) {
1680
- return {
1681
- ...infraResult,
1682
- success: false,
1683
- message: `Upload errors: ${uploadResult.errors.join(", ")}`,
1684
- filesUploaded: uploadResult.uploaded
1685
- };
1686
- }
1687
- if (infraResult.distributionId && uploadResult.uploaded > 0) {
1688
- onProgress?.("invalidate", "Invalidating CloudFront cache...");
1689
- await invalidateCache(infraResult.distributionId);
1690
- }
1691
- onProgress?.("complete", "Deployment complete!");
1692
- const message = uploadResult.skipped > 0 ? `Deployed ${uploadResult.uploaded} files (${uploadResult.skipped} unchanged)` : `Deployed ${uploadResult.uploaded} files successfully`;
1693
- return {
1694
- ...infraResult,
1695
- filesUploaded: uploadResult.uploaded,
1696
- filesSkipped: uploadResult.skipped,
1697
- message
1698
- };
1699
- }
1700
-
1701
- export { generateExternalDnsStaticSiteTemplate, deployStaticSiteWithExternalDns, deployStaticSiteWithExternalDnsFull, generateStaticSiteTemplate, deployStaticSite, uploadStaticFiles, invalidateCache, deleteStaticSite, deployStaticSiteFull };