@stacksjs/ts-cloud 0.9.3 → 0.10.0
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.
- package/dist/aws/index.js +1 -269
- package/dist/bin/chunk-08c0pzkb.js +1 -0
- package/dist/bin/chunk-12210qvj.js +1 -0
- package/dist/bin/chunk-1v6bt68w.js +6 -0
- package/dist/bin/chunk-35ttwn26.js +70 -0
- package/dist/bin/chunk-3r3be05z.js +1 -0
- package/dist/bin/chunk-41yqpdv7.js +2 -0
- package/dist/bin/chunk-5rv31chh.js +1 -0
- package/dist/bin/chunk-6eat8ezk.js +2 -0
- package/dist/bin/chunk-6gktf2m2.js +1 -0
- package/dist/bin/chunk-6zjy949y.js +141 -0
- package/dist/bin/chunk-7wzdaqh6.js +2 -0
- package/dist/bin/chunk-80ar3aty.js +1 -0
- package/dist/bin/chunk-8rwhsp5b.js +2 -0
- package/dist/bin/chunk-936vp7dx.js +44 -0
- package/dist/bin/chunk-9px3rg4a.js +4 -0
- package/dist/bin/chunk-anm3ae6z.js +7364 -0
- package/dist/bin/{chunk-ynjvbphf.js → chunk-axvr7dwa.js} +6 -6
- package/dist/bin/chunk-b0hj9m5e.js +2 -0
- package/dist/bin/{chunk-mh0kdzqp.js → chunk-b2ymh8k0.js} +1 -1
- package/dist/bin/chunk-b35989e3.js +50 -0
- package/dist/bin/chunk-c2b6nk0q.js +37 -0
- package/dist/bin/chunk-ckxpgwxw.js +3 -0
- package/dist/bin/chunk-d6zzgvtn.js +92 -0
- package/dist/bin/chunk-da8zpf82.js +1 -0
- package/dist/bin/chunk-ddmjjstg.js +1 -0
- package/dist/bin/chunk-dhhz4zyj.js +1 -0
- package/dist/bin/chunk-dj9a67c3.js +9 -0
- package/dist/bin/chunk-ds1z880f.js +1 -0
- package/dist/bin/chunk-dvkj4k6h.js +111 -0
- package/dist/bin/chunk-eh40fpvj.js +1 -0
- package/dist/bin/chunk-ey9fagrr.js +6 -0
- package/dist/bin/chunk-ezftw2q4.js +1 -0
- package/dist/bin/chunk-g6a4n3rh.js +1 -0
- package/dist/bin/chunk-g90nqfgv.js +585 -0
- package/dist/bin/chunk-gs40j75w.js +1 -0
- package/dist/bin/chunk-jjjts1vt.js +1 -0
- package/dist/bin/{chunk-ry030a6b.js → chunk-jnsbzqzz.js} +30 -30
- package/dist/bin/chunk-jnx4jgw8.js +1 -0
- package/dist/bin/chunk-jt1tndm2.js +3 -0
- package/dist/bin/chunk-kybdb0ws.js +2 -0
- package/dist/bin/chunk-m3nqw76h.js +3 -0
- package/dist/bin/chunk-mn6j22zn.js +6 -0
- package/dist/bin/chunk-nb0yjkv5.js +2 -0
- package/dist/bin/chunk-pjpa4x52.js +2 -0
- package/dist/bin/chunk-pkgkfe6s.js +2 -0
- package/dist/bin/chunk-pnz5hk8n.js +60 -0
- package/dist/bin/chunk-q46zr2tv.js +1 -0
- package/dist/bin/chunk-qtdngvs8.js +1 -0
- package/dist/bin/chunk-sbejksrh.js +2 -0
- package/dist/bin/chunk-svyd4v6c.js +34 -0
- package/dist/bin/chunk-tna7ay8d.js +1 -0
- package/dist/bin/chunk-tp4c2mv1.js +1 -0
- package/dist/bin/chunk-w5d6tr2x.js +1 -0
- package/dist/bin/chunk-wewcjam5.js +1 -0
- package/dist/bin/chunk-xv4bscrm.js +1 -0
- package/dist/bin/chunk-y9eyc5pw.js +1 -0
- package/dist/bin/chunk-y9f8649w.js +512 -0
- package/dist/bin/cli.js +428 -418
- package/dist/bin/dashboard-server.js +1 -1
- package/dist/chunk-06jk7ba9.js +7355 -0
- package/dist/chunk-0e16qt6k.js +5 -0
- package/dist/chunk-13hxhzaz.js +34 -0
- package/dist/chunk-2p89vt3b.js +2 -0
- package/dist/chunk-77t2ctwn.js +2 -0
- package/dist/chunk-88z37ey4.js +114 -0
- package/dist/chunk-8c4715hz.js +2 -0
- package/dist/chunk-9a22225r.js +60 -0
- package/dist/chunk-as6by62d.js +1 -0
- package/dist/chunk-cd0zpxhf.js +44 -0
- package/dist/chunk-cp54a0qn.js +6 -0
- package/dist/chunk-d67xbk4v.js +141 -0
- package/dist/chunk-dckz9xve.js +2 -0
- package/dist/chunk-e1666vjv.js +2 -0
- package/dist/chunk-hhdx5k96.js +442 -0
- package/dist/chunk-kv2ztkp4.js +1617 -0
- package/dist/chunk-nrz6zypg.js +1 -0
- package/dist/chunk-q816js1y.js +1 -0
- package/dist/chunk-qh9ep11z.js +704 -0
- package/dist/chunk-rg720cqg.js +1 -0
- package/dist/chunk-snak8s7m.js +92 -0
- package/dist/chunk-vq737g82.js +1 -0
- package/dist/chunk-xd6w0c11.js +50 -0
- package/dist/chunk-xtwjt370.js +37 -0
- package/dist/chunk-y26yh5zp.js +1 -0
- package/dist/chunk-zmxffmmq.js +4 -0
- package/dist/deploy/attach-credentials.d.ts +4 -8
- package/dist/deploy/index.js +1 -168
- package/dist/dns/cloudflare.d.ts +27 -0
- package/dist/dns/godaddy.d.ts +18 -1
- package/dist/dns/index.d.ts +2 -0
- package/dist/dns/index.js +1 -41
- package/dist/dns/zone-migration.d.ts +92 -0
- package/dist/dns/zone-migration.test.d.ts +1 -0
- package/dist/drivers/hetzner/driver.d.ts +8 -1
- package/dist/drivers/index.js +1 -154
- package/dist/drivers/shared/compute-provision.d.ts +14 -2
- package/dist/drivers/shared/db-provision.d.ts +3 -2
- package/dist/drivers/shared/mail-provision.d.ts +116 -0
- package/dist/drivers/shared/mail-provision.test.d.ts +1 -0
- package/dist/drivers/shared/managed-services-probe.d.ts +47 -0
- package/dist/drivers/shared/managed-services-probe.test.d.ts +1 -0
- package/dist/drivers/shared/package-manager.d.ts +1 -0
- package/dist/drivers/shared/remote-failure.d.ts +21 -0
- package/dist/drivers/shared/remote-failure.test.d.ts +1 -0
- package/dist/drivers/shared/sqlite-shared-path.d.ts +50 -0
- package/dist/drivers/shared/sqlite-shared-path.test.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +100 -6129
- package/dist/operations/plan.d.ts +136 -0
- package/dist/operations/plan.test.d.ts +1 -0
- package/dist/operations/server-rename.d.ts +84 -0
- package/dist/operations/server-rename.test.d.ts +1 -0
- package/dist/operations/site-move.d.ts +263 -0
- package/dist/operations/site-move.test.d.ts +1 -0
- package/dist/protection/index.js +1 -89
- package/dist/push/index.js +1 -509
- package/dist/spend/index.js +1 -173
- package/dist/ui/404.html +1 -1
- package/dist/ui/accept-invitation.html +1 -1
- package/dist/ui/access-denied.html +1 -1
- package/dist/ui/account/automation.html +5 -5
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +6 -6
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +80 -80
- package/dist/ui/data/services.html +78 -78
- package/dist/ui/data/volumes.html +50 -50
- package/dist/ui/index.html +50 -50
- package/dist/ui/infrastructure/topology.html +118 -118
- package/dist/ui/integrations.html +1 -1
- package/dist/ui/operations/alerts.html +47 -47
- package/dist/ui/operations/configuration.html +5 -5
- package/dist/ui/operations/jobs.html +76 -76
- package/dist/ui/operations/maintenance.html +6 -6
- package/dist/ui/operations/observability.html +82 -82
- package/dist/ui/operations/previews.html +5 -5
- package/dist/ui/operations/queue.html +6 -6
- package/dist/ui/operations/regions.html +6 -6
- package/dist/ui/operations/releases.html +6 -6
- package/dist/ui/operations/spend.html +54 -54
- package/dist/ui/operations/workloads.html +104 -104
- package/dist/ui/security.html +2 -2
- package/dist/ui/server/actions.html +56 -56
- package/dist/ui/server/activity.html +1 -1
- package/dist/ui/server/capacity.html +6 -6
- package/dist/ui/server/database.html +106 -106
- package/dist/ui/server/deployments.html +50 -50
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +76 -76
- package/dist/ui/server/fleet.html +34 -34
- package/dist/ui/server/logs.html +24 -24
- package/dist/ui/server/metrics.html +112 -112
- package/dist/ui/server/security.html +1 -1
- package/dist/ui/server/services.html +27 -27
- package/dist/ui/server/sites.html +120 -120
- package/dist/ui/server/ssh-keys.html +51 -51
- package/dist/ui/server/team.html +144 -144
- package/dist/ui/server/terminal.html +15 -15
- package/dist/ui/serverless/alarms.html +52 -52
- package/dist/ui/serverless/assets.html +27 -27
- package/dist/ui/serverless/cost.html +2 -2
- package/dist/ui/serverless/data.html +36 -36
- package/dist/ui/serverless/deployments.html +27 -27
- package/dist/ui/serverless/firewall.html +1 -1
- package/dist/ui/serverless/functions.html +40 -40
- package/dist/ui/serverless/logs.html +24 -24
- package/dist/ui/serverless/metrics.html +2 -2
- package/dist/ui/serverless/queues.html +47 -47
- package/dist/ui/serverless/secrets.html +52 -52
- package/dist/ui/serverless/traces.html +6 -6
- package/dist/ui/serverless.html +54 -54
- package/dist/ui-src/src/charts.ts +17499 -12662
- package/dist/ui-src/src/topology.ts +2 -2
- package/package.json +3 -3
- package/dist/bin/chunk-09nwf2gy.js +0 -34
- package/dist/bin/chunk-14cwr6eh.js +0 -70
- package/dist/bin/chunk-271gk1n2.js +0 -6
- package/dist/bin/chunk-38pq971s.js +0 -2
- package/dist/bin/chunk-3gykfyq4.js +0 -37
- package/dist/bin/chunk-3r4fb59r.js +0 -3
- package/dist/bin/chunk-41x17hw1.js +0 -1
- package/dist/bin/chunk-4vrqm9h0.js +0 -2
- package/dist/bin/chunk-57y1zxbf.js +0 -3
- package/dist/bin/chunk-598yf4bs.js +0 -1
- package/dist/bin/chunk-8v7ddyvf.js +0 -1
- package/dist/bin/chunk-92mea34a.js +0 -1
- package/dist/bin/chunk-9pvsff8g.js +0 -1
- package/dist/bin/chunk-9v39615t.js +0 -1
- package/dist/bin/chunk-avm7tnwe.js +0 -1
- package/dist/bin/chunk-ax9mr6wm.js +0 -44
- package/dist/bin/chunk-axhnkn5g.js +0 -2
- package/dist/bin/chunk-b5gd39s8.js +0 -512
- package/dist/bin/chunk-b9qbp8j1.js +0 -1
- package/dist/bin/chunk-bz828v8q.js +0 -3
- package/dist/bin/chunk-c1vcdgvr.js +0 -1
- package/dist/bin/chunk-ctd2vg9w.js +0 -2
- package/dist/bin/chunk-dbgffky8.js +0 -1
- package/dist/bin/chunk-dwr30vbv.js +0 -1
- package/dist/bin/chunk-e3xrwh9z.js +0 -1
- package/dist/bin/chunk-fhc6ntvd.js +0 -2
- package/dist/bin/chunk-fpm59y25.js +0 -6
- package/dist/bin/chunk-fvbxvq6b.js +0 -1
- package/dist/bin/chunk-fy8j3y8p.js +0 -92
- package/dist/bin/chunk-fz21b57w.js +0 -1
- package/dist/bin/chunk-gavvf0pg.js +0 -141
- package/dist/bin/chunk-ggn7ax8p.js +0 -4
- package/dist/bin/chunk-gpe2prkm.js +0 -1
- package/dist/bin/chunk-gqf6jsmy.js +0 -1
- package/dist/bin/chunk-h5pa5men.js +0 -2
- package/dist/bin/chunk-hm08xdxf.js +0 -2
- package/dist/bin/chunk-hzgz13nf.js +0 -1
- package/dist/bin/chunk-hzqw8xm5.js +0 -111
- package/dist/bin/chunk-j703btc5.js +0 -50
- package/dist/bin/chunk-jpjamjey.js +0 -6935
- package/dist/bin/chunk-m2zwtapr.js +0 -1
- package/dist/bin/chunk-m669wtgr.js +0 -1
- package/dist/bin/chunk-mr0atd2s.js +0 -2
- package/dist/bin/chunk-ngcyftm2.js +0 -1
- package/dist/bin/chunk-p66ptpmr.js +0 -2
- package/dist/bin/chunk-p8q9bq9g.js +0 -1
- package/dist/bin/chunk-r3kv0vm9.js +0 -6
- package/dist/bin/chunk-r9hjwcj9.js +0 -6
- package/dist/bin/chunk-rtczra13.js +0 -1
- package/dist/bin/chunk-rytk89az.js +0 -60
- package/dist/bin/chunk-sgyffpgm.js +0 -581
- package/dist/bin/chunk-vek8a0w6.js +0 -1
- package/dist/bin/chunk-waayf9ay.js +0 -2
- package/dist/bin/chunk-yv9sth30.js +0 -1
- package/dist/chunk-01d86gt1.js +0 -1324
- package/dist/chunk-050r7ycy.js +0 -46516
- package/dist/chunk-1c8dbv57.js +0 -7278
- package/dist/chunk-29rse4p1.js +0 -12203
- package/dist/chunk-32e7ya18.js +0 -642
- package/dist/chunk-4cjrg98a.js +0 -597
- package/dist/chunk-50jpda9q.js +0 -371
- package/dist/chunk-6a1t7fnq.js +0 -12121
- package/dist/chunk-728jvrr0.js +0 -1701
- package/dist/chunk-7m60qnc8.js +0 -1762
- package/dist/chunk-aa2dkn7b.js +0 -205
- package/dist/chunk-d48vtdj4.js +0 -433
- package/dist/chunk-f55nkpvb.js +0 -8
- package/dist/chunk-fgk8cqew.js +0 -152
- package/dist/chunk-he9a874b.js +0 -769
- package/dist/chunk-k2yyatfw.js +0 -10
- package/dist/chunk-k4px0bnv.js +0 -23
- package/dist/chunk-nsgqsmrh.js +0 -1779
- package/dist/chunk-q0st3se8.js +0 -42033
- package/dist/chunk-qr803fye.js +0 -2122
- package/dist/chunk-rds0sy87.js +0 -1771
- package/dist/chunk-ttz2ra5j.js +0 -12
- package/dist/chunk-v0bahtg2.js +0 -4
- package/dist/chunk-va2yd85b.js +0 -200
- package/dist/chunk-x9s0cqfx.js +0 -13
- package/dist/chunk-zqtpg06c.js +0 -1752
package/dist/chunk-qr803fye.js
DELETED
|
@@ -1,2122 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Route53Client
|
|
3
|
-
} from "./chunk-32e7ya18.js";
|
|
4
|
-
import {
|
|
5
|
-
AWSClient
|
|
6
|
-
} from "./chunk-zqtpg06c.js";
|
|
7
|
-
|
|
8
|
-
// src/dns/types.ts
|
|
9
|
-
var PROXIABLE_RECORD_TYPES = new Set(["A", "AAAA", "CNAME"]);
|
|
10
|
-
// src/dns/porkbun.ts
|
|
11
|
-
var PORKBUN_API_URL = "https://api.porkbun.com/api/json/v3";
|
|
12
|
-
var PORKBUN_MAX_ATTEMPTS = 8;
|
|
13
|
-
var PORKBUN_REQUEST_TIMEOUT_MS = 15000;
|
|
14
|
-
function isRetryablePorkbunResponse(status, message = "") {
|
|
15
|
-
return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500 || /rate limit|temporar|timed? ?out|try again/i.test(message);
|
|
16
|
-
}
|
|
17
|
-
async function waitForPorkbunRetry(attempt, response) {
|
|
18
|
-
const retryAfter = response?.headers.get("retry-after");
|
|
19
|
-
const retryAfterMs = retryAfter === null || retryAfter === undefined ? undefined : Number(retryAfter) * 1000;
|
|
20
|
-
const delay = Number.isFinite(retryAfterMs) ? Math.max(0, retryAfterMs) : Math.min(1000 * 2 ** (attempt - 1), 30000);
|
|
21
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
class PorkbunProvider {
|
|
25
|
-
name = "porkbun";
|
|
26
|
-
apiKey;
|
|
27
|
-
secretKey;
|
|
28
|
-
requestTimeoutMs;
|
|
29
|
-
constructor(apiKey, secretKey, requestTimeoutMs = PORKBUN_REQUEST_TIMEOUT_MS) {
|
|
30
|
-
this.apiKey = apiKey;
|
|
31
|
-
this.secretKey = secretKey;
|
|
32
|
-
this.requestTimeoutMs = requestTimeoutMs;
|
|
33
|
-
}
|
|
34
|
-
async request(endpoint, additionalBody = {}) {
|
|
35
|
-
let lastError;
|
|
36
|
-
for (let attempt = 1;attempt <= PORKBUN_MAX_ATTEMPTS; attempt += 1) {
|
|
37
|
-
let response;
|
|
38
|
-
const controller = new AbortController;
|
|
39
|
-
const timeout = setTimeout(() => {
|
|
40
|
-
controller.abort(new Error(`Porkbun API request timed out after ${this.requestTimeoutMs}ms`));
|
|
41
|
-
}, this.requestTimeoutMs);
|
|
42
|
-
try {
|
|
43
|
-
response = await fetch(`${PORKBUN_API_URL}${endpoint}`, {
|
|
44
|
-
method: "POST",
|
|
45
|
-
headers: {
|
|
46
|
-
"Content-Type": "application/json"
|
|
47
|
-
},
|
|
48
|
-
body: JSON.stringify({
|
|
49
|
-
apikey: this.apiKey,
|
|
50
|
-
secretapikey: this.secretKey,
|
|
51
|
-
...additionalBody
|
|
52
|
-
}),
|
|
53
|
-
signal: controller.signal
|
|
54
|
-
});
|
|
55
|
-
} catch (error) {
|
|
56
|
-
lastError = error;
|
|
57
|
-
if (attempt === PORKBUN_MAX_ATTEMPTS)
|
|
58
|
-
throw error;
|
|
59
|
-
await waitForPorkbunRetry(attempt);
|
|
60
|
-
continue;
|
|
61
|
-
} finally {
|
|
62
|
-
clearTimeout(timeout);
|
|
63
|
-
}
|
|
64
|
-
if (!response.ok) {
|
|
65
|
-
lastError = new Error(`Porkbun API error: ${response.status} ${response.statusText}`);
|
|
66
|
-
if (!isRetryablePorkbunResponse(response.status) || attempt === PORKBUN_MAX_ATTEMPTS)
|
|
67
|
-
throw lastError;
|
|
68
|
-
await waitForPorkbunRetry(attempt, response);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
const data = await response.json();
|
|
72
|
-
if (data.status === "ERROR") {
|
|
73
|
-
lastError = new Error(`Porkbun API error: ${data.message || "Unknown error"}`);
|
|
74
|
-
if (!isRetryablePorkbunResponse(0, data.message) || attempt === PORKBUN_MAX_ATTEMPTS)
|
|
75
|
-
throw lastError;
|
|
76
|
-
await waitForPorkbunRetry(attempt, response);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
return data;
|
|
80
|
-
}
|
|
81
|
-
throw lastError;
|
|
82
|
-
}
|
|
83
|
-
getSubdomain(recordName, domain) {
|
|
84
|
-
const cleanName = recordName.replace(/\.$/, "");
|
|
85
|
-
const cleanDomain = domain.replace(/\.$/, "");
|
|
86
|
-
if (cleanName === cleanDomain) {
|
|
87
|
-
return "";
|
|
88
|
-
}
|
|
89
|
-
if (cleanName.endsWith(`.${cleanDomain}`)) {
|
|
90
|
-
return cleanName.slice(0, -(cleanDomain.length + 1));
|
|
91
|
-
}
|
|
92
|
-
return cleanName;
|
|
93
|
-
}
|
|
94
|
-
getRootDomain(domain) {
|
|
95
|
-
const parts = domain.replace(/\.$/, "").split(".");
|
|
96
|
-
if (parts.length >= 2) {
|
|
97
|
-
return parts.slice(-2).join(".");
|
|
98
|
-
}
|
|
99
|
-
return domain;
|
|
100
|
-
}
|
|
101
|
-
static ADDRESS_TYPES = new Set(["A", "AAAA"]);
|
|
102
|
-
async clearConflictingAliases(domain, record) {
|
|
103
|
-
if (!PorkbunProvider.ADDRESS_TYPES.has(record.type))
|
|
104
|
-
return [];
|
|
105
|
-
const rootDomain = this.getRootDomain(domain);
|
|
106
|
-
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
107
|
-
const listed = await this.listRecords(domain);
|
|
108
|
-
if (!listed.success)
|
|
109
|
-
return [`could not list records to clear ALIAS/CNAME conflicts: ${listed.message ?? "unknown provider error"}`];
|
|
110
|
-
const warnings = [];
|
|
111
|
-
for (const existing of listed.records) {
|
|
112
|
-
if (existing.type !== "ALIAS" && existing.type !== "CNAME")
|
|
113
|
-
continue;
|
|
114
|
-
if (this.getSubdomain(existing.name, rootDomain) !== subdomain)
|
|
115
|
-
continue;
|
|
116
|
-
const removed = await this.deleteRecord(domain, existing);
|
|
117
|
-
if (!removed.success)
|
|
118
|
-
warnings.push(`could not remove conflicting ${existing.type} on ${existing.name}: ${removed.message ?? "unknown provider error"}`);
|
|
119
|
-
}
|
|
120
|
-
return warnings;
|
|
121
|
-
}
|
|
122
|
-
async addressRecordExists(domain, record) {
|
|
123
|
-
const rootDomain = this.getRootDomain(domain);
|
|
124
|
-
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
125
|
-
const listed = await this.listRecords(domain);
|
|
126
|
-
if (!listed.success)
|
|
127
|
-
return true;
|
|
128
|
-
return listed.records.some((r) => r.type === record.type && this.getSubdomain(r.name, rootDomain) === subdomain && r.content === record.content);
|
|
129
|
-
}
|
|
130
|
-
async createRecord(domain, record) {
|
|
131
|
-
try {
|
|
132
|
-
const rootDomain = this.getRootDomain(domain);
|
|
133
|
-
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
134
|
-
const body = {
|
|
135
|
-
type: record.type,
|
|
136
|
-
content: record.content,
|
|
137
|
-
ttl: String(record.ttl || 600)
|
|
138
|
-
};
|
|
139
|
-
if (subdomain) {
|
|
140
|
-
body.name = subdomain;
|
|
141
|
-
}
|
|
142
|
-
if ((record.type === "MX" || record.type === "SRV") && record.priority !== undefined) {
|
|
143
|
-
body.prio = String(record.priority);
|
|
144
|
-
}
|
|
145
|
-
if (record.type === "SRV" && record.weight !== undefined && record.port !== undefined) {
|
|
146
|
-
body.content = `${record.weight} ${record.port} ${record.content}`;
|
|
147
|
-
}
|
|
148
|
-
const conflicts = await this.clearConflictingAliases(domain, record);
|
|
149
|
-
const response = await this.request(`/dns/create/${rootDomain}`, body);
|
|
150
|
-
if (PorkbunProvider.ADDRESS_TYPES.has(record.type) && !await this.addressRecordExists(domain, record)) {
|
|
151
|
-
return {
|
|
152
|
-
success: false,
|
|
153
|
-
message: `Porkbun reported success but no ${record.type} record for ${record.name} exists${conflicts.length > 0 ? ` (${conflicts.join("; ")})` : ""}. A conflicting ALIAS/CNAME on the same name silently voids the write.`
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
success: true,
|
|
158
|
-
id: response.id?.toString(),
|
|
159
|
-
message: "Record created successfully"
|
|
160
|
-
};
|
|
161
|
-
} catch (error) {
|
|
162
|
-
return {
|
|
163
|
-
success: false,
|
|
164
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
async upsertRecord(domain, record) {
|
|
169
|
-
try {
|
|
170
|
-
const rootDomain = this.getRootDomain(domain);
|
|
171
|
-
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
172
|
-
const existing = await this.listRecords(domain, record.type);
|
|
173
|
-
if (existing.success) {
|
|
174
|
-
const matchingRecord = existing.records.find((r) => {
|
|
175
|
-
const existingSubdomain = this.getSubdomain(r.name, rootDomain);
|
|
176
|
-
return existingSubdomain === subdomain && r.type === record.type;
|
|
177
|
-
});
|
|
178
|
-
if (matchingRecord?.id) {
|
|
179
|
-
const body = {
|
|
180
|
-
type: record.type,
|
|
181
|
-
content: record.content,
|
|
182
|
-
ttl: String(record.ttl || 600)
|
|
183
|
-
};
|
|
184
|
-
if (subdomain) {
|
|
185
|
-
body.name = subdomain;
|
|
186
|
-
}
|
|
187
|
-
if ((record.type === "MX" || record.type === "SRV") && record.priority !== undefined) {
|
|
188
|
-
body.prio = String(record.priority);
|
|
189
|
-
}
|
|
190
|
-
if (record.type === "SRV" && record.weight !== undefined && record.port !== undefined) {
|
|
191
|
-
body.content = `${record.weight} ${record.port} ${record.content}`;
|
|
192
|
-
}
|
|
193
|
-
await this.request(`/dns/edit/${rootDomain}/${matchingRecord.id}`, body);
|
|
194
|
-
return {
|
|
195
|
-
success: true,
|
|
196
|
-
id: matchingRecord.id,
|
|
197
|
-
message: "Record updated successfully"
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return this.createRecord(domain, record);
|
|
202
|
-
} catch (error) {
|
|
203
|
-
return this.createRecord(domain, record);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
async deleteRecord(domain, record) {
|
|
207
|
-
try {
|
|
208
|
-
const rootDomain = this.getRootDomain(domain);
|
|
209
|
-
if ("id" in record && record.id) {
|
|
210
|
-
await this.request(`/dns/delete/${rootDomain}/${record.id}`);
|
|
211
|
-
return {
|
|
212
|
-
success: true,
|
|
213
|
-
message: "Record deleted successfully"
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
217
|
-
const existing = await this.listRecords(domain);
|
|
218
|
-
if (!existing.success) {
|
|
219
|
-
return {
|
|
220
|
-
success: false,
|
|
221
|
-
message: "Failed to list records"
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
const matchingRecord = existing.records.find((r) => {
|
|
225
|
-
const existingSubdomain = this.getSubdomain(r.name, rootDomain);
|
|
226
|
-
return existingSubdomain === subdomain && r.type === record.type && r.content === record.content;
|
|
227
|
-
});
|
|
228
|
-
if (!matchingRecord?.id) {
|
|
229
|
-
return {
|
|
230
|
-
success: false,
|
|
231
|
-
message: "Record not found"
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
await this.request(`/dns/delete/${rootDomain}/${matchingRecord.id}`);
|
|
235
|
-
return {
|
|
236
|
-
success: true,
|
|
237
|
-
message: "Record deleted successfully"
|
|
238
|
-
};
|
|
239
|
-
} catch (error) {
|
|
240
|
-
return {
|
|
241
|
-
success: false,
|
|
242
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
async listRecords(domain, type) {
|
|
247
|
-
try {
|
|
248
|
-
const rootDomain = this.getRootDomain(domain);
|
|
249
|
-
let endpoint = `/dns/retrieve/${rootDomain}`;
|
|
250
|
-
if (type) {
|
|
251
|
-
endpoint = `/dns/retrieveByNameType/${rootDomain}/${type}`;
|
|
252
|
-
}
|
|
253
|
-
const response = await this.request(endpoint);
|
|
254
|
-
const records = (response.records || []).map((r) => ({
|
|
255
|
-
id: r.id,
|
|
256
|
-
name: r.name || rootDomain,
|
|
257
|
-
type: r.type,
|
|
258
|
-
content: r.content,
|
|
259
|
-
ttl: Number.parseInt(r.ttl, 10),
|
|
260
|
-
priority: r.prio ? Number.parseInt(r.prio, 10) : undefined
|
|
261
|
-
}));
|
|
262
|
-
return {
|
|
263
|
-
success: true,
|
|
264
|
-
records
|
|
265
|
-
};
|
|
266
|
-
} catch (error) {
|
|
267
|
-
return {
|
|
268
|
-
success: false,
|
|
269
|
-
records: [],
|
|
270
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async canManageDomain(domain) {
|
|
275
|
-
try {
|
|
276
|
-
const rootDomain = this.getRootDomain(domain);
|
|
277
|
-
await this.request(`/dns/retrieve/${rootDomain}`);
|
|
278
|
-
return true;
|
|
279
|
-
} catch {
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
async listDomains() {
|
|
284
|
-
try {
|
|
285
|
-
const response = await this.request("/domain/listAll");
|
|
286
|
-
return (response.domains || []).map((d) => d.domain);
|
|
287
|
-
} catch {
|
|
288
|
-
return [];
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
async getNameServers(domain) {
|
|
292
|
-
try {
|
|
293
|
-
const rootDomain = this.getRootDomain(domain);
|
|
294
|
-
const response = await this.request(`/dns/getNS/${rootDomain}`);
|
|
295
|
-
return response.ns || [];
|
|
296
|
-
} catch {
|
|
297
|
-
return [];
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
async updateNameServers(domain, nameservers) {
|
|
301
|
-
try {
|
|
302
|
-
const rootDomain = this.getRootDomain(domain);
|
|
303
|
-
await this.request(`/dns/updateNS/${rootDomain}`, {
|
|
304
|
-
ns: nameservers
|
|
305
|
-
});
|
|
306
|
-
return true;
|
|
307
|
-
} catch {
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
// src/dns/godaddy.ts
|
|
313
|
-
var GODADDY_API_URL = "https://api.godaddy.com";
|
|
314
|
-
var GODADDY_OTE_API_URL = "https://api.ote-godaddy.com";
|
|
315
|
-
|
|
316
|
-
class GoDaddyProvider {
|
|
317
|
-
name = "godaddy";
|
|
318
|
-
apiKey;
|
|
319
|
-
apiSecret;
|
|
320
|
-
baseUrl;
|
|
321
|
-
constructor(apiKey, apiSecret, environment = "production") {
|
|
322
|
-
this.apiKey = apiKey;
|
|
323
|
-
this.apiSecret = apiSecret;
|
|
324
|
-
this.baseUrl = environment === "ote" ? GODADDY_OTE_API_URL : GODADDY_API_URL;
|
|
325
|
-
}
|
|
326
|
-
async request(method, endpoint, body) {
|
|
327
|
-
const url = `${this.baseUrl}${endpoint}`;
|
|
328
|
-
const headers = {
|
|
329
|
-
Authorization: `sso-key ${this.apiKey}:${this.apiSecret}`,
|
|
330
|
-
"Content-Type": "application/json",
|
|
331
|
-
Accept: "application/json"
|
|
332
|
-
};
|
|
333
|
-
const options = {
|
|
334
|
-
method,
|
|
335
|
-
headers
|
|
336
|
-
};
|
|
337
|
-
if (body) {
|
|
338
|
-
options.body = JSON.stringify(body);
|
|
339
|
-
}
|
|
340
|
-
const response = await fetch(url, options);
|
|
341
|
-
if (response.status === 204) {
|
|
342
|
-
return {};
|
|
343
|
-
}
|
|
344
|
-
if (response.ok) {
|
|
345
|
-
const text = await response.text();
|
|
346
|
-
if (text) {
|
|
347
|
-
return JSON.parse(text);
|
|
348
|
-
}
|
|
349
|
-
return {};
|
|
350
|
-
}
|
|
351
|
-
let errorMessage = `GoDaddy API error: ${response.status} ${response.statusText}`;
|
|
352
|
-
try {
|
|
353
|
-
const errorData = await response.json();
|
|
354
|
-
if (errorData.message) {
|
|
355
|
-
errorMessage = `GoDaddy API error: ${errorData.message}`;
|
|
356
|
-
}
|
|
357
|
-
if (errorData.fields) {
|
|
358
|
-
errorMessage += ` - Fields: ${JSON.stringify(errorData.fields)}`;
|
|
359
|
-
}
|
|
360
|
-
} catch {}
|
|
361
|
-
throw new Error(errorMessage);
|
|
362
|
-
}
|
|
363
|
-
getSubdomain(recordName, domain) {
|
|
364
|
-
const cleanName = recordName.replace(/\.$/, "");
|
|
365
|
-
const cleanDomain = domain.replace(/\.$/, "");
|
|
366
|
-
if (cleanName === cleanDomain) {
|
|
367
|
-
return "@";
|
|
368
|
-
}
|
|
369
|
-
if (cleanName.endsWith(`.${cleanDomain}`)) {
|
|
370
|
-
return cleanName.slice(0, -(cleanDomain.length + 1));
|
|
371
|
-
}
|
|
372
|
-
return cleanName;
|
|
373
|
-
}
|
|
374
|
-
getRootDomain(domain) {
|
|
375
|
-
const parts = domain.replace(/\.$/, "").split(".");
|
|
376
|
-
if (parts.length >= 2) {
|
|
377
|
-
return parts.slice(-2).join(".");
|
|
378
|
-
}
|
|
379
|
-
return domain;
|
|
380
|
-
}
|
|
381
|
-
toGoDaddyRecord(record, domain) {
|
|
382
|
-
const rootDomain = this.getRootDomain(domain);
|
|
383
|
-
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
384
|
-
const gdRecord = {
|
|
385
|
-
type: record.type,
|
|
386
|
-
name: subdomain,
|
|
387
|
-
data: record.content,
|
|
388
|
-
ttl: record.ttl || 600
|
|
389
|
-
};
|
|
390
|
-
if (record.type === "MX" && record.priority !== undefined) {
|
|
391
|
-
gdRecord.priority = record.priority;
|
|
392
|
-
}
|
|
393
|
-
return gdRecord;
|
|
394
|
-
}
|
|
395
|
-
fromGoDaddyRecord(record, domain) {
|
|
396
|
-
const rootDomain = this.getRootDomain(domain);
|
|
397
|
-
let name = record.name;
|
|
398
|
-
if (name === "@") {
|
|
399
|
-
name = rootDomain;
|
|
400
|
-
} else if (!name.endsWith(rootDomain)) {
|
|
401
|
-
name = `${name}.${rootDomain}`;
|
|
402
|
-
}
|
|
403
|
-
return {
|
|
404
|
-
name,
|
|
405
|
-
type: record.type,
|
|
406
|
-
content: record.data,
|
|
407
|
-
ttl: record.ttl,
|
|
408
|
-
priority: record.priority
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
async createRecord(domain, record) {
|
|
412
|
-
try {
|
|
413
|
-
const rootDomain = this.getRootDomain(domain);
|
|
414
|
-
const gdRecord = this.toGoDaddyRecord(record, domain);
|
|
415
|
-
await this.request("PATCH", `/v1/domains/${rootDomain}/records`, [gdRecord]);
|
|
416
|
-
return {
|
|
417
|
-
success: true,
|
|
418
|
-
message: "Record created successfully"
|
|
419
|
-
};
|
|
420
|
-
} catch (error) {
|
|
421
|
-
return {
|
|
422
|
-
success: false,
|
|
423
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
async upsertRecord(domain, record) {
|
|
428
|
-
try {
|
|
429
|
-
const rootDomain = this.getRootDomain(domain);
|
|
430
|
-
const gdRecord = this.toGoDaddyRecord(record, domain);
|
|
431
|
-
await this.request("PUT", `/v1/domains/${rootDomain}/records/${record.type}/${gdRecord.name}`, [gdRecord]);
|
|
432
|
-
return {
|
|
433
|
-
success: true,
|
|
434
|
-
message: "Record upserted successfully"
|
|
435
|
-
};
|
|
436
|
-
} catch (error) {
|
|
437
|
-
return this.createRecord(domain, record);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
async deleteRecord(domain, record) {
|
|
441
|
-
try {
|
|
442
|
-
const rootDomain = this.getRootDomain(domain);
|
|
443
|
-
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
444
|
-
const existingRecords = await this.request("GET", `/v1/domains/${rootDomain}/records/${record.type}/${subdomain}`);
|
|
445
|
-
const remainingRecords = existingRecords.filter((r) => r.data !== record.content);
|
|
446
|
-
if (remainingRecords.length === existingRecords.length) {
|
|
447
|
-
return {
|
|
448
|
-
success: false,
|
|
449
|
-
message: "Record not found"
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
if (remainingRecords.length === 0) {
|
|
453
|
-
try {
|
|
454
|
-
await this.request("DELETE", `/v1/domains/${rootDomain}/records/${record.type}/${subdomain}`);
|
|
455
|
-
} catch {}
|
|
456
|
-
} else {
|
|
457
|
-
await this.request("PUT", `/v1/domains/${rootDomain}/records/${record.type}/${subdomain}`, remainingRecords);
|
|
458
|
-
}
|
|
459
|
-
return {
|
|
460
|
-
success: true,
|
|
461
|
-
message: "Record deleted successfully"
|
|
462
|
-
};
|
|
463
|
-
} catch (error) {
|
|
464
|
-
return {
|
|
465
|
-
success: false,
|
|
466
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
async listRecords(domain, type) {
|
|
471
|
-
try {
|
|
472
|
-
const rootDomain = this.getRootDomain(domain);
|
|
473
|
-
let endpoint = `/v1/domains/${rootDomain}/records`;
|
|
474
|
-
if (type) {
|
|
475
|
-
endpoint = `/v1/domains/${rootDomain}/records/${type}`;
|
|
476
|
-
}
|
|
477
|
-
const records = await this.request("GET", endpoint);
|
|
478
|
-
return {
|
|
479
|
-
success: true,
|
|
480
|
-
records: records.map((r) => this.fromGoDaddyRecord(r, domain))
|
|
481
|
-
};
|
|
482
|
-
} catch (error) {
|
|
483
|
-
return {
|
|
484
|
-
success: false,
|
|
485
|
-
records: [],
|
|
486
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
async canManageDomain(domain) {
|
|
491
|
-
try {
|
|
492
|
-
const rootDomain = this.getRootDomain(domain);
|
|
493
|
-
await this.request("GET", `/v1/domains/${rootDomain}`);
|
|
494
|
-
return true;
|
|
495
|
-
} catch {
|
|
496
|
-
return false;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
async listDomains() {
|
|
500
|
-
try {
|
|
501
|
-
const response = await this.request("GET", "/v1/domains");
|
|
502
|
-
return response.map((d) => d.domain);
|
|
503
|
-
} catch {
|
|
504
|
-
return [];
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
async getDomainDetails(domain) {
|
|
508
|
-
try {
|
|
509
|
-
const rootDomain = this.getRootDomain(domain);
|
|
510
|
-
const details = await this.request("GET", `/v1/domains/${rootDomain}`);
|
|
511
|
-
return {
|
|
512
|
-
domain: details.domain,
|
|
513
|
-
status: details.status,
|
|
514
|
-
nameServers: details.nameServers,
|
|
515
|
-
expires: details.expires
|
|
516
|
-
};
|
|
517
|
-
} catch {
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
async updateNameServers(domain, nameservers) {
|
|
522
|
-
try {
|
|
523
|
-
const rootDomain = this.getRootDomain(domain);
|
|
524
|
-
await this.request("PUT", `/v1/domains/${rootDomain}/records/NS`, nameservers.map((ns) => ({
|
|
525
|
-
type: "NS",
|
|
526
|
-
name: "@",
|
|
527
|
-
data: ns,
|
|
528
|
-
ttl: 3600
|
|
529
|
-
})));
|
|
530
|
-
return true;
|
|
531
|
-
} catch {
|
|
532
|
-
return false;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
async checkDomainAvailability(domain) {
|
|
536
|
-
try {
|
|
537
|
-
const result = await this.request("GET", `/v1/domains/available?domain=${encodeURIComponent(domain)}`);
|
|
538
|
-
return {
|
|
539
|
-
available: result.available,
|
|
540
|
-
price: result.price,
|
|
541
|
-
currency: result.currency
|
|
542
|
-
};
|
|
543
|
-
} catch {
|
|
544
|
-
return { available: false };
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
// src/dns/cloudflare.ts
|
|
549
|
-
var CLOUDFLARE_API_URL = "https://api.cloudflare.com/client/v4";
|
|
550
|
-
var CLOUDFLARE_MANAGED_RULE_PREFIX = "[ts-cloud]";
|
|
551
|
-
|
|
552
|
-
class CloudflareProvider {
|
|
553
|
-
name = "cloudflare";
|
|
554
|
-
apiToken;
|
|
555
|
-
zoneCache = new Map;
|
|
556
|
-
configuredZoneId;
|
|
557
|
-
accountId;
|
|
558
|
-
configuredZoneName;
|
|
559
|
-
constructor(apiToken, options = {}) {
|
|
560
|
-
this.apiToken = apiToken;
|
|
561
|
-
this.configuredZoneId = options.zoneId;
|
|
562
|
-
this.accountId = options.accountId;
|
|
563
|
-
}
|
|
564
|
-
async request(method, endpoint, body) {
|
|
565
|
-
const url = `${CLOUDFLARE_API_URL}${endpoint}`;
|
|
566
|
-
const headers = {
|
|
567
|
-
Authorization: `Bearer ${this.apiToken}`,
|
|
568
|
-
"Content-Type": "application/json"
|
|
569
|
-
};
|
|
570
|
-
const options = {
|
|
571
|
-
method,
|
|
572
|
-
headers
|
|
573
|
-
};
|
|
574
|
-
if (body) {
|
|
575
|
-
options.body = JSON.stringify(body);
|
|
576
|
-
}
|
|
577
|
-
const response = await fetch(url, options);
|
|
578
|
-
const data = await response.json();
|
|
579
|
-
if (!data.success) {
|
|
580
|
-
const errorMessages = data.errors.map((e) => e.message).join(", ");
|
|
581
|
-
throw new Error(`Cloudflare API error: ${errorMessages}`);
|
|
582
|
-
}
|
|
583
|
-
return data;
|
|
584
|
-
}
|
|
585
|
-
getRootDomain(domain) {
|
|
586
|
-
const parts = domain.replace(/\.$/, "").split(".");
|
|
587
|
-
if (parts.length >= 2) {
|
|
588
|
-
return parts.slice(-2).join(".");
|
|
589
|
-
}
|
|
590
|
-
return domain;
|
|
591
|
-
}
|
|
592
|
-
async resolveZone(domain) {
|
|
593
|
-
const hostname = domain.replace(/\.$/, "").toLowerCase();
|
|
594
|
-
if (this.configuredZoneId) {
|
|
595
|
-
const name = await this.resolveConfiguredZoneName();
|
|
596
|
-
if (name && (hostname === name || hostname.endsWith(`.${name}`))) {
|
|
597
|
-
return { id: this.configuredZoneId, name };
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
const rootDomain = this.getRootDomain(hostname);
|
|
601
|
-
const cached = this.zoneCache.get(rootDomain);
|
|
602
|
-
if (cached) {
|
|
603
|
-
return { id: cached, name: rootDomain };
|
|
604
|
-
}
|
|
605
|
-
const response = await this.request("GET", `/zones?name=${encodeURIComponent(rootDomain)}`);
|
|
606
|
-
if (!response.result || response.result.length === 0) {
|
|
607
|
-
throw new Error(`Zone not found for domain: ${rootDomain}. ` + `If the API token is scoped to a single zone, set its zone id (CLOUDFLARE_ZONE_ID) — ` + `zone lookup by name needs account-wide Zone:Read.`);
|
|
608
|
-
}
|
|
609
|
-
const zoneId = response.result[0].id;
|
|
610
|
-
this.zoneCache.set(rootDomain, zoneId);
|
|
611
|
-
return { id: zoneId, name: response.result[0].name || rootDomain };
|
|
612
|
-
}
|
|
613
|
-
async resolveConfiguredZoneName() {
|
|
614
|
-
if (!this.configuredZoneId)
|
|
615
|
-
return;
|
|
616
|
-
if (this.configuredZoneName)
|
|
617
|
-
return this.configuredZoneName;
|
|
618
|
-
try {
|
|
619
|
-
const response = await this.request("GET", `/zones/${this.configuredZoneId}`);
|
|
620
|
-
const name = response.result?.name?.toLowerCase();
|
|
621
|
-
if (!name)
|
|
622
|
-
return;
|
|
623
|
-
this.configuredZoneName = name;
|
|
624
|
-
this.zoneCache.set(name, this.configuredZoneId);
|
|
625
|
-
return name;
|
|
626
|
-
} catch {
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
async getZoneId(domain) {
|
|
631
|
-
return (await this.resolveZone(domain)).id;
|
|
632
|
-
}
|
|
633
|
-
getFullRecordName(name, zoneName) {
|
|
634
|
-
const cleanName = name.replace(/\.$/, "");
|
|
635
|
-
if (!cleanName || cleanName === zoneName || cleanName === "@") {
|
|
636
|
-
return zoneName;
|
|
637
|
-
}
|
|
638
|
-
if (cleanName.endsWith(`.${zoneName}`)) {
|
|
639
|
-
return cleanName;
|
|
640
|
-
}
|
|
641
|
-
return `${cleanName}.${zoneName}`;
|
|
642
|
-
}
|
|
643
|
-
toCloudflareRecord(record, zoneName, proxied) {
|
|
644
|
-
const cfRecord = {
|
|
645
|
-
type: record.type,
|
|
646
|
-
name: this.getFullRecordName(record.name, zoneName),
|
|
647
|
-
content: record.content || record.value || "",
|
|
648
|
-
ttl: record.ttl || 1
|
|
649
|
-
};
|
|
650
|
-
const desiredProxy = proxied ?? record.proxied;
|
|
651
|
-
if (desiredProxy !== undefined && PROXIABLE_RECORD_TYPES.has(record.type)) {
|
|
652
|
-
cfRecord.proxied = desiredProxy;
|
|
653
|
-
if (desiredProxy)
|
|
654
|
-
cfRecord.ttl = 1;
|
|
655
|
-
}
|
|
656
|
-
if (record.type === "MX" && record.priority !== undefined) {
|
|
657
|
-
cfRecord.priority = record.priority;
|
|
658
|
-
}
|
|
659
|
-
if (record.type === "SRV") {
|
|
660
|
-
if (record.priority !== undefined) {
|
|
661
|
-
cfRecord.priority = record.priority;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return cfRecord;
|
|
665
|
-
}
|
|
666
|
-
fromCloudflareRecord(record) {
|
|
667
|
-
return {
|
|
668
|
-
id: record.id,
|
|
669
|
-
name: record.name,
|
|
670
|
-
type: record.type,
|
|
671
|
-
content: record.content,
|
|
672
|
-
ttl: record.ttl,
|
|
673
|
-
priority: record.priority,
|
|
674
|
-
proxied: record.proxied
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
async createRecord(domain, record) {
|
|
678
|
-
try {
|
|
679
|
-
const zone = await this.resolveZone(domain);
|
|
680
|
-
const cfRecord = this.toCloudflareRecord(record, zone.name);
|
|
681
|
-
const response = await this.request("POST", `/zones/${zone.id}/dns_records`, cfRecord);
|
|
682
|
-
return {
|
|
683
|
-
success: true,
|
|
684
|
-
id: response.result.id,
|
|
685
|
-
message: "Record created successfully"
|
|
686
|
-
};
|
|
687
|
-
} catch (error) {
|
|
688
|
-
return {
|
|
689
|
-
success: false,
|
|
690
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
async upsertRecord(domain, record) {
|
|
695
|
-
try {
|
|
696
|
-
const zone = await this.resolveZone(domain);
|
|
697
|
-
const fullName = this.getFullRecordName(record.name, zone.name);
|
|
698
|
-
const existingResponse = await this.request("GET", `/zones/${zone.id}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
|
|
699
|
-
const existing = existingResponse.result?.[0];
|
|
700
|
-
const proxied = record.proxied ?? existing?.proxied;
|
|
701
|
-
const cfRecord = this.toCloudflareRecord(record, zone.name, proxied);
|
|
702
|
-
if (existing) {
|
|
703
|
-
const response2 = await this.request("PUT", `/zones/${zone.id}/dns_records/${existing.id}`, cfRecord);
|
|
704
|
-
return {
|
|
705
|
-
success: true,
|
|
706
|
-
id: response2.result.id,
|
|
707
|
-
message: "Record updated successfully"
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
const response = await this.request("POST", `/zones/${zone.id}/dns_records`, cfRecord);
|
|
711
|
-
return {
|
|
712
|
-
success: true,
|
|
713
|
-
id: response.result.id,
|
|
714
|
-
message: "Record created successfully"
|
|
715
|
-
};
|
|
716
|
-
} catch (error) {
|
|
717
|
-
return this.createRecord(domain, record);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
async deleteRecord(domain, record) {
|
|
721
|
-
try {
|
|
722
|
-
const zone = await this.resolveZone(domain);
|
|
723
|
-
const zoneId = zone.id;
|
|
724
|
-
const fullName = this.getFullRecordName(record.name, zone.name);
|
|
725
|
-
const existingResponse = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
|
|
726
|
-
if (!existingResponse.result || existingResponse.result.length === 0) {
|
|
727
|
-
return {
|
|
728
|
-
success: false,
|
|
729
|
-
message: "Record not found"
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
const matchingRecord = existingResponse.result.find((r) => r.content === record.content);
|
|
733
|
-
if (!matchingRecord) {
|
|
734
|
-
return {
|
|
735
|
-
success: false,
|
|
736
|
-
message: "Record with matching content not found"
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
await this.request("DELETE", `/zones/${zoneId}/dns_records/${matchingRecord.id}`);
|
|
740
|
-
return {
|
|
741
|
-
success: true,
|
|
742
|
-
message: "Record deleted successfully"
|
|
743
|
-
};
|
|
744
|
-
} catch (error) {
|
|
745
|
-
return {
|
|
746
|
-
success: false,
|
|
747
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
async listRecords(domain, type) {
|
|
752
|
-
try {
|
|
753
|
-
const zoneId = await this.getZoneId(domain);
|
|
754
|
-
let endpoint = `/zones/${zoneId}/dns_records?per_page=100`;
|
|
755
|
-
if (type) {
|
|
756
|
-
endpoint += `&type=${type}`;
|
|
757
|
-
}
|
|
758
|
-
const allRecords = [];
|
|
759
|
-
let page = 1;
|
|
760
|
-
let hasMore = true;
|
|
761
|
-
while (hasMore) {
|
|
762
|
-
const response = await this.request("GET", `${endpoint}&page=${page}`);
|
|
763
|
-
allRecords.push(...response.result || []);
|
|
764
|
-
if (response.result_info) {
|
|
765
|
-
hasMore = page < response.result_info.total_pages;
|
|
766
|
-
page++;
|
|
767
|
-
} else {
|
|
768
|
-
hasMore = false;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
return {
|
|
772
|
-
success: true,
|
|
773
|
-
records: allRecords.map((r) => this.fromCloudflareRecord(r))
|
|
774
|
-
};
|
|
775
|
-
} catch (error) {
|
|
776
|
-
return {
|
|
777
|
-
success: false,
|
|
778
|
-
records: [],
|
|
779
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
async canManageDomain(domain) {
|
|
784
|
-
try {
|
|
785
|
-
await this.getZoneId(domain);
|
|
786
|
-
return true;
|
|
787
|
-
} catch {
|
|
788
|
-
return false;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
async listDomains() {
|
|
792
|
-
try {
|
|
793
|
-
const allZones = [];
|
|
794
|
-
let page = 1;
|
|
795
|
-
let hasMore = true;
|
|
796
|
-
while (hasMore) {
|
|
797
|
-
const response = await this.request("GET", `/zones?per_page=50&page=${page}`);
|
|
798
|
-
allZones.push(...response.result || []);
|
|
799
|
-
if (response.result_info) {
|
|
800
|
-
hasMore = page < response.result_info.total_pages;
|
|
801
|
-
page++;
|
|
802
|
-
} else {
|
|
803
|
-
hasMore = false;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
return allZones.map((z) => z.name);
|
|
807
|
-
} catch {
|
|
808
|
-
return [];
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
async getZoneDetails(domain) {
|
|
812
|
-
try {
|
|
813
|
-
const zoneId = await this.getZoneId(domain);
|
|
814
|
-
const response = await this.request("GET", `/zones/${zoneId}`);
|
|
815
|
-
return {
|
|
816
|
-
id: response.result.id,
|
|
817
|
-
name: response.result.name,
|
|
818
|
-
status: response.result.status,
|
|
819
|
-
nameServers: response.result.name_servers,
|
|
820
|
-
paused: response.result.paused
|
|
821
|
-
};
|
|
822
|
-
} catch {
|
|
823
|
-
return null;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
async purgeCache(domain, options) {
|
|
827
|
-
try {
|
|
828
|
-
const zoneId = await this.getZoneId(domain);
|
|
829
|
-
const body = {};
|
|
830
|
-
if (options?.purgeEverything) {
|
|
831
|
-
body.purge_everything = true;
|
|
832
|
-
} else {
|
|
833
|
-
if (options?.files)
|
|
834
|
-
body.files = options.files;
|
|
835
|
-
if (options?.tags)
|
|
836
|
-
body.tags = options.tags;
|
|
837
|
-
if (options?.hosts)
|
|
838
|
-
body.hosts = options.hosts;
|
|
839
|
-
}
|
|
840
|
-
if (Object.keys(body).length === 0) {
|
|
841
|
-
body.purge_everything = true;
|
|
842
|
-
}
|
|
843
|
-
await this.request("POST", `/zones/${zoneId}/purge_cache`, body);
|
|
844
|
-
return true;
|
|
845
|
-
} catch {
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
async getRecordProxyStatus(domain, record) {
|
|
850
|
-
try {
|
|
851
|
-
const zone = await this.resolveZone(domain);
|
|
852
|
-
const zoneId = zone.id;
|
|
853
|
-
const fullName = this.getFullRecordName(record.name, zone.name);
|
|
854
|
-
const response = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
|
|
855
|
-
if (response.result && response.result.length > 0) {
|
|
856
|
-
const matchingRecord = response.result.find((r) => r.content === record.content);
|
|
857
|
-
return matchingRecord?.proxied ?? null;
|
|
858
|
-
}
|
|
859
|
-
return null;
|
|
860
|
-
} catch {
|
|
861
|
-
return null;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
async setRecordProxyStatus(domain, record, proxied) {
|
|
865
|
-
try {
|
|
866
|
-
const zone = await this.resolveZone(domain);
|
|
867
|
-
const zoneId = zone.id;
|
|
868
|
-
const fullName = this.getFullRecordName(record.name, zone.name);
|
|
869
|
-
const response = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
|
|
870
|
-
if (!response.result || response.result.length === 0) {
|
|
871
|
-
return false;
|
|
872
|
-
}
|
|
873
|
-
const matchingRecord = response.result.find((r) => r.content === record.content);
|
|
874
|
-
if (!matchingRecord) {
|
|
875
|
-
return false;
|
|
876
|
-
}
|
|
877
|
-
await this.request("PATCH", `/zones/${zoneId}/dns_records/${matchingRecord.id}`, { proxied });
|
|
878
|
-
return true;
|
|
879
|
-
} catch {
|
|
880
|
-
return false;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
async zoneIdFor(domain) {
|
|
884
|
-
return this.getZoneId(domain);
|
|
885
|
-
}
|
|
886
|
-
get account() {
|
|
887
|
-
return this.accountId;
|
|
888
|
-
}
|
|
889
|
-
async getZoneSettings(domain) {
|
|
890
|
-
const zoneId = await this.getZoneId(domain);
|
|
891
|
-
const response = await this.request("GET", `/zones/${zoneId}/settings`);
|
|
892
|
-
const settings = {};
|
|
893
|
-
for (const setting of response.result || [])
|
|
894
|
-
settings[setting.id] = setting.value;
|
|
895
|
-
return settings;
|
|
896
|
-
}
|
|
897
|
-
async applyZoneSettings(domain, desired) {
|
|
898
|
-
const zoneId = await this.getZoneId(domain);
|
|
899
|
-
const current = await this.getZoneSettings(domain).catch(() => ({}));
|
|
900
|
-
const changed = [];
|
|
901
|
-
const failed = [];
|
|
902
|
-
for (const [id, value] of Object.entries(desired)) {
|
|
903
|
-
if (value === undefined)
|
|
904
|
-
continue;
|
|
905
|
-
const existing = current[id];
|
|
906
|
-
if (existing !== undefined && deepEqual(existing, value))
|
|
907
|
-
continue;
|
|
908
|
-
try {
|
|
909
|
-
await this.request("PATCH", `/zones/${zoneId}/settings/${id}`, { value });
|
|
910
|
-
changed.push({ id, from: existing, to: value });
|
|
911
|
-
} catch (error) {
|
|
912
|
-
failed.push({ id, error: error instanceof Error ? error.message : String(error) });
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
return { changed, failed };
|
|
916
|
-
}
|
|
917
|
-
async getPhaseRules(domain, phase) {
|
|
918
|
-
const zoneId = await this.getZoneId(domain);
|
|
919
|
-
try {
|
|
920
|
-
const response = await this.request("GET", `/zones/${zoneId}/rulesets/phases/${phase}/entrypoint`);
|
|
921
|
-
return response.result?.rules || [];
|
|
922
|
-
} catch {
|
|
923
|
-
return [];
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
async putManagedPhaseRules(domain, phase, rules) {
|
|
927
|
-
try {
|
|
928
|
-
const zoneId = await this.getZoneId(domain);
|
|
929
|
-
const existing = await this.getPhaseRules(domain, phase);
|
|
930
|
-
const foreign = existing.filter((rule) => !(rule.description || "").startsWith(CLOUDFLARE_MANAGED_RULE_PREFIX));
|
|
931
|
-
const managed = rules.map((rule) => ({
|
|
932
|
-
...rule,
|
|
933
|
-
description: rule.description?.startsWith(CLOUDFLARE_MANAGED_RULE_PREFIX) ? rule.description : `${CLOUDFLARE_MANAGED_RULE_PREFIX} ${rule.description || phase}`
|
|
934
|
-
}));
|
|
935
|
-
const payload = [...foreign, ...managed].map(({ id: _id, ...rule }) => rule);
|
|
936
|
-
await this.request("PUT", `/zones/${zoneId}/rulesets/phases/${phase}/entrypoint`, { rules: payload });
|
|
937
|
-
return { success: true };
|
|
938
|
-
} catch (error) {
|
|
939
|
-
return { success: false, message: error instanceof Error ? error.message : String(error) };
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
function deepEqual(a, b) {
|
|
944
|
-
if (a === b)
|
|
945
|
-
return true;
|
|
946
|
-
if (typeof a !== typeof b || a === null || b === null)
|
|
947
|
-
return false;
|
|
948
|
-
if (typeof a !== "object")
|
|
949
|
-
return false;
|
|
950
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
951
|
-
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
952
|
-
return false;
|
|
953
|
-
return a.every((item, index) => deepEqual(item, b[index]));
|
|
954
|
-
}
|
|
955
|
-
const aRecord = a;
|
|
956
|
-
const bRecord = b;
|
|
957
|
-
return Object.keys(bRecord).every((key) => deepEqual(aRecord[key], bRecord[key]));
|
|
958
|
-
}
|
|
959
|
-
// src/dns/declared-records.ts
|
|
960
|
-
var SINGLETON_TYPES = new Set(["A", "AAAA", "CNAME", "ALIAS"]);
|
|
961
|
-
function policyTag(content) {
|
|
962
|
-
const normalized = content.trim().replace(/^"|"$/g, "").toLowerCase();
|
|
963
|
-
if (normalized.startsWith("v=spf1"))
|
|
964
|
-
return "v=spf1";
|
|
965
|
-
if (normalized.startsWith("v=dmarc1"))
|
|
966
|
-
return "v=dmarc1";
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
function unquote(value) {
|
|
970
|
-
return value.trim().replace(/^"(.*)"$/s, "$1");
|
|
971
|
-
}
|
|
972
|
-
function normalizeName(name) {
|
|
973
|
-
return name.replace(/\.$/, "").toLowerCase();
|
|
974
|
-
}
|
|
975
|
-
function qualifyName(name, zone) {
|
|
976
|
-
const z = normalizeName(zone);
|
|
977
|
-
const raw = (name ?? "@").trim();
|
|
978
|
-
if (!raw || raw === "@")
|
|
979
|
-
return z;
|
|
980
|
-
const n = normalizeName(raw);
|
|
981
|
-
if (n === z || n.endsWith(`.${z}`))
|
|
982
|
-
return n;
|
|
983
|
-
return `${n}.${z}`;
|
|
984
|
-
}
|
|
985
|
-
function resolveDeclaredRecords(records, zone) {
|
|
986
|
-
return records.map((record) => ({
|
|
987
|
-
...record,
|
|
988
|
-
name: qualifyName(record.name, zone),
|
|
989
|
-
proxied: record.proxied ?? false
|
|
990
|
-
}));
|
|
991
|
-
}
|
|
992
|
-
function matches(existing, record) {
|
|
993
|
-
if (existing.type !== record.type)
|
|
994
|
-
return false;
|
|
995
|
-
if (normalizeName(existing.name) !== normalizeName(record.name))
|
|
996
|
-
return false;
|
|
997
|
-
if (SINGLETON_TYPES.has(record.type))
|
|
998
|
-
return true;
|
|
999
|
-
if (record.type === "TXT") {
|
|
1000
|
-
const tag = policyTag(record.content);
|
|
1001
|
-
if (tag)
|
|
1002
|
-
return policyTag(unquote(existing.content)) === tag;
|
|
1003
|
-
return unquote(existing.content) === unquote(record.content);
|
|
1004
|
-
}
|
|
1005
|
-
return unquote(existing.content) === unquote(record.content);
|
|
1006
|
-
}
|
|
1007
|
-
function isCurrent(existing, record) {
|
|
1008
|
-
if (unquote(existing.content) !== unquote(record.content))
|
|
1009
|
-
return false;
|
|
1010
|
-
if (record.priority !== undefined && existing.priority !== record.priority)
|
|
1011
|
-
return false;
|
|
1012
|
-
if (record.proxied !== undefined && existing.proxied !== undefined && existing.proxied !== record.proxied)
|
|
1013
|
-
return false;
|
|
1014
|
-
return true;
|
|
1015
|
-
}
|
|
1016
|
-
async function reconcileDeclaredRecords(options) {
|
|
1017
|
-
const { provider, zone, records } = options;
|
|
1018
|
-
const report = { outcomes: [], warnings: [] };
|
|
1019
|
-
if (records.length === 0)
|
|
1020
|
-
return report;
|
|
1021
|
-
const listed = await provider.listRecords(zone);
|
|
1022
|
-
if (!listed.success) {
|
|
1023
|
-
report.warnings.push(`could not list ${zone}: ${listed.message || "unknown provider error"} — records not reconciled`);
|
|
1024
|
-
return report;
|
|
1025
|
-
}
|
|
1026
|
-
const existing = listed.records;
|
|
1027
|
-
for (const record of records) {
|
|
1028
|
-
const current = existing.filter((e) => matches(e, record));
|
|
1029
|
-
if (current.length > 0 && current.every((e) => isCurrent(e, record))) {
|
|
1030
|
-
report.outcomes.push({ record, action: "unchanged" });
|
|
1031
|
-
continue;
|
|
1032
|
-
}
|
|
1033
|
-
const wire = {
|
|
1034
|
-
name: record.name,
|
|
1035
|
-
type: record.type,
|
|
1036
|
-
content: record.content,
|
|
1037
|
-
ttl: record.ttl ?? 3600,
|
|
1038
|
-
...record.priority !== undefined ? { priority: record.priority } : {},
|
|
1039
|
-
...record.proxied !== undefined ? { proxied: record.proxied } : {}
|
|
1040
|
-
};
|
|
1041
|
-
const supersedes = record.type === "TXT" && policyTag(record.content) !== undefined;
|
|
1042
|
-
if (supersedes) {
|
|
1043
|
-
for (const stale of current) {
|
|
1044
|
-
const removed = await provider.deleteRecord(zone, {
|
|
1045
|
-
name: stale.name,
|
|
1046
|
-
type: stale.type,
|
|
1047
|
-
content: stale.content
|
|
1048
|
-
});
|
|
1049
|
-
if (!removed.success)
|
|
1050
|
-
report.warnings.push(`could not remove superseded ${stale.type} at ${stale.name}: ${removed.message || "unknown error"}`);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
const result = supersedes || current.length === 0 ? await provider.createRecord(zone, wire) : await provider.upsertRecord(zone, wire);
|
|
1054
|
-
if (result.success)
|
|
1055
|
-
report.outcomes.push({ record, action: current.length > 0 ? "updated" : "created" });
|
|
1056
|
-
else
|
|
1057
|
-
report.outcomes.push({ record, action: "failed", message: result.message });
|
|
1058
|
-
}
|
|
1059
|
-
for (const type of ["MX"]) {
|
|
1060
|
-
const declaredNames = new Set(records.filter((r) => r.type === type).map((r) => normalizeName(r.name)));
|
|
1061
|
-
for (const name of declaredNames) {
|
|
1062
|
-
const declaredValues = new Set(records.filter((r) => r.type === type && normalizeName(r.name) === name).map((r) => unquote(r.content).toLowerCase()));
|
|
1063
|
-
const strays = existing.filter((e) => e.type === type && normalizeName(e.name) === name && !declaredValues.has(unquote(e.content).toLowerCase()));
|
|
1064
|
-
for (const stray of strays)
|
|
1065
|
-
report.warnings.push(`${name} has an undeclared ${type} record (${stray.content}) alongside the declared one — mail will be split between them. Remove it at the provider if it is stale.`);
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
return report;
|
|
1069
|
-
}
|
|
1070
|
-
// src/dns/route53-adapter.ts
|
|
1071
|
-
class Route53Provider {
|
|
1072
|
-
name = "route53";
|
|
1073
|
-
client;
|
|
1074
|
-
hostedZoneCache = new Map;
|
|
1075
|
-
providedHostedZoneId;
|
|
1076
|
-
constructor(region = "us-east-1", hostedZoneId) {
|
|
1077
|
-
this.client = new Route53Client(region);
|
|
1078
|
-
this.providedHostedZoneId = hostedZoneId;
|
|
1079
|
-
}
|
|
1080
|
-
getRootDomain(domain) {
|
|
1081
|
-
const parts = domain.replace(/\.$/, "").split(".");
|
|
1082
|
-
if (parts.length >= 2) {
|
|
1083
|
-
return parts.slice(-2).join(".");
|
|
1084
|
-
}
|
|
1085
|
-
return domain;
|
|
1086
|
-
}
|
|
1087
|
-
async getHostedZoneId(domain) {
|
|
1088
|
-
if (this.providedHostedZoneId) {
|
|
1089
|
-
return this.providedHostedZoneId;
|
|
1090
|
-
}
|
|
1091
|
-
const rootDomain = this.getRootDomain(domain);
|
|
1092
|
-
const cached = this.hostedZoneCache.get(rootDomain);
|
|
1093
|
-
if (cached) {
|
|
1094
|
-
return cached;
|
|
1095
|
-
}
|
|
1096
|
-
const zone = await this.client.findHostedZoneForDomain(domain);
|
|
1097
|
-
if (zone) {
|
|
1098
|
-
const zoneId = zone.Id.replace("/hostedzone/", "");
|
|
1099
|
-
this.hostedZoneCache.set(rootDomain, zoneId);
|
|
1100
|
-
return zoneId;
|
|
1101
|
-
}
|
|
1102
|
-
return null;
|
|
1103
|
-
}
|
|
1104
|
-
normalizeName(domain, name) {
|
|
1105
|
-
const zone = domain.replace(/\.$/, "");
|
|
1106
|
-
const record = name.replace(/\.$/, "");
|
|
1107
|
-
if (!record || record === "@")
|
|
1108
|
-
return `${zone}.`;
|
|
1109
|
-
if (record === zone || record.endsWith(`.${zone}`))
|
|
1110
|
-
return `${record}.`;
|
|
1111
|
-
return `${record}.${zone}.`;
|
|
1112
|
-
}
|
|
1113
|
-
async createRecord(domain, record) {
|
|
1114
|
-
try {
|
|
1115
|
-
const hostedZoneId = await this.getHostedZoneId(domain);
|
|
1116
|
-
if (!hostedZoneId) {
|
|
1117
|
-
return {
|
|
1118
|
-
success: false,
|
|
1119
|
-
message: `No hosted zone found for domain: ${domain}`
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
const recordName = this.normalizeName(domain, record.name);
|
|
1123
|
-
let recordValue = record.content;
|
|
1124
|
-
if (record.type === "TXT" && !recordValue.startsWith('"')) {
|
|
1125
|
-
recordValue = `"${recordValue}"`;
|
|
1126
|
-
}
|
|
1127
|
-
if (record.type === "MX" && record.priority !== undefined) {
|
|
1128
|
-
recordValue = `${record.priority} ${recordValue}`;
|
|
1129
|
-
}
|
|
1130
|
-
const result = await this.client.changeResourceRecordSets({
|
|
1131
|
-
HostedZoneId: hostedZoneId,
|
|
1132
|
-
ChangeBatch: {
|
|
1133
|
-
Comment: `Created by ts-cloud DNS provider`,
|
|
1134
|
-
Changes: [
|
|
1135
|
-
{
|
|
1136
|
-
Action: "CREATE",
|
|
1137
|
-
ResourceRecordSet: {
|
|
1138
|
-
Name: recordName,
|
|
1139
|
-
Type: record.type,
|
|
1140
|
-
TTL: record.ttl || 300,
|
|
1141
|
-
ResourceRecords: [{ Value: recordValue }]
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
]
|
|
1145
|
-
}
|
|
1146
|
-
});
|
|
1147
|
-
return {
|
|
1148
|
-
success: true,
|
|
1149
|
-
id: result.ChangeInfo?.Id,
|
|
1150
|
-
message: "Record created successfully"
|
|
1151
|
-
};
|
|
1152
|
-
} catch (error) {
|
|
1153
|
-
return {
|
|
1154
|
-
success: false,
|
|
1155
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
async upsertRecord(domain, record) {
|
|
1160
|
-
try {
|
|
1161
|
-
const hostedZoneId = await this.getHostedZoneId(domain);
|
|
1162
|
-
if (!hostedZoneId) {
|
|
1163
|
-
return {
|
|
1164
|
-
success: false,
|
|
1165
|
-
message: `No hosted zone found for domain: ${domain}`
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
const recordName = this.normalizeName(domain, record.name);
|
|
1169
|
-
let recordValue = record.content;
|
|
1170
|
-
if (record.type === "TXT" && !recordValue.startsWith('"')) {
|
|
1171
|
-
recordValue = `"${recordValue}"`;
|
|
1172
|
-
}
|
|
1173
|
-
if (record.type === "MX" && record.priority !== undefined) {
|
|
1174
|
-
recordValue = `${record.priority} ${recordValue}`;
|
|
1175
|
-
}
|
|
1176
|
-
const result = await this.client.changeResourceRecordSets({
|
|
1177
|
-
HostedZoneId: hostedZoneId,
|
|
1178
|
-
ChangeBatch: {
|
|
1179
|
-
Comment: `Upserted by ts-cloud DNS provider`,
|
|
1180
|
-
Changes: [
|
|
1181
|
-
{
|
|
1182
|
-
Action: "UPSERT",
|
|
1183
|
-
ResourceRecordSet: {
|
|
1184
|
-
Name: recordName,
|
|
1185
|
-
Type: record.type,
|
|
1186
|
-
TTL: record.ttl || 300,
|
|
1187
|
-
ResourceRecords: [{ Value: recordValue }]
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
]
|
|
1191
|
-
}
|
|
1192
|
-
});
|
|
1193
|
-
return {
|
|
1194
|
-
success: true,
|
|
1195
|
-
id: result.ChangeInfo?.Id,
|
|
1196
|
-
message: "Record upserted successfully"
|
|
1197
|
-
};
|
|
1198
|
-
} catch (error) {
|
|
1199
|
-
return {
|
|
1200
|
-
success: false,
|
|
1201
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
async deleteRecord(domain, record) {
|
|
1206
|
-
try {
|
|
1207
|
-
const hostedZoneId = await this.getHostedZoneId(domain);
|
|
1208
|
-
if (!hostedZoneId) {
|
|
1209
|
-
return {
|
|
1210
|
-
success: false,
|
|
1211
|
-
message: `No hosted zone found for domain: ${domain}`
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
const recordName = this.normalizeName(domain, record.name);
|
|
1215
|
-
let recordValue = record.content;
|
|
1216
|
-
if (record.type === "TXT" && !recordValue.startsWith('"')) {
|
|
1217
|
-
recordValue = `"${recordValue}"`;
|
|
1218
|
-
}
|
|
1219
|
-
if (record.type === "MX" && record.priority !== undefined) {
|
|
1220
|
-
recordValue = `${record.priority} ${recordValue}`;
|
|
1221
|
-
}
|
|
1222
|
-
await this.client.changeResourceRecordSets({
|
|
1223
|
-
HostedZoneId: hostedZoneId,
|
|
1224
|
-
ChangeBatch: {
|
|
1225
|
-
Comment: `Deleted by ts-cloud DNS provider`,
|
|
1226
|
-
Changes: [
|
|
1227
|
-
{
|
|
1228
|
-
Action: "DELETE",
|
|
1229
|
-
ResourceRecordSet: {
|
|
1230
|
-
Name: recordName,
|
|
1231
|
-
Type: record.type,
|
|
1232
|
-
TTL: record.ttl || 300,
|
|
1233
|
-
ResourceRecords: [{ Value: recordValue }]
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
]
|
|
1237
|
-
}
|
|
1238
|
-
});
|
|
1239
|
-
return {
|
|
1240
|
-
success: true,
|
|
1241
|
-
message: "Record deleted successfully"
|
|
1242
|
-
};
|
|
1243
|
-
} catch (error) {
|
|
1244
|
-
return {
|
|
1245
|
-
success: false,
|
|
1246
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
async listRecords(domain, type) {
|
|
1251
|
-
try {
|
|
1252
|
-
const hostedZoneId = await this.getHostedZoneId(domain);
|
|
1253
|
-
if (!hostedZoneId) {
|
|
1254
|
-
return {
|
|
1255
|
-
success: false,
|
|
1256
|
-
records: [],
|
|
1257
|
-
message: `No hosted zone found for domain: ${domain}`
|
|
1258
|
-
};
|
|
1259
|
-
}
|
|
1260
|
-
const result = await this.client.listResourceRecordSets({
|
|
1261
|
-
HostedZoneId: hostedZoneId,
|
|
1262
|
-
StartRecordType: type
|
|
1263
|
-
});
|
|
1264
|
-
const records = [];
|
|
1265
|
-
for (const rs of result.ResourceRecordSets) {
|
|
1266
|
-
if (type && rs.Type !== type) {
|
|
1267
|
-
continue;
|
|
1268
|
-
}
|
|
1269
|
-
if (rs.AliasTarget) {
|
|
1270
|
-
continue;
|
|
1271
|
-
}
|
|
1272
|
-
for (const rr of rs.ResourceRecords || []) {
|
|
1273
|
-
let content = rr.Value;
|
|
1274
|
-
let priority;
|
|
1275
|
-
if (rs.Type === "MX") {
|
|
1276
|
-
const parts = content.split(" ");
|
|
1277
|
-
if (parts.length >= 2) {
|
|
1278
|
-
priority = Number.parseInt(parts[0], 10);
|
|
1279
|
-
content = parts.slice(1).join(" ");
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
if (rs.Type === "TXT" && content.startsWith('"') && content.endsWith('"')) {
|
|
1283
|
-
content = content.slice(1, -1);
|
|
1284
|
-
}
|
|
1285
|
-
records.push({
|
|
1286
|
-
name: rs.Name.replace(/\.$/, ""),
|
|
1287
|
-
type: rs.Type,
|
|
1288
|
-
content,
|
|
1289
|
-
ttl: rs.TTL,
|
|
1290
|
-
priority
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
return {
|
|
1295
|
-
success: true,
|
|
1296
|
-
records
|
|
1297
|
-
};
|
|
1298
|
-
} catch (error) {
|
|
1299
|
-
return {
|
|
1300
|
-
success: false,
|
|
1301
|
-
records: [],
|
|
1302
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
async canManageDomain(domain) {
|
|
1307
|
-
const hostedZoneId = await this.getHostedZoneId(domain);
|
|
1308
|
-
return hostedZoneId !== null;
|
|
1309
|
-
}
|
|
1310
|
-
async listDomains() {
|
|
1311
|
-
try {
|
|
1312
|
-
const result = await this.client.listHostedZones();
|
|
1313
|
-
return result.HostedZones.map((z) => z.Name.replace(/\.$/, ""));
|
|
1314
|
-
} catch {
|
|
1315
|
-
return [];
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
getRoute53Client() {
|
|
1319
|
-
return this.client;
|
|
1320
|
-
}
|
|
1321
|
-
async createAliasRecord(params) {
|
|
1322
|
-
try {
|
|
1323
|
-
const hostedZoneId = await this.getHostedZoneId(params.domain);
|
|
1324
|
-
if (!hostedZoneId) {
|
|
1325
|
-
return {
|
|
1326
|
-
success: false,
|
|
1327
|
-
message: `No hosted zone found for domain: ${params.domain}`
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1330
|
-
const result = await this.client.createAliasRecord({
|
|
1331
|
-
HostedZoneId: hostedZoneId,
|
|
1332
|
-
Name: this.normalizeName(params.domain, params.name),
|
|
1333
|
-
TargetHostedZoneId: params.targetHostedZoneId,
|
|
1334
|
-
TargetDNSName: params.targetDnsName,
|
|
1335
|
-
EvaluateTargetHealth: params.evaluateTargetHealth,
|
|
1336
|
-
Type: params.type
|
|
1337
|
-
});
|
|
1338
|
-
return {
|
|
1339
|
-
success: true,
|
|
1340
|
-
id: result.ChangeInfo?.Id,
|
|
1341
|
-
message: "Alias record created successfully"
|
|
1342
|
-
};
|
|
1343
|
-
} catch (error) {
|
|
1344
|
-
return {
|
|
1345
|
-
success: false,
|
|
1346
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
async createCloudFrontAlias(params) {
|
|
1351
|
-
return this.createAliasRecord({
|
|
1352
|
-
domain: params.domain,
|
|
1353
|
-
name: params.name,
|
|
1354
|
-
targetHostedZoneId: Route53Client.CloudFrontHostedZoneId,
|
|
1355
|
-
targetDnsName: params.cloudFrontDomainName,
|
|
1356
|
-
evaluateTargetHealth: false
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
async createAlbAlias(params) {
|
|
1360
|
-
const hostedZoneId = Route53Client.ALBHostedZoneIds[params.region];
|
|
1361
|
-
if (!hostedZoneId) {
|
|
1362
|
-
return {
|
|
1363
|
-
success: false,
|
|
1364
|
-
message: `Unknown region for ALB: ${params.region}`
|
|
1365
|
-
};
|
|
1366
|
-
}
|
|
1367
|
-
return this.createAliasRecord({
|
|
1368
|
-
domain: params.domain,
|
|
1369
|
-
name: params.name,
|
|
1370
|
-
targetHostedZoneId: hostedZoneId,
|
|
1371
|
-
targetDnsName: params.albDnsName,
|
|
1372
|
-
evaluateTargetHealth: true
|
|
1373
|
-
});
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
// src/aws/acm.ts
|
|
1377
|
-
class ACMClient {
|
|
1378
|
-
client;
|
|
1379
|
-
region;
|
|
1380
|
-
constructor(region = "us-east-1") {
|
|
1381
|
-
this.client = new AWSClient;
|
|
1382
|
-
this.region = region;
|
|
1383
|
-
}
|
|
1384
|
-
async requestCertificate(params) {
|
|
1385
|
-
const requestBody = {
|
|
1386
|
-
DomainName: params.DomainName,
|
|
1387
|
-
ValidationMethod: params.ValidationMethod || "DNS"
|
|
1388
|
-
};
|
|
1389
|
-
if (params.SubjectAlternativeNames) {
|
|
1390
|
-
requestBody.SubjectAlternativeNames = params.SubjectAlternativeNames;
|
|
1391
|
-
}
|
|
1392
|
-
const result = await this.client.request({
|
|
1393
|
-
service: "acm",
|
|
1394
|
-
region: this.region,
|
|
1395
|
-
method: "POST",
|
|
1396
|
-
path: "/",
|
|
1397
|
-
headers: {
|
|
1398
|
-
"content-type": "application/x-amz-json-1.1",
|
|
1399
|
-
"x-amz-target": "CertificateManager.RequestCertificate"
|
|
1400
|
-
},
|
|
1401
|
-
body: JSON.stringify(requestBody)
|
|
1402
|
-
});
|
|
1403
|
-
return {
|
|
1404
|
-
CertificateArn: result.CertificateArn || ""
|
|
1405
|
-
};
|
|
1406
|
-
}
|
|
1407
|
-
async describeCertificate(params) {
|
|
1408
|
-
const result = await this.client.request({
|
|
1409
|
-
service: "acm",
|
|
1410
|
-
region: this.region,
|
|
1411
|
-
method: "POST",
|
|
1412
|
-
path: "/",
|
|
1413
|
-
headers: {
|
|
1414
|
-
"content-type": "application/x-amz-json-1.1",
|
|
1415
|
-
"x-amz-target": "CertificateManager.DescribeCertificate"
|
|
1416
|
-
},
|
|
1417
|
-
body: JSON.stringify({
|
|
1418
|
-
CertificateArn: params.CertificateArn
|
|
1419
|
-
})
|
|
1420
|
-
});
|
|
1421
|
-
const cert = result.Certificate || {};
|
|
1422
|
-
return {
|
|
1423
|
-
CertificateArn: cert.CertificateArn || "",
|
|
1424
|
-
DomainName: cert.DomainName || "",
|
|
1425
|
-
SubjectAlternativeNames: cert.SubjectAlternativeNames,
|
|
1426
|
-
Status: cert.Status || "PENDING_VALIDATION",
|
|
1427
|
-
Type: cert.Type,
|
|
1428
|
-
DomainValidationOptions: cert.DomainValidationOptions?.map((opt) => ({
|
|
1429
|
-
DomainName: opt.DomainName,
|
|
1430
|
-
ValidationDomain: opt.ValidationDomain,
|
|
1431
|
-
ValidationStatus: opt.ValidationStatus,
|
|
1432
|
-
ResourceRecord: opt.ResourceRecord ? {
|
|
1433
|
-
Name: opt.ResourceRecord.Name,
|
|
1434
|
-
Type: opt.ResourceRecord.Type,
|
|
1435
|
-
Value: opt.ResourceRecord.Value
|
|
1436
|
-
} : undefined,
|
|
1437
|
-
ValidationMethod: opt.ValidationMethod
|
|
1438
|
-
})),
|
|
1439
|
-
CreatedAt: cert.CreatedAt,
|
|
1440
|
-
IssuedAt: cert.IssuedAt,
|
|
1441
|
-
NotBefore: cert.NotBefore,
|
|
1442
|
-
NotAfter: cert.NotAfter
|
|
1443
|
-
};
|
|
1444
|
-
}
|
|
1445
|
-
async listCertificates(params) {
|
|
1446
|
-
const requestBody = {};
|
|
1447
|
-
if (params?.CertificateStatuses) {
|
|
1448
|
-
requestBody.CertificateStatuses = params.CertificateStatuses;
|
|
1449
|
-
}
|
|
1450
|
-
if (params?.MaxItems) {
|
|
1451
|
-
requestBody.MaxItems = params.MaxItems;
|
|
1452
|
-
}
|
|
1453
|
-
if (params?.NextToken) {
|
|
1454
|
-
requestBody.NextToken = params.NextToken;
|
|
1455
|
-
}
|
|
1456
|
-
const result = await this.client.request({
|
|
1457
|
-
service: "acm",
|
|
1458
|
-
region: this.region,
|
|
1459
|
-
method: "POST",
|
|
1460
|
-
path: "/",
|
|
1461
|
-
headers: {
|
|
1462
|
-
"content-type": "application/x-amz-json-1.1",
|
|
1463
|
-
"x-amz-target": "CertificateManager.ListCertificates"
|
|
1464
|
-
},
|
|
1465
|
-
body: JSON.stringify(requestBody)
|
|
1466
|
-
});
|
|
1467
|
-
return {
|
|
1468
|
-
CertificateSummaryList: (result.CertificateSummaryList || []).map((cert) => ({
|
|
1469
|
-
CertificateArn: cert.CertificateArn,
|
|
1470
|
-
DomainName: cert.DomainName
|
|
1471
|
-
})),
|
|
1472
|
-
NextToken: result.NextToken
|
|
1473
|
-
};
|
|
1474
|
-
}
|
|
1475
|
-
async deleteCertificate(params) {
|
|
1476
|
-
await this.client.request({
|
|
1477
|
-
service: "acm",
|
|
1478
|
-
region: this.region,
|
|
1479
|
-
method: "POST",
|
|
1480
|
-
path: "/",
|
|
1481
|
-
headers: {
|
|
1482
|
-
"content-type": "application/x-amz-json-1.1",
|
|
1483
|
-
"x-amz-target": "CertificateManager.DeleteCertificate"
|
|
1484
|
-
},
|
|
1485
|
-
body: JSON.stringify({
|
|
1486
|
-
CertificateArn: params.CertificateArn
|
|
1487
|
-
})
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
1490
|
-
async listTagsForCertificate(params) {
|
|
1491
|
-
const result = await this.client.request({
|
|
1492
|
-
service: "acm",
|
|
1493
|
-
region: this.region,
|
|
1494
|
-
method: "POST",
|
|
1495
|
-
path: "/",
|
|
1496
|
-
headers: {
|
|
1497
|
-
"content-type": "application/x-amz-json-1.1",
|
|
1498
|
-
"x-amz-target": "CertificateManager.ListTagsForCertificate"
|
|
1499
|
-
},
|
|
1500
|
-
body: JSON.stringify({
|
|
1501
|
-
CertificateArn: params.CertificateArn
|
|
1502
|
-
})
|
|
1503
|
-
});
|
|
1504
|
-
return {
|
|
1505
|
-
Tags: result.Tags || []
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
async addTagsToCertificate(params) {
|
|
1509
|
-
await this.client.request({
|
|
1510
|
-
service: "acm",
|
|
1511
|
-
region: this.region,
|
|
1512
|
-
method: "POST",
|
|
1513
|
-
path: "/",
|
|
1514
|
-
headers: {
|
|
1515
|
-
"content-type": "application/x-amz-json-1.1",
|
|
1516
|
-
"x-amz-target": "CertificateManager.AddTagsToCertificate"
|
|
1517
|
-
},
|
|
1518
|
-
body: JSON.stringify({
|
|
1519
|
-
CertificateArn: params.CertificateArn,
|
|
1520
|
-
Tags: params.Tags
|
|
1521
|
-
})
|
|
1522
|
-
});
|
|
1523
|
-
}
|
|
1524
|
-
async resendValidationEmail(params) {
|
|
1525
|
-
await this.client.request({
|
|
1526
|
-
service: "acm",
|
|
1527
|
-
region: this.region,
|
|
1528
|
-
method: "POST",
|
|
1529
|
-
path: "/",
|
|
1530
|
-
headers: {
|
|
1531
|
-
"content-type": "application/x-amz-json-1.1",
|
|
1532
|
-
"x-amz-target": "CertificateManager.ResendValidationEmail"
|
|
1533
|
-
},
|
|
1534
|
-
body: JSON.stringify({
|
|
1535
|
-
CertificateArn: params.CertificateArn,
|
|
1536
|
-
Domain: params.Domain,
|
|
1537
|
-
ValidationDomain: params.ValidationDomain
|
|
1538
|
-
})
|
|
1539
|
-
});
|
|
1540
|
-
}
|
|
1541
|
-
async findCertificateByDomain(domainName) {
|
|
1542
|
-
const result = await this.listCertificates({
|
|
1543
|
-
CertificateStatuses: ["ISSUED"]
|
|
1544
|
-
});
|
|
1545
|
-
const summary = result.CertificateSummaryList.find((c) => c.DomainName === domainName || c.DomainName === `*.${domainName.split(".").slice(1).join(".")}`);
|
|
1546
|
-
if (!summary) {
|
|
1547
|
-
return null;
|
|
1548
|
-
}
|
|
1549
|
-
return this.describeCertificate({ CertificateArn: summary.CertificateArn });
|
|
1550
|
-
}
|
|
1551
|
-
async waitForCertificateValidation(certificateArn, maxAttempts = 60, delayMs = 30000) {
|
|
1552
|
-
for (let i = 0;i < maxAttempts; i++) {
|
|
1553
|
-
const cert = await this.describeCertificate({ CertificateArn: certificateArn });
|
|
1554
|
-
if (cert.Status === "ISSUED") {
|
|
1555
|
-
return cert;
|
|
1556
|
-
}
|
|
1557
|
-
if (cert.Status === "FAILED" || cert.Status === "VALIDATION_TIMED_OUT") {
|
|
1558
|
-
return null;
|
|
1559
|
-
}
|
|
1560
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1561
|
-
}
|
|
1562
|
-
return null;
|
|
1563
|
-
}
|
|
1564
|
-
async getDnsValidationRecords(certificateArn) {
|
|
1565
|
-
const cert = await this.describeCertificate({ CertificateArn: certificateArn });
|
|
1566
|
-
if (!cert.DomainValidationOptions) {
|
|
1567
|
-
return [];
|
|
1568
|
-
}
|
|
1569
|
-
return cert.DomainValidationOptions.filter((opt) => opt.ResourceRecord && opt.ValidationMethod === "DNS").map((opt) => ({
|
|
1570
|
-
domainName: opt.DomainName,
|
|
1571
|
-
recordName: opt.ResourceRecord.Name,
|
|
1572
|
-
recordType: opt.ResourceRecord.Type,
|
|
1573
|
-
recordValue: opt.ResourceRecord.Value
|
|
1574
|
-
}));
|
|
1575
|
-
}
|
|
1576
|
-
async requestCertificateWithSans(params) {
|
|
1577
|
-
const sans = new Set;
|
|
1578
|
-
sans.add(params.DomainName);
|
|
1579
|
-
if (params.IncludeWww !== false) {
|
|
1580
|
-
sans.add(`www.${params.DomainName}`);
|
|
1581
|
-
}
|
|
1582
|
-
if (params.IncludeWildcard) {
|
|
1583
|
-
sans.add(`*.${params.DomainName}`);
|
|
1584
|
-
}
|
|
1585
|
-
if (params.AdditionalSans) {
|
|
1586
|
-
for (const san of params.AdditionalSans) {
|
|
1587
|
-
sans.add(san);
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
return this.requestCertificate({
|
|
1591
|
-
DomainName: params.DomainName,
|
|
1592
|
-
SubjectAlternativeNames: Array.from(sans),
|
|
1593
|
-
ValidationMethod: "DNS"
|
|
1594
|
-
});
|
|
1595
|
-
}
|
|
1596
|
-
async isCertificateValidForDomain(certificateArn, domainName) {
|
|
1597
|
-
const cert = await this.describeCertificate({ CertificateArn: certificateArn });
|
|
1598
|
-
if (cert.Status !== "ISSUED") {
|
|
1599
|
-
return false;
|
|
1600
|
-
}
|
|
1601
|
-
if (cert.DomainName === domainName) {
|
|
1602
|
-
return true;
|
|
1603
|
-
}
|
|
1604
|
-
if (cert.DomainName?.startsWith("*.")) {
|
|
1605
|
-
const baseDomain = cert.DomainName.slice(2);
|
|
1606
|
-
const domainParts = domainName.split(".");
|
|
1607
|
-
const baseParts = baseDomain.split(".");
|
|
1608
|
-
if (domainParts.slice(-baseParts.length).join(".") === baseDomain) {
|
|
1609
|
-
return true;
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
if (cert.SubjectAlternativeNames) {
|
|
1613
|
-
for (const san of cert.SubjectAlternativeNames) {
|
|
1614
|
-
if (san === domainName) {
|
|
1615
|
-
return true;
|
|
1616
|
-
}
|
|
1617
|
-
if (san.startsWith("*.")) {
|
|
1618
|
-
const baseDomain = san.slice(2);
|
|
1619
|
-
const domainParts = domainName.split(".");
|
|
1620
|
-
const baseParts = baseDomain.split(".");
|
|
1621
|
-
if (domainParts.slice(-baseParts.length).join(".") === baseDomain) {
|
|
1622
|
-
return true;
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
return false;
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
class ACMDnsValidator {
|
|
1632
|
-
acm;
|
|
1633
|
-
route53;
|
|
1634
|
-
dnsProvider;
|
|
1635
|
-
constructor(region = "us-east-1", dnsProviderConfig) {
|
|
1636
|
-
this.acm = new ACMClient(region);
|
|
1637
|
-
this.route53 = new Route53Client;
|
|
1638
|
-
if (dnsProviderConfig && dnsProviderConfig.provider !== "route53") {
|
|
1639
|
-
this.dnsProvider = createDnsProvider(dnsProviderConfig);
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
async requestAndValidate(params) {
|
|
1643
|
-
const {
|
|
1644
|
-
domainName,
|
|
1645
|
-
hostedZoneId,
|
|
1646
|
-
subjectAlternativeNames = [],
|
|
1647
|
-
waitForValidation = false,
|
|
1648
|
-
maxWaitMinutes = 30
|
|
1649
|
-
} = params;
|
|
1650
|
-
if (!this.dnsProvider && !hostedZoneId) {
|
|
1651
|
-
throw new Error("Either hostedZoneId or external DNS provider configuration is required");
|
|
1652
|
-
}
|
|
1653
|
-
const { CertificateArn } = await this.acm.requestCertificate({
|
|
1654
|
-
DomainName: domainName,
|
|
1655
|
-
SubjectAlternativeNames: subjectAlternativeNames.length > 0 ? [domainName, ...subjectAlternativeNames] : undefined,
|
|
1656
|
-
ValidationMethod: "DNS"
|
|
1657
|
-
});
|
|
1658
|
-
await this.waitForValidationOptions(CertificateArn);
|
|
1659
|
-
const validationRecords = await this.acm.getDnsValidationRecords(CertificateArn);
|
|
1660
|
-
if (this.dnsProvider) {
|
|
1661
|
-
for (const record of validationRecords) {
|
|
1662
|
-
const result = await this.dnsProvider.upsertRecord(domainName, {
|
|
1663
|
-
name: record.recordName,
|
|
1664
|
-
type: record.recordType,
|
|
1665
|
-
content: record.recordValue,
|
|
1666
|
-
ttl: 300
|
|
1667
|
-
});
|
|
1668
|
-
if (!result.success) {
|
|
1669
|
-
console.warn(`Failed to create validation record for ${record.domainName}: ${result.message}`);
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
} else if (hostedZoneId) {
|
|
1673
|
-
for (const record of validationRecords) {
|
|
1674
|
-
await this.route53.changeResourceRecordSets({
|
|
1675
|
-
HostedZoneId: hostedZoneId,
|
|
1676
|
-
ChangeBatch: {
|
|
1677
|
-
Comment: `ACM DNS validation for ${record.domainName}`,
|
|
1678
|
-
Changes: [
|
|
1679
|
-
{
|
|
1680
|
-
Action: "UPSERT",
|
|
1681
|
-
ResourceRecordSet: {
|
|
1682
|
-
Name: record.recordName,
|
|
1683
|
-
Type: record.recordType,
|
|
1684
|
-
TTL: 300,
|
|
1685
|
-
ResourceRecords: [{ Value: record.recordValue }]
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
]
|
|
1689
|
-
}
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
if (waitForValidation) {
|
|
1694
|
-
const cert = await this.acm.waitForCertificateValidation(CertificateArn, maxWaitMinutes * 2, 30000);
|
|
1695
|
-
if (!cert) {
|
|
1696
|
-
throw new Error(`Certificate validation timed out after ${maxWaitMinutes} minutes`);
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
return {
|
|
1700
|
-
certificateArn: CertificateArn,
|
|
1701
|
-
validationRecords
|
|
1702
|
-
};
|
|
1703
|
-
}
|
|
1704
|
-
async waitForValidationOptions(certificateArn, maxAttempts = 30) {
|
|
1705
|
-
for (let i = 0;i < maxAttempts; i++) {
|
|
1706
|
-
const cert = await this.acm.describeCertificate({ CertificateArn: certificateArn });
|
|
1707
|
-
if (cert.DomainValidationOptions && cert.DomainValidationOptions.length > 0 && cert.DomainValidationOptions[0].ResourceRecord) {
|
|
1708
|
-
return;
|
|
1709
|
-
}
|
|
1710
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1711
|
-
}
|
|
1712
|
-
throw new Error("Timeout waiting for DNS validation options");
|
|
1713
|
-
}
|
|
1714
|
-
async createValidationRecords(params) {
|
|
1715
|
-
const { certificateArn, hostedZoneId, domain } = params;
|
|
1716
|
-
if (!this.dnsProvider && !hostedZoneId) {
|
|
1717
|
-
throw new Error("Either hostedZoneId or external DNS provider configuration is required");
|
|
1718
|
-
}
|
|
1719
|
-
const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
|
|
1720
|
-
const results = [];
|
|
1721
|
-
if (this.dnsProvider) {
|
|
1722
|
-
const targetDomain = domain || validationRecords[0]?.domainName;
|
|
1723
|
-
for (const record of validationRecords) {
|
|
1724
|
-
const result = await this.dnsProvider.upsertRecord(targetDomain, {
|
|
1725
|
-
name: record.recordName,
|
|
1726
|
-
type: record.recordType,
|
|
1727
|
-
content: record.recordValue,
|
|
1728
|
-
ttl: 300
|
|
1729
|
-
});
|
|
1730
|
-
results.push({
|
|
1731
|
-
...record,
|
|
1732
|
-
changeId: result.success ? result.id : undefined
|
|
1733
|
-
});
|
|
1734
|
-
}
|
|
1735
|
-
} else if (hostedZoneId) {
|
|
1736
|
-
for (const record of validationRecords) {
|
|
1737
|
-
const result = await this.route53.changeResourceRecordSets({
|
|
1738
|
-
HostedZoneId: hostedZoneId,
|
|
1739
|
-
ChangeBatch: {
|
|
1740
|
-
Comment: `ACM DNS validation for ${record.domainName}`,
|
|
1741
|
-
Changes: [
|
|
1742
|
-
{
|
|
1743
|
-
Action: "UPSERT",
|
|
1744
|
-
ResourceRecordSet: {
|
|
1745
|
-
Name: record.recordName,
|
|
1746
|
-
Type: record.recordType,
|
|
1747
|
-
TTL: 300,
|
|
1748
|
-
ResourceRecords: [{ Value: record.recordValue }]
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
]
|
|
1752
|
-
}
|
|
1753
|
-
});
|
|
1754
|
-
results.push({
|
|
1755
|
-
...record,
|
|
1756
|
-
changeId: result.ChangeInfo?.Id
|
|
1757
|
-
});
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
return results;
|
|
1761
|
-
}
|
|
1762
|
-
async deleteValidationRecords(params) {
|
|
1763
|
-
const { certificateArn, hostedZoneId, domain } = params;
|
|
1764
|
-
const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
|
|
1765
|
-
if (this.dnsProvider) {
|
|
1766
|
-
const targetDomain = domain || validationRecords[0]?.domainName;
|
|
1767
|
-
for (const record of validationRecords) {
|
|
1768
|
-
try {
|
|
1769
|
-
await this.dnsProvider.deleteRecord(targetDomain, {
|
|
1770
|
-
name: record.recordName,
|
|
1771
|
-
type: record.recordType,
|
|
1772
|
-
content: record.recordValue
|
|
1773
|
-
});
|
|
1774
|
-
} catch {}
|
|
1775
|
-
}
|
|
1776
|
-
} else if (hostedZoneId) {
|
|
1777
|
-
for (const record of validationRecords) {
|
|
1778
|
-
try {
|
|
1779
|
-
await this.route53.changeResourceRecordSets({
|
|
1780
|
-
HostedZoneId: hostedZoneId,
|
|
1781
|
-
ChangeBatch: {
|
|
1782
|
-
Comment: `Cleanup ACM DNS validation for ${record.domainName}`,
|
|
1783
|
-
Changes: [
|
|
1784
|
-
{
|
|
1785
|
-
Action: "DELETE",
|
|
1786
|
-
ResourceRecordSet: {
|
|
1787
|
-
Name: record.recordName,
|
|
1788
|
-
Type: record.recordType,
|
|
1789
|
-
TTL: 300,
|
|
1790
|
-
ResourceRecords: [{ Value: record.recordValue }]
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
]
|
|
1794
|
-
}
|
|
1795
|
-
});
|
|
1796
|
-
} catch {}
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
async findOrCreateCertificate(params) {
|
|
1801
|
-
const { domainName, hostedZoneId, subjectAlternativeNames, waitForValidation = true } = params;
|
|
1802
|
-
if (!this.dnsProvider && !hostedZoneId) {
|
|
1803
|
-
throw new Error("Either hostedZoneId or external DNS provider configuration is required");
|
|
1804
|
-
}
|
|
1805
|
-
const existing = await this.acm.findCertificateByDomain(domainName);
|
|
1806
|
-
if (existing && existing.Status === "ISSUED") {
|
|
1807
|
-
return {
|
|
1808
|
-
certificateArn: existing.CertificateArn,
|
|
1809
|
-
isNew: false
|
|
1810
|
-
};
|
|
1811
|
-
}
|
|
1812
|
-
const { certificateArn } = await this.requestAndValidate({
|
|
1813
|
-
domainName,
|
|
1814
|
-
hostedZoneId,
|
|
1815
|
-
subjectAlternativeNames,
|
|
1816
|
-
waitForValidation
|
|
1817
|
-
});
|
|
1818
|
-
return {
|
|
1819
|
-
certificateArn,
|
|
1820
|
-
isNew: true
|
|
1821
|
-
};
|
|
1822
|
-
}
|
|
1823
|
-
hasExternalDnsProvider() {
|
|
1824
|
-
return this.dnsProvider !== undefined;
|
|
1825
|
-
}
|
|
1826
|
-
getDnsProviderName() {
|
|
1827
|
-
return this.dnsProvider?.name || "route53";
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
// src/dns/validator.ts
|
|
1832
|
-
class UnifiedDnsValidator {
|
|
1833
|
-
acm;
|
|
1834
|
-
dnsProvider;
|
|
1835
|
-
constructor(dnsProvider, acmRegion = "us-east-1") {
|
|
1836
|
-
this.acm = new ACMClient(acmRegion);
|
|
1837
|
-
if ("provider" in dnsProvider) {
|
|
1838
|
-
this.dnsProvider = createDnsProvider(dnsProvider);
|
|
1839
|
-
} else {
|
|
1840
|
-
this.dnsProvider = dnsProvider;
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
getProvider() {
|
|
1844
|
-
return this.dnsProvider;
|
|
1845
|
-
}
|
|
1846
|
-
async requestAndValidate(params) {
|
|
1847
|
-
const { domainName, subjectAlternativeNames = [], waitForValidation = false, maxWaitMinutes = 30 } = params;
|
|
1848
|
-
const { CertificateArn } = await this.acm.requestCertificate({
|
|
1849
|
-
DomainName: domainName,
|
|
1850
|
-
SubjectAlternativeNames: subjectAlternativeNames.length > 0 ? [domainName, ...subjectAlternativeNames] : undefined,
|
|
1851
|
-
ValidationMethod: "DNS"
|
|
1852
|
-
});
|
|
1853
|
-
await this.waitForValidationOptions(CertificateArn);
|
|
1854
|
-
const validationRecords = await this.acm.getDnsValidationRecords(CertificateArn);
|
|
1855
|
-
for (const record of validationRecords) {
|
|
1856
|
-
const result = await this.dnsProvider.upsertRecord(domainName, {
|
|
1857
|
-
name: record.recordName,
|
|
1858
|
-
type: record.recordType,
|
|
1859
|
-
content: record.recordValue,
|
|
1860
|
-
ttl: 300
|
|
1861
|
-
});
|
|
1862
|
-
if (!result.success) {
|
|
1863
|
-
console.warn(`Failed to create validation record for ${record.domainName}: ${result.message}`);
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
let status = "pending";
|
|
1867
|
-
if (waitForValidation) {
|
|
1868
|
-
const cert = await this.acm.waitForCertificateValidation(CertificateArn, maxWaitMinutes * 2, 30000);
|
|
1869
|
-
status = cert?.Status === "ISSUED" ? "issued" : "failed";
|
|
1870
|
-
}
|
|
1871
|
-
return {
|
|
1872
|
-
certificateArn: CertificateArn,
|
|
1873
|
-
validationRecords: validationRecords.map((r) => ({
|
|
1874
|
-
domainName: r.domainName,
|
|
1875
|
-
recordName: r.recordName,
|
|
1876
|
-
recordType: r.recordType,
|
|
1877
|
-
recordValue: r.recordValue
|
|
1878
|
-
})),
|
|
1879
|
-
isNew: true,
|
|
1880
|
-
status
|
|
1881
|
-
};
|
|
1882
|
-
}
|
|
1883
|
-
async createValidationRecords(params) {
|
|
1884
|
-
const { certificateArn, domain } = params;
|
|
1885
|
-
const errors = [];
|
|
1886
|
-
const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
|
|
1887
|
-
for (const record of validationRecords) {
|
|
1888
|
-
const result = await this.dnsProvider.upsertRecord(domain, {
|
|
1889
|
-
name: record.recordName,
|
|
1890
|
-
type: record.recordType,
|
|
1891
|
-
content: record.recordValue,
|
|
1892
|
-
ttl: 300
|
|
1893
|
-
});
|
|
1894
|
-
if (!result.success) {
|
|
1895
|
-
errors.push(`Failed to create record for ${record.domainName}: ${result.message}`);
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
return {
|
|
1899
|
-
success: errors.length === 0,
|
|
1900
|
-
records: validationRecords.map((r) => ({
|
|
1901
|
-
domainName: r.domainName,
|
|
1902
|
-
recordName: r.recordName,
|
|
1903
|
-
recordType: r.recordType,
|
|
1904
|
-
recordValue: r.recordValue
|
|
1905
|
-
})),
|
|
1906
|
-
errors
|
|
1907
|
-
};
|
|
1908
|
-
}
|
|
1909
|
-
async deleteValidationRecords(params) {
|
|
1910
|
-
const { certificateArn, domain } = params;
|
|
1911
|
-
const errors = [];
|
|
1912
|
-
const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
|
|
1913
|
-
for (const record of validationRecords) {
|
|
1914
|
-
const result = await this.dnsProvider.deleteRecord(domain, {
|
|
1915
|
-
name: record.recordName,
|
|
1916
|
-
type: record.recordType,
|
|
1917
|
-
content: record.recordValue
|
|
1918
|
-
});
|
|
1919
|
-
if (!result.success) {
|
|
1920
|
-
errors.push(`Failed to delete record for ${record.domainName}: ${result.message}`);
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
return {
|
|
1924
|
-
success: errors.length === 0,
|
|
1925
|
-
errors
|
|
1926
|
-
};
|
|
1927
|
-
}
|
|
1928
|
-
async findOrCreateCertificate(params) {
|
|
1929
|
-
const { domainName, subjectAlternativeNames = [], waitForValidation = true, maxWaitMinutes } = params;
|
|
1930
|
-
const existing = await this.acm.findCertificateByDomain(domainName);
|
|
1931
|
-
if (existing && existing.Status === "ISSUED") {
|
|
1932
|
-
const existingSans = existing.SubjectAlternativeNames || [existing.DomainName];
|
|
1933
|
-
const hasWildcard = existingSans.some((san) => san === `*.${domainName}`);
|
|
1934
|
-
const allSansCovered = subjectAlternativeNames.every((san) => existingSans.includes(san) || hasWildcard);
|
|
1935
|
-
if (allSansCovered) {
|
|
1936
|
-
return {
|
|
1937
|
-
certificateArn: existing.CertificateArn,
|
|
1938
|
-
validationRecords: [],
|
|
1939
|
-
isNew: false,
|
|
1940
|
-
status: "issued"
|
|
1941
|
-
};
|
|
1942
|
-
}
|
|
1943
|
-
console.log(`Existing certificate doesn't cover all required SANs, requesting new certificate...`);
|
|
1944
|
-
}
|
|
1945
|
-
return this.requestAndValidate({
|
|
1946
|
-
domainName,
|
|
1947
|
-
subjectAlternativeNames,
|
|
1948
|
-
waitForValidation,
|
|
1949
|
-
maxWaitMinutes
|
|
1950
|
-
});
|
|
1951
|
-
}
|
|
1952
|
-
async requestCertificateWithCommonSans(params) {
|
|
1953
|
-
const {
|
|
1954
|
-
domainName,
|
|
1955
|
-
includeWww = true,
|
|
1956
|
-
includeWildcard = false,
|
|
1957
|
-
additionalSans = [],
|
|
1958
|
-
waitForValidation = false
|
|
1959
|
-
} = params;
|
|
1960
|
-
const sans = [];
|
|
1961
|
-
if (includeWww) {
|
|
1962
|
-
sans.push(`www.${domainName}`);
|
|
1963
|
-
}
|
|
1964
|
-
if (includeWildcard) {
|
|
1965
|
-
sans.push(`*.${domainName}`);
|
|
1966
|
-
}
|
|
1967
|
-
sans.push(...additionalSans);
|
|
1968
|
-
return this.requestAndValidate({
|
|
1969
|
-
domainName,
|
|
1970
|
-
subjectAlternativeNames: sans,
|
|
1971
|
-
waitForValidation
|
|
1972
|
-
});
|
|
1973
|
-
}
|
|
1974
|
-
async waitForValidationOptions(certificateArn, maxAttempts = 30) {
|
|
1975
|
-
for (let i = 0;i < maxAttempts; i++) {
|
|
1976
|
-
const cert = await this.acm.describeCertificate({ CertificateArn: certificateArn });
|
|
1977
|
-
if (cert.DomainValidationOptions && cert.DomainValidationOptions.length > 0 && cert.DomainValidationOptions[0].ResourceRecord) {
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1981
|
-
}
|
|
1982
|
-
throw new Error("Timeout waiting for DNS validation options");
|
|
1983
|
-
}
|
|
1984
|
-
async getCertificateStatus(certificateArn) {
|
|
1985
|
-
const cert = await this.acm.describeCertificate({ CertificateArn: certificateArn });
|
|
1986
|
-
return {
|
|
1987
|
-
status: cert.Status,
|
|
1988
|
-
domainValidations: (cert.DomainValidationOptions || []).map((opt) => ({
|
|
1989
|
-
domain: opt.DomainName,
|
|
1990
|
-
status: opt.ValidationStatus || "UNKNOWN"
|
|
1991
|
-
}))
|
|
1992
|
-
};
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
function createPorkbunValidator(apiKey, secretKey, acmRegion) {
|
|
1996
|
-
return new UnifiedDnsValidator({ provider: "porkbun", apiKey, secretKey }, acmRegion);
|
|
1997
|
-
}
|
|
1998
|
-
function createGoDaddyValidator(apiKey, apiSecret, acmRegion, environment) {
|
|
1999
|
-
return new UnifiedDnsValidator({ provider: "godaddy", apiKey, apiSecret, environment }, acmRegion);
|
|
2000
|
-
}
|
|
2001
|
-
function createRoute53Validator(region, hostedZoneId, acmRegion) {
|
|
2002
|
-
return new UnifiedDnsValidator({ provider: "route53", region, hostedZoneId }, acmRegion || region);
|
|
2003
|
-
}
|
|
2004
|
-
// src/dns/index.ts
|
|
2005
|
-
function createDnsProvider(config) {
|
|
2006
|
-
switch (config.provider) {
|
|
2007
|
-
case "route53":
|
|
2008
|
-
return new Route53Provider(config.region, config.hostedZoneId);
|
|
2009
|
-
case "porkbun":
|
|
2010
|
-
return new PorkbunProvider(config.apiKey, config.secretKey);
|
|
2011
|
-
case "godaddy":
|
|
2012
|
-
return new GoDaddyProvider(config.apiKey, config.apiSecret, config.environment);
|
|
2013
|
-
case "cloudflare":
|
|
2014
|
-
return new CloudflareProvider(config.apiToken, { zoneId: config.zoneId, accountId: config.accountId });
|
|
2015
|
-
default:
|
|
2016
|
-
throw new Error(`Unknown DNS provider: ${config.provider}`);
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
async function detectDnsProvider(domain, configs) {
|
|
2020
|
-
for (const config of configs) {
|
|
2021
|
-
const provider = createDnsProvider(config);
|
|
2022
|
-
if (await provider.canManageDomain(domain)) {
|
|
2023
|
-
return provider;
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
return null;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
class DnsProviderFactory {
|
|
2030
|
-
providers = new Map;
|
|
2031
|
-
configs = [];
|
|
2032
|
-
addConfig(config) {
|
|
2033
|
-
this.configs.push(config);
|
|
2034
|
-
return this;
|
|
2035
|
-
}
|
|
2036
|
-
addRoute53(region, hostedZoneId) {
|
|
2037
|
-
this.configs.push({
|
|
2038
|
-
provider: "route53",
|
|
2039
|
-
region,
|
|
2040
|
-
hostedZoneId
|
|
2041
|
-
});
|
|
2042
|
-
return this;
|
|
2043
|
-
}
|
|
2044
|
-
addPorkbun(apiKey, secretKey) {
|
|
2045
|
-
this.configs.push({
|
|
2046
|
-
provider: "porkbun",
|
|
2047
|
-
apiKey,
|
|
2048
|
-
secretKey
|
|
2049
|
-
});
|
|
2050
|
-
return this;
|
|
2051
|
-
}
|
|
2052
|
-
addGoDaddy(apiKey, apiSecret, environment) {
|
|
2053
|
-
this.configs.push({
|
|
2054
|
-
provider: "godaddy",
|
|
2055
|
-
apiKey,
|
|
2056
|
-
apiSecret,
|
|
2057
|
-
environment
|
|
2058
|
-
});
|
|
2059
|
-
return this;
|
|
2060
|
-
}
|
|
2061
|
-
addCloudflare(apiToken, options = {}) {
|
|
2062
|
-
this.configs.push({
|
|
2063
|
-
provider: "cloudflare",
|
|
2064
|
-
apiToken,
|
|
2065
|
-
zoneId: options.zoneId,
|
|
2066
|
-
accountId: options.accountId
|
|
2067
|
-
});
|
|
2068
|
-
return this;
|
|
2069
|
-
}
|
|
2070
|
-
loadFromEnv() {
|
|
2071
|
-
if (process.env.AWS_ACCESS_KEY_ID || process.env.AWS_REGION) {
|
|
2072
|
-
this.addRoute53(process.env.AWS_REGION);
|
|
2073
|
-
}
|
|
2074
|
-
const porkbunApiKey = process.env.PORKBUN_API_KEY;
|
|
2075
|
-
const porkbunSecretKey = process.env.PORKBUN_SECRET_KEY;
|
|
2076
|
-
if (porkbunApiKey && porkbunSecretKey) {
|
|
2077
|
-
this.addPorkbun(porkbunApiKey, porkbunSecretKey);
|
|
2078
|
-
}
|
|
2079
|
-
const godaddyApiKey = process.env.GODADDY_API_KEY;
|
|
2080
|
-
const godaddyApiSecret = process.env.GODADDY_API_SECRET;
|
|
2081
|
-
if (godaddyApiKey && godaddyApiSecret) {
|
|
2082
|
-
const env = process.env.GODADDY_ENVIRONMENT;
|
|
2083
|
-
this.addGoDaddy(godaddyApiKey, godaddyApiSecret, env);
|
|
2084
|
-
}
|
|
2085
|
-
const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
2086
|
-
if (cloudflareApiToken) {
|
|
2087
|
-
this.addCloudflare(cloudflareApiToken, {
|
|
2088
|
-
zoneId: process.env.CLOUDFLARE_ZONE_ID,
|
|
2089
|
-
accountId: process.env.CLOUDFLARE_ACCOUNT_ID
|
|
2090
|
-
});
|
|
2091
|
-
}
|
|
2092
|
-
return this;
|
|
2093
|
-
}
|
|
2094
|
-
getProvider(name) {
|
|
2095
|
-
const cached = this.providers.get(name);
|
|
2096
|
-
if (cached) {
|
|
2097
|
-
return cached;
|
|
2098
|
-
}
|
|
2099
|
-
const config = this.configs.find((c) => c.provider === name);
|
|
2100
|
-
if (!config) {
|
|
2101
|
-
return null;
|
|
2102
|
-
}
|
|
2103
|
-
const provider = createDnsProvider(config);
|
|
2104
|
-
this.providers.set(name, provider);
|
|
2105
|
-
return provider;
|
|
2106
|
-
}
|
|
2107
|
-
async getProviderForDomain(domain) {
|
|
2108
|
-
for (const config of this.configs) {
|
|
2109
|
-
const provider = createDnsProvider(config);
|
|
2110
|
-
if (await provider.canManageDomain(domain)) {
|
|
2111
|
-
return provider;
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
return null;
|
|
2115
|
-
}
|
|
2116
|
-
getAllProviders() {
|
|
2117
|
-
return this.configs.map((config) => createDnsProvider(config));
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
var dnsProviders = new DnsProviderFactory;
|
|
2121
|
-
|
|
2122
|
-
export { PROXIABLE_RECORD_TYPES, PorkbunProvider, GoDaddyProvider, CLOUDFLARE_MANAGED_RULE_PREFIX, CloudflareProvider, policyTag, qualifyName, resolveDeclaredRecords, reconcileDeclaredRecords, Route53Provider, UnifiedDnsValidator, createPorkbunValidator, createGoDaddyValidator, createRoute53Validator, createDnsProvider, detectDnsProvider, DnsProviderFactory, dnsProviders, ACMClient, ACMDnsValidator };
|