@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gustavo Salomé
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,143 @@
1
- # Temporary Holding Version
1
+ # @gusnips/one-click-dns
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Put DNS records in a customer's zone. The package finds where their DNS lives, then gives you the best way to add the record: a signed Domain Connect link, a pasted API token, or the steps for that host.
4
+
5
+ ```bash
6
+ npm install @gusnips/one-click-dns
7
+ # or
8
+ bun add @gusnips/one-click-dns
9
+ ```
10
+
11
+ It has three entry points:
12
+
13
+ - `@gusnips/one-click-dns`: runs in the browser. The host list, the record spelling and the template presets.
14
+ - `@gusnips/one-click-dns/server`: reads DNS, builds signed links. Node or Bun.
15
+ - `@gusnips/one-click-dns/writers`: writes a record with an API token. Server only.
16
+
17
+ Keep keys and tokens on the server. Nothing in the browser entry needs them.
18
+
19
+ ## First call
20
+
21
+ ```ts
22
+ import { planConnection, systemDnsLookups } from "@gusnips/one-click-dns/server";
23
+
24
+ const plan = await planConnection({ hostname: "help.acme.com" }, systemDnsLookups(2000));
25
+
26
+ console.log(plan.reading?.hosts); // ["cloudflare"]
27
+ console.log(plan.methods.map((method) => method.kind)); // ["api-token", "manual"]
28
+ ```
29
+
30
+ `methods` lists the ways to connect, best first:
31
+
32
+ | `kind` | What it means |
33
+ | ---------------- | ----------------------------------------------------------------------------------------------------- |
34
+ | `domain-connect` | One click. You send the person to `url`, their DNS host asks them to approve, and the records appear. |
35
+ | `api-token` | The person pastes a token, and your server writes the record. |
36
+ | `manual` | The person copies the records by hand. The host's name is in `hosts`. Always the last one. |
37
+
38
+ `plan.reading` is `null` when DNS did not answer. That is not the same as "host unknown", so do not show it as one.
39
+
40
+ ## Add a record with a token
41
+
42
+ ```ts
43
+ import { cloudflare } from "@gusnips/one-click-dns/writers";
44
+
45
+ const outcome = await cloudflare.addRecord({
46
+ token, // the person's Cloudflare API token
47
+ record: { type: "CNAME", name: "help.acme.com", value: "abc123.domains.example.app" },
48
+ });
49
+ // "created", "unchanged" or "unproxied"
50
+ ```
51
+
52
+ The token is used for this call only. It is never stored, logged or put in an error. A record the person already has is never overwritten: you get a `conflict` instead.
53
+
54
+ When the write fails, it throws a `DnsWriteError` with a `code`. Word each code in your own languages:
55
+
56
+ | `code` | What to tell the person |
57
+ | ------------------- | ------------------------------------------------------------------------------------ |
58
+ | `token_invalid` | The host did not accept the token. Paste it again. |
59
+ | `zone_read_missing` | The token cannot list domains. Give it that permission. |
60
+ | `dns_edit_missing` | The token cannot change DNS records. Give it that permission. |
61
+ | `zone_not_found` | The token's account does not hold this domain. |
62
+ | `conflict` | The name already has another record (`params.type`). They decide what to do with it. |
63
+ | `unavailable` | The host did not answer. Try again. |
64
+
65
+ Cloudflare is the only writer today.
66
+
67
+ ## One-click links (Domain Connect)
68
+
69
+ A link needs a template that your DNS host has switched on, and a key pair. You publish the public key in DNS and keep the private key on your server.
70
+
71
+ ```ts
72
+ const plan = await planConnection(
73
+ {
74
+ hostname: "help.acme.com",
75
+ connect: {
76
+ providerId: "example.com", // your id in the Templates repository
77
+ keyId: "_dck1", // the TXT record name that holds your public key
78
+ serviceId: "custom-domain", // your template
79
+ privateKeyPem,
80
+ params: (zone) => ({
81
+ domain: zone,
82
+ host: "help",
83
+ target: "abc123",
84
+ redirect_uri: "https://app.example.com/done",
85
+ }),
86
+ },
87
+ },
88
+ systemDnsLookups(2000),
89
+ );
90
+ ```
91
+
92
+ When the host has your template, `plan.methods` starts with a `domain-connect` method. Sending the person back to `redirect_uri` proves nothing. Check the DNS yourself before you mark the domain done.
93
+
94
+ Only these providers are asked, because their `_domainconnect` value and settings were read from DNS: Cloudflare, Vercel, Simply.com and GoDaddy. A name in a customer's DNS is never fetched. More providers join the same way, one value at a time.
95
+
96
+ ## Templates
97
+
98
+ A template is a small JSON file in the public [Domain Connect Templates](https://github.com/Domain-Connect/Templates) repository. It lists the records you may ask a host to add. The presets build one that follows the repository's rules:
99
+
100
+ ```ts
101
+ import { cnameTemplate, templateFileName } from "@gusnips/one-click-dns";
102
+
103
+ const template = cnameTemplate(
104
+ {
105
+ providerId: "example.com",
106
+ providerName: "Example",
107
+ logoUrl: "https://example.com/logo.svg",
108
+ syncPubKeyDomain: "example.com",
109
+ syncRedirectDomain: "app.example.com",
110
+ },
111
+ { pointsTo: "%target%.domains.example.com" },
112
+ );
113
+
114
+ console.log(templateFileName(template)); // "example.com.custom-domain.json"
115
+ ```
116
+
117
+ There are presets for a CNAME, DMARC, SPF, DKIM, MX and a verification TXT.
118
+
119
+ A template belongs to the company that files it. It names your key domain, and the host checks every link against the key published there. So you cannot use someone else's template, and nobody can use yours. The customer's domain is not in the template: it comes with each link, so one template serves every customer.
120
+
121
+ To file a template: test it in the [online editor](https://domainconnect.paulonet.eu/dc/free/templateedit), run `dc-template-linter -merge-or-fail` on it, open a pull request, then ask each DNS host to switch it on.
122
+
123
+ ## Steps for the host
124
+
125
+ `DNS_HOSTS` holds 33 hosts: how to recognise them from their name servers, the labels on their DNS page and the traps (a proxy on by default, a trailing dot, a TXT that needs quotes). The package holds data and no sentences, so you write the steps in your own words and languages. `dnsRecordRow` spells a record the way a host wants it typed:
126
+
127
+ ```ts
128
+ import { dnsRecordRow } from "@gusnips/one-click-dns";
129
+
130
+ dnsRecordRow("route53", "acme.com", { type: "TXT", name: "acme.com", value: "v=spf1 ~all" });
131
+ // { type: "TXT", name: "", value: "\"v=spf1 ~all\"", atRoot: true, rootUnsure: false }
132
+ ```
133
+
134
+ A name server pattern is in the list only when the host documents it or it was seen in DNS. A label is quoted only when the host's own page shows it.
135
+
136
+ ## Checks
137
+
138
+ ```bash
139
+ bun run lint
140
+ bun run typecheck
141
+ bun run build
142
+ bun test src/path/to/file.test.ts
143
+ ```
@@ -0,0 +1,521 @@
1
+ import {
2
+ normalizeDnsName
3
+ } from "./chunk-P2O7INDB.js";
4
+
5
+ // src/hosts.ts
6
+ var DNS_HOST_IDS = [
7
+ "cloudflare",
8
+ "godaddy",
9
+ "namecheap",
10
+ "namecheap-hosting",
11
+ "squarespace",
12
+ "route53",
13
+ "google-cloud-dns",
14
+ "azure-dns",
15
+ "ionos",
16
+ "hostinger",
17
+ "porkbun",
18
+ "name-com",
19
+ "dynadot",
20
+ "namesilo",
21
+ "gandi",
22
+ "ovhcloud",
23
+ "digitalocean",
24
+ "hetzner",
25
+ "vercel",
26
+ "netlify",
27
+ "wix",
28
+ "wordpress-com",
29
+ "cpanel",
30
+ "bluehost",
31
+ "hostgator",
32
+ "hostgator-br",
33
+ "siteground",
34
+ "registro-br",
35
+ "locaweb",
36
+ "kinghost",
37
+ "uol-host",
38
+ "umbler",
39
+ "dnsimple"
40
+ ];
41
+ var NO_FIELDS = { name: null, value: null };
42
+ var SHORT = { kind: "shortName" };
43
+ var TRAILING_DOT = { kind: "trailingDot" };
44
+ var QUOTED_TXT = { kind: "quotedTxt" };
45
+ var CPANEL_EDITOR = {
46
+ link: null,
47
+ open: "Zone Editor",
48
+ add: "Add Record",
49
+ save: "Save Record",
50
+ cname: { name: "Name", value: "Record" },
51
+ txt: { name: "Name", value: "Record" },
52
+ root: null,
53
+ traps: []
54
+ };
55
+ var DNS_HOSTS = {
56
+ cloudflare: {
57
+ name: "Cloudflare",
58
+ // Documented: <name>.ns.cloudflare.com, and <color>.foundationdns.{com,net,org}.
59
+ ns: [/^[a-z0-9-]+\.ns\.cloudflare\.com$/, /^[a-z0-9-]+\.foundationdns\.(com|net|org)$/],
60
+ link: "https://dash.cloudflare.com/?to=/:account/:zone/dns/records",
61
+ open: "DNS Records",
62
+ add: "Add record",
63
+ save: "Save",
64
+ cname: { name: "Name", value: "Target" },
65
+ // The dashboard's label for a TXT value is not on a docs page.
66
+ txt: { name: "Name", value: null },
67
+ root: "@",
68
+ traps: [{ kind: "proxyOff", field: "Proxy status", value: "DNS only" }, SHORT]
69
+ },
70
+ godaddy: {
71
+ name: "GoDaddy",
72
+ // Observed: ns<NN>.domaincontrol.com (GoDaddy's docs show only ns1).
73
+ ns: [/^ns\d{1,2}\.domaincontrol\.com$/],
74
+ link: null,
75
+ open: "DNS",
76
+ add: "Add New Record",
77
+ save: "Save",
78
+ cname: { name: "Name", value: "Value" },
79
+ txt: { name: "Name", value: "Value" },
80
+ root: "@",
81
+ traps: [SHORT]
82
+ },
83
+ namecheap: {
84
+ name: "Namecheap",
85
+ // Documented: dns1/2.registrar-servers.com (BasicDNS), pdns1/2 (PremiumDNS),
86
+ // freedns1-5 (FreeDNS).
87
+ ns: [/^(dns|pdns|freedns)\d\.registrar-servers\.com$/],
88
+ link: null,
89
+ open: "Advanced DNS",
90
+ add: "Add New Record",
91
+ save: "Save All Changes",
92
+ cname: { name: "Host", value: "Value" },
93
+ txt: { name: "Host", value: "Value" },
94
+ root: "@",
95
+ traps: [SHORT]
96
+ },
97
+ "namecheap-hosting": {
98
+ name: "Namecheap hosting",
99
+ // Documented: dns1/2.namecheaphosting.com. The records live in cPanel, not Advanced DNS.
100
+ ns: [/^dns\d\.namecheaphosting\.com$/],
101
+ ...CPANEL_EDITOR
102
+ },
103
+ squarespace: {
104
+ name: "Squarespace",
105
+ ns: [
106
+ // Documented: Google Cloud DNS nameservers "ending in squarespacedns.com".
107
+ /^[a-z0-9-]+\.squarespacedns\.com$/,
108
+ // Observed, listed so that these names read as ambiguous (domains moved from Google
109
+ // Domains sit on Google's names; older Squarespace domains on NS1's).
110
+ /^ns-cloud-[a-z]\d\.googledomains\.com$/,
111
+ /^dns\d\.p\d{2}\.nsone\.net$/
112
+ ],
113
+ link: null,
114
+ open: "DNS Settings",
115
+ add: "Add record",
116
+ save: "Save",
117
+ cname: { name: "Name", value: "Data" },
118
+ txt: { name: "Name", value: "Text" },
119
+ root: "@",
120
+ traps: [SHORT]
121
+ },
122
+ route53: {
123
+ name: "Amazon Route 53",
124
+ // Documented: ns-<n>.awsdns-<nn>.{com,net,org,co.uk}.
125
+ ns: [/^ns-\d+\.awsdns-\d+\.(com|net|org|co\.uk)$/],
126
+ link: "https://console.aws.amazon.com/route53/v2/hostedzones",
127
+ open: "Hosted zones",
128
+ add: "Create record",
129
+ save: "Create records",
130
+ cname: { name: "Record name", value: "Value" },
131
+ txt: { name: "Record name", value: "Value" },
132
+ // "don't enter a value (for example, an @ symbol) in the Name field".
133
+ root: "",
134
+ traps: [SHORT, QUOTED_TXT]
135
+ },
136
+ "google-cloud-dns": {
137
+ name: "Google Cloud DNS",
138
+ // Documented: ns-cloud-<a-d><1-4>.googledomains.com. Squarespace domains moved from
139
+ // Google Domains use the same names, so a set of them is ambiguous.
140
+ ns: [/^ns-cloud-[a-z]\d\.googledomains\.com$/],
141
+ link: null,
142
+ open: null,
143
+ add: "Add standard",
144
+ save: "Create",
145
+ cname: { name: "DNS name", value: null },
146
+ txt: { name: "DNS name", value: null },
147
+ // "To create a resource record at the domain apex, leave the DNS name field blank."
148
+ root: "",
149
+ traps: [SHORT]
150
+ },
151
+ "azure-dns": {
152
+ name: "Azure DNS",
153
+ // Documented: ns<1-4>-<nn>.azure-dns.{com,net,org,info}.
154
+ ns: [/^ns\d-\d+\.azure-dns\.(com|net|org|info)$/],
155
+ link: null,
156
+ open: "Record sets",
157
+ add: "Add",
158
+ save: "Add",
159
+ cname: { name: "Name", value: null },
160
+ txt: { name: "Name", value: null },
161
+ root: "@",
162
+ traps: [SHORT]
163
+ },
164
+ ionos: {
165
+ name: "IONOS",
166
+ // Documented: ns1<ZZZ>.ui-dns.{de,biz,org,com}, ZZZ from 016 to 126.
167
+ ns: [/^ns1\d{3}\.ui-dns\.(de|biz|org|com)$/],
168
+ link: null,
169
+ open: "DNS",
170
+ add: "Add Record",
171
+ save: "Save",
172
+ // The two articles spell the name box differently.
173
+ cname: { name: "Hostname", value: "Point to" },
174
+ txt: { name: "Host name", value: "Value" },
175
+ root: "@",
176
+ traps: [SHORT]
177
+ },
178
+ hostinger: {
179
+ name: "Hostinger",
180
+ // No pattern: ns1/2.dns-parking.com is a search snippet, never confirmed on a page of
181
+ // theirs. The picker reaches Hostinger.
182
+ ns: [],
183
+ link: null,
184
+ open: "DNS Zone Editor",
185
+ add: null,
186
+ save: "Add Record",
187
+ cname: { name: "Name", value: "Points to" },
188
+ txt: { name: "Name", value: "TXT value" },
189
+ root: "@",
190
+ traps: [SHORT, { kind: "oneSpf" }]
191
+ },
192
+ porkbun: {
193
+ name: "Porkbun",
194
+ // Documented: <city>.ns.porkbun.com.
195
+ ns: [/^[a-z]+\.ns\.porkbun\.com$/],
196
+ link: null,
197
+ open: "DNS",
198
+ add: "Add Record",
199
+ save: "Add",
200
+ cname: { name: "Host", value: "Answer" },
201
+ txt: { name: "Host", value: "Answer" },
202
+ // "If you're adding DNS records to your root domain, you will leave this field blank."
203
+ root: "",
204
+ traps: [SHORT]
205
+ },
206
+ "name-com": {
207
+ name: "Name.com",
208
+ // Observed: ns1<abc>.name.com to ns4<abc>.name.com.
209
+ ns: [/^ns[1-4][a-z]{3}\.name\.com$/],
210
+ link: null,
211
+ open: "Manage DNS Records",
212
+ add: null,
213
+ save: "Add Record",
214
+ cname: { name: "Host", value: "Answer" },
215
+ txt: { name: "Host", value: "Answer" },
216
+ root: null,
217
+ traps: [SHORT]
218
+ },
219
+ dynadot: {
220
+ name: "Dynadot",
221
+ // A Dynadot staff answer on its forum: ns1/2.dyna-ns.net.
222
+ ns: [/^ns\d\.dyna-ns\.net$/],
223
+ link: null,
224
+ open: "DNS Settings",
225
+ add: null,
226
+ save: "Save Settings",
227
+ cname: { name: "Subdomain Record", value: "IP Address or Target Host" },
228
+ txt: NO_FIELDS,
229
+ // The zone's own name goes in a separate "Domain Record" section, never as "@".
230
+ root: null,
231
+ traps: [SHORT]
232
+ },
233
+ namesilo: {
234
+ name: "NameSilo",
235
+ // Documented: ns1-3.dnsowl.com.
236
+ ns: [/^ns\d\.dnsowl\.com$/],
237
+ link: null,
238
+ open: null,
239
+ add: null,
240
+ save: "Submit",
241
+ cname: NO_FIELDS,
242
+ txt: NO_FIELDS,
243
+ // "You can leave the hostname blank ... If you do enter an "@", we will convert that".
244
+ root: "",
245
+ traps: []
246
+ },
247
+ gandi: {
248
+ name: "Gandi",
249
+ // Observed: ns-<n>-<a|b|c>.gandi.net.
250
+ ns: [/^ns-\d+-[abc]\.gandi\.net$/],
251
+ link: null,
252
+ open: "DNS Records",
253
+ add: "Add",
254
+ save: null,
255
+ cname: NO_FIELDS,
256
+ txt: NO_FIELDS,
257
+ root: null,
258
+ traps: [SHORT]
259
+ },
260
+ ovhcloud: {
261
+ name: "OVHcloud",
262
+ // Documented: (ns|dns)<nn or nnn>.ovh.net, and (ns|dns)200.anycast.me.
263
+ ns: [/^(ns|dns)\d{2,3}\.ovh\.net$/, /^(ns|dns)200\.anycast\.me$/],
264
+ link: null,
265
+ open: "DNS zone",
266
+ add: "Add an entry",
267
+ save: null,
268
+ cname: { name: "Subdomain", value: "Target" },
269
+ txt: { name: "Subdomain", value: "Target" },
270
+ root: null,
271
+ traps: [SHORT, TRAILING_DOT]
272
+ },
273
+ digitalocean: {
274
+ name: "DigitalOcean",
275
+ // Documented: ns1-3.digitalocean.com.
276
+ ns: [/^ns[1-3]\.digitalocean\.com$/],
277
+ link: null,
278
+ open: "Domains",
279
+ add: "Create a record",
280
+ save: "Create Record",
281
+ cname: { name: "Hostname", value: "Is an alias of" },
282
+ txt: { name: "Hostname", value: "TXT Value" },
283
+ root: "@",
284
+ traps: [SHORT, TRAILING_DOT]
285
+ },
286
+ hetzner: {
287
+ name: "Hetzner",
288
+ ns: [
289
+ // Documented: the primary, secondary (still used by older zones) and konsoleH names.
290
+ /^(hydrogen|oxygen|helium)\.ns\.hetzner\.(com|de)$/,
291
+ /^[a-z0-9]+\.first-ns\.de$/,
292
+ /^[a-z0-9]+\.second-ns\.(de|com)$/,
293
+ /^ns1\.your-server\.de$/
294
+ ],
295
+ link: null,
296
+ open: "DNS",
297
+ add: null,
298
+ save: "Add",
299
+ cname: { name: "Name", value: "Alias / Value" },
300
+ txt: { name: "Name", value: null },
301
+ root: "@",
302
+ traps: [SHORT, TRAILING_DOT, QUOTED_TXT]
303
+ },
304
+ vercel: {
305
+ name: "Vercel",
306
+ // Documented: ns1-4.vercel-dns.com (the "alternates" are not named).
307
+ ns: [/^ns[1-4]\.vercel-dns\.com$/],
308
+ link: "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fdomains",
309
+ open: "Domains",
310
+ add: null,
311
+ save: "Add",
312
+ cname: { name: "Name", value: "Value" },
313
+ txt: { name: "Name", value: "Value" },
314
+ // "Leave Name blank for the root domain."
315
+ root: "",
316
+ traps: [SHORT]
317
+ },
318
+ netlify: {
319
+ name: "Netlify",
320
+ // Documented: "a format similar to dns1.p01.nsone.net". NS1's names are shared with
321
+ // older Squarespace zones, so a set of them is ambiguous.
322
+ ns: [/^dns\d\.p\d{2}\.nsone\.net$/],
323
+ link: null,
324
+ open: "DNS",
325
+ add: "Add new record",
326
+ save: "Save",
327
+ cname: NO_FIELDS,
328
+ txt: NO_FIELDS,
329
+ root: null,
330
+ traps: []
331
+ },
332
+ wix: {
333
+ name: "Wix",
334
+ // Observed: ns<n>.wixdns.net. A domain "connected to Wix via pointing" keeps its old
335
+ // name servers, so it never matches here.
336
+ ns: [/^ns\d+\.wixdns\.net$/],
337
+ link: null,
338
+ open: "Manage DNS Records",
339
+ add: "Add Record",
340
+ save: "Save",
341
+ cname: { name: "Host Name", value: "Value" },
342
+ txt: { name: "Host Name", value: "Value" },
343
+ // "Leave this field blank if you were instructed to add an @ sign."
344
+ root: "",
345
+ traps: []
346
+ },
347
+ "wordpress-com": {
348
+ name: "WordPress.com",
349
+ // Documented: ns1-3.wordpress.com.
350
+ ns: [/^ns[1-3]\.wordpress\.com$/],
351
+ link: "https://wordpress.com/domains/",
352
+ open: "DNS records",
353
+ add: "Add record",
354
+ save: null,
355
+ cname: { name: "Name (host)", value: "Alias of (points to)" },
356
+ txt: { name: "Name", value: "Text" },
357
+ root: "@",
358
+ // The Name box takes the label or the whole name, and ignores quotes around a TXT value.
359
+ traps: []
360
+ },
361
+ cpanel: {
362
+ // Hosting companies run their own name servers, so there is no pattern: the picker
363
+ // reaches it.
364
+ name: "cPanel",
365
+ ns: [],
366
+ ...CPANEL_EDITOR
367
+ },
368
+ bluehost: {
369
+ name: "Bluehost",
370
+ // Documented: ns1/2.bluehost.com. Its help is behind a captcha and the menu path was
371
+ // read from a 2024 archive, so no label is quoted.
372
+ ns: [/^ns[12]\.bluehost\.com$/],
373
+ link: null,
374
+ open: null,
375
+ add: null,
376
+ save: null,
377
+ cname: NO_FIELDS,
378
+ txt: NO_FIELDS,
379
+ root: null,
380
+ traps: []
381
+ },
382
+ hostgator: {
383
+ name: "HostGator",
384
+ // Documented: hgns1/2.hostgator.com for new customers, ns<NNNN>.hostgator.com before
385
+ // November 2023.
386
+ ns: [/^hgns\d\.hostgator\.com$/, /^ns\d{4}\.hostgator\.com$/],
387
+ link: null,
388
+ open: "DNS",
389
+ add: "+ ADD RECORD",
390
+ save: "ADD",
391
+ cname: NO_FIELDS,
392
+ txt: NO_FIELDS,
393
+ root: null,
394
+ traps: []
395
+ },
396
+ "hostgator-br": {
397
+ name: "HostGator Brasil",
398
+ // Documented: dns3/4.hostgator.com.br for a registered domain with no plan yet. A plan's
399
+ // names come in its activation email and are not on a page.
400
+ ns: [/^dns[34]\.hostgator\.com\.br$/],
401
+ link: null,
402
+ open: "Editar Zona Avan\xE7ada de DNS",
403
+ add: "Adicionar registro",
404
+ save: "Adicionar",
405
+ // The value box's name changes with the type ("O nome é alterado conforme o Tipo").
406
+ cname: { name: "Nome", value: null },
407
+ txt: { name: "Nome", value: null },
408
+ root: null,
409
+ traps: []
410
+ },
411
+ siteground: {
412
+ name: "SiteGround",
413
+ // Documented: ns1/2.siteground.net.
414
+ ns: [/^ns[12]\.siteground\.net$/],
415
+ link: null,
416
+ open: "DNS Zone Editor",
417
+ add: null,
418
+ save: "Create",
419
+ cname: { name: "Name", value: null },
420
+ txt: { name: "Name", value: null },
421
+ root: null,
422
+ traps: [SHORT]
423
+ },
424
+ "registro-br": {
425
+ name: "Registro.br",
426
+ // Observed: a.auto.dns.br and b.auto.dns.br.
427
+ ns: [/^[ab]\.auto\.dns\.br$/],
428
+ link: null,
429
+ open: "CONFIGURAR ZONA DNS",
430
+ add: null,
431
+ save: null,
432
+ cname: NO_FIELDS,
433
+ txt: NO_FIELDS,
434
+ root: null,
435
+ traps: [{ kind: "advancedMode", mode: "Modo Avan\xE7ado" }]
436
+ },
437
+ locaweb: {
438
+ name: "Locaweb",
439
+ // Documented: ns1-3.locaweb.com.br.
440
+ ns: [/^ns[1-3]\.locaweb\.com\.br$/],
441
+ link: null,
442
+ open: "Editar zona DNS",
443
+ add: "Adicionar entrada",
444
+ save: null,
445
+ cname: { name: "Entrada", value: "Conte\xFAdo" },
446
+ txt: { name: "Entrada", value: "Conte\xFAdo" },
447
+ // "o campo Entrada é preenchido por padrão com apenas um ponto (.)".
448
+ root: ".",
449
+ traps: []
450
+ },
451
+ kinghost: {
452
+ name: "KingHost",
453
+ // Documented: dns1-6.kinghost.com.br.
454
+ ns: [/^dns[1-6]\.kinghost\.com\.br$/],
455
+ link: null,
456
+ open: "Gerenciar DNS",
457
+ add: "+ ADICIONAR DNS",
458
+ save: "ADICIONAR DNS",
459
+ // The form is a screenshot, so the case of "host" and "destino" is unconfirmed.
460
+ cname: NO_FIELDS,
461
+ txt: NO_FIELDS,
462
+ root: "@",
463
+ traps: [{ kind: "secondSave", button: "SALVAR ALTERA\xC7\xD5ES" }]
464
+ },
465
+ "uol-host": {
466
+ name: "UOL Host",
467
+ // Documented: ns1-3.dominios.uol.com.br.
468
+ ns: [/^ns[1-3]\.dominios\.uol\.com\.br$/],
469
+ link: null,
470
+ open: "Administrar DNS",
471
+ add: "Criar nova entrada",
472
+ save: "Salvar entrada",
473
+ cname: { name: "Entrada", value: "Destino" },
474
+ txt: { name: "Entrada", value: "Valor" },
475
+ root: "",
476
+ traps: []
477
+ },
478
+ umbler: {
479
+ name: "Umbler",
480
+ // Documented: ns<NNN>.umbler.{com,in,com.br,co.uk}.
481
+ ns: [/^ns\d{3}\.umbler\.(com|in|com\.br|co\.uk)$/],
482
+ link: null,
483
+ open: "Editor de DNS",
484
+ add: "Adicionar",
485
+ save: "Salvar",
486
+ cname: { name: "Nome", value: "Endere\xE7o" },
487
+ txt: { name: "Nome", value: "Dados" },
488
+ root: "@",
489
+ traps: [SHORT]
490
+ },
491
+ dnsimple: {
492
+ name: "DNSimple",
493
+ // Documented: ns1-4.dnsimple-edge.{com,net,io,org}, and the legacy ns1-4.dnsimple.com.
494
+ ns: [/^ns[1-4]\.dnsimple-edge\.(com|net|io|org)$/, /^ns[1-4]\.dnsimple\.com$/],
495
+ link: null,
496
+ open: "Record editor",
497
+ add: "Add record",
498
+ save: "Add Record",
499
+ cname: { name: "Name", value: "Content" },
500
+ txt: { name: "Name", value: "Content" },
501
+ root: "",
502
+ traps: [SHORT]
503
+ }
504
+ };
505
+ function trapOf(host, kind) {
506
+ return host.traps.find((trap) => trap.kind === kind);
507
+ }
508
+ function detectDnsHosts(nameservers) {
509
+ const names = nameservers.map(normalizeDnsName).filter(Boolean);
510
+ if (!names.length) return [];
511
+ return DNS_HOST_IDS.filter(
512
+ (id) => names.every((name) => DNS_HOSTS[id].ns.some((pattern) => pattern.test(name)))
513
+ );
514
+ }
515
+
516
+ export {
517
+ DNS_HOST_IDS,
518
+ DNS_HOSTS,
519
+ trapOf,
520
+ detectDnsHosts
521
+ };