@stacksjs/ts-cloud 0.7.4 → 0.7.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 (58) hide show
  1. package/dist/aws/index.js +259 -0
  2. package/dist/bin/cli.js +316 -316
  3. package/dist/chunk-0wxyppza.js +146 -0
  4. package/dist/chunk-3knnr7wh.js +601 -0
  5. package/dist/chunk-3rsfns7x.js +10427 -0
  6. package/dist/chunk-93hjhs78.js +1752 -0
  7. package/dist/chunk-arsh1g5h.js +1749 -0
  8. package/dist/chunk-b82pbxyp.js +1572 -0
  9. package/dist/chunk-c6rgvg1j.js +46124 -0
  10. package/dist/chunk-d2p5n2aq.js +23 -0
  11. package/dist/chunk-d7p84vz5.js +1699 -0
  12. package/dist/chunk-dpkk640m.js +4392 -0
  13. package/dist/chunk-eq08r166.js +8 -0
  14. package/dist/chunk-hsk6fe6x.js +371 -0
  15. package/dist/chunk-mj1tmhte.js +13 -0
  16. package/dist/chunk-p6309384.js +12828 -0
  17. package/dist/chunk-pegd7rmf.js +10 -0
  18. package/dist/chunk-pqfzdg68.js +12 -0
  19. package/dist/chunk-qpj3edwz.js +601 -0
  20. package/dist/chunk-tjjgajbh.js +1032 -0
  21. package/dist/chunk-tnztxpcb.js +630 -0
  22. package/dist/chunk-tt4kxske.js +1788 -0
  23. package/dist/chunk-v0bahtg2.js +4 -0
  24. package/dist/chunk-vd87cpvn.js +1754 -0
  25. package/dist/chunk-zn0nxxa8.js +1779 -0
  26. package/dist/deploy/index.js +87 -0
  27. package/dist/dns/index.js +31 -0
  28. package/dist/drivers/hetzner/provision.d.ts +53 -0
  29. package/dist/drivers/index.d.ts +5 -1
  30. package/dist/drivers/index.js +98 -0
  31. package/dist/drivers/shared/remote-exec.d.ts +47 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +4383 -91358
  34. package/dist/push/index.js +509 -0
  35. package/dist/ui/index.html +3 -3
  36. package/dist/ui/server/actions.html +3 -3
  37. package/dist/ui/server/backups.html +3 -3
  38. package/dist/ui/server/database.html +3 -3
  39. package/dist/ui/server/deployments.html +3 -3
  40. package/dist/ui/server/firewall.html +3 -3
  41. package/dist/ui/server/logs.html +3 -3
  42. package/dist/ui/server/services.html +3 -3
  43. package/dist/ui/server/sites.html +3 -3
  44. package/dist/ui/server/ssh-keys.html +3 -3
  45. package/dist/ui/server/terminal.html +3 -3
  46. package/dist/ui/server/workers.html +3 -3
  47. package/dist/ui/serverless/alarms.html +3 -3
  48. package/dist/ui/serverless/assets.html +3 -3
  49. package/dist/ui/serverless/data.html +3 -3
  50. package/dist/ui/serverless/deployments.html +3 -3
  51. package/dist/ui/serverless/functions.html +3 -3
  52. package/dist/ui/serverless/logs.html +3 -3
  53. package/dist/ui/serverless/queues.html +3 -3
  54. package/dist/ui/serverless/scheduler.html +3 -3
  55. package/dist/ui/serverless/secrets.html +3 -3
  56. package/dist/ui/serverless/traces.html +3 -3
  57. package/dist/ui/serverless.html +3 -3
  58. package/package.json +3 -3
@@ -0,0 +1,1032 @@
1
+ import {
2
+ AWSClient
3
+ } from "./chunk-arsh1g5h.js";
4
+
5
+ // src/aws/cloudfront.ts
6
+ class CloudFrontClient {
7
+ client;
8
+ constructor(profile) {
9
+ this.client = new AWSClient;
10
+ }
11
+ async createInvalidation(options) {
12
+ const callerReference = options.callerReference || Date.now().toString();
13
+ const invalidationBatchXml = `<?xml version="1.0" encoding="UTF-8"?>
14
+ <InvalidationBatch>
15
+ <Paths>
16
+ <Quantity>${options.paths.length}</Quantity>
17
+ <Items>
18
+ ${options.paths.map((path) => `<Path>${path}</Path>`).join(`
19
+ `)}
20
+ </Items>
21
+ </Paths>
22
+ <CallerReference>${callerReference}</CallerReference>
23
+ </InvalidationBatch>`;
24
+ const result = await this.client.request({
25
+ service: "cloudfront",
26
+ region: "us-east-1",
27
+ method: "POST",
28
+ path: `/2020-05-31/distribution/${options.distributionId}/invalidation`,
29
+ body: invalidationBatchXml,
30
+ headers: {
31
+ "Content-Type": "application/xml"
32
+ }
33
+ });
34
+ return {
35
+ Id: result.Id || result.Invalidation?.Id,
36
+ Status: result.Status || result.Invalidation?.Status || "InProgress",
37
+ CreateTime: result.CreateTime || result.Invalidation?.CreateTime || new Date().toISOString()
38
+ };
39
+ }
40
+ async getInvalidation(distributionId, invalidationId) {
41
+ const result = await this.client.request({
42
+ service: "cloudfront",
43
+ region: "us-east-1",
44
+ method: "GET",
45
+ path: `/2020-05-31/distribution/${distributionId}/invalidation/${invalidationId}`
46
+ });
47
+ return {
48
+ Id: result.Id || result.Invalidation?.Id,
49
+ Status: result.Status || result.Invalidation?.Status,
50
+ CreateTime: result.CreateTime || result.Invalidation?.CreateTime
51
+ };
52
+ }
53
+ async listInvalidations(distributionId) {
54
+ const result = await this.client.request({
55
+ service: "cloudfront",
56
+ region: "us-east-1",
57
+ method: "GET",
58
+ path: `/2020-05-31/distribution/${distributionId}/invalidation`
59
+ });
60
+ const invalidations = [];
61
+ if (result.InvalidationSummary) {
62
+ const summaries = Array.isArray(result.InvalidationSummary) ? result.InvalidationSummary : [result.InvalidationSummary];
63
+ invalidations.push(...summaries.map((item) => ({
64
+ Id: item.Id,
65
+ Status: item.Status,
66
+ CreateTime: item.CreateTime
67
+ })));
68
+ }
69
+ return invalidations;
70
+ }
71
+ async waitForInvalidation(distributionId, invalidationId) {
72
+ const maxAttempts = 60;
73
+ let attempts = 0;
74
+ while (attempts < maxAttempts) {
75
+ const invalidation = await this.getInvalidation(distributionId, invalidationId);
76
+ if (invalidation.Status === "Completed") {
77
+ return;
78
+ }
79
+ await new Promise((resolve) => setTimeout(resolve, 5000));
80
+ attempts++;
81
+ }
82
+ throw new Error(`Timeout waiting for invalidation ${invalidationId} to complete`);
83
+ }
84
+ async listDistributions() {
85
+ const result = await this.client.request({
86
+ service: "cloudfront",
87
+ region: "us-east-1",
88
+ method: "GET",
89
+ path: "/2020-05-31/distribution"
90
+ });
91
+ const distributions = [];
92
+ const distList = result.DistributionList || result;
93
+ const items = distList.Items;
94
+ let summaries = [];
95
+ if (Array.isArray(items)) {
96
+ summaries = items;
97
+ } else if (items?.DistributionSummary) {
98
+ summaries = Array.isArray(items.DistributionSummary) ? items.DistributionSummary : [items.DistributionSummary];
99
+ }
100
+ distributions.push(...summaries.map((item) => ({
101
+ Id: item.Id,
102
+ ARN: item.ARN,
103
+ Status: item.Status,
104
+ DomainName: item.DomainName,
105
+ Aliases: item.Aliases || undefined,
106
+ Enabled: item.Enabled === "true" || item.Enabled === true
107
+ })));
108
+ return distributions;
109
+ }
110
+ async getDistribution(distributionId) {
111
+ const result = await this.client.request({
112
+ service: "cloudfront",
113
+ region: "us-east-1",
114
+ method: "GET",
115
+ path: `/2020-05-31/distribution/${distributionId}`
116
+ });
117
+ const dist = result.Distribution || result;
118
+ return {
119
+ Id: dist.Id,
120
+ ARN: dist.ARN,
121
+ Status: dist.Status,
122
+ DomainName: dist.DomainName,
123
+ Aliases: dist.DistributionConfig?.Aliases?.Items || dist.Aliases?.Items || [],
124
+ Enabled: dist.DistributionConfig?.Enabled === "true" || dist.DistributionConfig?.Enabled === true
125
+ };
126
+ }
127
+ async getDistributionConfig(distributionId) {
128
+ const result = await this.client.request({
129
+ service: "cloudfront",
130
+ region: "us-east-1",
131
+ method: "GET",
132
+ path: `/2020-05-31/distribution/${distributionId}/config`
133
+ });
134
+ return {
135
+ ETag: result.ETag || "",
136
+ DistributionConfig: result.DistributionConfig || result
137
+ };
138
+ }
139
+ async invalidateAll(distributionId) {
140
+ return this.createInvalidation({
141
+ distributionId,
142
+ paths: ["/*"]
143
+ });
144
+ }
145
+ async invalidatePaths(distributionId, paths) {
146
+ const formattedPaths = paths.map((path) => path.startsWith("/") ? path : `/${path}`);
147
+ return this.createInvalidation({
148
+ distributionId,
149
+ paths: formattedPaths
150
+ });
151
+ }
152
+ async invalidatePattern(distributionId, pattern) {
153
+ const path = pattern.startsWith("/") ? pattern : `/${pattern}`;
154
+ return this.createInvalidation({
155
+ distributionId,
156
+ paths: [path]
157
+ });
158
+ }
159
+ async invalidateAfterDeployment(options) {
160
+ const { distributionId, changedPaths, invalidateAll = false, wait = false } = options;
161
+ let result;
162
+ if (invalidateAll || !changedPaths || changedPaths.length === 0) {
163
+ result = await this.invalidateAll(distributionId);
164
+ } else {
165
+ result = await this.invalidatePaths(distributionId, changedPaths);
166
+ }
167
+ if (wait) {
168
+ await this.waitForInvalidation(distributionId, result.Id);
169
+ }
170
+ return {
171
+ invalidationId: result.Id,
172
+ status: result.Status
173
+ };
174
+ }
175
+ async findDistributionByDomain(domain) {
176
+ const distributions = await this.listDistributions();
177
+ const found = distributions.find((dist) => {
178
+ if (dist.DomainName === domain) {
179
+ return true;
180
+ }
181
+ if (dist.Aliases?.Items && dist.Aliases.Items.includes(domain)) {
182
+ return true;
183
+ }
184
+ return false;
185
+ });
186
+ return found || null;
187
+ }
188
+ async batchInvalidate(distributionIds, paths = ["/*"]) {
189
+ const results = await Promise.all(distributionIds.map(async (distributionId) => {
190
+ const result = await this.createInvalidation({
191
+ distributionId,
192
+ paths
193
+ });
194
+ return {
195
+ distributionId,
196
+ invalidationId: result.Id,
197
+ status: result.Status
198
+ };
199
+ }));
200
+ return results;
201
+ }
202
+ async updateCustomErrorResponses(options) {
203
+ const { distributionId, customErrorResponses } = options;
204
+ const getResult = await this.client.request({
205
+ service: "cloudfront",
206
+ region: "us-east-1",
207
+ method: "GET",
208
+ path: `/2020-05-31/distribution/${distributionId}/config`,
209
+ returnHeaders: true
210
+ });
211
+ const etag = getResult.headers?.etag || getResult.headers?.ETag || "";
212
+ const currentConfig = getResult.body?.DistributionConfig || (getResult.body?.Enabled !== undefined ? getResult.body : undefined) || getResult.DistributionConfig;
213
+ if (!currentConfig || currentConfig.Enabled === undefined) {
214
+ throw new Error("Failed to get current distribution config");
215
+ }
216
+ if (customErrorResponses.length === 0) {
217
+ currentConfig.CustomErrorResponses = {
218
+ Quantity: 0
219
+ };
220
+ } else {
221
+ currentConfig.CustomErrorResponses = {
222
+ Quantity: customErrorResponses.length,
223
+ Items: {
224
+ CustomErrorResponse: customErrorResponses.map((err) => ({
225
+ ErrorCode: err.errorCode,
226
+ ...err.responsePagePath && { ResponsePagePath: err.responsePagePath },
227
+ ...err.responseCode && { ResponseCode: err.responseCode },
228
+ ...err.errorCachingMinTTL !== undefined && { ErrorCachingMinTTL: err.errorCachingMinTTL }
229
+ }))
230
+ }
231
+ };
232
+ }
233
+ const configXml = this.buildDistributionConfigXml(currentConfig);
234
+ const result = await this.client.request({
235
+ service: "cloudfront",
236
+ region: "us-east-1",
237
+ method: "PUT",
238
+ path: `/2020-05-31/distribution/${distributionId}/config`,
239
+ body: configXml,
240
+ headers: {
241
+ "Content-Type": "application/xml",
242
+ "If-Match": etag
243
+ }
244
+ });
245
+ const dist = result.Distribution || result;
246
+ return {
247
+ Distribution: {
248
+ Id: dist.Id,
249
+ ARN: dist.ARN,
250
+ Status: dist.Status,
251
+ DomainName: dist.DomainName,
252
+ Aliases: dist.DistributionConfig?.Aliases?.Items || [],
253
+ Enabled: dist.DistributionConfig?.Enabled === "true" || dist.DistributionConfig?.Enabled === true
254
+ },
255
+ ETag: result.ETag || ""
256
+ };
257
+ }
258
+ async removeCustomErrorResponses(distributionId) {
259
+ return this.updateCustomErrorResponses({
260
+ distributionId,
261
+ customErrorResponses: []
262
+ });
263
+ }
264
+ async updateDistribution(options) {
265
+ const { distributionId, aliases, certificateArn, comment } = options;
266
+ const getResult = await this.client.request({
267
+ service: "cloudfront",
268
+ region: "us-east-1",
269
+ method: "GET",
270
+ path: `/2020-05-31/distribution/${distributionId}/config`,
271
+ returnHeaders: true
272
+ });
273
+ const etag = getResult.headers?.etag || getResult.headers?.ETag || "";
274
+ const currentConfig = getResult.body?.DistributionConfig || (getResult.body?.Enabled !== undefined ? getResult.body : undefined) || getResult.DistributionConfig;
275
+ if (!currentConfig || currentConfig.Enabled === undefined) {
276
+ throw new Error("Failed to get current distribution config");
277
+ }
278
+ if (aliases && aliases.length > 0) {
279
+ currentConfig.Aliases = {
280
+ Quantity: aliases.length,
281
+ Items: { Item: aliases }
282
+ };
283
+ }
284
+ if (certificateArn) {
285
+ currentConfig.ViewerCertificate = {
286
+ ACMCertificateArn: certificateArn,
287
+ SSLSupportMethod: "sni-only",
288
+ MinimumProtocolVersion: "TLSv1.2_2021",
289
+ CertificateSource: "acm"
290
+ };
291
+ }
292
+ if (comment) {
293
+ currentConfig.Comment = comment;
294
+ }
295
+ const configXml = this.buildDistributionConfigXml(currentConfig);
296
+ const result = await this.client.request({
297
+ service: "cloudfront",
298
+ region: "us-east-1",
299
+ method: "PUT",
300
+ path: `/2020-05-31/distribution/${distributionId}/config`,
301
+ body: configXml,
302
+ headers: {
303
+ "Content-Type": "application/xml",
304
+ "If-Match": etag
305
+ }
306
+ });
307
+ const dist = result.Distribution || result;
308
+ return {
309
+ Distribution: {
310
+ Id: dist.Id,
311
+ ARN: dist.ARN,
312
+ Status: dist.Status,
313
+ DomainName: dist.DomainName,
314
+ Aliases: aliases ? { Quantity: aliases.length, Items: aliases } : { Quantity: 0, Items: [] },
315
+ Enabled: dist.DistributionConfig?.Enabled === "true" || dist.DistributionConfig?.Enabled === true
316
+ },
317
+ ETag: result.ETag || ""
318
+ };
319
+ }
320
+ buildDistributionConfigXml(config) {
321
+ const escapeXml = (str) => {
322
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
323
+ };
324
+ const arrayChildNames = {
325
+ Items: "",
326
+ Methods: "Method",
327
+ Headers: "Name",
328
+ Cookies: "Name",
329
+ QueryStringCacheKeys: "Name",
330
+ TrustedKeyGroups: "KeyGroup",
331
+ TrustedSigners: "AwsAccountNumber",
332
+ LambdaFunctionAssociations: "LambdaFunctionAssociation",
333
+ FunctionAssociations: "FunctionAssociation",
334
+ CacheBehaviors: "CacheBehavior",
335
+ CustomErrorResponses: "CustomErrorResponse",
336
+ GeoRestriction: "Location"
337
+ };
338
+ const itemsChildNames = {
339
+ Origins: "Origin",
340
+ Aliases: "CNAME",
341
+ AllowedMethods: "Method",
342
+ CachedMethods: "Method",
343
+ CustomErrorResponses: "CustomErrorResponse",
344
+ CacheBehaviors: "CacheBehavior"
345
+ };
346
+ const buildXmlElement = (name, value, indent = "", parentContext = "") => {
347
+ if (value === null || value === undefined) {
348
+ return "";
349
+ }
350
+ if (name.startsWith("@_") || name === "?xml") {
351
+ return "";
352
+ }
353
+ if (typeof value === "boolean") {
354
+ return `${indent}<${name}>${value}</${name}>
355
+ `;
356
+ }
357
+ if (typeof value === "number" || typeof value === "string") {
358
+ return `${indent}<${name}>${escapeXml(String(value))}</${name}>
359
+ `;
360
+ }
361
+ if (Array.isArray(value)) {
362
+ const childName = arrayChildNames[name] || name.replace(/s$/, "");
363
+ return value.map((item) => buildXmlElement(childName, item, indent, name)).join("");
364
+ }
365
+ if (typeof value === "object") {
366
+ if (name === "Items") {
367
+ const childElementName = itemsChildNames[parentContext] || "";
368
+ const keys = Object.keys(value).filter((k) => !k.startsWith("@_"));
369
+ if (keys.length === 1 && !Array.isArray(value[keys[0]])) {
370
+ const childKey = keys[0];
371
+ const childValue = value[childKey];
372
+ if (typeof childValue === "string") {
373
+ return `${indent}<Items>
374
+ ${indent} <${childKey}>${escapeXml(childValue)}</${childKey}>
375
+ ${indent}</Items>
376
+ `;
377
+ } else if (typeof childValue === "object" && !Array.isArray(childValue)) {
378
+ return `${indent}<Items>
379
+ ${buildXmlElement(childKey, childValue, indent + " ", name)}${indent}</Items>
380
+ `;
381
+ }
382
+ }
383
+ if (keys.length === 1 && Array.isArray(value[keys[0]])) {
384
+ const childKey = keys[0];
385
+ const childArray = value[childKey];
386
+ let children2 = "";
387
+ for (const item of childArray) {
388
+ if (typeof item === "string") {
389
+ children2 += `${indent} <${childKey}>${escapeXml(item)}</${childKey}>
390
+ `;
391
+ } else {
392
+ children2 += buildXmlElement(childKey, item, indent + " ", name);
393
+ }
394
+ }
395
+ return `${indent}<Items>
396
+ ${children2}${indent}</Items>
397
+ `;
398
+ }
399
+ if (Array.isArray(value)) {
400
+ let children2 = "";
401
+ const childName = childElementName || "Item";
402
+ for (const item of value) {
403
+ if (typeof item === "string") {
404
+ children2 += `${indent} <${childName}>${escapeXml(item)}</${childName}>
405
+ `;
406
+ } else {
407
+ children2 += buildXmlElement(childName, item, indent + " ", name);
408
+ }
409
+ }
410
+ return `${indent}<Items>
411
+ ${children2}${indent}</Items>
412
+ `;
413
+ }
414
+ }
415
+ let children = "";
416
+ for (const [key, val] of Object.entries(value)) {
417
+ if (!key.startsWith("@_")) {
418
+ children += buildXmlElement(key, val, indent + " ", name);
419
+ }
420
+ }
421
+ if (children === "") {
422
+ return `${indent}<${name}/>
423
+ `;
424
+ }
425
+ return `${indent}<${name}>
426
+ ${children}${indent}</${name}>
427
+ `;
428
+ }
429
+ return "";
430
+ };
431
+ return `<?xml version="1.0" encoding="UTF-8"?>
432
+ <DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
433
+ ${Object.entries(config).filter(([k]) => !k.startsWith("@_")).map(([key, val]) => buildXmlElement(key, val, " ", "DistributionConfig")).join("")}</DistributionConfig>`;
434
+ }
435
+ async ensureDynamicHttpMethods(distributionId) {
436
+ const getResult = await this.client.request({
437
+ service: "cloudfront",
438
+ region: "us-east-1",
439
+ method: "GET",
440
+ path: `/2020-05-31/distribution/${distributionId}/config`,
441
+ returnHeaders: true
442
+ });
443
+ const etag = getResult.headers?.etag || getResult.headers?.ETag || "";
444
+ const currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig || getResult.body;
445
+ if (!currentConfig) {
446
+ throw new Error("Failed to get current distribution config");
447
+ }
448
+ let changed = false;
449
+ const dynamicMethods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"];
450
+ const parseAllowedMethods = (allowedMethods) => {
451
+ if (!allowedMethods)
452
+ return [];
453
+ const items = allowedMethods.Items;
454
+ if (!items)
455
+ return [];
456
+ if (Array.isArray(items))
457
+ return items.map(String);
458
+ if (typeof items === "object" && items.Method) {
459
+ const method = items.Method;
460
+ return Array.isArray(method) ? method.map(String) : [String(method)];
461
+ }
462
+ if (items.Item) {
463
+ return Array.isArray(items.Item) ? items.Item.map(String) : [String(items.Item)];
464
+ }
465
+ return [];
466
+ };
467
+ const defaultBehavior = currentConfig.DefaultCacheBehavior;
468
+ if (defaultBehavior) {
469
+ const allowed = parseAllowedMethods(defaultBehavior.AllowedMethods);
470
+ if (!allowed.includes("POST")) {
471
+ defaultBehavior.AllowedMethods = {
472
+ Quantity: dynamicMethods.length,
473
+ Items: { Method: dynamicMethods },
474
+ CachedMethods: {
475
+ Quantity: 2,
476
+ Items: { Method: ["GET", "HEAD"] }
477
+ }
478
+ };
479
+ changed = true;
480
+ }
481
+ }
482
+ if (!changed) {
483
+ return false;
484
+ }
485
+ const configXml = this.buildDistributionConfigXml(currentConfig);
486
+ await this.client.request({
487
+ service: "cloudfront",
488
+ region: "us-east-1",
489
+ method: "PUT",
490
+ path: `/2020-05-31/distribution/${distributionId}/config`,
491
+ body: configXml,
492
+ headers: {
493
+ "Content-Type": "application/xml",
494
+ "If-Match": etag
495
+ }
496
+ });
497
+ return true;
498
+ }
499
+ async addAliases(distributionId, aliases, certificateArn) {
500
+ return this.updateDistribution({
501
+ distributionId,
502
+ aliases,
503
+ certificateArn
504
+ });
505
+ }
506
+ async createFunction(options) {
507
+ const { name, code, comment = "", runtime = "cloudfront-js-2.0" } = options;
508
+ const functionXml = `<?xml version="1.0" encoding="UTF-8"?>
509
+ <CreateFunctionRequest xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
510
+ <Name>${name}</Name>
511
+ <FunctionConfig>
512
+ <Comment>${comment}</Comment>
513
+ <Runtime>${runtime}</Runtime>
514
+ </FunctionConfig>
515
+ <FunctionCode>${Buffer.from(code).toString("base64")}</FunctionCode>
516
+ </CreateFunctionRequest>`;
517
+ const result = await this.client.request({
518
+ service: "cloudfront",
519
+ region: "us-east-1",
520
+ method: "POST",
521
+ path: "/2020-05-31/function",
522
+ body: functionXml,
523
+ headers: {
524
+ "Content-Type": "application/xml"
525
+ }
526
+ });
527
+ const func = result.FunctionSummary || result;
528
+ return {
529
+ FunctionARN: func.FunctionMetadata?.FunctionARN || func.FunctionARN,
530
+ Name: func.Name || name,
531
+ Stage: func.FunctionMetadata?.Stage || "DEVELOPMENT",
532
+ ETag: result.ETag || ""
533
+ };
534
+ }
535
+ async listFunctions() {
536
+ const result = await this.client.request({
537
+ service: "cloudfront",
538
+ region: "us-east-1",
539
+ method: "GET",
540
+ path: "/2020-05-31/function"
541
+ });
542
+ const functions = [];
543
+ const items = result.FunctionList?.Items?.FunctionSummary;
544
+ if (items) {
545
+ const list = Array.isArray(items) ? items : [items];
546
+ for (const item of list) {
547
+ functions.push({
548
+ Name: item.Name,
549
+ FunctionARN: item.FunctionMetadata?.FunctionARN,
550
+ Stage: item.FunctionMetadata?.Stage,
551
+ CreatedTime: item.FunctionMetadata?.CreatedTime,
552
+ LastModifiedTime: item.FunctionMetadata?.LastModifiedTime
553
+ });
554
+ }
555
+ }
556
+ return functions;
557
+ }
558
+ async getFunction(name, stage = "LIVE") {
559
+ try {
560
+ const result = await this.client.request({
561
+ service: "cloudfront",
562
+ region: "us-east-1",
563
+ method: "GET",
564
+ path: `/2020-05-31/function/${name}`,
565
+ queryParams: { Stage: stage },
566
+ returnHeaders: true
567
+ });
568
+ const func = result.body?.FunctionSummary || result.FunctionSummary || result.body || result;
569
+ return {
570
+ FunctionARN: func.FunctionMetadata?.FunctionARN,
571
+ Name: func.Name || name,
572
+ Stage: func.FunctionMetadata?.Stage || stage,
573
+ ETag: result.headers?.etag || result.ETag || "",
574
+ FunctionCode: func.FunctionCode
575
+ };
576
+ } catch (err) {
577
+ if (err.message?.includes("404") || err.message?.includes("NoSuchFunctionExists")) {
578
+ return null;
579
+ }
580
+ throw err;
581
+ }
582
+ }
583
+ async publishFunction(nameOrOptions, etag) {
584
+ let name;
585
+ let functionETag;
586
+ if (typeof nameOrOptions === "object") {
587
+ name = nameOrOptions.Name;
588
+ functionETag = nameOrOptions.IfMatch;
589
+ } else {
590
+ name = nameOrOptions;
591
+ functionETag = etag;
592
+ }
593
+ if (!functionETag) {
594
+ const func2 = await this.getFunction(name, "DEVELOPMENT");
595
+ if (!func2) {
596
+ throw new Error(`Function ${name} not found`);
597
+ }
598
+ functionETag = func2.ETag;
599
+ }
600
+ const result = await this.client.request({
601
+ service: "cloudfront",
602
+ region: "us-east-1",
603
+ method: "POST",
604
+ path: `/2020-05-31/function/${name}/publish`,
605
+ headers: {
606
+ "If-Match": functionETag
607
+ }
608
+ });
609
+ const func = result.FunctionSummary || result;
610
+ return {
611
+ FunctionARN: func.FunctionMetadata?.FunctionARN,
612
+ Stage: func.FunctionMetadata?.Stage || "LIVE",
613
+ FunctionSummary: func
614
+ };
615
+ }
616
+ async describeFunction(options) {
617
+ const { Name, Stage = "DEVELOPMENT" } = options;
618
+ const result = await this.client.request({
619
+ service: "cloudfront",
620
+ region: "us-east-1",
621
+ method: "GET",
622
+ path: `/2020-05-31/function/${Name}/describe`,
623
+ queryParams: { Stage },
624
+ returnHeaders: true
625
+ });
626
+ return {
627
+ ETag: result.headers?.etag || result.ETag || "",
628
+ FunctionSummary: result.body?.FunctionSummary || result.FunctionSummary || result.body || result
629
+ };
630
+ }
631
+ async updateFunction(options) {
632
+ const { Name, FunctionCode, FunctionConfig, IfMatch } = options;
633
+ const functionXml = `<?xml version="1.0" encoding="UTF-8"?>
634
+ <UpdateFunctionRequest xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
635
+ <FunctionConfig>
636
+ <Comment>${FunctionConfig.Comment}</Comment>
637
+ <Runtime>${FunctionConfig.Runtime}</Runtime>
638
+ </FunctionConfig>
639
+ <FunctionCode>${Buffer.from(FunctionCode).toString("base64")}</FunctionCode>
640
+ </UpdateFunctionRequest>`;
641
+ const result = await this.client.request({
642
+ service: "cloudfront",
643
+ region: "us-east-1",
644
+ method: "PUT",
645
+ path: `/2020-05-31/function/${Name}`,
646
+ body: functionXml,
647
+ headers: {
648
+ "Content-Type": "application/xml",
649
+ "If-Match": IfMatch
650
+ },
651
+ returnHeaders: true
652
+ });
653
+ return {
654
+ ETag: result.headers?.etag || result.ETag || "",
655
+ FunctionSummary: result.body?.FunctionSummary || result.FunctionSummary || result.body || result
656
+ };
657
+ }
658
+ async deleteFunction(name, etag) {
659
+ let functionETag = etag;
660
+ if (!functionETag) {
661
+ const func = await this.getFunction(name, "DEVELOPMENT");
662
+ if (!func) {
663
+ return;
664
+ }
665
+ functionETag = func.ETag;
666
+ }
667
+ await this.client.request({
668
+ service: "cloudfront",
669
+ region: "us-east-1",
670
+ method: "DELETE",
671
+ path: `/2020-05-31/function/${name}`,
672
+ headers: {
673
+ "If-Match": functionETag
674
+ }
675
+ });
676
+ }
677
+ async createIndexRewriteFunction(name) {
678
+ const code = `function handler(event) {
679
+ const request = event.request;
680
+ var uri = request.uri;
681
+
682
+ // Check if the request is for a directory (ends with /)
683
+ if (uri.endsWith('/')) {
684
+ request.uri += 'index.html';
685
+ }
686
+ // Check if the request doesn't have a file extension
687
+ else if (!uri.includes('.')) {
688
+ // Add trailing slash to redirect to directory
689
+ request.uri += '/index.html';
690
+ }
691
+
692
+ return request;
693
+ }`;
694
+ return this.createFunction({
695
+ name,
696
+ code,
697
+ comment: "Rewrite directory requests to index.html for S3 static sites",
698
+ runtime: "cloudfront-js-2.0"
699
+ });
700
+ }
701
+ async listOriginAccessControls() {
702
+ const result = await this.client.request({
703
+ service: "cloudfront",
704
+ region: "us-east-1",
705
+ method: "GET",
706
+ path: "/2020-05-31/origin-access-control"
707
+ });
708
+ const items = [];
709
+ if (result.OriginAccessControlList?.Items?.OriginAccessControlSummary) {
710
+ const summaries = Array.isArray(result.OriginAccessControlList.Items.OriginAccessControlSummary) ? result.OriginAccessControlList.Items.OriginAccessControlSummary : [result.OriginAccessControlList.Items.OriginAccessControlSummary];
711
+ items.push(...summaries.map((item) => ({
712
+ Id: item.Id,
713
+ Name: item.Name,
714
+ Description: item.Description,
715
+ SigningProtocol: item.SigningProtocol,
716
+ SigningBehavior: item.SigningBehavior,
717
+ OriginAccessControlOriginType: item.OriginAccessControlOriginType
718
+ })));
719
+ }
720
+ return items;
721
+ }
722
+ async createOriginAccessControl(options) {
723
+ const {
724
+ name,
725
+ description = `OAC for ${name}`,
726
+ signingProtocol = "sigv4",
727
+ signingBehavior = "always",
728
+ originType = "s3"
729
+ } = options;
730
+ const body = `<?xml version="1.0" encoding="UTF-8"?>
731
+ <OriginAccessControlConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
732
+ <Name>${name}</Name>
733
+ <Description>${description}</Description>
734
+ <SigningProtocol>${signingProtocol}</SigningProtocol>
735
+ <SigningBehavior>${signingBehavior}</SigningBehavior>
736
+ <OriginAccessControlOriginType>${originType}</OriginAccessControlOriginType>
737
+ </OriginAccessControlConfig>`;
738
+ const result = await this.client.request({
739
+ service: "cloudfront",
740
+ region: "us-east-1",
741
+ method: "POST",
742
+ path: "/2020-05-31/origin-access-control",
743
+ body,
744
+ headers: {
745
+ "Content-Type": "application/xml"
746
+ },
747
+ returnHeaders: true
748
+ });
749
+ const oac = result.body?.OriginAccessControl || result.OriginAccessControl || result.body || result;
750
+ return {
751
+ Id: oac.Id,
752
+ Name: oac.OriginAccessControlConfig?.Name || name,
753
+ Description: oac.OriginAccessControlConfig?.Description || description,
754
+ SigningProtocol: oac.OriginAccessControlConfig?.SigningProtocol || signingProtocol,
755
+ SigningBehavior: oac.OriginAccessControlConfig?.SigningBehavior || signingBehavior,
756
+ OriginAccessControlOriginType: oac.OriginAccessControlConfig?.OriginAccessControlOriginType || originType,
757
+ ETag: result.headers?.etag || result.ETag || ""
758
+ };
759
+ }
760
+ async findOrCreateOriginAccessControl(name) {
761
+ const oacs = await this.listOriginAccessControls();
762
+ const existing = oacs.find((oac) => oac.Name === name);
763
+ if (existing) {
764
+ return { Id: existing.Id, Name: existing.Name, isNew: false };
765
+ }
766
+ const created = await this.createOriginAccessControl({ name });
767
+ return { Id: created.Id, Name: created.Name, isNew: true };
768
+ }
769
+ async createDistributionForS3(options) {
770
+ const {
771
+ bucketName,
772
+ bucketRegion,
773
+ originAccessControlId,
774
+ aliases = [],
775
+ certificateArn,
776
+ defaultRootObject = "index.html",
777
+ comment = `Distribution for ${bucketName}`,
778
+ priceClass = "PriceClass_100",
779
+ enabled = true
780
+ } = options;
781
+ const originId = `S3-${bucketName}`;
782
+ const s3DomainName = `${bucketName}.s3.${bucketRegion}.amazonaws.com`;
783
+ const callerReference = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
784
+ let aliasesXml = "<Aliases><Quantity>0</Quantity></Aliases>";
785
+ if (aliases.length > 0) {
786
+ aliasesXml = `<Aliases>
787
+ <Quantity>${aliases.length}</Quantity>
788
+ <Items>
789
+ ${aliases.map((a) => `<CNAME>${a}</CNAME>`).join(`
790
+ `)}
791
+ </Items>
792
+ </Aliases>`;
793
+ }
794
+ let viewerCertificateXml = `<ViewerCertificate>
795
+ <CloudFrontDefaultCertificate>true</CloudFrontDefaultCertificate>
796
+ </ViewerCertificate>`;
797
+ if (certificateArn && aliases.length > 0) {
798
+ viewerCertificateXml = `<ViewerCertificate>
799
+ <ACMCertificateArn>${certificateArn}</ACMCertificateArn>
800
+ <SSLSupportMethod>sni-only</SSLSupportMethod>
801
+ <MinimumProtocolVersion>TLSv1.2_2021</MinimumProtocolVersion>
802
+ <CertificateSource>acm</CertificateSource>
803
+ </ViewerCertificate>`;
804
+ }
805
+ const body = `<?xml version="1.0" encoding="UTF-8"?>
806
+ <DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
807
+ <CallerReference>${callerReference}</CallerReference>
808
+ <Comment>${comment}</Comment>
809
+ <DefaultRootObject>${defaultRootObject}</DefaultRootObject>
810
+ <Origins>
811
+ <Quantity>1</Quantity>
812
+ <Items>
813
+ <Origin>
814
+ <Id>${originId}</Id>
815
+ <DomainName>${s3DomainName}</DomainName>
816
+ <OriginPath></OriginPath>
817
+ <S3OriginConfig>
818
+ <OriginAccessIdentity></OriginAccessIdentity>
819
+ </S3OriginConfig>
820
+ <OriginAccessControlId>${originAccessControlId}</OriginAccessControlId>
821
+ </Origin>
822
+ </Items>
823
+ </Origins>
824
+ <DefaultCacheBehavior>
825
+ <TargetOriginId>${originId}</TargetOriginId>
826
+ <ViewerProtocolPolicy>redirect-to-https</ViewerProtocolPolicy>
827
+ <AllowedMethods>
828
+ <Quantity>2</Quantity>
829
+ <Items>
830
+ <Method>GET</Method>
831
+ <Method>HEAD</Method>
832
+ </Items>
833
+ <CachedMethods>
834
+ <Quantity>2</Quantity>
835
+ <Items>
836
+ <Method>GET</Method>
837
+ <Method>HEAD</Method>
838
+ </Items>
839
+ </CachedMethods>
840
+ </AllowedMethods>
841
+ <Compress>true</Compress>
842
+ <CachePolicyId>658327ea-f89d-4fab-a63d-7e88639e58f6</CachePolicyId>
843
+ </DefaultCacheBehavior>
844
+ ${aliasesXml}
845
+ ${viewerCertificateXml}
846
+ <PriceClass>${priceClass}</PriceClass>
847
+ <Enabled>${enabled}</Enabled>
848
+ <HttpVersion>http2and3</HttpVersion>
849
+ <IsIPV6Enabled>true</IsIPV6Enabled>
850
+ <CustomErrorResponses>
851
+ <Quantity>1</Quantity>
852
+ <Items>
853
+ <CustomErrorResponse>
854
+ <ErrorCode>403</ErrorCode>
855
+ <ResponsePagePath>/index.html</ResponsePagePath>
856
+ <ResponseCode>200</ResponseCode>
857
+ <ErrorCachingMinTTL>300</ErrorCachingMinTTL>
858
+ </CustomErrorResponse>
859
+ </Items>
860
+ </CustomErrorResponses>
861
+ </DistributionConfig>`;
862
+ const result = await this.client.request({
863
+ service: "cloudfront",
864
+ region: "us-east-1",
865
+ method: "POST",
866
+ path: "/2020-05-31/distribution",
867
+ body,
868
+ headers: {
869
+ "Content-Type": "application/xml"
870
+ },
871
+ returnHeaders: true
872
+ });
873
+ const dist = result.body?.Distribution || result.Distribution || result.body || result;
874
+ return {
875
+ Id: dist.Id,
876
+ ARN: dist.ARN,
877
+ DomainName: dist.DomainName,
878
+ Status: dist.Status,
879
+ ETag: result.headers?.etag || result.ETag || ""
880
+ };
881
+ }
882
+ static getS3BucketPolicyForCloudFront(bucketName, distributionArn) {
883
+ return {
884
+ Version: "2012-10-17",
885
+ Statement: [
886
+ {
887
+ Sid: "AllowCloudFrontServicePrincipal",
888
+ Effect: "Allow",
889
+ Principal: {
890
+ Service: "cloudfront.amazonaws.com"
891
+ },
892
+ Action: "s3:GetObject",
893
+ Resource: `arn:aws:s3:::${bucketName}/*`,
894
+ Condition: {
895
+ StringEquals: {
896
+ "AWS:SourceArn": distributionArn
897
+ }
898
+ }
899
+ }
900
+ ]
901
+ };
902
+ }
903
+ async waitForDistributionDeployed(distributionId, maxAttempts = 60) {
904
+ for (let i = 0;i < maxAttempts; i++) {
905
+ const dist = await this.getDistribution(distributionId);
906
+ if (dist.Status === "Deployed") {
907
+ return true;
908
+ }
909
+ await new Promise((resolve) => setTimeout(resolve, 30000));
910
+ }
911
+ return false;
912
+ }
913
+ async disableDistribution(distributionId) {
914
+ const getResult = await this.client.request({
915
+ service: "cloudfront",
916
+ region: "us-east-1",
917
+ method: "GET",
918
+ path: `/2020-05-31/distribution/${distributionId}/config`,
919
+ returnHeaders: true
920
+ });
921
+ const etag = getResult.headers?.etag || getResult.headers?.ETag || "";
922
+ const currentConfig = getResult.body?.DistributionConfig || (getResult.body?.Enabled !== undefined ? getResult.body : undefined) || getResult.DistributionConfig;
923
+ if (!currentConfig || currentConfig.Enabled === undefined) {
924
+ throw new Error("Failed to get current distribution config");
925
+ }
926
+ currentConfig.Enabled = false;
927
+ const configXml = this.buildDistributionConfigXml(currentConfig);
928
+ const result = await this.client.request({
929
+ service: "cloudfront",
930
+ region: "us-east-1",
931
+ method: "PUT",
932
+ path: `/2020-05-31/distribution/${distributionId}/config`,
933
+ body: configXml,
934
+ headers: {
935
+ "Content-Type": "application/xml",
936
+ "If-Match": etag
937
+ },
938
+ returnHeaders: true
939
+ });
940
+ return { ETag: result.headers?.etag || result.headers?.ETag || result.ETag || "" };
941
+ }
942
+ async deleteDistribution(distributionId, etag) {
943
+ let etagToUse = etag || "";
944
+ if (!etagToUse) {
945
+ const getResult = await this.client.request({
946
+ service: "cloudfront",
947
+ region: "us-east-1",
948
+ method: "GET",
949
+ path: `/2020-05-31/distribution/${distributionId}`,
950
+ returnHeaders: true
951
+ });
952
+ etagToUse = getResult.headers?.etag || getResult.headers?.ETag || "";
953
+ }
954
+ await this.client.request({
955
+ service: "cloudfront",
956
+ region: "us-east-1",
957
+ method: "DELETE",
958
+ path: `/2020-05-31/distribution/${distributionId}`,
959
+ headers: {
960
+ "If-Match": etagToUse
961
+ }
962
+ });
963
+ }
964
+ async waitForDistributionDisabled(distributionId, maxAttempts = 60) {
965
+ for (let i = 0;i < maxAttempts; i++) {
966
+ const dist = await this.getDistribution(distributionId);
967
+ if (dist.Status === "Deployed" && !dist.Enabled) {
968
+ return true;
969
+ }
970
+ await new Promise((resolve) => setTimeout(resolve, 30000));
971
+ }
972
+ return false;
973
+ }
974
+ async removeAlias(distributionId, alias) {
975
+ const getResult = await this.client.request({
976
+ service: "cloudfront",
977
+ region: "us-east-1",
978
+ method: "GET",
979
+ path: `/2020-05-31/distribution/${distributionId}/config`,
980
+ returnHeaders: true
981
+ });
982
+ const etag = getResult.headers?.etag || getResult.headers?.ETag || "";
983
+ const currentConfig = getResult.body?.DistributionConfig || (getResult.body?.Enabled !== undefined ? getResult.body : undefined) || getResult.DistributionConfig;
984
+ if (!currentConfig || currentConfig.Enabled === undefined) {
985
+ throw new Error("Failed to get current distribution config");
986
+ }
987
+ let items = [];
988
+ if (currentConfig.Aliases?.Items) {
989
+ if (Array.isArray(currentConfig.Aliases.Items)) {
990
+ items = currentConfig.Aliases.Items;
991
+ } else if (typeof currentConfig.Aliases.Items === "object") {
992
+ const cname = currentConfig.Aliases.Items.CNAME;
993
+ if (typeof cname === "string") {
994
+ items = [cname];
995
+ } else if (Array.isArray(cname)) {
996
+ items = cname;
997
+ }
998
+ }
999
+ }
1000
+ if (items.length === 0) {
1001
+ throw new Error(`Distribution has no aliases to remove`);
1002
+ }
1003
+ const newItems = items.filter((a) => a !== alias);
1004
+ if (newItems.length === items.length) {
1005
+ throw new Error(`Alias ${alias} not found in distribution`);
1006
+ }
1007
+ currentConfig.Aliases.Quantity = newItems.length;
1008
+ currentConfig.Aliases.Items = newItems.length > 0 ? newItems : undefined;
1009
+ if (newItems.length === 0) {
1010
+ currentConfig.ViewerCertificate = {
1011
+ CloudFrontDefaultCertificate: true,
1012
+ MinimumProtocolVersion: "TLSv1.2_2021"
1013
+ };
1014
+ }
1015
+ const configXml = this.buildDistributionConfigXml(currentConfig);
1016
+ const result = await this.client.request({
1017
+ service: "cloudfront",
1018
+ region: "us-east-1",
1019
+ method: "PUT",
1020
+ path: `/2020-05-31/distribution/${distributionId}/config`,
1021
+ body: configXml,
1022
+ headers: {
1023
+ "Content-Type": "application/xml",
1024
+ "If-Match": etag
1025
+ },
1026
+ returnHeaders: true
1027
+ });
1028
+ return { ETag: result.headers?.etag || result.headers?.ETag || result.ETag || "" };
1029
+ }
1030
+ }
1031
+
1032
+ export { CloudFrontClient };