puls-dev 0.1.0 → 0.1.8

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 (50) hide show
  1. package/README.md +10 -8
  2. package/dist/core/checker.d.ts +1 -1
  3. package/dist/core/checker.js +88 -56
  4. package/dist/core/config.test.d.ts +1 -0
  5. package/dist/core/config.test.js +21 -0
  6. package/dist/core/decorators.js +8 -2
  7. package/dist/core/output.test.d.ts +1 -0
  8. package/dist/core/output.test.js +18 -0
  9. package/dist/core/resource.js +2 -2
  10. package/dist/core/stack.d.ts +1 -1
  11. package/dist/core/stack.js +2 -2
  12. package/dist/providers/aws/acm.d.ts +1 -1
  13. package/dist/providers/aws/acm.js +27 -23
  14. package/dist/providers/aws/api.d.ts +14 -14
  15. package/dist/providers/aws/api.js +21 -21
  16. package/dist/providers/aws/apigateway.d.ts +2 -2
  17. package/dist/providers/aws/apigateway.js +33 -29
  18. package/dist/providers/aws/cloudfront.d.ts +3 -3
  19. package/dist/providers/aws/cloudfront.js +49 -34
  20. package/dist/providers/aws/fargate.d.ts +2 -2
  21. package/dist/providers/aws/fargate.js +99 -52
  22. package/dist/providers/aws/lambda.d.ts +2 -2
  23. package/dist/providers/aws/lambda.js +63 -32
  24. package/dist/providers/aws/rds.d.ts +1 -1
  25. package/dist/providers/aws/rds.js +77 -39
  26. package/dist/providers/aws/route53.d.ts +5 -5
  27. package/dist/providers/aws/route53.js +42 -35
  28. package/dist/providers/aws/s3.d.ts +2 -2
  29. package/dist/providers/aws/s3.js +40 -33
  30. package/dist/providers/aws/secrets.js +15 -7
  31. package/dist/providers/aws/sqs.d.ts +1 -1
  32. package/dist/providers/aws/sqs.js +47 -23
  33. package/dist/providers/do/domain.d.ts +4 -4
  34. package/dist/providers/do/domain.js +15 -11
  35. package/dist/providers/firebase/auth.d.ts +1 -1
  36. package/dist/providers/firebase/auth.js +65 -33
  37. package/dist/providers/firebase/firestore.d.ts +2 -2
  38. package/dist/providers/firebase/firestore.js +45 -28
  39. package/dist/providers/firebase/functions.d.ts +1 -1
  40. package/dist/providers/firebase/functions.js +75 -42
  41. package/dist/providers/firebase/hosting.d.ts +1 -1
  42. package/dist/providers/firebase/hosting.js +92 -52
  43. package/dist/providers/firebase/remoteconfig.d.ts +1 -1
  44. package/dist/providers/firebase/remoteconfig.js +42 -33
  45. package/dist/providers/firebase/storage.d.ts +1 -1
  46. package/dist/providers/firebase/storage.js +38 -24
  47. package/dist/providers/proxmox/vm.d.ts +1 -1
  48. package/dist/providers/proxmox/vm.js +43 -24
  49. package/dist/types/aws.js +1 -1
  50. package/package.json +3 -2
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Puls-dev
2
2
 
3
- Intent-driven infrastructure-as-code. Describe what you want — Puls figures out create, update, or skip.
3
+ **Intent-driven infrastructure-as-code. Describe what you want — Puls figures out create, update, or skip.**
4
+
5
+ [Live Documentation](https://puls-docs.web.app/) | Discord: **pulsdev.io** ([Join](https://discord.gg/CjgRayuH))
4
6
 
5
7
  ```typescript
6
8
  @Deploy({ proxmox: CONFIG.STAGING })
@@ -14,7 +16,7 @@ class GameInfra extends Stack {
14
16
  }
15
17
  ```
16
18
 
17
- No state files. No plan step. Runs against real APIs idempotent by default.
19
+ No state files. No plan step. Runs against real APIs - idempotent by default.
18
20
 
19
21
  ---
20
22
 
@@ -28,7 +30,7 @@ Declare resource → Discovery fires immediately (async)
28
30
  → deploy() awaits discovery, diffs, acts
29
31
  ```
30
32
 
31
- Running the same stack twice is always safe existing resources are detected and skipped.
33
+ Running the same stack twice is always safe - existing resources are detected and skipped.
32
34
 
33
35
  ---
34
36
 
@@ -47,7 +49,7 @@ Running the same stack twice is always safe — existing resources are detected
47
49
  ### DigitalOcean
48
50
 
49
51
  ```typescript
50
- import { DO, DO_TYPES, Stack, Deploy } from "puls";
52
+ import { DO, DO_TYPES, Stack, Deploy } from "puls-dev";
51
53
  const { SIZE, REGION } = DO_TYPES;
52
54
 
53
55
  @Deploy({ token: process.env.DO_TOKEN! })
@@ -60,7 +62,7 @@ class Production extends Stack {
60
62
  ### AWS
61
63
 
62
64
  ```typescript
63
- import { AWS, AWS_TYPES, Stack, Deploy } from "puls";
65
+ import { AWS, AWS_TYPES, Stack, Deploy } from "puls-dev";
64
66
  const { DISTRO, BUCKET, DOMAIN_REGISTER, REGION } = AWS_TYPES;
65
67
 
66
68
  @Deploy({ region: REGION.US_EAST_1 })
@@ -68,7 +70,7 @@ class CDNStack extends Stack {
68
70
  domain = AWS.Route53().randomDomain().register(DOMAIN_REGISTER).withWildcardSSL();
69
71
 
70
72
  cdn = AWS.CloudFront(`CDN-${this.domain.zoneName.slice(0, 8)}`)
71
- .copyFrom(DISTRO.TURKEY_CDN)
73
+ .copyFrom(DISTRO.CDN)
72
74
  .forDomain(this.domain, ["ec", "nc"]);
73
75
 
74
76
  bucket = AWS.S3(BUCKET.NLC_GAMES_UREG)
@@ -80,7 +82,7 @@ class CDNStack extends Stack {
80
82
  ### Proxmox
81
83
 
82
84
  ```typescript
83
- import { Proxmox, PROXMOX_TYPES, Stack, Deploy, Protected } from "puls";
85
+ import { Proxmox, PROXMOX_TYPES, Stack, Deploy, Protected } from "puls-dev";
84
86
  const { CONFIG, OS, KEYS } = PROXMOX_TYPES;
85
87
 
86
88
  @Deploy({ proxmox: CONFIG.STAGING })
@@ -121,7 +123,7 @@ See [docs/decorators.md](docs/decorators.md) for full reference.
121
123
  ## Running
122
124
 
123
125
  ```bash
124
- npm install
126
+ npm install puls-dev
125
127
  npx tsx your-stack.ts
126
128
  ```
127
129
 
@@ -1,5 +1,5 @@
1
1
  import "reflect-metadata";
2
- import type { InventoryResult } from '../types/inventory.ts';
2
+ import type { InventoryResult } from "../types/inventory.ts";
3
3
  export declare abstract class Checker {
4
4
  check(): Promise<InventoryResult>;
5
5
  }
@@ -1,108 +1,128 @@
1
1
  import "reflect-metadata";
2
- import { Config } from './config.js';
3
- import { listProxmoxVMs } from '../providers/proxmox/list.js';
4
- import { listDoResources } from '../providers/do/list.js';
5
- import { listAwsResources } from '../providers/aws/list.js';
2
+ import { Config } from "./config.js";
3
+ import { listProxmoxVMs } from "../providers/proxmox/list.js";
4
+ import { listDoResources } from "../providers/do/list.js";
5
+ import { listAwsResources } from "../providers/aws/list.js";
6
6
  function clip(text, width) {
7
- return text.length > width ? text.slice(0, width - 1) + '…' : text.padEnd(width);
7
+ return text.length > width
8
+ ? text.slice(0, width - 1) + "…"
9
+ : text.padEnd(width);
8
10
  }
9
11
  function printSection(title, rows, cols) {
10
12
  const colsWidth = cols.reduce((s, c) => s + c.width, 0) + (cols.length - 1) * 2;
11
13
  const innerWidth = Math.max(colsWidth + 4, title.length + 4);
12
- const bar = (l, r) => ` ${l}${''.repeat(innerWidth)}${r}`;
14
+ const bar = (l, r) => ` ${l}${"".repeat(innerWidth)}${r}`;
13
15
  const row = (content) => ` │ ${content.padEnd(innerWidth - 4)} │`;
14
- console.log('');
15
- console.log(bar('', ''));
16
+ console.log("");
17
+ console.log(bar("", ""));
16
18
  console.log(row(title));
17
- console.log(bar('', ''));
19
+ console.log(bar("", ""));
18
20
  if (rows.length === 0) {
19
- console.log(row('(none)'));
21
+ console.log(row("(none)"));
20
22
  }
21
23
  else {
22
- const headerLine = cols.map((c) => clip(c.header.toUpperCase(), c.width)).join(' ');
24
+ const headerLine = cols
25
+ .map((c) => clip(c.header.toUpperCase(), c.width))
26
+ .join(" ");
23
27
  console.log(row(headerLine));
24
- console.log(bar('', ''));
28
+ console.log(bar("", ""));
25
29
  for (const r of rows) {
26
- console.log(row(cols.map((c) => clip(c.render(r), c.width)).join(' ')));
30
+ console.log(row(cols.map((c) => clip(c.render(r), c.width)).join(" ")));
27
31
  }
28
32
  }
29
- console.log(bar('', ''));
33
+ console.log(bar("", ""));
30
34
  }
31
35
  // ─── Per-provider renderers ───────────────────────────────────────────────────
32
36
  function renderProxmox(inv) {
33
- const running = inv.vms.filter((v) => v.status === 'running').length;
34
- printSection(`Proxmox · ${inv.vms.length} VM${inv.vms.length !== 1 ? 's' : ''} (${running} running)`, inv.vms, [
35
- { header: 'Name', width: 26, render: (v) => v.name },
36
- { header: 'VMID', width: 6, render: (v) => String(v.vmid) },
37
- { header: 'Node', width: 12, render: (v) => v.node },
38
- { header: 'Status', width: 8, render: (v) => v.status },
39
- { header: 'Mem', width: 6, render: (v) => `${Math.round(v.maxmem / 1024 ** 3)}GB` },
40
- { header: 'Disk', width: 6, render: (v) => `${Math.round(v.maxdisk / 1024 ** 3)}GB` },
37
+ const running = inv.vms.filter((v) => v.status === "running").length;
38
+ printSection(`Proxmox · ${inv.vms.length} VM${inv.vms.length !== 1 ? "s" : ""} (${running} running)`, inv.vms, [
39
+ { header: "Name", width: 26, render: (v) => v.name },
40
+ { header: "VMID", width: 6, render: (v) => String(v.vmid) },
41
+ { header: "Node", width: 12, render: (v) => v.node },
42
+ { header: "Status", width: 8, render: (v) => v.status },
43
+ {
44
+ header: "Mem",
45
+ width: 6,
46
+ render: (v) => `${Math.round(v.maxmem / 1024 ** 3)}GB`,
47
+ },
48
+ {
49
+ header: "Disk",
50
+ width: 6,
51
+ render: (v) => `${Math.round(v.maxdisk / 1024 ** 3)}GB`,
52
+ },
41
53
  ]);
42
54
  }
43
55
  function renderDo(inv) {
44
- const costStr = inv.totalMonthlyCost > 0 ? ` · $${inv.totalMonthlyCost}/mo` : '';
56
+ const costStr = inv.totalMonthlyCost > 0 ? ` · $${inv.totalMonthlyCost}/mo` : "";
45
57
  printSection(`DigitalOcean Droplets · ${inv.droplets.length}${costStr}`, inv.droplets, [
46
- { header: 'Name', width: 24, render: (d) => d.name },
47
- { header: 'Region', width: 6, render: (d) => d.region },
48
- { header: 'Size', width: 18, render: (d) => d.size },
49
- { header: 'Status', width: 8, render: (d) => d.status },
50
- { header: 'IP', width: 15, render: (d) => d.ip ?? '—' },
51
- { header: '$/mo', width: 5, render: (d) => d.monthlyCost > 0 ? `$${d.monthlyCost}` : '?' },
58
+ { header: "Name", width: 24, render: (d) => d.name },
59
+ { header: "Region", width: 6, render: (d) => d.region },
60
+ { header: "Size", width: 18, render: (d) => d.size },
61
+ { header: "Status", width: 8, render: (d) => d.status },
62
+ { header: "IP", width: 15, render: (d) => d.ip ?? "-" },
63
+ {
64
+ header: "$/mo",
65
+ width: 5,
66
+ render: (d) => (d.monthlyCost > 0 ? `$${d.monthlyCost}` : "?"),
67
+ },
52
68
  ]);
53
69
  if (inv.firewalls.length > 0) {
54
70
  printSection(`DigitalOcean Firewalls · ${inv.firewalls.length}`, inv.firewalls, [
55
- { header: 'Name', width: 32, render: (f) => f.name },
56
- { header: 'Droplets', width: 8, render: (f) => String(f.dropletCount) },
71
+ { header: "Name", width: 32, render: (f) => f.name },
72
+ { header: "Droplets", width: 8, render: (f) => String(f.dropletCount) },
57
73
  ]);
58
74
  }
59
75
  if (inv.loadBalancers.length > 0) {
60
76
  printSection(`DigitalOcean Load Balancers · ${inv.loadBalancers.length}`, inv.loadBalancers, [
61
- { header: 'Name', width: 24, render: (lb) => lb.name },
62
- { header: 'Region', width: 6, render: (lb) => lb.region },
63
- { header: 'IP', width: 15, render: (lb) => lb.ip },
64
- { header: 'Status', width: 8, render: (lb) => lb.status },
77
+ { header: "Name", width: 24, render: (lb) => lb.name },
78
+ { header: "Region", width: 6, render: (lb) => lb.region },
79
+ { header: "IP", width: 15, render: (lb) => lb.ip },
80
+ { header: "Status", width: 8, render: (lb) => lb.status },
65
81
  ]);
66
82
  }
67
83
  if (inv.domains.length > 0) {
68
84
  printSection(`DigitalOcean Domains · ${inv.domains.length}`, inv.domains, [
69
- { header: 'Domain', width: 42, render: (d) => d.name },
70
- { header: 'TTL', width: 6, render: (d) => String(d.ttl) },
85
+ { header: "Domain", width: 42, render: (d) => d.name },
86
+ { header: "TTL", width: 6, render: (d) => String(d.ttl) },
71
87
  ]);
72
88
  }
73
89
  }
74
90
  function renderAws(inv) {
75
91
  if (inv.distributions.length > 0) {
76
92
  printSection(`AWS CloudFront · ${inv.distributions.length} · ${inv.region}`, inv.distributions, [
77
- { header: 'ID', width: 14, render: (d) => d.id },
78
- { header: 'Domain', width: 34, render: (d) => d.aliases[0] ?? d.domain },
79
- { header: 'Status', width: 10, render: (d) => d.status },
93
+ { header: "ID", width: 14, render: (d) => d.id },
94
+ {
95
+ header: "Domain",
96
+ width: 34,
97
+ render: (d) => d.aliases[0] ?? d.domain,
98
+ },
99
+ { header: "Status", width: 10, render: (d) => d.status },
80
100
  ]);
81
101
  }
82
102
  if (inv.buckets.length > 0) {
83
103
  printSection(`AWS S3 · ${inv.buckets.length} buckets`, inv.buckets, [
84
- { header: 'Bucket', width: 52, render: (b) => b.name },
104
+ { header: "Bucket", width: 52, render: (b) => b.name },
85
105
  ]);
86
106
  }
87
107
  if (inv.lambdas.length > 0) {
88
108
  printSection(`AWS Lambda · ${inv.lambdas.length} functions`, inv.lambdas, [
89
- { header: 'Function', width: 32, render: (f) => f.name },
90
- { header: 'Runtime', width: 12, render: (f) => f.runtime },
91
- { header: 'Memory', width: 8, render: (f) => `${f.memorySizeMb}MB` },
109
+ { header: "Function", width: 32, render: (f) => f.name },
110
+ { header: "Runtime", width: 12, render: (f) => f.runtime },
111
+ { header: "Memory", width: 8, render: (f) => `${f.memorySizeMb}MB` },
92
112
  ]);
93
113
  }
94
114
  if (inv.rdsInstances.length > 0) {
95
115
  printSection(`AWS RDS · ${inv.rdsInstances.length} instances`, inv.rdsInstances, [
96
- { header: 'Identifier', width: 26, render: (i) => i.identifier },
97
- { header: 'Engine', width: 18, render: (i) => i.engine },
98
- { header: 'Class', width: 14, render: (i) => i.instanceClass },
99
- { header: 'Status', width: 10, render: (i) => i.status },
116
+ { header: "Identifier", width: 26, render: (i) => i.identifier },
117
+ { header: "Engine", width: 18, render: (i) => i.engine },
118
+ { header: "Class", width: 14, render: (i) => i.instanceClass },
119
+ { header: "Status", width: 10, render: (i) => i.status },
100
120
  ]);
101
121
  }
102
122
  if (inv.hostedZones.length > 0) {
103
123
  printSection(`AWS Route53 · ${inv.hostedZones.length} zones`, inv.hostedZones, [
104
- { header: 'Zone', width: 38, render: (z) => z.name },
105
- { header: 'Records', width: 7, render: (z) => String(z.recordCount) },
124
+ { header: "Zone", width: 38, render: (z) => z.name },
125
+ { header: "Records", width: 7, render: (z) => String(z.recordCount) },
106
126
  ]);
107
127
  }
108
128
  }
@@ -116,18 +136,30 @@ export class Checker {
116
136
  console.log(`\n🔍 Checking infrastructure...`);
117
137
  if (cfg.providers.proxmox?.url && cfg.providers.proxmox?.tokenSecret) {
118
138
  tasks.push(listProxmoxVMs()
119
- .then((inv) => { result.proxmox = inv; })
120
- .catch((err) => { errors.push({ provider: 'proxmox', message: err.message }); }));
139
+ .then((inv) => {
140
+ result.proxmox = inv;
141
+ })
142
+ .catch((err) => {
143
+ errors.push({ provider: "proxmox", message: err.message });
144
+ }));
121
145
  }
122
146
  if (cfg.providers.do?.token) {
123
147
  tasks.push(listDoResources()
124
- .then((inv) => { result.do = inv; })
125
- .catch((err) => { errors.push({ provider: 'do', message: err.message }); }));
148
+ .then((inv) => {
149
+ result.do = inv;
150
+ })
151
+ .catch((err) => {
152
+ errors.push({ provider: "do", message: err.message });
153
+ }));
126
154
  }
127
155
  if (cfg.providers.aws?.region) {
128
156
  tasks.push(listAwsResources()
129
- .then((inv) => { result.aws = inv; })
130
- .catch((err) => { errors.push({ provider: 'aws', message: err.message }); }));
157
+ .then((inv) => {
158
+ result.aws = inv;
159
+ })
160
+ .catch((err) => {
161
+ errors.push({ provider: "aws", message: err.message });
162
+ }));
131
163
  }
132
164
  await Promise.all(tasks);
133
165
  if (result.proxmox)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { test, describe } from "node:test";
2
+ import assert from "node:assert";
3
+ import { Config } from "./config.js";
4
+ describe("ConfigManager", () => {
5
+ test("sets and gets config correctly", () => {
6
+ Config.set({ dryRun: true });
7
+ assert.strictEqual(Config.get().dryRun, true);
8
+ assert.strictEqual(Config.isGlobalDryRun(), true);
9
+ Config.set({ dryRun: false });
10
+ assert.strictEqual(Config.get().dryRun, false);
11
+ assert.strictEqual(Config.isGlobalDryRun(), false);
12
+ });
13
+ test("merges provider config correctly", () => {
14
+ Config.set({
15
+ providers: {
16
+ aws: { region: "us-east-1" }
17
+ }
18
+ });
19
+ assert.strictEqual(Config.get().providers.aws?.region, "us-east-1");
20
+ });
21
+ });
@@ -20,7 +20,13 @@ function applyConfig(opts) {
20
20
  if (opts.firebase) {
21
21
  const sa = JSON.parse(readFileSync(opts.firebase, "utf8"));
22
22
  Config.set({
23
- providers: { ...Config.get().providers, firebase: { projectId: sa.project_id, serviceAccountPath: opts.firebase } },
23
+ providers: {
24
+ ...Config.get().providers,
25
+ firebase: {
26
+ projectId: sa.project_id,
27
+ serviceAccountPath: opts.firebase,
28
+ },
29
+ },
24
30
  });
25
31
  }
26
32
  }
@@ -75,7 +81,7 @@ export function Check(opts = {}) {
75
81
  });
76
82
  };
77
83
  }
78
- // Shortcut for Dry Run accepts the same options as @Deploy
84
+ // Shortcut for Dry Run - accepts the same options as @Deploy
79
85
  export function DryRun(opts = {}) {
80
86
  if (typeof opts === "function") {
81
87
  Deploy({ dryRun: true })(opts);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { test, describe } from "node:test";
2
+ import assert from "node:assert";
3
+ import { Output } from "./output.js";
4
+ describe("Output", () => {
5
+ test("resolves a value correctly", async () => {
6
+ const out = new Output();
7
+ out.resolve("success");
8
+ const val = await out.get();
9
+ assert.strictEqual(val, "success");
10
+ });
11
+ test("applies transformations via .apply()", async () => {
12
+ const out = new Output();
13
+ const doubled = out.apply(n => n * 2);
14
+ out.resolve(10);
15
+ const result = await doubled.get();
16
+ assert.strictEqual(result, 20);
17
+ });
18
+ });
@@ -31,7 +31,7 @@ export class BaseBuilder {
31
31
  return false;
32
32
  }
33
33
  // Waits for a long-running cloud operation to complete.
34
- // In dry-run mode: skips entirely no waiting.
34
+ // In dry-run mode: skips entirely - no waiting.
35
35
  // In real mode: polls via the provided condition fn until it returns true.
36
36
  // The mock fallback simulates a realistic delay with progress output.
37
37
  async waitFor(label, condition, opts = {}) {
@@ -70,7 +70,7 @@ export class BaseBuilder {
70
70
  async destroy() {
71
71
  const dryRun = this.isDryRunActive();
72
72
  console.log(`\n🗑️ Destroying "${this.name}"...`);
73
- console.log(` ✅ [${dryRun ? 'PLAN' : 'OK'}] Resource "${this.name}" marked for destruction.`);
73
+ console.log(` ✅ [${dryRun ? "PLAN" : "OK"}] Resource "${this.name}" marked for destruction.`);
74
74
  await this.destroySidecars();
75
75
  return { destroyed: this.name };
76
76
  }
@@ -1,6 +1,6 @@
1
1
  import "reflect-metadata";
2
2
  export declare abstract class Stack {
3
- /** @internal called by @Deploy to register the instance for cross-stack references. */
3
+ /** @internal - called by @Deploy to register the instance for cross-stack references. */
4
4
  static _register(cls: Function, instance: Stack): void;
5
5
  /**
6
6
  * Returns the already-constructed instance of another Stack so you can reference
@@ -23,7 +23,7 @@ function formatEntry(val) {
23
23
  const inline = pairs.map(([, v]) => v).join(" · ");
24
24
  if (inline.length <= 52)
25
25
  return { primary: inline };
26
- // Too long first value as primary, rest as sub-lines
26
+ // Too long - first value as primary, rest as sub-lines
27
27
  const [[, first], ...rest] = pairs;
28
28
  return {
29
29
  primary: first,
@@ -59,7 +59,7 @@ function printOutputs(stackName, outputs) {
59
59
  console.log(` └${line}┘`);
60
60
  }
61
61
  export class Stack {
62
- /** @internal called by @Deploy to register the instance for cross-stack references. */
62
+ /** @internal - called by @Deploy to register the instance for cross-stack references. */
63
63
  static _register(cls, instance) {
64
64
  _registry.set(cls, instance);
65
65
  }
@@ -1,4 +1,4 @@
1
- import { BaseBuilder } from '../../core/resource.js';
1
+ import { BaseBuilder } from "../../core/resource.js";
2
2
  interface ZoneRef {
3
3
  zoneId?: string;
4
4
  zoneName: string;
@@ -1,7 +1,7 @@
1
- import { ListCertificatesCommand, RequestCertificateCommand, DescribeCertificateCommand, } from '@aws-sdk/client-acm';
2
- import { ChangeResourceRecordSetsCommand } from '@aws-sdk/client-route-53';
3
- import { BaseBuilder } from '../../core/resource.js';
4
- import { getACMClient, getR53Client } from './api.js';
1
+ import { ListCertificatesCommand, RequestCertificateCommand, DescribeCertificateCommand, } from "@aws-sdk/client-acm";
2
+ import { ChangeResourceRecordSetsCommand } from "@aws-sdk/client-route-53";
3
+ import { BaseBuilder } from "../../core/resource.js";
4
+ import { getACMClient, getR53Client } from "./api.js";
5
5
  export class ACMCertificateBuilder extends BaseBuilder {
6
6
  domainName;
7
7
  wildcard;
@@ -22,7 +22,9 @@ export class ACMCertificateBuilder extends BaseBuilder {
22
22
  try {
23
23
  const acm = getACMClient();
24
24
  const primaryName = wildcard ? `*.${domain}` : domain;
25
- const list = await acm.send(new ListCertificatesCommand({ CertificateStatuses: ['ISSUED', 'PENDING_VALIDATION'] }));
25
+ const list = await acm.send(new ListCertificatesCommand({
26
+ CertificateStatuses: ["ISSUED", "PENDING_VALIDATION"],
27
+ }));
26
28
  for (const cert of list.CertificateSummaryList ?? []) {
27
29
  if (cert.DomainName === primaryName && cert.CertificateArn) {
28
30
  this.resolvedArn = cert.CertificateArn;
@@ -32,7 +34,7 @@ export class ACMCertificateBuilder extends BaseBuilder {
32
34
  return null;
33
35
  }
34
36
  catch (e) {
35
- if (e.name === 'CredentialsProviderError')
37
+ if (e.name === "CredentialsProviderError")
36
38
  return null;
37
39
  throw e;
38
40
  }
@@ -42,13 +44,15 @@ export class ACMCertificateBuilder extends BaseBuilder {
42
44
  const existing = await this.discoveryPromise;
43
45
  console.log(`\n🔐 Finalizing ACM Certificate for "${this.domainName}"...`);
44
46
  if (existing) {
45
- console.log(` ✅ Certificate already exists (${existing.Status ?? 'ISSUED'}): ${this.resolvedArn}`);
47
+ console.log(` ✅ Certificate already exists (${existing.Status ?? "ISSUED"}): ${this.resolvedArn}`);
46
48
  return { arn: this.resolvedArn };
47
49
  }
48
- const primaryName = this.wildcard ? `*.${this.domainName}` : this.domainName;
50
+ const primaryName = this.wildcard
51
+ ? `*.${this.domainName}`
52
+ : this.domainName;
49
53
  const sanNames = this.wildcard ? [this.domainName] : [];
50
54
  if (dryRun) {
51
- console.log(` 📝 [PLAN] Request ${this.wildcard ? 'wildcard ' : ''}certificate: ${primaryName}`);
55
+ console.log(` 📝 [PLAN] Request ${this.wildcard ? "wildcard " : ""}certificate: ${primaryName}`);
52
56
  console.log(` 📝 [PLAN] DNS validation CNAMEs will be auto-written to Route53`);
53
57
  this.resolvedArn = `arn:aws:acm:us-east-1:DRYRUN:certificate/pending`;
54
58
  return { arn: this.resolvedArn };
@@ -57,51 +61,51 @@ export class ACMCertificateBuilder extends BaseBuilder {
57
61
  const result = await acm.send(new RequestCertificateCommand({
58
62
  DomainName: primaryName,
59
63
  SubjectAlternativeNames: sanNames.length ? sanNames : undefined,
60
- ValidationMethod: 'DNS',
64
+ ValidationMethod: "DNS",
61
65
  }));
62
66
  this.resolvedArn = result.CertificateArn;
63
67
  console.log(`🚀 Requested certificate ${primaryName} (arn=${this.resolvedArn})`);
64
68
  // Step 1: wait until ALL DomainValidationOptions have their ResourceRecord
65
69
  // (wildcard + apex SANs each get an entry; ACM can generate them at different times)
66
- await this.waitFor('validation records to be generated', async () => {
70
+ await this.waitFor("validation records to be generated", async () => {
67
71
  const detail = await acm.send(new DescribeCertificateCommand({ CertificateArn: this.resolvedArn }));
68
72
  const options = detail.Certificate?.DomainValidationOptions ?? [];
69
73
  if (options.length === 0)
70
74
  return false;
71
- const withRecords = options.filter(o => o.ResourceRecord);
75
+ const withRecords = options.filter((o) => o.ResourceRecord);
72
76
  if (withRecords.length < options.length)
73
77
  return false;
74
- this.validationRecords = withRecords.map(o => ({
78
+ this.validationRecords = withRecords.map((o) => ({
75
79
  name: o.ResourceRecord.Name,
76
80
  value: o.ResourceRecord.Value,
77
81
  }));
78
82
  return true;
79
83
  }, { intervalMs: 5_000, timeoutMs: 60_000 });
80
- // Step 2: write them into Route53 automatically no manual DNS work needed
84
+ // Step 2: write them into Route53 automatically - no manual DNS work needed
81
85
  if (this._zone?.zoneId) {
82
- // Wildcard certs produce duplicate CNAME names across SANs deduplicate before sending
83
- const unique = Array.from(new Map(this.validationRecords.map(r => [r.name, r])).values());
86
+ // Wildcard certs produce duplicate CNAME names across SANs - deduplicate before sending
87
+ const unique = Array.from(new Map(this.validationRecords.map((r) => [r.name, r])).values());
84
88
  const r53 = getR53Client();
85
89
  await r53.send(new ChangeResourceRecordSetsCommand({
86
90
  HostedZoneId: this._zone.zoneId,
87
91
  ChangeBatch: {
88
- Changes: unique.map(r => ({
89
- Action: 'UPSERT',
92
+ Changes: unique.map((r) => ({
93
+ Action: "UPSERT",
90
94
  ResourceRecordSet: {
91
- Name: r.name.replace(/\.$/, ''),
92
- Type: 'CNAME',
95
+ Name: r.name.replace(/\.$/, ""),
96
+ Type: "CNAME",
93
97
  TTL: 300,
94
- ResourceRecords: [{ Value: r.value.replace(/\.$/, '') }],
98
+ ResourceRecords: [{ Value: r.value.replace(/\.$/, "") }],
95
99
  },
96
100
  })),
97
101
  },
98
102
  }));
99
103
  console.log(` ✅ Auto-wrote ${unique.length} validation CNAME(s) to Route53`);
100
104
  }
101
- // Step 3: now wait for ISSUED records are in DNS so this will actually resolve
105
+ // Step 3: now wait for ISSUED - records are in DNS so this will actually resolve
102
106
  await this.waitFor(`certificate "${this.domainName}" to be validated`, async () => {
103
107
  const detail = await acm.send(new DescribeCertificateCommand({ CertificateArn: this.resolvedArn }));
104
- return detail.Certificate?.Status === 'ISSUED';
108
+ return detail.Certificate?.Status === "ISSUED";
105
109
  }, { intervalMs: 15_000, timeoutMs: 600_000 });
106
110
  console.log(` ✅ Certificate issued: ${this.resolvedArn}`);
107
111
  return { arn: this.resolvedArn };
@@ -1,17 +1,17 @@
1
- import { S3Client } from '@aws-sdk/client-s3';
2
- import { CloudFrontClient } from '@aws-sdk/client-cloudfront';
3
- import { Route53Client } from '@aws-sdk/client-route-53';
4
- import { Route53DomainsClient } from '@aws-sdk/client-route-53-domains';
5
- import { ACMClient } from '@aws-sdk/client-acm';
6
- import { LambdaClient } from '@aws-sdk/client-lambda';
7
- import { IAMClient } from '@aws-sdk/client-iam';
8
- import { ApiGatewayV2Client } from '@aws-sdk/client-apigatewayv2';
9
- import { ECSClient } from '@aws-sdk/client-ecs';
10
- import { EC2Client } from '@aws-sdk/client-ec2';
11
- import { CloudWatchLogsClient } from '@aws-sdk/client-cloudwatch-logs';
12
- import { RDSClient } from '@aws-sdk/client-rds';
13
- import { SQSClient } from '@aws-sdk/client-sqs';
14
- import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
1
+ import { S3Client } from "@aws-sdk/client-s3";
2
+ import { CloudFrontClient } from "@aws-sdk/client-cloudfront";
3
+ import { Route53Client } from "@aws-sdk/client-route-53";
4
+ import { Route53DomainsClient } from "@aws-sdk/client-route-53-domains";
5
+ import { ACMClient } from "@aws-sdk/client-acm";
6
+ import { LambdaClient } from "@aws-sdk/client-lambda";
7
+ import { IAMClient } from "@aws-sdk/client-iam";
8
+ import { ApiGatewayV2Client } from "@aws-sdk/client-apigatewayv2";
9
+ import { ECSClient } from "@aws-sdk/client-ecs";
10
+ import { EC2Client } from "@aws-sdk/client-ec2";
11
+ import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs";
12
+ import { RDSClient } from "@aws-sdk/client-rds";
13
+ import { SQSClient } from "@aws-sdk/client-sqs";
14
+ import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
15
15
  export declare const getS3Client: (region?: string) => S3Client;
16
16
  export declare const getCFClient: () => CloudFrontClient;
17
17
  export declare const getR53Client: () => Route53Client;
@@ -1,18 +1,18 @@
1
- import { S3Client } from '@aws-sdk/client-s3';
2
- import { CloudFrontClient } from '@aws-sdk/client-cloudfront';
3
- import { Route53Client } from '@aws-sdk/client-route-53';
4
- import { Route53DomainsClient } from '@aws-sdk/client-route-53-domains';
5
- import { ACMClient } from '@aws-sdk/client-acm';
6
- import { LambdaClient } from '@aws-sdk/client-lambda';
7
- import { IAMClient } from '@aws-sdk/client-iam';
8
- import { ApiGatewayV2Client } from '@aws-sdk/client-apigatewayv2';
9
- import { ECSClient } from '@aws-sdk/client-ecs';
10
- import { EC2Client } from '@aws-sdk/client-ec2';
11
- import { CloudWatchLogsClient } from '@aws-sdk/client-cloudwatch-logs';
12
- import { RDSClient } from '@aws-sdk/client-rds';
13
- import { SQSClient } from '@aws-sdk/client-sqs';
14
- import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
15
- import { Config } from '../../core/config.js';
1
+ import { S3Client } from "@aws-sdk/client-s3";
2
+ import { CloudFrontClient } from "@aws-sdk/client-cloudfront";
3
+ import { Route53Client } from "@aws-sdk/client-route-53";
4
+ import { Route53DomainsClient } from "@aws-sdk/client-route-53-domains";
5
+ import { ACMClient } from "@aws-sdk/client-acm";
6
+ import { LambdaClient } from "@aws-sdk/client-lambda";
7
+ import { IAMClient } from "@aws-sdk/client-iam";
8
+ import { ApiGatewayV2Client } from "@aws-sdk/client-apigatewayv2";
9
+ import { ECSClient } from "@aws-sdk/client-ecs";
10
+ import { EC2Client } from "@aws-sdk/client-ec2";
11
+ import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs";
12
+ import { RDSClient } from "@aws-sdk/client-rds";
13
+ import { SQSClient } from "@aws-sdk/client-sqs";
14
+ import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
15
+ import { Config } from "../../core/config.js";
16
16
  function getRegion() {
17
17
  const region = Config.get().providers.aws?.region;
18
18
  if (!region)
@@ -20,12 +20,12 @@ function getRegion() {
20
20
  return region;
21
21
  }
22
22
  export const getS3Client = (region) => new S3Client({ region: region ?? getRegion() });
23
- // CloudFront, Route53, ACM, Route53 Domains, and IAM are all global must use us-east-1
24
- export const getCFClient = () => new CloudFrontClient({ region: 'us-east-1' });
25
- export const getR53Client = () => new Route53Client({ region: 'us-east-1' });
26
- export const getR53DomainsClient = () => new Route53DomainsClient({ region: 'us-east-1' });
27
- export const getACMClient = () => new ACMClient({ region: 'us-east-1' });
28
- export const getIAMClient = () => new IAMClient({ region: 'us-east-1' });
23
+ // CloudFront, Route53, ACM, Route53 Domains, and IAM are all global - must use us-east-1
24
+ export const getCFClient = () => new CloudFrontClient({ region: "us-east-1" });
25
+ export const getR53Client = () => new Route53Client({ region: "us-east-1" });
26
+ export const getR53DomainsClient = () => new Route53DomainsClient({ region: "us-east-1" });
27
+ export const getACMClient = () => new ACMClient({ region: "us-east-1" });
28
+ export const getIAMClient = () => new IAMClient({ region: "us-east-1" });
29
29
  export const getLambdaClient = (region) => new LambdaClient({ region: region ?? getRegion() });
30
30
  export const getAPIGWClient = (region) => new ApiGatewayV2Client({ region: region ?? getRegion() });
31
31
  export const getECSClient = (region) => new ECSClient({ region: region ?? getRegion() });
@@ -1,5 +1,5 @@
1
- import { BaseBuilder } from '../../core/resource.js';
2
- import { LambdaBuilder } from './lambda.js';
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { LambdaBuilder } from "./lambda.js";
3
3
  export declare class APIGatewayBuilder extends BaseBuilder {
4
4
  private _routes;
5
5
  resolvedId: string | null;