@gusnips/one-click-dns 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +142 -2
- package/dist/chunk-2T2PEF4H.js +521 -0
- package/dist/chunk-FGM7S7GQ.js +222 -0
- package/dist/chunk-P2O7INDB.js +8 -0
- package/dist/hosts-B0_V4Ned.d.cts +106 -0
- package/dist/hosts-B0_V4Ned.d.ts +106 -0
- package/dist/index.cjs +745 -0
- package/dist/index.d.cts +119 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.js +198 -0
- package/dist/server/index.cjs +1020 -0
- package/dist/server/index.d.cts +144 -0
- package/dist/server/index.d.ts +144 -0
- package/dist/server/index.js +274 -0
- package/dist/writers/index.cjs +249 -0
- package/dist/writers/index.d.cts +57 -0
- package/dist/writers/index.d.ts +57 -0
- package/dist/writers/index.js +11 -0
- package/package.json +93 -4
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server/index.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
CONNECT_PROVIDERS: () => CONNECT_PROVIDERS,
|
|
24
|
+
discoverConnect: () => discoverConnect,
|
|
25
|
+
dnsHostOf: () => dnsHostOf,
|
|
26
|
+
planConnection: () => planConnection,
|
|
27
|
+
readDnsZone: () => readDnsZone,
|
|
28
|
+
signedApplyUrl: () => signedApplyUrl,
|
|
29
|
+
systemDnsLookups: () => systemDnsLookups,
|
|
30
|
+
systemResolver: () => systemResolver
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(server_exports);
|
|
33
|
+
|
|
34
|
+
// src/server/dns.ts
|
|
35
|
+
var import_promises = require("dns/promises");
|
|
36
|
+
var ABSENT_CODES = /* @__PURE__ */ new Set(["ENOTFOUND", "ENODATA"]);
|
|
37
|
+
var errorCode = (error) => typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
38
|
+
function systemResolver(timeoutMs) {
|
|
39
|
+
const resolver = new import_promises.Resolver({ timeout: timeoutMs, tries: 1 });
|
|
40
|
+
return async (question) => {
|
|
41
|
+
let timer;
|
|
42
|
+
try {
|
|
43
|
+
return await Promise.race([
|
|
44
|
+
question(resolver),
|
|
45
|
+
new Promise((_, reject) => {
|
|
46
|
+
timer = setTimeout(() => {
|
|
47
|
+
reject(new Error("DNS lookup timed out"));
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
})
|
|
50
|
+
]);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return ABSENT_CODES.has(errorCode(error)) ? "absent" : "failed";
|
|
53
|
+
} finally {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function systemDnsLookups(timeoutMs) {
|
|
59
|
+
const ask2 = systemResolver(timeoutMs);
|
|
60
|
+
return {
|
|
61
|
+
async txt(name) {
|
|
62
|
+
const answer = await ask2((resolver) => resolver.resolveTxt(name));
|
|
63
|
+
return typeof answer === "string" ? answer : { txts: answer.map((parts) => parts.join("")) };
|
|
64
|
+
},
|
|
65
|
+
async cname(name) {
|
|
66
|
+
const answer = await ask2((resolver) => resolver.resolveCname(name));
|
|
67
|
+
if (typeof answer === "string") return answer;
|
|
68
|
+
const first = answer[0];
|
|
69
|
+
return first ? { name: first } : "absent";
|
|
70
|
+
},
|
|
71
|
+
async ns(name) {
|
|
72
|
+
const answer = await ask2((resolver) => resolver.resolveNs(name));
|
|
73
|
+
return typeof answer === "string" ? answer : { names: answer };
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/dns-name.ts
|
|
79
|
+
function normalizeDnsName(name) {
|
|
80
|
+
return name.trim().toLowerCase().replace(/\.$/, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/hosts.ts
|
|
84
|
+
var DNS_HOST_IDS = [
|
|
85
|
+
"cloudflare",
|
|
86
|
+
"godaddy",
|
|
87
|
+
"namecheap",
|
|
88
|
+
"namecheap-hosting",
|
|
89
|
+
"squarespace",
|
|
90
|
+
"route53",
|
|
91
|
+
"google-cloud-dns",
|
|
92
|
+
"azure-dns",
|
|
93
|
+
"ionos",
|
|
94
|
+
"hostinger",
|
|
95
|
+
"porkbun",
|
|
96
|
+
"name-com",
|
|
97
|
+
"dynadot",
|
|
98
|
+
"namesilo",
|
|
99
|
+
"gandi",
|
|
100
|
+
"ovhcloud",
|
|
101
|
+
"digitalocean",
|
|
102
|
+
"hetzner",
|
|
103
|
+
"vercel",
|
|
104
|
+
"netlify",
|
|
105
|
+
"wix",
|
|
106
|
+
"wordpress-com",
|
|
107
|
+
"cpanel",
|
|
108
|
+
"bluehost",
|
|
109
|
+
"hostgator",
|
|
110
|
+
"hostgator-br",
|
|
111
|
+
"siteground",
|
|
112
|
+
"registro-br",
|
|
113
|
+
"locaweb",
|
|
114
|
+
"kinghost",
|
|
115
|
+
"uol-host",
|
|
116
|
+
"umbler",
|
|
117
|
+
"dnsimple"
|
|
118
|
+
];
|
|
119
|
+
var NO_FIELDS = { name: null, value: null };
|
|
120
|
+
var SHORT = { kind: "shortName" };
|
|
121
|
+
var TRAILING_DOT = { kind: "trailingDot" };
|
|
122
|
+
var QUOTED_TXT = { kind: "quotedTxt" };
|
|
123
|
+
var CPANEL_EDITOR = {
|
|
124
|
+
link: null,
|
|
125
|
+
open: "Zone Editor",
|
|
126
|
+
add: "Add Record",
|
|
127
|
+
save: "Save Record",
|
|
128
|
+
cname: { name: "Name", value: "Record" },
|
|
129
|
+
txt: { name: "Name", value: "Record" },
|
|
130
|
+
root: null,
|
|
131
|
+
traps: []
|
|
132
|
+
};
|
|
133
|
+
var DNS_HOSTS = {
|
|
134
|
+
cloudflare: {
|
|
135
|
+
name: "Cloudflare",
|
|
136
|
+
// Documented: <name>.ns.cloudflare.com, and <color>.foundationdns.{com,net,org}.
|
|
137
|
+
ns: [/^[a-z0-9-]+\.ns\.cloudflare\.com$/, /^[a-z0-9-]+\.foundationdns\.(com|net|org)$/],
|
|
138
|
+
link: "https://dash.cloudflare.com/?to=/:account/:zone/dns/records",
|
|
139
|
+
open: "DNS Records",
|
|
140
|
+
add: "Add record",
|
|
141
|
+
save: "Save",
|
|
142
|
+
cname: { name: "Name", value: "Target" },
|
|
143
|
+
// The dashboard's label for a TXT value is not on a docs page.
|
|
144
|
+
txt: { name: "Name", value: null },
|
|
145
|
+
root: "@",
|
|
146
|
+
traps: [{ kind: "proxyOff", field: "Proxy status", value: "DNS only" }, SHORT]
|
|
147
|
+
},
|
|
148
|
+
godaddy: {
|
|
149
|
+
name: "GoDaddy",
|
|
150
|
+
// Observed: ns<NN>.domaincontrol.com (GoDaddy's docs show only ns1).
|
|
151
|
+
ns: [/^ns\d{1,2}\.domaincontrol\.com$/],
|
|
152
|
+
link: null,
|
|
153
|
+
open: "DNS",
|
|
154
|
+
add: "Add New Record",
|
|
155
|
+
save: "Save",
|
|
156
|
+
cname: { name: "Name", value: "Value" },
|
|
157
|
+
txt: { name: "Name", value: "Value" },
|
|
158
|
+
root: "@",
|
|
159
|
+
traps: [SHORT]
|
|
160
|
+
},
|
|
161
|
+
namecheap: {
|
|
162
|
+
name: "Namecheap",
|
|
163
|
+
// Documented: dns1/2.registrar-servers.com (BasicDNS), pdns1/2 (PremiumDNS),
|
|
164
|
+
// freedns1-5 (FreeDNS).
|
|
165
|
+
ns: [/^(dns|pdns|freedns)\d\.registrar-servers\.com$/],
|
|
166
|
+
link: null,
|
|
167
|
+
open: "Advanced DNS",
|
|
168
|
+
add: "Add New Record",
|
|
169
|
+
save: "Save All Changes",
|
|
170
|
+
cname: { name: "Host", value: "Value" },
|
|
171
|
+
txt: { name: "Host", value: "Value" },
|
|
172
|
+
root: "@",
|
|
173
|
+
traps: [SHORT]
|
|
174
|
+
},
|
|
175
|
+
"namecheap-hosting": {
|
|
176
|
+
name: "Namecheap hosting",
|
|
177
|
+
// Documented: dns1/2.namecheaphosting.com. The records live in cPanel, not Advanced DNS.
|
|
178
|
+
ns: [/^dns\d\.namecheaphosting\.com$/],
|
|
179
|
+
...CPANEL_EDITOR
|
|
180
|
+
},
|
|
181
|
+
squarespace: {
|
|
182
|
+
name: "Squarespace",
|
|
183
|
+
ns: [
|
|
184
|
+
// Documented: Google Cloud DNS nameservers "ending in squarespacedns.com".
|
|
185
|
+
/^[a-z0-9-]+\.squarespacedns\.com$/,
|
|
186
|
+
// Observed, listed so that these names read as ambiguous (domains moved from Google
|
|
187
|
+
// Domains sit on Google's names; older Squarespace domains on NS1's).
|
|
188
|
+
/^ns-cloud-[a-z]\d\.googledomains\.com$/,
|
|
189
|
+
/^dns\d\.p\d{2}\.nsone\.net$/
|
|
190
|
+
],
|
|
191
|
+
link: null,
|
|
192
|
+
open: "DNS Settings",
|
|
193
|
+
add: "Add record",
|
|
194
|
+
save: "Save",
|
|
195
|
+
cname: { name: "Name", value: "Data" },
|
|
196
|
+
txt: { name: "Name", value: "Text" },
|
|
197
|
+
root: "@",
|
|
198
|
+
traps: [SHORT]
|
|
199
|
+
},
|
|
200
|
+
route53: {
|
|
201
|
+
name: "Amazon Route 53",
|
|
202
|
+
// Documented: ns-<n>.awsdns-<nn>.{com,net,org,co.uk}.
|
|
203
|
+
ns: [/^ns-\d+\.awsdns-\d+\.(com|net|org|co\.uk)$/],
|
|
204
|
+
link: "https://console.aws.amazon.com/route53/v2/hostedzones",
|
|
205
|
+
open: "Hosted zones",
|
|
206
|
+
add: "Create record",
|
|
207
|
+
save: "Create records",
|
|
208
|
+
cname: { name: "Record name", value: "Value" },
|
|
209
|
+
txt: { name: "Record name", value: "Value" },
|
|
210
|
+
// "don't enter a value (for example, an @ symbol) in the Name field".
|
|
211
|
+
root: "",
|
|
212
|
+
traps: [SHORT, QUOTED_TXT]
|
|
213
|
+
},
|
|
214
|
+
"google-cloud-dns": {
|
|
215
|
+
name: "Google Cloud DNS",
|
|
216
|
+
// Documented: ns-cloud-<a-d><1-4>.googledomains.com. Squarespace domains moved from
|
|
217
|
+
// Google Domains use the same names, so a set of them is ambiguous.
|
|
218
|
+
ns: [/^ns-cloud-[a-z]\d\.googledomains\.com$/],
|
|
219
|
+
link: null,
|
|
220
|
+
open: null,
|
|
221
|
+
add: "Add standard",
|
|
222
|
+
save: "Create",
|
|
223
|
+
cname: { name: "DNS name", value: null },
|
|
224
|
+
txt: { name: "DNS name", value: null },
|
|
225
|
+
// "To create a resource record at the domain apex, leave the DNS name field blank."
|
|
226
|
+
root: "",
|
|
227
|
+
traps: [SHORT]
|
|
228
|
+
},
|
|
229
|
+
"azure-dns": {
|
|
230
|
+
name: "Azure DNS",
|
|
231
|
+
// Documented: ns<1-4>-<nn>.azure-dns.{com,net,org,info}.
|
|
232
|
+
ns: [/^ns\d-\d+\.azure-dns\.(com|net|org|info)$/],
|
|
233
|
+
link: null,
|
|
234
|
+
open: "Record sets",
|
|
235
|
+
add: "Add",
|
|
236
|
+
save: "Add",
|
|
237
|
+
cname: { name: "Name", value: null },
|
|
238
|
+
txt: { name: "Name", value: null },
|
|
239
|
+
root: "@",
|
|
240
|
+
traps: [SHORT]
|
|
241
|
+
},
|
|
242
|
+
ionos: {
|
|
243
|
+
name: "IONOS",
|
|
244
|
+
// Documented: ns1<ZZZ>.ui-dns.{de,biz,org,com}, ZZZ from 016 to 126.
|
|
245
|
+
ns: [/^ns1\d{3}\.ui-dns\.(de|biz|org|com)$/],
|
|
246
|
+
link: null,
|
|
247
|
+
open: "DNS",
|
|
248
|
+
add: "Add Record",
|
|
249
|
+
save: "Save",
|
|
250
|
+
// The two articles spell the name box differently.
|
|
251
|
+
cname: { name: "Hostname", value: "Point to" },
|
|
252
|
+
txt: { name: "Host name", value: "Value" },
|
|
253
|
+
root: "@",
|
|
254
|
+
traps: [SHORT]
|
|
255
|
+
},
|
|
256
|
+
hostinger: {
|
|
257
|
+
name: "Hostinger",
|
|
258
|
+
// No pattern: ns1/2.dns-parking.com is a search snippet, never confirmed on a page of
|
|
259
|
+
// theirs. The picker reaches Hostinger.
|
|
260
|
+
ns: [],
|
|
261
|
+
link: null,
|
|
262
|
+
open: "DNS Zone Editor",
|
|
263
|
+
add: null,
|
|
264
|
+
save: "Add Record",
|
|
265
|
+
cname: { name: "Name", value: "Points to" },
|
|
266
|
+
txt: { name: "Name", value: "TXT value" },
|
|
267
|
+
root: "@",
|
|
268
|
+
traps: [SHORT, { kind: "oneSpf" }]
|
|
269
|
+
},
|
|
270
|
+
porkbun: {
|
|
271
|
+
name: "Porkbun",
|
|
272
|
+
// Documented: <city>.ns.porkbun.com.
|
|
273
|
+
ns: [/^[a-z]+\.ns\.porkbun\.com$/],
|
|
274
|
+
link: null,
|
|
275
|
+
open: "DNS",
|
|
276
|
+
add: "Add Record",
|
|
277
|
+
save: "Add",
|
|
278
|
+
cname: { name: "Host", value: "Answer" },
|
|
279
|
+
txt: { name: "Host", value: "Answer" },
|
|
280
|
+
// "If you're adding DNS records to your root domain, you will leave this field blank."
|
|
281
|
+
root: "",
|
|
282
|
+
traps: [SHORT]
|
|
283
|
+
},
|
|
284
|
+
"name-com": {
|
|
285
|
+
name: "Name.com",
|
|
286
|
+
// Observed: ns1<abc>.name.com to ns4<abc>.name.com.
|
|
287
|
+
ns: [/^ns[1-4][a-z]{3}\.name\.com$/],
|
|
288
|
+
link: null,
|
|
289
|
+
open: "Manage DNS Records",
|
|
290
|
+
add: null,
|
|
291
|
+
save: "Add Record",
|
|
292
|
+
cname: { name: "Host", value: "Answer" },
|
|
293
|
+
txt: { name: "Host", value: "Answer" },
|
|
294
|
+
root: null,
|
|
295
|
+
traps: [SHORT]
|
|
296
|
+
},
|
|
297
|
+
dynadot: {
|
|
298
|
+
name: "Dynadot",
|
|
299
|
+
// A Dynadot staff answer on its forum: ns1/2.dyna-ns.net.
|
|
300
|
+
ns: [/^ns\d\.dyna-ns\.net$/],
|
|
301
|
+
link: null,
|
|
302
|
+
open: "DNS Settings",
|
|
303
|
+
add: null,
|
|
304
|
+
save: "Save Settings",
|
|
305
|
+
cname: { name: "Subdomain Record", value: "IP Address or Target Host" },
|
|
306
|
+
txt: NO_FIELDS,
|
|
307
|
+
// The zone's own name goes in a separate "Domain Record" section, never as "@".
|
|
308
|
+
root: null,
|
|
309
|
+
traps: [SHORT]
|
|
310
|
+
},
|
|
311
|
+
namesilo: {
|
|
312
|
+
name: "NameSilo",
|
|
313
|
+
// Documented: ns1-3.dnsowl.com.
|
|
314
|
+
ns: [/^ns\d\.dnsowl\.com$/],
|
|
315
|
+
link: null,
|
|
316
|
+
open: null,
|
|
317
|
+
add: null,
|
|
318
|
+
save: "Submit",
|
|
319
|
+
cname: NO_FIELDS,
|
|
320
|
+
txt: NO_FIELDS,
|
|
321
|
+
// "You can leave the hostname blank ... If you do enter an "@", we will convert that".
|
|
322
|
+
root: "",
|
|
323
|
+
traps: []
|
|
324
|
+
},
|
|
325
|
+
gandi: {
|
|
326
|
+
name: "Gandi",
|
|
327
|
+
// Observed: ns-<n>-<a|b|c>.gandi.net.
|
|
328
|
+
ns: [/^ns-\d+-[abc]\.gandi\.net$/],
|
|
329
|
+
link: null,
|
|
330
|
+
open: "DNS Records",
|
|
331
|
+
add: "Add",
|
|
332
|
+
save: null,
|
|
333
|
+
cname: NO_FIELDS,
|
|
334
|
+
txt: NO_FIELDS,
|
|
335
|
+
root: null,
|
|
336
|
+
traps: [SHORT]
|
|
337
|
+
},
|
|
338
|
+
ovhcloud: {
|
|
339
|
+
name: "OVHcloud",
|
|
340
|
+
// Documented: (ns|dns)<nn or nnn>.ovh.net, and (ns|dns)200.anycast.me.
|
|
341
|
+
ns: [/^(ns|dns)\d{2,3}\.ovh\.net$/, /^(ns|dns)200\.anycast\.me$/],
|
|
342
|
+
link: null,
|
|
343
|
+
open: "DNS zone",
|
|
344
|
+
add: "Add an entry",
|
|
345
|
+
save: null,
|
|
346
|
+
cname: { name: "Subdomain", value: "Target" },
|
|
347
|
+
txt: { name: "Subdomain", value: "Target" },
|
|
348
|
+
root: null,
|
|
349
|
+
traps: [SHORT, TRAILING_DOT]
|
|
350
|
+
},
|
|
351
|
+
digitalocean: {
|
|
352
|
+
name: "DigitalOcean",
|
|
353
|
+
// Documented: ns1-3.digitalocean.com.
|
|
354
|
+
ns: [/^ns[1-3]\.digitalocean\.com$/],
|
|
355
|
+
link: null,
|
|
356
|
+
open: "Domains",
|
|
357
|
+
add: "Create a record",
|
|
358
|
+
save: "Create Record",
|
|
359
|
+
cname: { name: "Hostname", value: "Is an alias of" },
|
|
360
|
+
txt: { name: "Hostname", value: "TXT Value" },
|
|
361
|
+
root: "@",
|
|
362
|
+
traps: [SHORT, TRAILING_DOT]
|
|
363
|
+
},
|
|
364
|
+
hetzner: {
|
|
365
|
+
name: "Hetzner",
|
|
366
|
+
ns: [
|
|
367
|
+
// Documented: the primary, secondary (still used by older zones) and konsoleH names.
|
|
368
|
+
/^(hydrogen|oxygen|helium)\.ns\.hetzner\.(com|de)$/,
|
|
369
|
+
/^[a-z0-9]+\.first-ns\.de$/,
|
|
370
|
+
/^[a-z0-9]+\.second-ns\.(de|com)$/,
|
|
371
|
+
/^ns1\.your-server\.de$/
|
|
372
|
+
],
|
|
373
|
+
link: null,
|
|
374
|
+
open: "DNS",
|
|
375
|
+
add: null,
|
|
376
|
+
save: "Add",
|
|
377
|
+
cname: { name: "Name", value: "Alias / Value" },
|
|
378
|
+
txt: { name: "Name", value: null },
|
|
379
|
+
root: "@",
|
|
380
|
+
traps: [SHORT, TRAILING_DOT, QUOTED_TXT]
|
|
381
|
+
},
|
|
382
|
+
vercel: {
|
|
383
|
+
name: "Vercel",
|
|
384
|
+
// Documented: ns1-4.vercel-dns.com (the "alternates" are not named).
|
|
385
|
+
ns: [/^ns[1-4]\.vercel-dns\.com$/],
|
|
386
|
+
link: "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fdomains",
|
|
387
|
+
open: "Domains",
|
|
388
|
+
add: null,
|
|
389
|
+
save: "Add",
|
|
390
|
+
cname: { name: "Name", value: "Value" },
|
|
391
|
+
txt: { name: "Name", value: "Value" },
|
|
392
|
+
// "Leave Name blank for the root domain."
|
|
393
|
+
root: "",
|
|
394
|
+
traps: [SHORT]
|
|
395
|
+
},
|
|
396
|
+
netlify: {
|
|
397
|
+
name: "Netlify",
|
|
398
|
+
// Documented: "a format similar to dns1.p01.nsone.net". NS1's names are shared with
|
|
399
|
+
// older Squarespace zones, so a set of them is ambiguous.
|
|
400
|
+
ns: [/^dns\d\.p\d{2}\.nsone\.net$/],
|
|
401
|
+
link: null,
|
|
402
|
+
open: "DNS",
|
|
403
|
+
add: "Add new record",
|
|
404
|
+
save: "Save",
|
|
405
|
+
cname: NO_FIELDS,
|
|
406
|
+
txt: NO_FIELDS,
|
|
407
|
+
root: null,
|
|
408
|
+
traps: []
|
|
409
|
+
},
|
|
410
|
+
wix: {
|
|
411
|
+
name: "Wix",
|
|
412
|
+
// Observed: ns<n>.wixdns.net. A domain "connected to Wix via pointing" keeps its old
|
|
413
|
+
// name servers, so it never matches here.
|
|
414
|
+
ns: [/^ns\d+\.wixdns\.net$/],
|
|
415
|
+
link: null,
|
|
416
|
+
open: "Manage DNS Records",
|
|
417
|
+
add: "Add Record",
|
|
418
|
+
save: "Save",
|
|
419
|
+
cname: { name: "Host Name", value: "Value" },
|
|
420
|
+
txt: { name: "Host Name", value: "Value" },
|
|
421
|
+
// "Leave this field blank if you were instructed to add an @ sign."
|
|
422
|
+
root: "",
|
|
423
|
+
traps: []
|
|
424
|
+
},
|
|
425
|
+
"wordpress-com": {
|
|
426
|
+
name: "WordPress.com",
|
|
427
|
+
// Documented: ns1-3.wordpress.com.
|
|
428
|
+
ns: [/^ns[1-3]\.wordpress\.com$/],
|
|
429
|
+
link: "https://wordpress.com/domains/",
|
|
430
|
+
open: "DNS records",
|
|
431
|
+
add: "Add record",
|
|
432
|
+
save: null,
|
|
433
|
+
cname: { name: "Name (host)", value: "Alias of (points to)" },
|
|
434
|
+
txt: { name: "Name", value: "Text" },
|
|
435
|
+
root: "@",
|
|
436
|
+
// The Name box takes the label or the whole name, and ignores quotes around a TXT value.
|
|
437
|
+
traps: []
|
|
438
|
+
},
|
|
439
|
+
cpanel: {
|
|
440
|
+
// Hosting companies run their own name servers, so there is no pattern: the picker
|
|
441
|
+
// reaches it.
|
|
442
|
+
name: "cPanel",
|
|
443
|
+
ns: [],
|
|
444
|
+
...CPANEL_EDITOR
|
|
445
|
+
},
|
|
446
|
+
bluehost: {
|
|
447
|
+
name: "Bluehost",
|
|
448
|
+
// Documented: ns1/2.bluehost.com. Its help is behind a captcha and the menu path was
|
|
449
|
+
// read from a 2024 archive, so no label is quoted.
|
|
450
|
+
ns: [/^ns[12]\.bluehost\.com$/],
|
|
451
|
+
link: null,
|
|
452
|
+
open: null,
|
|
453
|
+
add: null,
|
|
454
|
+
save: null,
|
|
455
|
+
cname: NO_FIELDS,
|
|
456
|
+
txt: NO_FIELDS,
|
|
457
|
+
root: null,
|
|
458
|
+
traps: []
|
|
459
|
+
},
|
|
460
|
+
hostgator: {
|
|
461
|
+
name: "HostGator",
|
|
462
|
+
// Documented: hgns1/2.hostgator.com for new customers, ns<NNNN>.hostgator.com before
|
|
463
|
+
// November 2023.
|
|
464
|
+
ns: [/^hgns\d\.hostgator\.com$/, /^ns\d{4}\.hostgator\.com$/],
|
|
465
|
+
link: null,
|
|
466
|
+
open: "DNS",
|
|
467
|
+
add: "+ ADD RECORD",
|
|
468
|
+
save: "ADD",
|
|
469
|
+
cname: NO_FIELDS,
|
|
470
|
+
txt: NO_FIELDS,
|
|
471
|
+
root: null,
|
|
472
|
+
traps: []
|
|
473
|
+
},
|
|
474
|
+
"hostgator-br": {
|
|
475
|
+
name: "HostGator Brasil",
|
|
476
|
+
// Documented: dns3/4.hostgator.com.br for a registered domain with no plan yet. A plan's
|
|
477
|
+
// names come in its activation email and are not on a page.
|
|
478
|
+
ns: [/^dns[34]\.hostgator\.com\.br$/],
|
|
479
|
+
link: null,
|
|
480
|
+
open: "Editar Zona Avan\xE7ada de DNS",
|
|
481
|
+
add: "Adicionar registro",
|
|
482
|
+
save: "Adicionar",
|
|
483
|
+
// The value box's name changes with the type ("O nome é alterado conforme o Tipo").
|
|
484
|
+
cname: { name: "Nome", value: null },
|
|
485
|
+
txt: { name: "Nome", value: null },
|
|
486
|
+
root: null,
|
|
487
|
+
traps: []
|
|
488
|
+
},
|
|
489
|
+
siteground: {
|
|
490
|
+
name: "SiteGround",
|
|
491
|
+
// Documented: ns1/2.siteground.net.
|
|
492
|
+
ns: [/^ns[12]\.siteground\.net$/],
|
|
493
|
+
link: null,
|
|
494
|
+
open: "DNS Zone Editor",
|
|
495
|
+
add: null,
|
|
496
|
+
save: "Create",
|
|
497
|
+
cname: { name: "Name", value: null },
|
|
498
|
+
txt: { name: "Name", value: null },
|
|
499
|
+
root: null,
|
|
500
|
+
traps: [SHORT]
|
|
501
|
+
},
|
|
502
|
+
"registro-br": {
|
|
503
|
+
name: "Registro.br",
|
|
504
|
+
// Observed: a.auto.dns.br and b.auto.dns.br.
|
|
505
|
+
ns: [/^[ab]\.auto\.dns\.br$/],
|
|
506
|
+
link: null,
|
|
507
|
+
open: "CONFIGURAR ZONA DNS",
|
|
508
|
+
add: null,
|
|
509
|
+
save: null,
|
|
510
|
+
cname: NO_FIELDS,
|
|
511
|
+
txt: NO_FIELDS,
|
|
512
|
+
root: null,
|
|
513
|
+
traps: [{ kind: "advancedMode", mode: "Modo Avan\xE7ado" }]
|
|
514
|
+
},
|
|
515
|
+
locaweb: {
|
|
516
|
+
name: "Locaweb",
|
|
517
|
+
// Documented: ns1-3.locaweb.com.br.
|
|
518
|
+
ns: [/^ns[1-3]\.locaweb\.com\.br$/],
|
|
519
|
+
link: null,
|
|
520
|
+
open: "Editar zona DNS",
|
|
521
|
+
add: "Adicionar entrada",
|
|
522
|
+
save: null,
|
|
523
|
+
cname: { name: "Entrada", value: "Conte\xFAdo" },
|
|
524
|
+
txt: { name: "Entrada", value: "Conte\xFAdo" },
|
|
525
|
+
// "o campo Entrada é preenchido por padrão com apenas um ponto (.)".
|
|
526
|
+
root: ".",
|
|
527
|
+
traps: []
|
|
528
|
+
},
|
|
529
|
+
kinghost: {
|
|
530
|
+
name: "KingHost",
|
|
531
|
+
// Documented: dns1-6.kinghost.com.br.
|
|
532
|
+
ns: [/^dns[1-6]\.kinghost\.com\.br$/],
|
|
533
|
+
link: null,
|
|
534
|
+
open: "Gerenciar DNS",
|
|
535
|
+
add: "+ ADICIONAR DNS",
|
|
536
|
+
save: "ADICIONAR DNS",
|
|
537
|
+
// The form is a screenshot, so the case of "host" and "destino" is unconfirmed.
|
|
538
|
+
cname: NO_FIELDS,
|
|
539
|
+
txt: NO_FIELDS,
|
|
540
|
+
root: "@",
|
|
541
|
+
traps: [{ kind: "secondSave", button: "SALVAR ALTERA\xC7\xD5ES" }]
|
|
542
|
+
},
|
|
543
|
+
"uol-host": {
|
|
544
|
+
name: "UOL Host",
|
|
545
|
+
// Documented: ns1-3.dominios.uol.com.br.
|
|
546
|
+
ns: [/^ns[1-3]\.dominios\.uol\.com\.br$/],
|
|
547
|
+
link: null,
|
|
548
|
+
open: "Administrar DNS",
|
|
549
|
+
add: "Criar nova entrada",
|
|
550
|
+
save: "Salvar entrada",
|
|
551
|
+
cname: { name: "Entrada", value: "Destino" },
|
|
552
|
+
txt: { name: "Entrada", value: "Valor" },
|
|
553
|
+
root: "",
|
|
554
|
+
traps: []
|
|
555
|
+
},
|
|
556
|
+
umbler: {
|
|
557
|
+
name: "Umbler",
|
|
558
|
+
// Documented: ns<NNN>.umbler.{com,in,com.br,co.uk}.
|
|
559
|
+
ns: [/^ns\d{3}\.umbler\.(com|in|com\.br|co\.uk)$/],
|
|
560
|
+
link: null,
|
|
561
|
+
open: "Editor de DNS",
|
|
562
|
+
add: "Adicionar",
|
|
563
|
+
save: "Salvar",
|
|
564
|
+
cname: { name: "Nome", value: "Endere\xE7o" },
|
|
565
|
+
txt: { name: "Nome", value: "Dados" },
|
|
566
|
+
root: "@",
|
|
567
|
+
traps: [SHORT]
|
|
568
|
+
},
|
|
569
|
+
dnsimple: {
|
|
570
|
+
name: "DNSimple",
|
|
571
|
+
// Documented: ns1-4.dnsimple-edge.{com,net,io,org}, and the legacy ns1-4.dnsimple.com.
|
|
572
|
+
ns: [/^ns[1-4]\.dnsimple-edge\.(com|net|io|org)$/, /^ns[1-4]\.dnsimple\.com$/],
|
|
573
|
+
link: null,
|
|
574
|
+
open: "Record editor",
|
|
575
|
+
add: "Add record",
|
|
576
|
+
save: "Add Record",
|
|
577
|
+
cname: { name: "Name", value: "Content" },
|
|
578
|
+
txt: { name: "Name", value: "Content" },
|
|
579
|
+
root: "",
|
|
580
|
+
traps: [SHORT]
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
function detectDnsHosts(nameservers) {
|
|
584
|
+
const names = nameservers.map(normalizeDnsName).filter(Boolean);
|
|
585
|
+
if (!names.length) return [];
|
|
586
|
+
return DNS_HOST_IDS.filter(
|
|
587
|
+
(id) => names.every((name) => DNS_HOSTS[id].ns.some((pattern) => pattern.test(name)))
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/server/zone.ts
|
|
592
|
+
function dnsHostOf(zone) {
|
|
593
|
+
return { ...zone, hosts: detectDnsHosts(zone.nameservers) };
|
|
594
|
+
}
|
|
595
|
+
async function readDnsZone(lookups, hostname, budgetMs) {
|
|
596
|
+
let timer;
|
|
597
|
+
const outOfTime = new Promise((resolve) => {
|
|
598
|
+
timer = setTimeout(() => resolve(null), budgetMs);
|
|
599
|
+
});
|
|
600
|
+
try {
|
|
601
|
+
return await Promise.race([walk(lookups, hostname), outOfTime]);
|
|
602
|
+
} finally {
|
|
603
|
+
clearTimeout(timer);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function walk(lookups, hostname) {
|
|
607
|
+
const labels = normalizeDnsName(hostname).split(".");
|
|
608
|
+
for (let start = 0; start < labels.length - 1; start++) {
|
|
609
|
+
const zone = labels.slice(start).join(".");
|
|
610
|
+
const answer = await lookups.ns(zone);
|
|
611
|
+
if (answer === "failed") return null;
|
|
612
|
+
if (answer === "absent" || !answer.names.length) continue;
|
|
613
|
+
const alias = await lookups.cname(zone);
|
|
614
|
+
if (alias === "failed") return null;
|
|
615
|
+
if (alias !== "absent") continue;
|
|
616
|
+
return { zone, nameservers: [...new Set(answer.names.map(normalizeDnsName))].sort() };
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/server/connect/providers.ts
|
|
622
|
+
var CONNECT_PROVIDERS = [
|
|
623
|
+
// `_domainconnect.colding.app` read 2026-10-29; settings answer `urlSyncUX`
|
|
624
|
+
// https://dash.cloudflare.com/domainconnect.
|
|
625
|
+
{
|
|
626
|
+
id: "cloudflare",
|
|
627
|
+
txt: "api.cloudflare.com/client/v4/dns/domainconnect",
|
|
628
|
+
api: "https://api.cloudflare.com/client/v4/dns/domainconnect",
|
|
629
|
+
syncHost: "dash.cloudflare.com"
|
|
630
|
+
},
|
|
631
|
+
// `_domainconnect.nextjs.org` read 2026-10-29: a CNAME to `_domainconnect.vercel-dns.com`, whose
|
|
632
|
+
// TXT is `domainconnect.vercel.com`. The settings name another address for the API.
|
|
633
|
+
{
|
|
634
|
+
id: "vercel",
|
|
635
|
+
txt: "domainconnect.vercel.com",
|
|
636
|
+
api: "https://vercel.com/api/domain-connect",
|
|
637
|
+
syncHost: "vercel.com"
|
|
638
|
+
},
|
|
639
|
+
// `_domainconnect.simply.com` read 2026-10-29.
|
|
640
|
+
{
|
|
641
|
+
id: "simply",
|
|
642
|
+
txt: "domainconnect.simply.com",
|
|
643
|
+
api: "https://domainconnect.simply.com",
|
|
644
|
+
syncHost: "domainconnect.simply.com"
|
|
645
|
+
},
|
|
646
|
+
// Settings read 2026-10-29 from domainconnect.api.godaddy.com (`urlSyncUX`
|
|
647
|
+
// https://dcc.godaddy.com/manage). The TXT value on a customer's zone is not read yet: if it
|
|
648
|
+
// differs, GoDaddy answers "not offered", which is the safe way to be wrong.
|
|
649
|
+
{
|
|
650
|
+
id: "godaddy",
|
|
651
|
+
txt: "domainconnect.api.godaddy.com",
|
|
652
|
+
api: "https://domainconnect.api.godaddy.com",
|
|
653
|
+
syncHost: "dcc.godaddy.com"
|
|
654
|
+
}
|
|
655
|
+
];
|
|
656
|
+
|
|
657
|
+
// src/bounded-fetch.ts
|
|
658
|
+
var import_server = require("@gusnips/server");
|
|
659
|
+
async function fetchBounded(doFetch, url, init, limits) {
|
|
660
|
+
try {
|
|
661
|
+
const res = await doFetch(url, {
|
|
662
|
+
...init,
|
|
663
|
+
redirect: "manual",
|
|
664
|
+
signal: AbortSignal.timeout(limits.timeoutMs)
|
|
665
|
+
});
|
|
666
|
+
if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
|
|
667
|
+
await res.body?.cancel().catch(() => void 0);
|
|
668
|
+
return { status: res.status || 302, text: "" };
|
|
669
|
+
}
|
|
670
|
+
const { bytes, truncated } = await (0, import_server.readBounded)(res.body, limits.maxBytes);
|
|
671
|
+
if (truncated) return null;
|
|
672
|
+
return { status: res.status, text: new TextDecoder().decode(bytes) };
|
|
673
|
+
} catch {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/guards.ts
|
|
679
|
+
function isObject(value) {
|
|
680
|
+
return typeof value === "object" && value !== null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/server/connect/discover.ts
|
|
684
|
+
var MAX_ANSWER_BYTES = 64 * 1024;
|
|
685
|
+
var NOT_OFFERED = { found: false, reason: "not_offered" };
|
|
686
|
+
var UNANSWERED = { found: false, reason: "unanswered" };
|
|
687
|
+
var ZONE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
688
|
+
var text = (value, max) => typeof value === "string" && value.length <= max ? value : void 0;
|
|
689
|
+
function readSettings(body) {
|
|
690
|
+
if (!isObject(body)) return null;
|
|
691
|
+
const providerName = text(body.providerName, 100);
|
|
692
|
+
const urlAPI = text(body.urlAPI, 512);
|
|
693
|
+
if (!providerName || !urlAPI) return null;
|
|
694
|
+
return {
|
|
695
|
+
providerName,
|
|
696
|
+
providerDisplayName: text(body.providerDisplayName, 100),
|
|
697
|
+
urlSyncUX: text(body.urlSyncUX, 512),
|
|
698
|
+
urlAPI
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
var normalizeTxt = (value) => value.trim().replace(/^"+|"+$/g, "").trim().toLowerCase().replace(/\/+$/, "");
|
|
702
|
+
function plainHttps(raw) {
|
|
703
|
+
try {
|
|
704
|
+
const url = new URL(raw);
|
|
705
|
+
const plain = url.protocol === "https:" && !url.username && !url.password && !url.port;
|
|
706
|
+
return plain ? url : null;
|
|
707
|
+
} catch {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
var withoutTrailingSlash = (url) => `${url.origin}${url.pathname}`.replace(/\/+$/, "");
|
|
712
|
+
var transient = (status) => status === 429 || status >= 500;
|
|
713
|
+
async function discoverConnect(io, args) {
|
|
714
|
+
if (!ZONE.test(args.zone)) return NOT_OFFERED;
|
|
715
|
+
const deadline = Date.now() + args.budgetMs;
|
|
716
|
+
let timer;
|
|
717
|
+
const outOfTime = new Promise((resolve) => {
|
|
718
|
+
timer = setTimeout(() => resolve(UNANSWERED), args.budgetMs);
|
|
719
|
+
});
|
|
720
|
+
try {
|
|
721
|
+
return await Promise.race([ask(io, args, deadline), outOfTime]);
|
|
722
|
+
} catch {
|
|
723
|
+
return UNANSWERED;
|
|
724
|
+
} finally {
|
|
725
|
+
clearTimeout(timer);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async function ask(io, args, deadline) {
|
|
729
|
+
const { zone, providerId, serviceId, providers = CONNECT_PROVIDERS } = args;
|
|
730
|
+
const get = (url) => {
|
|
731
|
+
const left = deadline - Date.now();
|
|
732
|
+
if (left <= 0) return Promise.resolve(null);
|
|
733
|
+
return fetchBounded(
|
|
734
|
+
io.fetch ?? fetch,
|
|
735
|
+
url,
|
|
736
|
+
{ method: "GET", headers: { Accept: "application/json" } },
|
|
737
|
+
{ timeoutMs: left, maxBytes: MAX_ANSWER_BYTES }
|
|
738
|
+
);
|
|
739
|
+
};
|
|
740
|
+
const published = await io.txt(`_domainconnect.${zone}`);
|
|
741
|
+
if (published === "failed") return UNANSWERED;
|
|
742
|
+
if (published === "absent") return NOT_OFFERED;
|
|
743
|
+
const values = published.txts.map(normalizeTxt);
|
|
744
|
+
const provider = providers.find((entry) => values.includes(entry.txt));
|
|
745
|
+
if (!provider) return NOT_OFFERED;
|
|
746
|
+
const settings = await get(`https://${provider.txt}/v2/${zone}/settings`);
|
|
747
|
+
if (!settings) return UNANSWERED;
|
|
748
|
+
if (settings.status !== 200) return transient(settings.status) ? UNANSWERED : NOT_OFFERED;
|
|
749
|
+
let body;
|
|
750
|
+
try {
|
|
751
|
+
body = JSON.parse(settings.text);
|
|
752
|
+
} catch {
|
|
753
|
+
return NOT_OFFERED;
|
|
754
|
+
}
|
|
755
|
+
const parsed = readSettings(body);
|
|
756
|
+
if (!parsed) return NOT_OFFERED;
|
|
757
|
+
const api = plainHttps(parsed.urlAPI);
|
|
758
|
+
const sync = parsed.urlSyncUX ? plainHttps(parsed.urlSyncUX) : null;
|
|
759
|
+
if (!api || withoutTrailingSlash(api) !== provider.api) return NOT_OFFERED;
|
|
760
|
+
if (!sync || sync.hostname !== provider.syncHost || sync.search || sync.hash) return NOT_OFFERED;
|
|
761
|
+
const template = await get(
|
|
762
|
+
`${provider.api}/v2/domainTemplates/providers/${providerId}/services/${serviceId}`
|
|
763
|
+
);
|
|
764
|
+
if (!template) return UNANSWERED;
|
|
765
|
+
if (template.status !== 200) return transient(template.status) ? UNANSWERED : NOT_OFFERED;
|
|
766
|
+
return {
|
|
767
|
+
found: true,
|
|
768
|
+
id: provider.id,
|
|
769
|
+
provider: parsed.providerDisplayName?.trim() || parsed.providerName,
|
|
770
|
+
syncUrl: withoutTrailingSlash(sync)
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/server/connect/sign.ts
|
|
775
|
+
var import_node_crypto = require("crypto");
|
|
776
|
+
var encode = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`).replace(/%20/g, "+");
|
|
777
|
+
function signedApplyUrl(args) {
|
|
778
|
+
const query = Object.entries(args.params).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, value]) => `${encode(name)}=${encode(value)}`).join("&");
|
|
779
|
+
const signature = (0, import_node_crypto.sign)(
|
|
780
|
+
"sha256",
|
|
781
|
+
Buffer.from(query, "utf8"),
|
|
782
|
+
(0, import_node_crypto.createPrivateKey)(args.privateKeyPem)
|
|
783
|
+
).toString("base64");
|
|
784
|
+
const path = `/v2/domainTemplates/providers/${args.providerId}/services/${args.serviceId}/apply`;
|
|
785
|
+
return `${args.syncUrl}${path}?${query}&key=${args.keyId}&sig=${encode(signature)}`;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/writers/types.ts
|
|
789
|
+
var DnsWriteError = class extends Error {
|
|
790
|
+
constructor(code, params = {}) {
|
|
791
|
+
super(MESSAGES[code](params));
|
|
792
|
+
this.code = code;
|
|
793
|
+
this.params = params;
|
|
794
|
+
this.name = "DnsWriteError";
|
|
795
|
+
}
|
|
796
|
+
code;
|
|
797
|
+
params;
|
|
798
|
+
};
|
|
799
|
+
var MESSAGES = {
|
|
800
|
+
token_invalid: () => "The DNS host didn't accept that token",
|
|
801
|
+
zone_read_missing: () => "That token can't read your domains",
|
|
802
|
+
dns_edit_missing: () => "That token can't change DNS records",
|
|
803
|
+
zone_not_found: (p) => `No zone holds ${p.hostname ?? "that name"}`,
|
|
804
|
+
conflict: (p) => `${p.hostname ?? "That name"} already has a record (type ${p.type ?? "?"})`,
|
|
805
|
+
unavailable: () => "Couldn't reach the DNS host"
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// src/writers/cloudflare.ts
|
|
809
|
+
var API = "https://api.cloudflare.com/client/v4";
|
|
810
|
+
var TIMEOUT_MS = 1e4;
|
|
811
|
+
var MAX_BODY_BYTES = 256 * 1024;
|
|
812
|
+
var TOKEN_CODES = /* @__PURE__ */ new Set([1e3, 6003, 6111, 1e4]);
|
|
813
|
+
var CONFLICT_CODES = /* @__PURE__ */ new Set([81053, 81054, 81057]);
|
|
814
|
+
var ID = /^[A-Za-z0-9]{1,64}$/;
|
|
815
|
+
var RECORD_TYPE = /^[A-Z0-9]{1,12}$/;
|
|
816
|
+
function readEnvelope(json) {
|
|
817
|
+
if (!isObject(json) || typeof json.success !== "boolean") return null;
|
|
818
|
+
const codes = [];
|
|
819
|
+
if (Array.isArray(json.errors)) {
|
|
820
|
+
for (const error of json.errors) {
|
|
821
|
+
if (!isObject(error)) return null;
|
|
822
|
+
if (typeof error.code === "number") codes.push(error.code);
|
|
823
|
+
if (Array.isArray(error.error_chain)) {
|
|
824
|
+
for (const link of error.error_chain)
|
|
825
|
+
if (isObject(link) && typeof link.code === "number") codes.push(link.code);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return { success: json.success, codes, result: json.result };
|
|
830
|
+
}
|
|
831
|
+
async function call(doFetch, token, path, send) {
|
|
832
|
+
const res = await fetchBounded(
|
|
833
|
+
doFetch,
|
|
834
|
+
`${API}${path}`,
|
|
835
|
+
{
|
|
836
|
+
method: send?.method ?? "GET",
|
|
837
|
+
headers: {
|
|
838
|
+
Authorization: `Bearer ${token}`,
|
|
839
|
+
Accept: "application/json",
|
|
840
|
+
...send ? { "Content-Type": "application/json" } : {}
|
|
841
|
+
},
|
|
842
|
+
body: send ? JSON.stringify(send.body) : void 0
|
|
843
|
+
},
|
|
844
|
+
{ timeoutMs: TIMEOUT_MS, maxBytes: MAX_BODY_BYTES }
|
|
845
|
+
);
|
|
846
|
+
if (!res) return { status: 0, body: null };
|
|
847
|
+
let json = null;
|
|
848
|
+
try {
|
|
849
|
+
json = JSON.parse(res.text);
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
return { status: res.status, body: readEnvelope(json) };
|
|
853
|
+
}
|
|
854
|
+
function refusal(answer, stage, subject) {
|
|
855
|
+
const codes = answer.body?.codes ?? [];
|
|
856
|
+
const refusedToken = answer.status === 401 || codes.some((c) => TOKEN_CODES.has(c));
|
|
857
|
+
if (stage === "zones" && refusedToken) return new DnsWriteError("token_invalid");
|
|
858
|
+
if (codes.some((c) => CONFLICT_CODES.has(c)))
|
|
859
|
+
return new DnsWriteError("conflict", { ...subject });
|
|
860
|
+
if (answer.status === 403 || refusedToken) {
|
|
861
|
+
if (stage === "zones") return new DnsWriteError("zone_read_missing");
|
|
862
|
+
return new DnsWriteError("dns_edit_missing");
|
|
863
|
+
}
|
|
864
|
+
return new DnsWriteError("unavailable");
|
|
865
|
+
}
|
|
866
|
+
function resultOf(answer, stage, subject) {
|
|
867
|
+
const ok = answer.status >= 200 && answer.status < 300 && answer.body?.success === true;
|
|
868
|
+
if (!ok) throw refusal(answer, stage, subject);
|
|
869
|
+
return answer.body?.result;
|
|
870
|
+
}
|
|
871
|
+
function zonesOf(result) {
|
|
872
|
+
if (!Array.isArray(result)) throw new DnsWriteError("unavailable");
|
|
873
|
+
return result.map((row) => {
|
|
874
|
+
if (!isObject(row) || typeof row.id !== "string" || typeof row.name !== "string")
|
|
875
|
+
throw new DnsWriteError("unavailable");
|
|
876
|
+
if (!ID.test(row.id)) throw new DnsWriteError("unavailable");
|
|
877
|
+
return { id: row.id, name: row.name };
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
function recordsOf(result) {
|
|
881
|
+
if (!Array.isArray(result)) throw new DnsWriteError("unavailable");
|
|
882
|
+
return result.map((row) => {
|
|
883
|
+
if (!isObject(row) || typeof row.id !== "string" || typeof row.type !== "string" || typeof row.content !== "string" || !ID.test(row.id) || !RECORD_TYPE.test(row.type))
|
|
884
|
+
throw new DnsWriteError("unavailable");
|
|
885
|
+
return {
|
|
886
|
+
id: row.id,
|
|
887
|
+
type: row.type,
|
|
888
|
+
content: row.content,
|
|
889
|
+
proxied: typeof row.proxied === "boolean" ? row.proxied : void 0
|
|
890
|
+
};
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
function zoneCandidates(hostname) {
|
|
894
|
+
const labels = hostname.split(".");
|
|
895
|
+
const names = [];
|
|
896
|
+
for (let start = 0; start < labels.length - 1; start++) names.push(labels.slice(start).join("."));
|
|
897
|
+
return names;
|
|
898
|
+
}
|
|
899
|
+
var unquoted = (content) => content.replace(/"\s+"/g, "").replace(/^"|"$/g, "");
|
|
900
|
+
var sameValue = (record, existing) => record.type === "CNAME" ? normalizeDnsName(existing.content) === normalizeDnsName(record.value) : unquoted(existing.content) === record.value;
|
|
901
|
+
var isSpf = (text2) => /^v=spf1(?:\s|$)/i.test(unquoted(text2).trim());
|
|
902
|
+
function blocks(record, existing) {
|
|
903
|
+
if (record.type === "CNAME" || existing.type === "CNAME") return true;
|
|
904
|
+
return existing.type === "TXT" && isSpf(record.value) && isSpf(existing.content);
|
|
905
|
+
}
|
|
906
|
+
var cloudflare = {
|
|
907
|
+
host: "cloudflare",
|
|
908
|
+
/** Add the record (DNS only) to the Cloudflare zone that holds its name. An identical record
|
|
909
|
+
* is left as it is; the same CNAME behind the proxy gets the proxy turned off. */
|
|
910
|
+
async addRecord({ token, record, comment, fetch: doFetch = fetch }) {
|
|
911
|
+
const hostname = normalizeDnsName(record.name);
|
|
912
|
+
const subject = { hostname, type: record.type };
|
|
913
|
+
let zoneId;
|
|
914
|
+
for (const candidate of zoneCandidates(hostname)) {
|
|
915
|
+
const query = new URLSearchParams({ name: candidate });
|
|
916
|
+
const zones = zonesOf(
|
|
917
|
+
resultOf(await call(doFetch, token, `/zones?${query}`), "zones", subject)
|
|
918
|
+
);
|
|
919
|
+
zoneId = zones.find((z) => normalizeDnsName(z.name) === candidate)?.id;
|
|
920
|
+
if (zoneId) break;
|
|
921
|
+
}
|
|
922
|
+
if (!zoneId) throw new DnsWriteError("zone_not_found", { hostname });
|
|
923
|
+
const existing = recordsOf(
|
|
924
|
+
resultOf(
|
|
925
|
+
await call(
|
|
926
|
+
doFetch,
|
|
927
|
+
token,
|
|
928
|
+
`/zones/${zoneId}/dns_records?${new URLSearchParams({ name: hostname })}`
|
|
929
|
+
),
|
|
930
|
+
"records",
|
|
931
|
+
subject
|
|
932
|
+
)
|
|
933
|
+
);
|
|
934
|
+
const ours = existing.find((r) => r.type === record.type && sameValue(record, r));
|
|
935
|
+
if (ours) {
|
|
936
|
+
if (record.type !== "CNAME" || ours.proxied !== true) return "unchanged";
|
|
937
|
+
resultOf(
|
|
938
|
+
await call(doFetch, token, `/zones/${zoneId}/dns_records/${ours.id}`, {
|
|
939
|
+
method: "PATCH",
|
|
940
|
+
body: { proxied: false }
|
|
941
|
+
}),
|
|
942
|
+
"records",
|
|
943
|
+
subject
|
|
944
|
+
);
|
|
945
|
+
return "unproxied";
|
|
946
|
+
}
|
|
947
|
+
const blocker = existing.find((r) => blocks(record, r));
|
|
948
|
+
if (blocker) throw new DnsWriteError("conflict", { hostname, type: blocker.type });
|
|
949
|
+
resultOf(
|
|
950
|
+
await call(doFetch, token, `/zones/${zoneId}/dns_records`, {
|
|
951
|
+
method: "POST",
|
|
952
|
+
body: {
|
|
953
|
+
type: record.type,
|
|
954
|
+
name: hostname,
|
|
955
|
+
content: record.value,
|
|
956
|
+
// A proxied record hides the CNAME our checker reads.
|
|
957
|
+
...record.type === "CNAME" ? { proxied: false } : {},
|
|
958
|
+
// 1 is Cloudflare's "automatic".
|
|
959
|
+
ttl: 1,
|
|
960
|
+
...comment ? { comment } : {}
|
|
961
|
+
}
|
|
962
|
+
}),
|
|
963
|
+
"records",
|
|
964
|
+
subject
|
|
965
|
+
);
|
|
966
|
+
return "created";
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
// src/writers/index.ts
|
|
971
|
+
var writers = { cloudflare };
|
|
972
|
+
|
|
973
|
+
// src/server/plan.ts
|
|
974
|
+
async function planConnection(input, io) {
|
|
975
|
+
const budgetMs = input.budgetMs ?? 3e3;
|
|
976
|
+
const zone = await readDnsZone(io, input.hostname, budgetMs);
|
|
977
|
+
if (!zone)
|
|
978
|
+
return { reading: null, methods: [{ kind: "manual", hosts: [] }], connectUnanswered: false };
|
|
979
|
+
const reading = dnsHostOf(zone);
|
|
980
|
+
const methods = [];
|
|
981
|
+
let connectUnanswered = false;
|
|
982
|
+
const { connect } = input;
|
|
983
|
+
if (connect) {
|
|
984
|
+
const found = await discoverConnect(io, {
|
|
985
|
+
zone: zone.zone,
|
|
986
|
+
providerId: connect.providerId,
|
|
987
|
+
serviceId: connect.serviceId,
|
|
988
|
+
budgetMs
|
|
989
|
+
});
|
|
990
|
+
if (found.found)
|
|
991
|
+
methods.push({
|
|
992
|
+
kind: "domain-connect",
|
|
993
|
+
provider: found.provider,
|
|
994
|
+
url: signedApplyUrl({
|
|
995
|
+
providerId: connect.providerId,
|
|
996
|
+
keyId: connect.keyId,
|
|
997
|
+
serviceId: connect.serviceId,
|
|
998
|
+
privateKeyPem: connect.privateKeyPem,
|
|
999
|
+
syncUrl: found.syncUrl,
|
|
1000
|
+
params: connect.params(zone.zone)
|
|
1001
|
+
})
|
|
1002
|
+
});
|
|
1003
|
+
else connectUnanswered = found.reason === "unanswered";
|
|
1004
|
+
}
|
|
1005
|
+
const [only, ...others] = reading.hosts;
|
|
1006
|
+
if (only && !others.length && writers[only]) methods.push({ kind: "api-token", host: only });
|
|
1007
|
+
methods.push({ kind: "manual", hosts: reading.hosts });
|
|
1008
|
+
return { reading, methods, connectUnanswered };
|
|
1009
|
+
}
|
|
1010
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1011
|
+
0 && (module.exports = {
|
|
1012
|
+
CONNECT_PROVIDERS,
|
|
1013
|
+
discoverConnect,
|
|
1014
|
+
dnsHostOf,
|
|
1015
|
+
planConnection,
|
|
1016
|
+
readDnsZone,
|
|
1017
|
+
signedApplyUrl,
|
|
1018
|
+
systemDnsLookups,
|
|
1019
|
+
systemResolver
|
|
1020
|
+
});
|