puls-dev 0.1.9 → 0.2.1

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 (69) hide show
  1. package/README.md +8 -8
  2. package/dist/index.d.ts +0 -7
  3. package/dist/index.js +0 -7
  4. package/dist/providers/aws/api.d.ts +4 -0
  5. package/dist/providers/aws/api.js +4 -0
  6. package/dist/providers/aws/cloudwatch.d.ts +44 -0
  7. package/dist/providers/aws/cloudwatch.js +205 -0
  8. package/dist/providers/aws/cloudwatch.test.d.ts +1 -0
  9. package/dist/providers/aws/cloudwatch.test.js +224 -0
  10. package/dist/providers/aws/fargate.d.ts +2 -0
  11. package/dist/providers/aws/fargate.js +6 -0
  12. package/dist/providers/aws/iam.d.ts +52 -0
  13. package/dist/providers/aws/iam.js +307 -0
  14. package/dist/providers/aws/iam.test.d.ts +1 -0
  15. package/dist/providers/aws/iam.test.js +367 -0
  16. package/dist/providers/aws/index.d.ts +8 -0
  17. package/dist/providers/aws/index.js +8 -0
  18. package/dist/providers/aws/lambda.d.ts +3 -1
  19. package/dist/providers/aws/lambda.js +17 -8
  20. package/dist/providers/aws/lambda.test.d.ts +1 -0
  21. package/dist/providers/aws/lambda.test.js +189 -0
  22. package/dist/providers/aws/rds.d.ts +1 -0
  23. package/dist/providers/aws/rds.js +4 -1
  24. package/dist/providers/aws/route53.d.ts +1 -1
  25. package/dist/providers/aws/route53.js +20 -12
  26. package/dist/providers/aws/route53.test.d.ts +1 -0
  27. package/dist/providers/aws/route53.test.js +229 -0
  28. package/dist/providers/aws/s3.d.ts +3 -0
  29. package/dist/providers/aws/s3.js +65 -3
  30. package/dist/providers/aws/s3.test.d.ts +1 -0
  31. package/dist/providers/aws/s3.test.js +172 -0
  32. package/dist/providers/aws/sns.d.ts +22 -0
  33. package/dist/providers/aws/sns.js +146 -0
  34. package/dist/providers/aws/sns.test.d.ts +1 -0
  35. package/dist/providers/aws/sns.test.js +162 -0
  36. package/dist/providers/do/api.js +5 -1
  37. package/dist/providers/do/certificate.test.d.ts +1 -0
  38. package/dist/providers/do/certificate.test.js +133 -0
  39. package/dist/providers/do/domain.d.ts +12 -1
  40. package/dist/providers/do/domain.js +129 -13
  41. package/dist/providers/do/domain.test.d.ts +1 -0
  42. package/dist/providers/do/domain.test.js +200 -0
  43. package/dist/providers/do/droplet.js +2 -2
  44. package/dist/providers/do/droplet.test.d.ts +1 -0
  45. package/dist/providers/do/droplet.test.js +265 -0
  46. package/dist/providers/do/firewall.test.d.ts +1 -0
  47. package/dist/providers/do/firewall.test.js +176 -0
  48. package/dist/providers/do/index.d.ts +1 -0
  49. package/dist/providers/do/index.js +1 -0
  50. package/dist/providers/do/load_balancer.d.ts +39 -5
  51. package/dist/providers/do/load_balancer.js +272 -30
  52. package/dist/providers/do/load_balancer.test.d.ts +1 -0
  53. package/dist/providers/do/load_balancer.test.js +269 -0
  54. package/dist/providers/firebase/api.js +2 -2
  55. package/dist/providers/firebase/functions.d.ts +1 -0
  56. package/dist/providers/firebase/functions.js +24 -10
  57. package/dist/providers/firebase/functions.test.d.ts +1 -0
  58. package/dist/providers/firebase/functions.test.js +297 -0
  59. package/dist/providers/firebase/hosting.js +5 -5
  60. package/dist/providers/firebase/hosting.test.d.ts +1 -0
  61. package/dist/providers/firebase/hosting.test.js +181 -0
  62. package/dist/providers/proxmox/index.d.ts +1 -0
  63. package/dist/providers/proxmox/index.js +1 -0
  64. package/dist/providers/proxmox/vm.d.ts +2 -1
  65. package/dist/providers/proxmox/vm.js +39 -53
  66. package/dist/providers/proxmox/vm.test.d.ts +1 -0
  67. package/dist/providers/proxmox/vm.test.js +155 -0
  68. package/dist/types/aws.d.ts +11 -0
  69. package/package.json +105 -6
@@ -0,0 +1,133 @@
1
+ import { test, describe, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { CertificateBuilder } from './certificate.js';
4
+ import { Config } from '../../core/config.js';
5
+ describe('CertificateBuilder Unit Tests', () => {
6
+ let originalFetch;
7
+ let fetchCalls = [];
8
+ let mockResponses = {};
9
+ beforeEach(() => {
10
+ Config.set({
11
+ dryRun: false,
12
+ providers: {
13
+ do: { token: 'fake-do-token' }
14
+ }
15
+ });
16
+ originalFetch = globalThis.fetch;
17
+ fetchCalls = [];
18
+ mockResponses = {};
19
+ globalThis.fetch = async (input, init) => {
20
+ const url = String(input);
21
+ const method = init?.method ?? 'GET';
22
+ const body = init?.body ? JSON.parse(init.body) : undefined;
23
+ const headers = init?.headers;
24
+ fetchCalls.push({ url, method, body, headers });
25
+ const matchKey = Object.keys(mockResponses).find(key => {
26
+ const [mMethod, mPath] = key.split(' ');
27
+ return method === mMethod && url.includes(mPath);
28
+ });
29
+ if (matchKey) {
30
+ const resp = mockResponses[matchKey];
31
+ return {
32
+ ok: resp.status >= 200 && resp.status < 300,
33
+ status: resp.status,
34
+ json: async () => resp.body,
35
+ text: async () => JSON.stringify(resp.body),
36
+ };
37
+ }
38
+ return {
39
+ ok: false,
40
+ status: 404,
41
+ json: async () => ({ message: 'Not found' }),
42
+ text: async () => 'Not found',
43
+ };
44
+ };
45
+ });
46
+ afterEach(() => {
47
+ globalThis.fetch = originalFetch;
48
+ });
49
+ test('gracefully handles discovery when certificate does not exist', async () => {
50
+ mockResponses['GET /certificates'] = {
51
+ status: 200,
52
+ body: { certificates: [] }
53
+ };
54
+ const builder = new CertificateBuilder('example.com');
55
+ const discoveryResult = await builder.discoveryPromise;
56
+ assert.strictEqual(discoveryResult, null);
57
+ assert.strictEqual(fetchCalls.length, 1);
58
+ assert.strictEqual(fetchCalls[0].method, 'GET');
59
+ assert.ok(fetchCalls[0].url.endsWith('/certificates?per_page=200'));
60
+ });
61
+ test('discovers certificate successfully when it exists', async () => {
62
+ mockResponses['GET /certificates'] = {
63
+ status: 200,
64
+ body: {
65
+ certificates: [
66
+ { id: 'cert-123', name: 'ssl-example.com', state: 'active' }
67
+ ]
68
+ }
69
+ };
70
+ const builder = new CertificateBuilder('example.com');
71
+ const discoveryResult = await builder.discoveryPromise;
72
+ assert.ok(discoveryResult);
73
+ assert.strictEqual(discoveryResult.id, 'cert-123');
74
+ assert.strictEqual(discoveryResult.name, 'ssl-example.com');
75
+ });
76
+ test('performs clean dry-run planning without making write requests', async () => {
77
+ Config.set({
78
+ dryRun: true,
79
+ providers: { do: { token: 'fake-token' } }
80
+ });
81
+ mockResponses['GET /certificates'] = {
82
+ status: 200,
83
+ body: { certificates: [] }
84
+ };
85
+ const builder = new CertificateBuilder('example.com');
86
+ const result = await builder.deploy();
87
+ assert.ok(result);
88
+ assert.strictEqual(result.name, 'ssl-example.com');
89
+ const writeCalls = fetchCalls.filter(c => c.method !== 'GET');
90
+ assert.strictEqual(writeCalls.length, 0);
91
+ });
92
+ test('deploys new certificate when missing', async () => {
93
+ mockResponses['GET /certificates'] = {
94
+ status: 200,
95
+ body: { certificates: [] }
96
+ };
97
+ mockResponses['POST /certificates'] = {
98
+ status: 201,
99
+ body: {
100
+ certificate: { id: 'cert-789', name: 'ssl-example.com', type: 'lets_encrypt' }
101
+ }
102
+ };
103
+ const builder = new CertificateBuilder('example.com');
104
+ const result = await builder.deploy();
105
+ assert.ok(result);
106
+ assert.strictEqual(result.id, 'cert-789');
107
+ const postCall = fetchCalls.find(c => c.method === 'POST');
108
+ assert.ok(postCall);
109
+ assert.ok(postCall.url.endsWith('/certificates'));
110
+ assert.deepStrictEqual(postCall.body, {
111
+ name: 'ssl-example.com',
112
+ type: 'lets_encrypt',
113
+ dns_names: ['*.example.com', 'example.com']
114
+ });
115
+ });
116
+ test('skips certificate deployment if certificate already exists', async () => {
117
+ mockResponses['GET /certificates'] = {
118
+ status: 200,
119
+ body: {
120
+ certificates: [
121
+ { id: 'cert-123', name: 'ssl-example.com', state: 'active' }
122
+ ]
123
+ }
124
+ };
125
+ const builder = new CertificateBuilder('example.com');
126
+ const result = await builder.deploy();
127
+ assert.ok(result);
128
+ assert.strictEqual(result.id, 'cert-123');
129
+ // Only GET discovery should have run, no writes
130
+ const writeCalls = fetchCalls.filter(c => c.method !== 'GET');
131
+ assert.strictEqual(writeCalls.length, 0);
132
+ });
133
+ });
@@ -2,9 +2,14 @@ import { BaseBuilder } from "../../core/resource.js";
2
2
  import { Output } from "../../core/output.js";
3
3
  import { DropletBuilder } from "./droplet.js";
4
4
  export interface DNSRecord {
5
- type: "A" | "CNAME" | "TXT" | "MX";
5
+ type: "A" | "CNAME" | "TXT" | "MX" | "AAAA" | "SRV" | "CAA";
6
6
  name: string;
7
7
  value: string | DropletBuilder | Output<string>;
8
+ priority?: number;
9
+ port?: number;
10
+ weight?: number;
11
+ flags?: number;
12
+ tag?: string;
8
13
  }
9
14
  export declare class DomainBuilder extends BaseBuilder {
10
15
  domainName: string;
@@ -14,8 +19,14 @@ export declare class DomainBuilder extends BaseBuilder {
14
19
  withSSL(): this;
15
20
  pointer(name: string, target: DropletBuilder | Output<string> | string): this;
16
21
  cname(name: string, target: string): this;
22
+ aaaa(name: string, target: string | Output<string>): this;
23
+ txt(name: string, target: string): this;
24
+ mx(name: string, target: string, priority?: number): this;
25
+ srv(name: string, target: string, port: number, priority?: number, weight?: number): this;
26
+ caa(name: string, tag: string, target: string, flags?: number): this;
17
27
  deploy(): Promise<{
18
28
  domain: string;
19
29
  records: DNSRecord[];
20
30
  }>;
31
+ destroy(): Promise<any>;
21
32
  }
@@ -35,6 +35,26 @@ export class DomainBuilder extends BaseBuilder {
35
35
  this.records.push({ type: "CNAME", name, value: target });
36
36
  return this;
37
37
  }
38
+ aaaa(name, target) {
39
+ this.records.push({ type: "AAAA", name, value: target });
40
+ return this;
41
+ }
42
+ txt(name, target) {
43
+ this.records.push({ type: "TXT", name, value: target });
44
+ return this;
45
+ }
46
+ mx(name, target, priority = 10) {
47
+ this.records.push({ type: "MX", name, value: target, priority });
48
+ return this;
49
+ }
50
+ srv(name, target, port, priority = 10, weight = 10) {
51
+ this.records.push({ type: "SRV", name, value: target, port, priority, weight });
52
+ return this;
53
+ }
54
+ caa(name, tag, target, flags = 0) {
55
+ this.records.push({ type: "CAA", name, value: target, tag, flags });
56
+ return this;
57
+ }
38
58
  async deploy() {
39
59
  const dryRun = this.isDryRunActive();
40
60
  const existing = await this.discoveryPromise;
@@ -49,6 +69,19 @@ export class DomainBuilder extends BaseBuilder {
49
69
  console.log(`🚀 Created domain ${this.domainName}`);
50
70
  }
51
71
  }
72
+ // Fetch existing records in a single batch
73
+ let existingRecords = [];
74
+ if (existing) {
75
+ try {
76
+ const res = await api.get(`/domains/${this.domainName}/records?per_page=200`);
77
+ existingRecords = res.domain_records;
78
+ }
79
+ catch {
80
+ existingRecords = [];
81
+ }
82
+ }
83
+ // Track matched existing records
84
+ const consumedRecordIds = new Set();
52
85
  for (const record of this.records) {
53
86
  let data;
54
87
  if (record.value instanceof Output) {
@@ -62,24 +95,107 @@ export class DomainBuilder extends BaseBuilder {
62
95
  else {
63
96
  data = record.value;
64
97
  }
65
- if (dryRun) {
66
- console.log(` 📝 [PLAN] ${record.type} ${record.name}.${this.domainName} ${data}`);
98
+ const targetPriority = record.priority ?? null;
99
+ const targetPort = record.port ?? null;
100
+ const targetWeight = record.weight ?? null;
101
+ const targetFlags = record.flags ?? null;
102
+ const targetTag = record.tag ?? null;
103
+ // 1. Check for a perfect match
104
+ const perfectMatch = existingRecords.find((r) => {
105
+ if (consumedRecordIds.has(r.id))
106
+ return false;
107
+ return (r.type === record.type &&
108
+ r.name === record.name &&
109
+ String(r.data) === String(data) &&
110
+ (r.priority ?? null) === targetPriority &&
111
+ (r.port ?? null) === targetPort &&
112
+ (r.weight ?? null) === targetWeight &&
113
+ (r.flags ?? null) === targetFlags &&
114
+ (r.tag ?? null) === targetTag);
115
+ });
116
+ if (perfectMatch) {
117
+ consumedRecordIds.add(perfectMatch.id);
118
+ console.log(` ✅ ${record.type} ${record.name}.${this.domainName} is up to date (→ ${data})`);
67
119
  continue;
68
120
  }
69
- // Delete existing record with same type+name before creating
70
- const existing_records = await api.get(`/domains/${this.domainName}/records?per_page=200`);
71
- const dupe = existing_records.domain_records.find((r) => r.type === record.type && r.name === record.name);
72
- if (dupe)
73
- await api.delete(`/domains/${this.domainName}/records/${dupe.id}`);
74
- await api.post(`/domains/${this.domainName}/records`, {
75
- type: record.type,
76
- name: record.name,
77
- data,
78
- ttl: 3600,
121
+ // 2. Look for updateable match (same type and name, different data)
122
+ const updateableMatch = existingRecords.find((r) => {
123
+ if (consumedRecordIds.has(r.id))
124
+ return false;
125
+ return r.type === record.type && r.name === record.name;
79
126
  });
80
- console.log(` ✅ ${record.type} ${record.name}.${this.domainName} → ${data}`);
127
+ if (updateableMatch) {
128
+ consumedRecordIds.add(updateableMatch.id);
129
+ if (dryRun) {
130
+ console.log(` 📝 [PLAN] Update ${record.type} ${record.name}.${this.domainName} → ${data} (was ${updateableMatch.data})`);
131
+ }
132
+ else {
133
+ await api.put(`/domains/${this.domainName}/records/${updateableMatch.id}`, {
134
+ type: record.type,
135
+ name: record.name,
136
+ data,
137
+ ttl: 3600,
138
+ priority: targetPriority,
139
+ port: targetPort,
140
+ weight: targetWeight,
141
+ flags: targetFlags,
142
+ tag: targetTag,
143
+ });
144
+ console.log(` 🔄 Updated ${record.type} ${record.name}.${this.domainName} → ${data}`);
145
+ }
146
+ }
147
+ else {
148
+ // 3. No match found, create a new record
149
+ if (dryRun) {
150
+ console.log(` 📝 [PLAN] Create ${record.type} ${record.name}.${this.domainName} → ${data}`);
151
+ }
152
+ else {
153
+ await api.post(`/domains/${this.domainName}/records`, {
154
+ type: record.type,
155
+ name: record.name,
156
+ data,
157
+ ttl: 3600,
158
+ priority: targetPriority,
159
+ port: targetPort,
160
+ weight: targetWeight,
161
+ flags: targetFlags,
162
+ tag: targetTag,
163
+ });
164
+ console.log(` 🚀 Created ${record.type} ${record.name}.${this.domainName} → ${data}`);
165
+ }
166
+ }
167
+ }
168
+ // 4. Delete duplicate/stale records of the types we declared
169
+ const declaredTypesAndNames = new Set(this.records.map((r) => `${r.type}:${r.name}`));
170
+ for (const r of existingRecords) {
171
+ if (consumedRecordIds.has(r.id))
172
+ continue;
173
+ if (declaredTypesAndNames.has(`${r.type}:${r.name}`)) {
174
+ if (dryRun) {
175
+ console.log(` 📝 [PLAN] Delete stale ${r.type} ${r.name}.${this.domainName} (→ ${r.data})`);
176
+ }
177
+ else {
178
+ await api.delete(`/domains/${this.domainName}/records/${r.id}`);
179
+ console.log(` 🗑️ Deleted stale ${r.type} ${r.name}.${this.domainName}`);
180
+ }
181
+ }
81
182
  }
82
183
  await this.deploySidecars();
83
184
  return { domain: this.domainName, records: this.records };
84
185
  }
186
+ async destroy() {
187
+ const dryRun = this.isDryRunActive();
188
+ await this.discoveryPromise;
189
+ console.log(`\n🗑️ Destroying DNS domain "${this.domainName}"...`);
190
+ if (dryRun) {
191
+ console.log(` 📝 [PLAN] Would delete domain ${this.domainName}`);
192
+ }
193
+ else {
194
+ const api = getDoApi();
195
+ await api.delete(`/domains/${this.domainName}`);
196
+ console.log(` ✅ Deleted.`);
197
+ }
198
+ await this.destroySidecars();
199
+ return { destroyed: this.domainName };
200
+ }
85
201
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,200 @@
1
+ import { test, describe, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { DomainBuilder } from "./domain.js";
4
+ import { Config } from "../../core/config.js";
5
+ describe("DomainBuilder Unit Tests", () => {
6
+ let originalFetch;
7
+ let fetchCalls = [];
8
+ let mockResponses = {};
9
+ beforeEach(() => {
10
+ Config.set({
11
+ dryRun: false,
12
+ providers: {
13
+ do: { token: "fake-do-token" }
14
+ }
15
+ });
16
+ originalFetch = globalThis.fetch;
17
+ fetchCalls = [];
18
+ mockResponses = {};
19
+ globalThis.fetch = async (input, init) => {
20
+ const url = String(input);
21
+ const method = init?.method ?? "GET";
22
+ const body = init?.body ? JSON.parse(init.body) : undefined;
23
+ const headers = init?.headers;
24
+ fetchCalls.push({ url, method, body, headers });
25
+ const matchKey = Object.keys(mockResponses).find(key => {
26
+ const [mMethod, mPath] = key.split(" ");
27
+ return method === mMethod && url.endsWith(mPath);
28
+ });
29
+ if (matchKey) {
30
+ const resp = mockResponses[matchKey];
31
+ return {
32
+ ok: resp.status >= 200 && resp.status < 300,
33
+ status: resp.status,
34
+ json: async () => resp.body,
35
+ text: async () => JSON.stringify(resp.body),
36
+ };
37
+ }
38
+ return {
39
+ ok: false,
40
+ status: 404,
41
+ json: async () => ({ message: "Not found" }),
42
+ text: async () => "Not found",
43
+ };
44
+ };
45
+ });
46
+ afterEach(() => {
47
+ globalThis.fetch = originalFetch;
48
+ });
49
+ test("gracefully handles discovery when domain does not exist", async () => {
50
+ mockResponses["GET /domains/new-domain.com"] = {
51
+ status: 404,
52
+ body: { message: "Domain not found" }
53
+ };
54
+ const builder = new DomainBuilder("new-domain.com");
55
+ const discoveryResult = await builder.discoveryPromise;
56
+ assert.strictEqual(discoveryResult, null);
57
+ assert.strictEqual(fetchCalls.length, 1);
58
+ assert.strictEqual(fetchCalls[0].method, "GET");
59
+ assert.ok(fetchCalls[0].url.endsWith("/domains/new-domain.com"));
60
+ });
61
+ test("discovers domain successfully when it exists", async () => {
62
+ mockResponses["GET /domains/exists.com"] = {
63
+ status: 200,
64
+ body: {
65
+ domain: {
66
+ name: "exists.com",
67
+ ttl: 1800,
68
+ zone_file: "..."
69
+ }
70
+ }
71
+ };
72
+ const builder = new DomainBuilder("exists.com");
73
+ const discoveryResult = await builder.discoveryPromise;
74
+ assert.ok(discoveryResult);
75
+ assert.strictEqual(discoveryResult.name, "exists.com");
76
+ assert.strictEqual(fetchCalls.length, 1);
77
+ });
78
+ test("performs clean dry-run planning without making write requests", async () => {
79
+ Config.set({
80
+ dryRun: true,
81
+ providers: { do: { token: "fake-token" } }
82
+ });
83
+ mockResponses["GET /domains/dryrun.com"] = {
84
+ status: 404,
85
+ body: { message: "Domain not found" }
86
+ };
87
+ const builder = new DomainBuilder("dryrun.com");
88
+ builder.pointer("www", "1.2.3.4");
89
+ builder.cname("blog", "blog.dryrun.com");
90
+ const result = await builder.deploy();
91
+ assert.deepStrictEqual(result, {
92
+ domain: "dryrun.com",
93
+ records: [
94
+ { type: "A", name: "www", value: "1.2.3.4" },
95
+ { type: "CNAME", name: "blog", value: "blog.dryrun.com" }
96
+ ]
97
+ });
98
+ const writeCalls = fetchCalls.filter(c => c.method !== "GET");
99
+ assert.strictEqual(writeCalls.length, 0);
100
+ });
101
+ test("deploys new domain and creates records when domain is missing", async () => {
102
+ mockResponses["GET /domains/new.com"] = {
103
+ status: 404,
104
+ body: { message: "Domain not found" }
105
+ };
106
+ mockResponses["POST /domains"] = {
107
+ status: 201,
108
+ body: { domain: { name: "new.com" } }
109
+ };
110
+ mockResponses["POST /domains/new.com/records"] = {
111
+ status: 201,
112
+ body: { domain_record: { id: 101 } }
113
+ };
114
+ const builder = new DomainBuilder("new.com");
115
+ builder.pointer("www", "5.6.7.8");
116
+ await builder.deploy();
117
+ assert.strictEqual(fetchCalls.length, 3);
118
+ assert.strictEqual(fetchCalls[0].method, "GET");
119
+ assert.strictEqual(fetchCalls[1].method, "POST");
120
+ assert.ok(fetchCalls[1].url.endsWith("/domains"));
121
+ assert.deepStrictEqual(fetchCalls[1].body, { name: "new.com" });
122
+ assert.strictEqual(fetchCalls[2].method, "POST");
123
+ assert.ok(fetchCalls[2].url.endsWith("/domains/new.com/records"));
124
+ assert.deepStrictEqual(fetchCalls[2].body, {
125
+ type: "A",
126
+ name: "www",
127
+ data: "5.6.7.8",
128
+ ttl: 3600,
129
+ priority: null,
130
+ port: null,
131
+ weight: null,
132
+ flags: null,
133
+ tag: null
134
+ });
135
+ });
136
+ test("syncs records: skips matching, updates out-of-date, deletes stale/duplicate", async () => {
137
+ mockResponses["GET /domains/sync.com"] = {
138
+ status: 200,
139
+ body: { domain: { name: "sync.com" } }
140
+ };
141
+ mockResponses["GET /domains/sync.com/records?per_page=200"] = {
142
+ status: 200,
143
+ body: {
144
+ domain_records: [
145
+ { id: 10, type: "A", name: "www", data: "1.1.1.1" },
146
+ { id: 20, type: "A", name: "api", data: "2.2.2.2" },
147
+ { id: 30, type: "A", name: "api", data: "4.4.4.4" }
148
+ ]
149
+ }
150
+ };
151
+ mockResponses["PUT /domains/sync.com/records/20"] = {
152
+ status: 200,
153
+ body: { domain_record: { id: 20 } }
154
+ };
155
+ mockResponses["DELETE /domains/sync.com/records/30"] = {
156
+ status: 204,
157
+ body: {}
158
+ };
159
+ const builder = new DomainBuilder("sync.com");
160
+ builder.pointer("www", "1.1.1.1");
161
+ builder.pointer("api", "3.3.3.3");
162
+ await builder.deploy();
163
+ const putCall = fetchCalls.find(c => c.method === "PUT");
164
+ assert.ok(putCall);
165
+ assert.ok(putCall.url.endsWith("/domains/sync.com/records/20"));
166
+ assert.deepStrictEqual(putCall.body, {
167
+ type: "A",
168
+ name: "api",
169
+ data: "3.3.3.3",
170
+ ttl: 3600,
171
+ priority: null,
172
+ port: null,
173
+ weight: null,
174
+ flags: null,
175
+ tag: null
176
+ });
177
+ const deleteCall = fetchCalls.find(c => c.method === "DELETE");
178
+ assert.ok(deleteCall);
179
+ assert.ok(deleteCall.url.endsWith("/domains/sync.com/records/30"));
180
+ const postCall = fetchCalls.find(c => c.method === "POST");
181
+ assert.strictEqual(postCall, undefined);
182
+ });
183
+ test("destroys domain successfully", async () => {
184
+ mockResponses["GET /domains/destroy.com"] = {
185
+ status: 200,
186
+ body: { domain: { name: "destroy.com" } }
187
+ };
188
+ mockResponses["DELETE /domains/destroy.com"] = {
189
+ status: 204,
190
+ body: {}
191
+ };
192
+ const builder = new DomainBuilder("destroy.com");
193
+ await builder.discoveryPromise;
194
+ const result = await builder.destroy();
195
+ assert.deepStrictEqual(result, { destroyed: "destroy.com" });
196
+ const deleteCall = fetchCalls.find(c => c.method === "DELETE");
197
+ assert.ok(deleteCall);
198
+ assert.ok(deleteCall.url.endsWith("/domains/destroy.com"));
199
+ });
200
+ });
@@ -1,4 +1,4 @@
1
- import { readFileSync } from 'fs';
1
+ import fs from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import { OS, REGION, SIZE, NETWORK } from '../../types/do.js';
4
4
  import { Config } from '../../core/config.js';
@@ -71,7 +71,7 @@ export class DropletBuilder extends BaseBuilder {
71
71
  }
72
72
  async resolveOrRegisterSshKey(api) {
73
73
  const pubPath = this.sshKeyPath.replace(/\.pub$/, '') + '.pub';
74
- const pubKey = readFileSync(pubPath, 'utf8').trim();
74
+ const pubKey = fs.readFileSync(pubPath, 'utf8').trim();
75
75
  const { ssh_keys } = await api.get('/account/keys?per_page=200');
76
76
  const existing = ssh_keys.find(k => k.public_key.trim() === pubKey);
77
77
  if (existing)
@@ -0,0 +1 @@
1
+ export {};