@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,1788 @@
1
+ import {
2
+ Route53Client
3
+ } from "./chunk-tnztxpcb.js";
4
+ import {
5
+ AWSClient
6
+ } from "./chunk-arsh1g5h.js";
7
+ // src/dns/porkbun.ts
8
+ var PORKBUN_API_URL = "https://api.porkbun.com/api/json/v3";
9
+
10
+ class PorkbunProvider {
11
+ name = "porkbun";
12
+ apiKey;
13
+ secretKey;
14
+ constructor(apiKey, secretKey) {
15
+ this.apiKey = apiKey;
16
+ this.secretKey = secretKey;
17
+ }
18
+ async request(endpoint, additionalBody = {}) {
19
+ const response = await fetch(`${PORKBUN_API_URL}${endpoint}`, {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json"
23
+ },
24
+ body: JSON.stringify({
25
+ apikey: this.apiKey,
26
+ secretapikey: this.secretKey,
27
+ ...additionalBody
28
+ })
29
+ });
30
+ if (!response.ok) {
31
+ throw new Error(`Porkbun API error: ${response.status} ${response.statusText}`);
32
+ }
33
+ const data = await response.json();
34
+ if (data.status === "ERROR") {
35
+ throw new Error(`Porkbun API error: ${data.message || "Unknown error"}`);
36
+ }
37
+ return data;
38
+ }
39
+ getSubdomain(recordName, domain) {
40
+ const cleanName = recordName.replace(/\.$/, "");
41
+ const cleanDomain = domain.replace(/\.$/, "");
42
+ if (cleanName === cleanDomain) {
43
+ return "";
44
+ }
45
+ if (cleanName.endsWith(`.${cleanDomain}`)) {
46
+ return cleanName.slice(0, -(cleanDomain.length + 1));
47
+ }
48
+ return cleanName;
49
+ }
50
+ getRootDomain(domain) {
51
+ const parts = domain.replace(/\.$/, "").split(".");
52
+ if (parts.length >= 2) {
53
+ return parts.slice(-2).join(".");
54
+ }
55
+ return domain;
56
+ }
57
+ async createRecord(domain, record) {
58
+ try {
59
+ const rootDomain = this.getRootDomain(domain);
60
+ const subdomain = this.getSubdomain(record.name, rootDomain);
61
+ const body = {
62
+ type: record.type,
63
+ content: record.content,
64
+ ttl: String(record.ttl || 600)
65
+ };
66
+ if (subdomain) {
67
+ body.name = subdomain;
68
+ }
69
+ if ((record.type === "MX" || record.type === "SRV") && record.priority !== undefined) {
70
+ body.prio = String(record.priority);
71
+ }
72
+ if (record.type === "SRV" && record.weight !== undefined && record.port !== undefined) {
73
+ body.content = `${record.weight} ${record.port} ${record.content}`;
74
+ }
75
+ const response = await this.request(`/dns/create/${rootDomain}`, body);
76
+ return {
77
+ success: true,
78
+ id: response.id?.toString(),
79
+ message: "Record created successfully"
80
+ };
81
+ } catch (error) {
82
+ return {
83
+ success: false,
84
+ message: error instanceof Error ? error.message : "Unknown error"
85
+ };
86
+ }
87
+ }
88
+ async upsertRecord(domain, record) {
89
+ try {
90
+ const rootDomain = this.getRootDomain(domain);
91
+ const subdomain = this.getSubdomain(record.name, rootDomain);
92
+ const existing = await this.listRecords(domain, record.type);
93
+ if (existing.success) {
94
+ const matchingRecord = existing.records.find((r) => {
95
+ const existingSubdomain = this.getSubdomain(r.name, rootDomain);
96
+ return existingSubdomain === subdomain && r.type === record.type;
97
+ });
98
+ if (matchingRecord?.id) {
99
+ const body = {
100
+ type: record.type,
101
+ content: record.content,
102
+ ttl: String(record.ttl || 600)
103
+ };
104
+ if (subdomain) {
105
+ body.name = subdomain;
106
+ }
107
+ if ((record.type === "MX" || record.type === "SRV") && record.priority !== undefined) {
108
+ body.prio = String(record.priority);
109
+ }
110
+ if (record.type === "SRV" && record.weight !== undefined && record.port !== undefined) {
111
+ body.content = `${record.weight} ${record.port} ${record.content}`;
112
+ }
113
+ await this.request(`/dns/edit/${rootDomain}/${matchingRecord.id}`, body);
114
+ return {
115
+ success: true,
116
+ id: matchingRecord.id,
117
+ message: "Record updated successfully"
118
+ };
119
+ }
120
+ }
121
+ return this.createRecord(domain, record);
122
+ } catch (error) {
123
+ return this.createRecord(domain, record);
124
+ }
125
+ }
126
+ async deleteRecord(domain, record) {
127
+ try {
128
+ const rootDomain = this.getRootDomain(domain);
129
+ const subdomain = this.getSubdomain(record.name, rootDomain);
130
+ const existing = await this.listRecords(domain, record.type);
131
+ if (!existing.success) {
132
+ return {
133
+ success: false,
134
+ message: "Failed to list records"
135
+ };
136
+ }
137
+ const matchingRecord = existing.records.find((r) => {
138
+ const existingSubdomain = this.getSubdomain(r.name, rootDomain);
139
+ return existingSubdomain === subdomain && r.type === record.type && r.content === record.content;
140
+ });
141
+ if (!matchingRecord?.id) {
142
+ return {
143
+ success: false,
144
+ message: "Record not found"
145
+ };
146
+ }
147
+ await this.request(`/dns/delete/${rootDomain}/${matchingRecord.id}`);
148
+ return {
149
+ success: true,
150
+ message: "Record deleted successfully"
151
+ };
152
+ } catch (error) {
153
+ return {
154
+ success: false,
155
+ message: error instanceof Error ? error.message : "Unknown error"
156
+ };
157
+ }
158
+ }
159
+ async listRecords(domain, type) {
160
+ try {
161
+ const rootDomain = this.getRootDomain(domain);
162
+ let endpoint = `/dns/retrieve/${rootDomain}`;
163
+ if (type) {
164
+ endpoint = `/dns/retrieveByNameType/${rootDomain}/${type}`;
165
+ }
166
+ const response = await this.request(endpoint);
167
+ const records = (response.records || []).map((r) => ({
168
+ id: r.id,
169
+ name: r.name || rootDomain,
170
+ type: r.type,
171
+ content: r.content,
172
+ ttl: Number.parseInt(r.ttl, 10),
173
+ priority: r.prio ? Number.parseInt(r.prio, 10) : undefined
174
+ }));
175
+ return {
176
+ success: true,
177
+ records
178
+ };
179
+ } catch (error) {
180
+ return {
181
+ success: false,
182
+ records: [],
183
+ message: error instanceof Error ? error.message : "Unknown error"
184
+ };
185
+ }
186
+ }
187
+ async canManageDomain(domain) {
188
+ try {
189
+ const rootDomain = this.getRootDomain(domain);
190
+ await this.request(`/dns/retrieve/${rootDomain}`);
191
+ return true;
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+ async listDomains() {
197
+ try {
198
+ const response = await this.request("/domain/listAll");
199
+ return (response.domains || []).map((d) => d.domain);
200
+ } catch {
201
+ return [];
202
+ }
203
+ }
204
+ async getNameServers(domain) {
205
+ try {
206
+ const rootDomain = this.getRootDomain(domain);
207
+ const response = await this.request(`/dns/getNS/${rootDomain}`);
208
+ return response.ns || [];
209
+ } catch {
210
+ return [];
211
+ }
212
+ }
213
+ async updateNameServers(domain, nameservers) {
214
+ try {
215
+ const rootDomain = this.getRootDomain(domain);
216
+ await this.request(`/dns/updateNS/${rootDomain}`, {
217
+ ns: nameservers
218
+ });
219
+ return true;
220
+ } catch {
221
+ return false;
222
+ }
223
+ }
224
+ }
225
+ // src/dns/godaddy.ts
226
+ var GODADDY_API_URL = "https://api.godaddy.com";
227
+ var GODADDY_OTE_API_URL = "https://api.ote-godaddy.com";
228
+
229
+ class GoDaddyProvider {
230
+ name = "godaddy";
231
+ apiKey;
232
+ apiSecret;
233
+ baseUrl;
234
+ constructor(apiKey, apiSecret, environment = "production") {
235
+ this.apiKey = apiKey;
236
+ this.apiSecret = apiSecret;
237
+ this.baseUrl = environment === "ote" ? GODADDY_OTE_API_URL : GODADDY_API_URL;
238
+ }
239
+ async request(method, endpoint, body) {
240
+ const url = `${this.baseUrl}${endpoint}`;
241
+ const headers = {
242
+ Authorization: `sso-key ${this.apiKey}:${this.apiSecret}`,
243
+ "Content-Type": "application/json",
244
+ Accept: "application/json"
245
+ };
246
+ const options = {
247
+ method,
248
+ headers
249
+ };
250
+ if (body) {
251
+ options.body = JSON.stringify(body);
252
+ }
253
+ const response = await fetch(url, options);
254
+ if (response.status === 204) {
255
+ return {};
256
+ }
257
+ if (response.ok) {
258
+ const text = await response.text();
259
+ if (text) {
260
+ return JSON.parse(text);
261
+ }
262
+ return {};
263
+ }
264
+ let errorMessage = `GoDaddy API error: ${response.status} ${response.statusText}`;
265
+ try {
266
+ const errorData = await response.json();
267
+ if (errorData.message) {
268
+ errorMessage = `GoDaddy API error: ${errorData.message}`;
269
+ }
270
+ if (errorData.fields) {
271
+ errorMessage += ` - Fields: ${JSON.stringify(errorData.fields)}`;
272
+ }
273
+ } catch {}
274
+ throw new Error(errorMessage);
275
+ }
276
+ getSubdomain(recordName, domain) {
277
+ const cleanName = recordName.replace(/\.$/, "");
278
+ const cleanDomain = domain.replace(/\.$/, "");
279
+ if (cleanName === cleanDomain) {
280
+ return "@";
281
+ }
282
+ if (cleanName.endsWith(`.${cleanDomain}`)) {
283
+ return cleanName.slice(0, -(cleanDomain.length + 1));
284
+ }
285
+ return cleanName;
286
+ }
287
+ getRootDomain(domain) {
288
+ const parts = domain.replace(/\.$/, "").split(".");
289
+ if (parts.length >= 2) {
290
+ return parts.slice(-2).join(".");
291
+ }
292
+ return domain;
293
+ }
294
+ toGoDaddyRecord(record, domain) {
295
+ const rootDomain = this.getRootDomain(domain);
296
+ const subdomain = this.getSubdomain(record.name, rootDomain);
297
+ const gdRecord = {
298
+ type: record.type,
299
+ name: subdomain,
300
+ data: record.content,
301
+ ttl: record.ttl || 600
302
+ };
303
+ if (record.type === "MX" && record.priority !== undefined) {
304
+ gdRecord.priority = record.priority;
305
+ }
306
+ return gdRecord;
307
+ }
308
+ fromGoDaddyRecord(record, domain) {
309
+ const rootDomain = this.getRootDomain(domain);
310
+ let name = record.name;
311
+ if (name === "@") {
312
+ name = rootDomain;
313
+ } else if (!name.endsWith(rootDomain)) {
314
+ name = `${name}.${rootDomain}`;
315
+ }
316
+ return {
317
+ name,
318
+ type: record.type,
319
+ content: record.data,
320
+ ttl: record.ttl,
321
+ priority: record.priority
322
+ };
323
+ }
324
+ async createRecord(domain, record) {
325
+ try {
326
+ const rootDomain = this.getRootDomain(domain);
327
+ const gdRecord = this.toGoDaddyRecord(record, domain);
328
+ await this.request("PATCH", `/v1/domains/${rootDomain}/records`, [gdRecord]);
329
+ return {
330
+ success: true,
331
+ message: "Record created successfully"
332
+ };
333
+ } catch (error) {
334
+ return {
335
+ success: false,
336
+ message: error instanceof Error ? error.message : "Unknown error"
337
+ };
338
+ }
339
+ }
340
+ async upsertRecord(domain, record) {
341
+ try {
342
+ const rootDomain = this.getRootDomain(domain);
343
+ const gdRecord = this.toGoDaddyRecord(record, domain);
344
+ await this.request("PUT", `/v1/domains/${rootDomain}/records/${record.type}/${gdRecord.name}`, [gdRecord]);
345
+ return {
346
+ success: true,
347
+ message: "Record upserted successfully"
348
+ };
349
+ } catch (error) {
350
+ return this.createRecord(domain, record);
351
+ }
352
+ }
353
+ async deleteRecord(domain, record) {
354
+ try {
355
+ const rootDomain = this.getRootDomain(domain);
356
+ const subdomain = this.getSubdomain(record.name, rootDomain);
357
+ const existingRecords = await this.request("GET", `/v1/domains/${rootDomain}/records/${record.type}/${subdomain}`);
358
+ const remainingRecords = existingRecords.filter((r) => r.data !== record.content);
359
+ if (remainingRecords.length === existingRecords.length) {
360
+ return {
361
+ success: false,
362
+ message: "Record not found"
363
+ };
364
+ }
365
+ if (remainingRecords.length === 0) {
366
+ try {
367
+ await this.request("DELETE", `/v1/domains/${rootDomain}/records/${record.type}/${subdomain}`);
368
+ } catch {}
369
+ } else {
370
+ await this.request("PUT", `/v1/domains/${rootDomain}/records/${record.type}/${subdomain}`, remainingRecords);
371
+ }
372
+ return {
373
+ success: true,
374
+ message: "Record deleted successfully"
375
+ };
376
+ } catch (error) {
377
+ return {
378
+ success: false,
379
+ message: error instanceof Error ? error.message : "Unknown error"
380
+ };
381
+ }
382
+ }
383
+ async listRecords(domain, type) {
384
+ try {
385
+ const rootDomain = this.getRootDomain(domain);
386
+ let endpoint = `/v1/domains/${rootDomain}/records`;
387
+ if (type) {
388
+ endpoint = `/v1/domains/${rootDomain}/records/${type}`;
389
+ }
390
+ const records = await this.request("GET", endpoint);
391
+ return {
392
+ success: true,
393
+ records: records.map((r) => this.fromGoDaddyRecord(r, domain))
394
+ };
395
+ } catch (error) {
396
+ return {
397
+ success: false,
398
+ records: [],
399
+ message: error instanceof Error ? error.message : "Unknown error"
400
+ };
401
+ }
402
+ }
403
+ async canManageDomain(domain) {
404
+ try {
405
+ const rootDomain = this.getRootDomain(domain);
406
+ await this.request("GET", `/v1/domains/${rootDomain}`);
407
+ return true;
408
+ } catch {
409
+ return false;
410
+ }
411
+ }
412
+ async listDomains() {
413
+ try {
414
+ const response = await this.request("GET", "/v1/domains");
415
+ return response.map((d) => d.domain);
416
+ } catch {
417
+ return [];
418
+ }
419
+ }
420
+ async getDomainDetails(domain) {
421
+ try {
422
+ const rootDomain = this.getRootDomain(domain);
423
+ const details = await this.request("GET", `/v1/domains/${rootDomain}`);
424
+ return {
425
+ domain: details.domain,
426
+ status: details.status,
427
+ nameServers: details.nameServers,
428
+ expires: details.expires
429
+ };
430
+ } catch {
431
+ return null;
432
+ }
433
+ }
434
+ async updateNameServers(domain, nameservers) {
435
+ try {
436
+ const rootDomain = this.getRootDomain(domain);
437
+ await this.request("PUT", `/v1/domains/${rootDomain}/records/NS`, nameservers.map((ns) => ({
438
+ type: "NS",
439
+ name: "@",
440
+ data: ns,
441
+ ttl: 3600
442
+ })));
443
+ return true;
444
+ } catch {
445
+ return false;
446
+ }
447
+ }
448
+ async checkDomainAvailability(domain) {
449
+ try {
450
+ const result = await this.request("GET", `/v1/domains/available?domain=${encodeURIComponent(domain)}`);
451
+ return {
452
+ available: result.available,
453
+ price: result.price,
454
+ currency: result.currency
455
+ };
456
+ } catch {
457
+ return { available: false };
458
+ }
459
+ }
460
+ }
461
+ // src/dns/cloudflare.ts
462
+ var CLOUDFLARE_API_URL = "https://api.cloudflare.com/client/v4";
463
+
464
+ class CloudflareProvider {
465
+ name = "cloudflare";
466
+ apiToken;
467
+ zoneCache = new Map;
468
+ constructor(apiToken) {
469
+ this.apiToken = apiToken;
470
+ }
471
+ async request(method, endpoint, body) {
472
+ const url = `${CLOUDFLARE_API_URL}${endpoint}`;
473
+ const headers = {
474
+ Authorization: `Bearer ${this.apiToken}`,
475
+ "Content-Type": "application/json"
476
+ };
477
+ const options = {
478
+ method,
479
+ headers
480
+ };
481
+ if (body) {
482
+ options.body = JSON.stringify(body);
483
+ }
484
+ const response = await fetch(url, options);
485
+ const data = await response.json();
486
+ if (!data.success) {
487
+ const errorMessages = data.errors.map((e) => e.message).join(", ");
488
+ throw new Error(`Cloudflare API error: ${errorMessages}`);
489
+ }
490
+ return data;
491
+ }
492
+ getRootDomain(domain) {
493
+ const parts = domain.replace(/\.$/, "").split(".");
494
+ if (parts.length >= 2) {
495
+ return parts.slice(-2).join(".");
496
+ }
497
+ return domain;
498
+ }
499
+ async getZoneId(domain) {
500
+ const rootDomain = this.getRootDomain(domain);
501
+ const cached = this.zoneCache.get(rootDomain);
502
+ if (cached) {
503
+ return cached;
504
+ }
505
+ const response = await this.request("GET", `/zones?name=${encodeURIComponent(rootDomain)}`);
506
+ if (!response.result || response.result.length === 0) {
507
+ throw new Error(`Zone not found for domain: ${rootDomain}`);
508
+ }
509
+ const zoneId = response.result[0].id;
510
+ this.zoneCache.set(rootDomain, zoneId);
511
+ return zoneId;
512
+ }
513
+ getFullRecordName(name, domain) {
514
+ const rootDomain = this.getRootDomain(domain);
515
+ const cleanName = name.replace(/\.$/, "");
516
+ if (!cleanName || cleanName === rootDomain || cleanName === "@") {
517
+ return rootDomain;
518
+ }
519
+ if (cleanName.endsWith(`.${rootDomain}`)) {
520
+ return cleanName;
521
+ }
522
+ return `${cleanName}.${rootDomain}`;
523
+ }
524
+ toCloudflareRecord(record, domain) {
525
+ const cfRecord = {
526
+ type: record.type,
527
+ name: this.getFullRecordName(record.name, domain),
528
+ content: record.content || record.value || "",
529
+ ttl: record.ttl || 1
530
+ };
531
+ if (record.type === "MX" && record.priority !== undefined) {
532
+ cfRecord.priority = record.priority;
533
+ }
534
+ if (record.type === "SRV") {
535
+ if (record.priority !== undefined) {
536
+ cfRecord.priority = record.priority;
537
+ }
538
+ }
539
+ return cfRecord;
540
+ }
541
+ fromCloudflareRecord(record) {
542
+ return {
543
+ id: record.id,
544
+ name: record.name,
545
+ type: record.type,
546
+ content: record.content,
547
+ ttl: record.ttl,
548
+ priority: record.priority
549
+ };
550
+ }
551
+ async createRecord(domain, record) {
552
+ try {
553
+ const zoneId = await this.getZoneId(domain);
554
+ const cfRecord = this.toCloudflareRecord(record, domain);
555
+ const response = await this.request("POST", `/zones/${zoneId}/dns_records`, cfRecord);
556
+ return {
557
+ success: true,
558
+ id: response.result.id,
559
+ message: "Record created successfully"
560
+ };
561
+ } catch (error) {
562
+ return {
563
+ success: false,
564
+ message: error instanceof Error ? error.message : "Unknown error"
565
+ };
566
+ }
567
+ }
568
+ async upsertRecord(domain, record) {
569
+ try {
570
+ const zoneId = await this.getZoneId(domain);
571
+ const fullName = this.getFullRecordName(record.name, domain);
572
+ const existingResponse = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
573
+ const cfRecord = this.toCloudflareRecord(record, domain);
574
+ if (existingResponse.result && existingResponse.result.length > 0) {
575
+ const existingId = existingResponse.result[0].id;
576
+ const response2 = await this.request("PUT", `/zones/${zoneId}/dns_records/${existingId}`, cfRecord);
577
+ return {
578
+ success: true,
579
+ id: response2.result.id,
580
+ message: "Record updated successfully"
581
+ };
582
+ }
583
+ const response = await this.request("POST", `/zones/${zoneId}/dns_records`, cfRecord);
584
+ return {
585
+ success: true,
586
+ id: response.result.id,
587
+ message: "Record created successfully"
588
+ };
589
+ } catch (error) {
590
+ return this.createRecord(domain, record);
591
+ }
592
+ }
593
+ async deleteRecord(domain, record) {
594
+ try {
595
+ const zoneId = await this.getZoneId(domain);
596
+ const fullName = this.getFullRecordName(record.name, domain);
597
+ const existingResponse = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
598
+ if (!existingResponse.result || existingResponse.result.length === 0) {
599
+ return {
600
+ success: false,
601
+ message: "Record not found"
602
+ };
603
+ }
604
+ const matchingRecord = existingResponse.result.find((r) => r.content === record.content);
605
+ if (!matchingRecord) {
606
+ return {
607
+ success: false,
608
+ message: "Record with matching content not found"
609
+ };
610
+ }
611
+ await this.request("DELETE", `/zones/${zoneId}/dns_records/${matchingRecord.id}`);
612
+ return {
613
+ success: true,
614
+ message: "Record deleted successfully"
615
+ };
616
+ } catch (error) {
617
+ return {
618
+ success: false,
619
+ message: error instanceof Error ? error.message : "Unknown error"
620
+ };
621
+ }
622
+ }
623
+ async listRecords(domain, type) {
624
+ try {
625
+ const zoneId = await this.getZoneId(domain);
626
+ let endpoint = `/zones/${zoneId}/dns_records?per_page=100`;
627
+ if (type) {
628
+ endpoint += `&type=${type}`;
629
+ }
630
+ const allRecords = [];
631
+ let page = 1;
632
+ let hasMore = true;
633
+ while (hasMore) {
634
+ const response = await this.request("GET", `${endpoint}&page=${page}`);
635
+ allRecords.push(...response.result || []);
636
+ if (response.result_info) {
637
+ hasMore = page < response.result_info.total_pages;
638
+ page++;
639
+ } else {
640
+ hasMore = false;
641
+ }
642
+ }
643
+ return {
644
+ success: true,
645
+ records: allRecords.map((r) => this.fromCloudflareRecord(r))
646
+ };
647
+ } catch (error) {
648
+ return {
649
+ success: false,
650
+ records: [],
651
+ message: error instanceof Error ? error.message : "Unknown error"
652
+ };
653
+ }
654
+ }
655
+ async canManageDomain(domain) {
656
+ try {
657
+ await this.getZoneId(domain);
658
+ return true;
659
+ } catch {
660
+ return false;
661
+ }
662
+ }
663
+ async listDomains() {
664
+ try {
665
+ const allZones = [];
666
+ let page = 1;
667
+ let hasMore = true;
668
+ while (hasMore) {
669
+ const response = await this.request("GET", `/zones?per_page=50&page=${page}`);
670
+ allZones.push(...response.result || []);
671
+ if (response.result_info) {
672
+ hasMore = page < response.result_info.total_pages;
673
+ page++;
674
+ } else {
675
+ hasMore = false;
676
+ }
677
+ }
678
+ return allZones.map((z) => z.name);
679
+ } catch {
680
+ return [];
681
+ }
682
+ }
683
+ async getZoneDetails(domain) {
684
+ try {
685
+ const zoneId = await this.getZoneId(domain);
686
+ const response = await this.request("GET", `/zones/${zoneId}`);
687
+ return {
688
+ id: response.result.id,
689
+ name: response.result.name,
690
+ status: response.result.status,
691
+ nameServers: response.result.name_servers,
692
+ paused: response.result.paused
693
+ };
694
+ } catch {
695
+ return null;
696
+ }
697
+ }
698
+ async purgeCache(domain, options) {
699
+ try {
700
+ const zoneId = await this.getZoneId(domain);
701
+ const body = {};
702
+ if (options?.purgeEverything) {
703
+ body.purge_everything = true;
704
+ } else {
705
+ if (options?.files)
706
+ body.files = options.files;
707
+ if (options?.tags)
708
+ body.tags = options.tags;
709
+ if (options?.hosts)
710
+ body.hosts = options.hosts;
711
+ }
712
+ if (Object.keys(body).length === 0) {
713
+ body.purge_everything = true;
714
+ }
715
+ await this.request("POST", `/zones/${zoneId}/purge_cache`, body);
716
+ return true;
717
+ } catch {
718
+ return false;
719
+ }
720
+ }
721
+ async getRecordProxyStatus(domain, record) {
722
+ try {
723
+ const zoneId = await this.getZoneId(domain);
724
+ const fullName = this.getFullRecordName(record.name, domain);
725
+ const response = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
726
+ if (response.result && response.result.length > 0) {
727
+ const matchingRecord = response.result.find((r) => r.content === record.content);
728
+ return matchingRecord?.proxied ?? null;
729
+ }
730
+ return null;
731
+ } catch {
732
+ return null;
733
+ }
734
+ }
735
+ async setRecordProxyStatus(domain, record, proxied) {
736
+ try {
737
+ const zoneId = await this.getZoneId(domain);
738
+ const fullName = this.getFullRecordName(record.name, domain);
739
+ const response = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
740
+ if (!response.result || response.result.length === 0) {
741
+ return false;
742
+ }
743
+ const matchingRecord = response.result.find((r) => r.content === record.content);
744
+ if (!matchingRecord) {
745
+ return false;
746
+ }
747
+ await this.request("PATCH", `/zones/${zoneId}/dns_records/${matchingRecord.id}`, { proxied });
748
+ return true;
749
+ } catch {
750
+ return false;
751
+ }
752
+ }
753
+ }
754
+ // src/dns/route53-adapter.ts
755
+ class Route53Provider {
756
+ name = "route53";
757
+ client;
758
+ hostedZoneCache = new Map;
759
+ providedHostedZoneId;
760
+ constructor(region = "us-east-1", hostedZoneId) {
761
+ this.client = new Route53Client(region);
762
+ this.providedHostedZoneId = hostedZoneId;
763
+ }
764
+ getRootDomain(domain) {
765
+ const parts = domain.replace(/\.$/, "").split(".");
766
+ if (parts.length >= 2) {
767
+ return parts.slice(-2).join(".");
768
+ }
769
+ return domain;
770
+ }
771
+ async getHostedZoneId(domain) {
772
+ if (this.providedHostedZoneId) {
773
+ return this.providedHostedZoneId;
774
+ }
775
+ const rootDomain = this.getRootDomain(domain);
776
+ const cached = this.hostedZoneCache.get(rootDomain);
777
+ if (cached) {
778
+ return cached;
779
+ }
780
+ const zone = await this.client.findHostedZoneForDomain(domain);
781
+ if (zone) {
782
+ const zoneId = zone.Id.replace("/hostedzone/", "");
783
+ this.hostedZoneCache.set(rootDomain, zoneId);
784
+ return zoneId;
785
+ }
786
+ return null;
787
+ }
788
+ normalizeName(name) {
789
+ return name.endsWith(".") ? name : `${name}.`;
790
+ }
791
+ async createRecord(domain, record) {
792
+ try {
793
+ const hostedZoneId = await this.getHostedZoneId(domain);
794
+ if (!hostedZoneId) {
795
+ return {
796
+ success: false,
797
+ message: `No hosted zone found for domain: ${domain}`
798
+ };
799
+ }
800
+ const recordName = this.normalizeName(record.name);
801
+ let recordValue = record.content;
802
+ if (record.type === "TXT" && !recordValue.startsWith('"')) {
803
+ recordValue = `"${recordValue}"`;
804
+ }
805
+ if (record.type === "MX" && record.priority !== undefined) {
806
+ recordValue = `${record.priority} ${recordValue}`;
807
+ }
808
+ const result = await this.client.changeResourceRecordSets({
809
+ HostedZoneId: hostedZoneId,
810
+ ChangeBatch: {
811
+ Comment: `Created by ts-cloud DNS provider`,
812
+ Changes: [{
813
+ Action: "CREATE",
814
+ ResourceRecordSet: {
815
+ Name: recordName,
816
+ Type: record.type,
817
+ TTL: record.ttl || 300,
818
+ ResourceRecords: [{ Value: recordValue }]
819
+ }
820
+ }]
821
+ }
822
+ });
823
+ return {
824
+ success: true,
825
+ id: result.ChangeInfo?.Id,
826
+ message: "Record created successfully"
827
+ };
828
+ } catch (error) {
829
+ return {
830
+ success: false,
831
+ message: error instanceof Error ? error.message : "Unknown error"
832
+ };
833
+ }
834
+ }
835
+ async upsertRecord(domain, record) {
836
+ try {
837
+ const hostedZoneId = await this.getHostedZoneId(domain);
838
+ if (!hostedZoneId) {
839
+ return {
840
+ success: false,
841
+ message: `No hosted zone found for domain: ${domain}`
842
+ };
843
+ }
844
+ const recordName = this.normalizeName(record.name);
845
+ let recordValue = record.content;
846
+ if (record.type === "TXT" && !recordValue.startsWith('"')) {
847
+ recordValue = `"${recordValue}"`;
848
+ }
849
+ if (record.type === "MX" && record.priority !== undefined) {
850
+ recordValue = `${record.priority} ${recordValue}`;
851
+ }
852
+ const result = await this.client.changeResourceRecordSets({
853
+ HostedZoneId: hostedZoneId,
854
+ ChangeBatch: {
855
+ Comment: `Upserted by ts-cloud DNS provider`,
856
+ Changes: [{
857
+ Action: "UPSERT",
858
+ ResourceRecordSet: {
859
+ Name: recordName,
860
+ Type: record.type,
861
+ TTL: record.ttl || 300,
862
+ ResourceRecords: [{ Value: recordValue }]
863
+ }
864
+ }]
865
+ }
866
+ });
867
+ return {
868
+ success: true,
869
+ id: result.ChangeInfo?.Id,
870
+ message: "Record upserted successfully"
871
+ };
872
+ } catch (error) {
873
+ return {
874
+ success: false,
875
+ message: error instanceof Error ? error.message : "Unknown error"
876
+ };
877
+ }
878
+ }
879
+ async deleteRecord(domain, record) {
880
+ try {
881
+ const hostedZoneId = await this.getHostedZoneId(domain);
882
+ if (!hostedZoneId) {
883
+ return {
884
+ success: false,
885
+ message: `No hosted zone found for domain: ${domain}`
886
+ };
887
+ }
888
+ const recordName = this.normalizeName(record.name);
889
+ let recordValue = record.content;
890
+ if (record.type === "TXT" && !recordValue.startsWith('"')) {
891
+ recordValue = `"${recordValue}"`;
892
+ }
893
+ if (record.type === "MX" && record.priority !== undefined) {
894
+ recordValue = `${record.priority} ${recordValue}`;
895
+ }
896
+ await this.client.changeResourceRecordSets({
897
+ HostedZoneId: hostedZoneId,
898
+ ChangeBatch: {
899
+ Comment: `Deleted by ts-cloud DNS provider`,
900
+ Changes: [{
901
+ Action: "DELETE",
902
+ ResourceRecordSet: {
903
+ Name: recordName,
904
+ Type: record.type,
905
+ TTL: record.ttl || 300,
906
+ ResourceRecords: [{ Value: recordValue }]
907
+ }
908
+ }]
909
+ }
910
+ });
911
+ return {
912
+ success: true,
913
+ message: "Record deleted successfully"
914
+ };
915
+ } catch (error) {
916
+ return {
917
+ success: false,
918
+ message: error instanceof Error ? error.message : "Unknown error"
919
+ };
920
+ }
921
+ }
922
+ async listRecords(domain, type) {
923
+ try {
924
+ const hostedZoneId = await this.getHostedZoneId(domain);
925
+ if (!hostedZoneId) {
926
+ return {
927
+ success: false,
928
+ records: [],
929
+ message: `No hosted zone found for domain: ${domain}`
930
+ };
931
+ }
932
+ const result = await this.client.listResourceRecordSets({
933
+ HostedZoneId: hostedZoneId,
934
+ StartRecordType: type
935
+ });
936
+ const records = [];
937
+ for (const rs of result.ResourceRecordSets) {
938
+ if (type && rs.Type !== type) {
939
+ continue;
940
+ }
941
+ if (rs.AliasTarget) {
942
+ continue;
943
+ }
944
+ for (const rr of rs.ResourceRecords || []) {
945
+ let content = rr.Value;
946
+ let priority;
947
+ if (rs.Type === "MX") {
948
+ const parts = content.split(" ");
949
+ if (parts.length >= 2) {
950
+ priority = Number.parseInt(parts[0], 10);
951
+ content = parts.slice(1).join(" ");
952
+ }
953
+ }
954
+ if (rs.Type === "TXT" && content.startsWith('"') && content.endsWith('"')) {
955
+ content = content.slice(1, -1);
956
+ }
957
+ records.push({
958
+ name: rs.Name.replace(/\.$/, ""),
959
+ type: rs.Type,
960
+ content,
961
+ ttl: rs.TTL,
962
+ priority
963
+ });
964
+ }
965
+ }
966
+ return {
967
+ success: true,
968
+ records
969
+ };
970
+ } catch (error) {
971
+ return {
972
+ success: false,
973
+ records: [],
974
+ message: error instanceof Error ? error.message : "Unknown error"
975
+ };
976
+ }
977
+ }
978
+ async canManageDomain(domain) {
979
+ const hostedZoneId = await this.getHostedZoneId(domain);
980
+ return hostedZoneId !== null;
981
+ }
982
+ async listDomains() {
983
+ try {
984
+ const result = await this.client.listHostedZones();
985
+ return result.HostedZones.map((z) => z.Name.replace(/\.$/, ""));
986
+ } catch {
987
+ return [];
988
+ }
989
+ }
990
+ getRoute53Client() {
991
+ return this.client;
992
+ }
993
+ async createAliasRecord(params) {
994
+ try {
995
+ const hostedZoneId = await this.getHostedZoneId(params.domain);
996
+ if (!hostedZoneId) {
997
+ return {
998
+ success: false,
999
+ message: `No hosted zone found for domain: ${params.domain}`
1000
+ };
1001
+ }
1002
+ const result = await this.client.createAliasRecord({
1003
+ HostedZoneId: hostedZoneId,
1004
+ Name: this.normalizeName(params.name),
1005
+ TargetHostedZoneId: params.targetHostedZoneId,
1006
+ TargetDNSName: params.targetDnsName,
1007
+ EvaluateTargetHealth: params.evaluateTargetHealth,
1008
+ Type: params.type
1009
+ });
1010
+ return {
1011
+ success: true,
1012
+ id: result.ChangeInfo?.Id,
1013
+ message: "Alias record created successfully"
1014
+ };
1015
+ } catch (error) {
1016
+ return {
1017
+ success: false,
1018
+ message: error instanceof Error ? error.message : "Unknown error"
1019
+ };
1020
+ }
1021
+ }
1022
+ async createCloudFrontAlias(params) {
1023
+ return this.createAliasRecord({
1024
+ domain: params.domain,
1025
+ name: params.name,
1026
+ targetHostedZoneId: Route53Client.CloudFrontHostedZoneId,
1027
+ targetDnsName: params.cloudFrontDomainName,
1028
+ evaluateTargetHealth: false
1029
+ });
1030
+ }
1031
+ async createAlbAlias(params) {
1032
+ const hostedZoneId = Route53Client.ALBHostedZoneIds[params.region];
1033
+ if (!hostedZoneId) {
1034
+ return {
1035
+ success: false,
1036
+ message: `Unknown region for ALB: ${params.region}`
1037
+ };
1038
+ }
1039
+ return this.createAliasRecord({
1040
+ domain: params.domain,
1041
+ name: params.name,
1042
+ targetHostedZoneId: hostedZoneId,
1043
+ targetDnsName: params.albDnsName,
1044
+ evaluateTargetHealth: true
1045
+ });
1046
+ }
1047
+ }
1048
+ // src/aws/acm.ts
1049
+ class ACMClient {
1050
+ client;
1051
+ region;
1052
+ constructor(region = "us-east-1") {
1053
+ this.client = new AWSClient;
1054
+ this.region = region;
1055
+ }
1056
+ async requestCertificate(params) {
1057
+ const requestBody = {
1058
+ DomainName: params.DomainName,
1059
+ ValidationMethod: params.ValidationMethod || "DNS"
1060
+ };
1061
+ if (params.SubjectAlternativeNames) {
1062
+ requestBody.SubjectAlternativeNames = params.SubjectAlternativeNames;
1063
+ }
1064
+ const result = await this.client.request({
1065
+ service: "acm",
1066
+ region: this.region,
1067
+ method: "POST",
1068
+ path: "/",
1069
+ headers: {
1070
+ "content-type": "application/x-amz-json-1.1",
1071
+ "x-amz-target": "CertificateManager.RequestCertificate"
1072
+ },
1073
+ body: JSON.stringify(requestBody)
1074
+ });
1075
+ return {
1076
+ CertificateArn: result.CertificateArn || ""
1077
+ };
1078
+ }
1079
+ async describeCertificate(params) {
1080
+ const result = await this.client.request({
1081
+ service: "acm",
1082
+ region: this.region,
1083
+ method: "POST",
1084
+ path: "/",
1085
+ headers: {
1086
+ "content-type": "application/x-amz-json-1.1",
1087
+ "x-amz-target": "CertificateManager.DescribeCertificate"
1088
+ },
1089
+ body: JSON.stringify({
1090
+ CertificateArn: params.CertificateArn
1091
+ })
1092
+ });
1093
+ const cert = result.Certificate || {};
1094
+ return {
1095
+ CertificateArn: cert.CertificateArn || "",
1096
+ DomainName: cert.DomainName || "",
1097
+ SubjectAlternativeNames: cert.SubjectAlternativeNames,
1098
+ Status: cert.Status || "PENDING_VALIDATION",
1099
+ Type: cert.Type,
1100
+ DomainValidationOptions: cert.DomainValidationOptions?.map((opt) => ({
1101
+ DomainName: opt.DomainName,
1102
+ ValidationDomain: opt.ValidationDomain,
1103
+ ValidationStatus: opt.ValidationStatus,
1104
+ ResourceRecord: opt.ResourceRecord ? {
1105
+ Name: opt.ResourceRecord.Name,
1106
+ Type: opt.ResourceRecord.Type,
1107
+ Value: opt.ResourceRecord.Value
1108
+ } : undefined,
1109
+ ValidationMethod: opt.ValidationMethod
1110
+ })),
1111
+ CreatedAt: cert.CreatedAt,
1112
+ IssuedAt: cert.IssuedAt,
1113
+ NotBefore: cert.NotBefore,
1114
+ NotAfter: cert.NotAfter
1115
+ };
1116
+ }
1117
+ async listCertificates(params) {
1118
+ const requestBody = {};
1119
+ if (params?.CertificateStatuses) {
1120
+ requestBody.CertificateStatuses = params.CertificateStatuses;
1121
+ }
1122
+ if (params?.MaxItems) {
1123
+ requestBody.MaxItems = params.MaxItems;
1124
+ }
1125
+ if (params?.NextToken) {
1126
+ requestBody.NextToken = params.NextToken;
1127
+ }
1128
+ const result = await this.client.request({
1129
+ service: "acm",
1130
+ region: this.region,
1131
+ method: "POST",
1132
+ path: "/",
1133
+ headers: {
1134
+ "content-type": "application/x-amz-json-1.1",
1135
+ "x-amz-target": "CertificateManager.ListCertificates"
1136
+ },
1137
+ body: JSON.stringify(requestBody)
1138
+ });
1139
+ return {
1140
+ CertificateSummaryList: (result.CertificateSummaryList || []).map((cert) => ({
1141
+ CertificateArn: cert.CertificateArn,
1142
+ DomainName: cert.DomainName
1143
+ })),
1144
+ NextToken: result.NextToken
1145
+ };
1146
+ }
1147
+ async deleteCertificate(params) {
1148
+ await this.client.request({
1149
+ service: "acm",
1150
+ region: this.region,
1151
+ method: "POST",
1152
+ path: "/",
1153
+ headers: {
1154
+ "content-type": "application/x-amz-json-1.1",
1155
+ "x-amz-target": "CertificateManager.DeleteCertificate"
1156
+ },
1157
+ body: JSON.stringify({
1158
+ CertificateArn: params.CertificateArn
1159
+ })
1160
+ });
1161
+ }
1162
+ async listTagsForCertificate(params) {
1163
+ const result = await this.client.request({
1164
+ service: "acm",
1165
+ region: this.region,
1166
+ method: "POST",
1167
+ path: "/",
1168
+ headers: {
1169
+ "content-type": "application/x-amz-json-1.1",
1170
+ "x-amz-target": "CertificateManager.ListTagsForCertificate"
1171
+ },
1172
+ body: JSON.stringify({
1173
+ CertificateArn: params.CertificateArn
1174
+ })
1175
+ });
1176
+ return {
1177
+ Tags: result.Tags || []
1178
+ };
1179
+ }
1180
+ async addTagsToCertificate(params) {
1181
+ await this.client.request({
1182
+ service: "acm",
1183
+ region: this.region,
1184
+ method: "POST",
1185
+ path: "/",
1186
+ headers: {
1187
+ "content-type": "application/x-amz-json-1.1",
1188
+ "x-amz-target": "CertificateManager.AddTagsToCertificate"
1189
+ },
1190
+ body: JSON.stringify({
1191
+ CertificateArn: params.CertificateArn,
1192
+ Tags: params.Tags
1193
+ })
1194
+ });
1195
+ }
1196
+ async resendValidationEmail(params) {
1197
+ await this.client.request({
1198
+ service: "acm",
1199
+ region: this.region,
1200
+ method: "POST",
1201
+ path: "/",
1202
+ headers: {
1203
+ "content-type": "application/x-amz-json-1.1",
1204
+ "x-amz-target": "CertificateManager.ResendValidationEmail"
1205
+ },
1206
+ body: JSON.stringify({
1207
+ CertificateArn: params.CertificateArn,
1208
+ Domain: params.Domain,
1209
+ ValidationDomain: params.ValidationDomain
1210
+ })
1211
+ });
1212
+ }
1213
+ async findCertificateByDomain(domainName) {
1214
+ const result = await this.listCertificates({
1215
+ CertificateStatuses: ["ISSUED"]
1216
+ });
1217
+ const summary = result.CertificateSummaryList.find((c) => c.DomainName === domainName || c.DomainName === `*.${domainName.split(".").slice(1).join(".")}`);
1218
+ if (!summary) {
1219
+ return null;
1220
+ }
1221
+ return this.describeCertificate({ CertificateArn: summary.CertificateArn });
1222
+ }
1223
+ async waitForCertificateValidation(certificateArn, maxAttempts = 60, delayMs = 30000) {
1224
+ for (let i = 0;i < maxAttempts; i++) {
1225
+ const cert = await this.describeCertificate({ CertificateArn: certificateArn });
1226
+ if (cert.Status === "ISSUED") {
1227
+ return cert;
1228
+ }
1229
+ if (cert.Status === "FAILED" || cert.Status === "VALIDATION_TIMED_OUT") {
1230
+ return null;
1231
+ }
1232
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1233
+ }
1234
+ return null;
1235
+ }
1236
+ async getDnsValidationRecords(certificateArn) {
1237
+ const cert = await this.describeCertificate({ CertificateArn: certificateArn });
1238
+ if (!cert.DomainValidationOptions) {
1239
+ return [];
1240
+ }
1241
+ return cert.DomainValidationOptions.filter((opt) => opt.ResourceRecord && opt.ValidationMethod === "DNS").map((opt) => ({
1242
+ domainName: opt.DomainName,
1243
+ recordName: opt.ResourceRecord.Name,
1244
+ recordType: opt.ResourceRecord.Type,
1245
+ recordValue: opt.ResourceRecord.Value
1246
+ }));
1247
+ }
1248
+ async requestCertificateWithSans(params) {
1249
+ const sans = new Set;
1250
+ sans.add(params.DomainName);
1251
+ if (params.IncludeWww !== false) {
1252
+ sans.add(`www.${params.DomainName}`);
1253
+ }
1254
+ if (params.IncludeWildcard) {
1255
+ sans.add(`*.${params.DomainName}`);
1256
+ }
1257
+ if (params.AdditionalSans) {
1258
+ for (const san of params.AdditionalSans) {
1259
+ sans.add(san);
1260
+ }
1261
+ }
1262
+ return this.requestCertificate({
1263
+ DomainName: params.DomainName,
1264
+ SubjectAlternativeNames: Array.from(sans),
1265
+ ValidationMethod: "DNS"
1266
+ });
1267
+ }
1268
+ async isCertificateValidForDomain(certificateArn, domainName) {
1269
+ const cert = await this.describeCertificate({ CertificateArn: certificateArn });
1270
+ if (cert.Status !== "ISSUED") {
1271
+ return false;
1272
+ }
1273
+ if (cert.DomainName === domainName) {
1274
+ return true;
1275
+ }
1276
+ if (cert.DomainName?.startsWith("*.")) {
1277
+ const baseDomain = cert.DomainName.slice(2);
1278
+ const domainParts = domainName.split(".");
1279
+ const baseParts = baseDomain.split(".");
1280
+ if (domainParts.slice(-baseParts.length).join(".") === baseDomain) {
1281
+ return true;
1282
+ }
1283
+ }
1284
+ if (cert.SubjectAlternativeNames) {
1285
+ for (const san of cert.SubjectAlternativeNames) {
1286
+ if (san === domainName) {
1287
+ return true;
1288
+ }
1289
+ if (san.startsWith("*.")) {
1290
+ const baseDomain = san.slice(2);
1291
+ const domainParts = domainName.split(".");
1292
+ const baseParts = baseDomain.split(".");
1293
+ if (domainParts.slice(-baseParts.length).join(".") === baseDomain) {
1294
+ return true;
1295
+ }
1296
+ }
1297
+ }
1298
+ }
1299
+ return false;
1300
+ }
1301
+ }
1302
+
1303
+ class ACMDnsValidator {
1304
+ acm;
1305
+ route53;
1306
+ dnsProvider;
1307
+ constructor(region = "us-east-1", dnsProviderConfig) {
1308
+ this.acm = new ACMClient(region);
1309
+ this.route53 = new Route53Client;
1310
+ if (dnsProviderConfig && dnsProviderConfig.provider !== "route53") {
1311
+ this.dnsProvider = createDnsProvider(dnsProviderConfig);
1312
+ }
1313
+ }
1314
+ async requestAndValidate(params) {
1315
+ const {
1316
+ domainName,
1317
+ hostedZoneId,
1318
+ subjectAlternativeNames = [],
1319
+ waitForValidation = false,
1320
+ maxWaitMinutes = 30
1321
+ } = params;
1322
+ if (!this.dnsProvider && !hostedZoneId) {
1323
+ throw new Error("Either hostedZoneId or external DNS provider configuration is required");
1324
+ }
1325
+ const { CertificateArn } = await this.acm.requestCertificate({
1326
+ DomainName: domainName,
1327
+ SubjectAlternativeNames: subjectAlternativeNames.length > 0 ? [domainName, ...subjectAlternativeNames] : undefined,
1328
+ ValidationMethod: "DNS"
1329
+ });
1330
+ await this.waitForValidationOptions(CertificateArn);
1331
+ const validationRecords = await this.acm.getDnsValidationRecords(CertificateArn);
1332
+ if (this.dnsProvider) {
1333
+ for (const record of validationRecords) {
1334
+ const result = await this.dnsProvider.upsertRecord(domainName, {
1335
+ name: record.recordName,
1336
+ type: record.recordType,
1337
+ content: record.recordValue,
1338
+ ttl: 300
1339
+ });
1340
+ if (!result.success) {
1341
+ console.warn(`Failed to create validation record for ${record.domainName}: ${result.message}`);
1342
+ }
1343
+ }
1344
+ } else if (hostedZoneId) {
1345
+ for (const record of validationRecords) {
1346
+ await this.route53.changeResourceRecordSets({
1347
+ HostedZoneId: hostedZoneId,
1348
+ ChangeBatch: {
1349
+ Comment: `ACM DNS validation for ${record.domainName}`,
1350
+ Changes: [{
1351
+ Action: "UPSERT",
1352
+ ResourceRecordSet: {
1353
+ Name: record.recordName,
1354
+ Type: record.recordType,
1355
+ TTL: 300,
1356
+ ResourceRecords: [{ Value: record.recordValue }]
1357
+ }
1358
+ }]
1359
+ }
1360
+ });
1361
+ }
1362
+ }
1363
+ if (waitForValidation) {
1364
+ const cert = await this.acm.waitForCertificateValidation(CertificateArn, maxWaitMinutes * 2, 30000);
1365
+ if (!cert) {
1366
+ throw new Error(`Certificate validation timed out after ${maxWaitMinutes} minutes`);
1367
+ }
1368
+ }
1369
+ return {
1370
+ certificateArn: CertificateArn,
1371
+ validationRecords
1372
+ };
1373
+ }
1374
+ async waitForValidationOptions(certificateArn, maxAttempts = 30) {
1375
+ for (let i = 0;i < maxAttempts; i++) {
1376
+ const cert = await this.acm.describeCertificate({ CertificateArn: certificateArn });
1377
+ if (cert.DomainValidationOptions && cert.DomainValidationOptions.length > 0 && cert.DomainValidationOptions[0].ResourceRecord) {
1378
+ return;
1379
+ }
1380
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1381
+ }
1382
+ throw new Error("Timeout waiting for DNS validation options");
1383
+ }
1384
+ async createValidationRecords(params) {
1385
+ const { certificateArn, hostedZoneId, domain } = params;
1386
+ if (!this.dnsProvider && !hostedZoneId) {
1387
+ throw new Error("Either hostedZoneId or external DNS provider configuration is required");
1388
+ }
1389
+ const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
1390
+ const results = [];
1391
+ if (this.dnsProvider) {
1392
+ const targetDomain = domain || validationRecords[0]?.domainName;
1393
+ for (const record of validationRecords) {
1394
+ const result = await this.dnsProvider.upsertRecord(targetDomain, {
1395
+ name: record.recordName,
1396
+ type: record.recordType,
1397
+ content: record.recordValue,
1398
+ ttl: 300
1399
+ });
1400
+ results.push({
1401
+ ...record,
1402
+ changeId: result.success ? result.id : undefined
1403
+ });
1404
+ }
1405
+ } else if (hostedZoneId) {
1406
+ for (const record of validationRecords) {
1407
+ const result = await this.route53.changeResourceRecordSets({
1408
+ HostedZoneId: hostedZoneId,
1409
+ ChangeBatch: {
1410
+ Comment: `ACM DNS validation for ${record.domainName}`,
1411
+ Changes: [{
1412
+ Action: "UPSERT",
1413
+ ResourceRecordSet: {
1414
+ Name: record.recordName,
1415
+ Type: record.recordType,
1416
+ TTL: 300,
1417
+ ResourceRecords: [{ Value: record.recordValue }]
1418
+ }
1419
+ }]
1420
+ }
1421
+ });
1422
+ results.push({
1423
+ ...record,
1424
+ changeId: result.ChangeInfo?.Id
1425
+ });
1426
+ }
1427
+ }
1428
+ return results;
1429
+ }
1430
+ async deleteValidationRecords(params) {
1431
+ const { certificateArn, hostedZoneId, domain } = params;
1432
+ const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
1433
+ if (this.dnsProvider) {
1434
+ const targetDomain = domain || validationRecords[0]?.domainName;
1435
+ for (const record of validationRecords) {
1436
+ try {
1437
+ await this.dnsProvider.deleteRecord(targetDomain, {
1438
+ name: record.recordName,
1439
+ type: record.recordType,
1440
+ content: record.recordValue
1441
+ });
1442
+ } catch {}
1443
+ }
1444
+ } else if (hostedZoneId) {
1445
+ for (const record of validationRecords) {
1446
+ try {
1447
+ await this.route53.changeResourceRecordSets({
1448
+ HostedZoneId: hostedZoneId,
1449
+ ChangeBatch: {
1450
+ Comment: `Cleanup ACM DNS validation for ${record.domainName}`,
1451
+ Changes: [{
1452
+ Action: "DELETE",
1453
+ ResourceRecordSet: {
1454
+ Name: record.recordName,
1455
+ Type: record.recordType,
1456
+ TTL: 300,
1457
+ ResourceRecords: [{ Value: record.recordValue }]
1458
+ }
1459
+ }]
1460
+ }
1461
+ });
1462
+ } catch {}
1463
+ }
1464
+ }
1465
+ }
1466
+ async findOrCreateCertificate(params) {
1467
+ const { domainName, hostedZoneId, subjectAlternativeNames, waitForValidation = true } = params;
1468
+ if (!this.dnsProvider && !hostedZoneId) {
1469
+ throw new Error("Either hostedZoneId or external DNS provider configuration is required");
1470
+ }
1471
+ const existing = await this.acm.findCertificateByDomain(domainName);
1472
+ if (existing && existing.Status === "ISSUED") {
1473
+ return {
1474
+ certificateArn: existing.CertificateArn,
1475
+ isNew: false
1476
+ };
1477
+ }
1478
+ const { certificateArn } = await this.requestAndValidate({
1479
+ domainName,
1480
+ hostedZoneId,
1481
+ subjectAlternativeNames,
1482
+ waitForValidation
1483
+ });
1484
+ return {
1485
+ certificateArn,
1486
+ isNew: true
1487
+ };
1488
+ }
1489
+ hasExternalDnsProvider() {
1490
+ return this.dnsProvider !== undefined;
1491
+ }
1492
+ getDnsProviderName() {
1493
+ return this.dnsProvider?.name || "route53";
1494
+ }
1495
+ }
1496
+
1497
+ // src/dns/validator.ts
1498
+ class UnifiedDnsValidator {
1499
+ acm;
1500
+ dnsProvider;
1501
+ constructor(dnsProvider, acmRegion = "us-east-1") {
1502
+ this.acm = new ACMClient(acmRegion);
1503
+ if ("provider" in dnsProvider) {
1504
+ this.dnsProvider = createDnsProvider(dnsProvider);
1505
+ } else {
1506
+ this.dnsProvider = dnsProvider;
1507
+ }
1508
+ }
1509
+ getProvider() {
1510
+ return this.dnsProvider;
1511
+ }
1512
+ async requestAndValidate(params) {
1513
+ const {
1514
+ domainName,
1515
+ subjectAlternativeNames = [],
1516
+ waitForValidation = false,
1517
+ maxWaitMinutes = 30
1518
+ } = params;
1519
+ const { CertificateArn } = await this.acm.requestCertificate({
1520
+ DomainName: domainName,
1521
+ SubjectAlternativeNames: subjectAlternativeNames.length > 0 ? [domainName, ...subjectAlternativeNames] : undefined,
1522
+ ValidationMethod: "DNS"
1523
+ });
1524
+ await this.waitForValidationOptions(CertificateArn);
1525
+ const validationRecords = await this.acm.getDnsValidationRecords(CertificateArn);
1526
+ for (const record of validationRecords) {
1527
+ const result = await this.dnsProvider.upsertRecord(domainName, {
1528
+ name: record.recordName,
1529
+ type: record.recordType,
1530
+ content: record.recordValue,
1531
+ ttl: 300
1532
+ });
1533
+ if (!result.success) {
1534
+ console.warn(`Failed to create validation record for ${record.domainName}: ${result.message}`);
1535
+ }
1536
+ }
1537
+ let status = "pending";
1538
+ if (waitForValidation) {
1539
+ const cert = await this.acm.waitForCertificateValidation(CertificateArn, maxWaitMinutes * 2, 30000);
1540
+ status = cert?.Status === "ISSUED" ? "issued" : "failed";
1541
+ }
1542
+ return {
1543
+ certificateArn: CertificateArn,
1544
+ validationRecords: validationRecords.map((r) => ({
1545
+ domainName: r.domainName,
1546
+ recordName: r.recordName,
1547
+ recordType: r.recordType,
1548
+ recordValue: r.recordValue
1549
+ })),
1550
+ isNew: true,
1551
+ status
1552
+ };
1553
+ }
1554
+ async createValidationRecords(params) {
1555
+ const { certificateArn, domain } = params;
1556
+ const errors = [];
1557
+ const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
1558
+ for (const record of validationRecords) {
1559
+ const result = await this.dnsProvider.upsertRecord(domain, {
1560
+ name: record.recordName,
1561
+ type: record.recordType,
1562
+ content: record.recordValue,
1563
+ ttl: 300
1564
+ });
1565
+ if (!result.success) {
1566
+ errors.push(`Failed to create record for ${record.domainName}: ${result.message}`);
1567
+ }
1568
+ }
1569
+ return {
1570
+ success: errors.length === 0,
1571
+ records: validationRecords.map((r) => ({
1572
+ domainName: r.domainName,
1573
+ recordName: r.recordName,
1574
+ recordType: r.recordType,
1575
+ recordValue: r.recordValue
1576
+ })),
1577
+ errors
1578
+ };
1579
+ }
1580
+ async deleteValidationRecords(params) {
1581
+ const { certificateArn, domain } = params;
1582
+ const errors = [];
1583
+ const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
1584
+ for (const record of validationRecords) {
1585
+ const result = await this.dnsProvider.deleteRecord(domain, {
1586
+ name: record.recordName,
1587
+ type: record.recordType,
1588
+ content: record.recordValue
1589
+ });
1590
+ if (!result.success) {
1591
+ errors.push(`Failed to delete record for ${record.domainName}: ${result.message}`);
1592
+ }
1593
+ }
1594
+ return {
1595
+ success: errors.length === 0,
1596
+ errors
1597
+ };
1598
+ }
1599
+ async findOrCreateCertificate(params) {
1600
+ const { domainName, subjectAlternativeNames = [], waitForValidation = true, maxWaitMinutes } = params;
1601
+ const existing = await this.acm.findCertificateByDomain(domainName);
1602
+ if (existing && existing.Status === "ISSUED") {
1603
+ const existingSans = existing.SubjectAlternativeNames || [existing.DomainName];
1604
+ const hasWildcard = existingSans.some((san) => san === `*.${domainName}`);
1605
+ const allSansCovered = subjectAlternativeNames.every((san) => existingSans.includes(san) || hasWildcard);
1606
+ if (allSansCovered) {
1607
+ return {
1608
+ certificateArn: existing.CertificateArn,
1609
+ validationRecords: [],
1610
+ isNew: false,
1611
+ status: "issued"
1612
+ };
1613
+ }
1614
+ console.log(`Existing certificate doesn't cover all required SANs, requesting new certificate...`);
1615
+ }
1616
+ return this.requestAndValidate({
1617
+ domainName,
1618
+ subjectAlternativeNames,
1619
+ waitForValidation,
1620
+ maxWaitMinutes
1621
+ });
1622
+ }
1623
+ async requestCertificateWithCommonSans(params) {
1624
+ const {
1625
+ domainName,
1626
+ includeWww = true,
1627
+ includeWildcard = false,
1628
+ additionalSans = [],
1629
+ waitForValidation = false
1630
+ } = params;
1631
+ const sans = [];
1632
+ if (includeWww) {
1633
+ sans.push(`www.${domainName}`);
1634
+ }
1635
+ if (includeWildcard) {
1636
+ sans.push(`*.${domainName}`);
1637
+ }
1638
+ sans.push(...additionalSans);
1639
+ return this.requestAndValidate({
1640
+ domainName,
1641
+ subjectAlternativeNames: sans,
1642
+ waitForValidation
1643
+ });
1644
+ }
1645
+ async waitForValidationOptions(certificateArn, maxAttempts = 30) {
1646
+ for (let i = 0;i < maxAttempts; i++) {
1647
+ const cert = await this.acm.describeCertificate({ CertificateArn: certificateArn });
1648
+ if (cert.DomainValidationOptions && cert.DomainValidationOptions.length > 0 && cert.DomainValidationOptions[0].ResourceRecord) {
1649
+ return;
1650
+ }
1651
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1652
+ }
1653
+ throw new Error("Timeout waiting for DNS validation options");
1654
+ }
1655
+ async getCertificateStatus(certificateArn) {
1656
+ const cert = await this.acm.describeCertificate({ CertificateArn: certificateArn });
1657
+ return {
1658
+ status: cert.Status,
1659
+ domainValidations: (cert.DomainValidationOptions || []).map((opt) => ({
1660
+ domain: opt.DomainName,
1661
+ status: opt.ValidationStatus || "UNKNOWN"
1662
+ }))
1663
+ };
1664
+ }
1665
+ }
1666
+ function createPorkbunValidator(apiKey, secretKey, acmRegion) {
1667
+ return new UnifiedDnsValidator({ provider: "porkbun", apiKey, secretKey }, acmRegion);
1668
+ }
1669
+ function createGoDaddyValidator(apiKey, apiSecret, acmRegion, environment) {
1670
+ return new UnifiedDnsValidator({ provider: "godaddy", apiKey, apiSecret, environment }, acmRegion);
1671
+ }
1672
+ function createRoute53Validator(region, hostedZoneId, acmRegion) {
1673
+ return new UnifiedDnsValidator({ provider: "route53", region, hostedZoneId }, acmRegion || region);
1674
+ }
1675
+ // src/dns/index.ts
1676
+ function createDnsProvider(config) {
1677
+ switch (config.provider) {
1678
+ case "route53":
1679
+ return new Route53Provider(config.region, config.hostedZoneId);
1680
+ case "porkbun":
1681
+ return new PorkbunProvider(config.apiKey, config.secretKey);
1682
+ case "godaddy":
1683
+ return new GoDaddyProvider(config.apiKey, config.apiSecret, config.environment);
1684
+ case "cloudflare":
1685
+ return new CloudflareProvider(config.apiToken);
1686
+ default:
1687
+ throw new Error(`Unknown DNS provider: ${config.provider}`);
1688
+ }
1689
+ }
1690
+ async function detectDnsProvider(domain, configs) {
1691
+ for (const config of configs) {
1692
+ const provider = createDnsProvider(config);
1693
+ if (await provider.canManageDomain(domain)) {
1694
+ return provider;
1695
+ }
1696
+ }
1697
+ return null;
1698
+ }
1699
+
1700
+ class DnsProviderFactory {
1701
+ providers = new Map;
1702
+ configs = [];
1703
+ addConfig(config) {
1704
+ this.configs.push(config);
1705
+ return this;
1706
+ }
1707
+ addRoute53(region, hostedZoneId) {
1708
+ this.configs.push({
1709
+ provider: "route53",
1710
+ region,
1711
+ hostedZoneId
1712
+ });
1713
+ return this;
1714
+ }
1715
+ addPorkbun(apiKey, secretKey) {
1716
+ this.configs.push({
1717
+ provider: "porkbun",
1718
+ apiKey,
1719
+ secretKey
1720
+ });
1721
+ return this;
1722
+ }
1723
+ addGoDaddy(apiKey, apiSecret, environment) {
1724
+ this.configs.push({
1725
+ provider: "godaddy",
1726
+ apiKey,
1727
+ apiSecret,
1728
+ environment
1729
+ });
1730
+ return this;
1731
+ }
1732
+ addCloudflare(apiToken) {
1733
+ this.configs.push({
1734
+ provider: "cloudflare",
1735
+ apiToken
1736
+ });
1737
+ return this;
1738
+ }
1739
+ loadFromEnv() {
1740
+ if (process.env.AWS_ACCESS_KEY_ID || process.env.AWS_REGION) {
1741
+ this.addRoute53(process.env.AWS_REGION);
1742
+ }
1743
+ const porkbunApiKey = process.env.PORKBUN_API_KEY;
1744
+ const porkbunSecretKey = process.env.PORKBUN_SECRET_KEY;
1745
+ if (porkbunApiKey && porkbunSecretKey) {
1746
+ this.addPorkbun(porkbunApiKey, porkbunSecretKey);
1747
+ }
1748
+ const godaddyApiKey = process.env.GODADDY_API_KEY;
1749
+ const godaddyApiSecret = process.env.GODADDY_API_SECRET;
1750
+ if (godaddyApiKey && godaddyApiSecret) {
1751
+ const env = process.env.GODADDY_ENVIRONMENT;
1752
+ this.addGoDaddy(godaddyApiKey, godaddyApiSecret, env);
1753
+ }
1754
+ const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN;
1755
+ if (cloudflareApiToken) {
1756
+ this.addCloudflare(cloudflareApiToken);
1757
+ }
1758
+ return this;
1759
+ }
1760
+ getProvider(name) {
1761
+ const cached = this.providers.get(name);
1762
+ if (cached) {
1763
+ return cached;
1764
+ }
1765
+ const config = this.configs.find((c) => c.provider === name);
1766
+ if (!config) {
1767
+ return null;
1768
+ }
1769
+ const provider = createDnsProvider(config);
1770
+ this.providers.set(name, provider);
1771
+ return provider;
1772
+ }
1773
+ async getProviderForDomain(domain) {
1774
+ for (const config of this.configs) {
1775
+ const provider = createDnsProvider(config);
1776
+ if (await provider.canManageDomain(domain)) {
1777
+ return provider;
1778
+ }
1779
+ }
1780
+ return null;
1781
+ }
1782
+ getAllProviders() {
1783
+ return this.configs.map((config) => createDnsProvider(config));
1784
+ }
1785
+ }
1786
+ var dnsProviders = new DnsProviderFactory;
1787
+
1788
+ export { PorkbunProvider, GoDaddyProvider, CloudflareProvider, Route53Provider, UnifiedDnsValidator, createPorkbunValidator, createGoDaddyValidator, createRoute53Validator, createDnsProvider, detectDnsProvider, DnsProviderFactory, dnsProviders, ACMClient, ACMDnsValidator };