@aoxborrow/registrar-client 0.9.0 → 0.9.2

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/README.md CHANGED
@@ -425,6 +425,35 @@ sandbox (Name.com offers no domain deletion/reset there); it is excluded from th
425
425
  default offline suite. Transfer completion and production behavior need separate
426
426
  verification; sandbox transfers are not deterministic.
427
427
 
428
+ ## DNS record syntax
429
+
430
+ Pure functions for checking DNS record syntax and putting records in one
431
+ canonical form, with no network calls, so a UI can validate what someone types
432
+ before it reaches a registrar.
433
+
434
+ ```ts
435
+ import { findCnameConflicts, prepareEntry, toDnsEntry } from '@aoxborrow/registrar-client';
436
+
437
+ // throws DnsSyntaxError with a message safe to show a person
438
+ prepareEntry(toDnsEntry({ type: 'MX', name: '@', value: 'Mail.Example.net.', priority: 10 }));
439
+ // → { owner: '@', kind: 'MX', ttl: 3600, rdata: '10 mail.example.net' }
440
+
441
+ findCnameConflicts(records.map(r => toDnsEntry(r))); // [{ owner, kind, detail }]
442
+ ```
443
+
444
+ - Names are folded to lowercase with one trailing dot removed, and are never
445
+ joined to a zone: `www` and `www.example.com` are different owners, and `@`
446
+ stays `@`. Underscore labels (`_dmarc`) and one leftmost `*` are allowed.
447
+ - Data syntax is checked for A, AAAA, CNAME, NS, MX (including null MX), TXT,
448
+ SRV, CAA and DS. Any other type is trimmed and passed through, so a
449
+ provider-specific type still round-trips.
450
+ - `findCnameConflicts` and `findRepeatedRecords` check a whole set of records.
451
+ - `toDnsEntry` adapts a `DnsRecord` (separate MX/SRV number fields) to the
452
+ entry shape the checks take.
453
+
454
+ These are DNS rules only. Each provider still applies its own limits, such as
455
+ a minimum TTL or the record types it accepts.
456
+
428
457
  ## Sandbox environments
429
458
 
430
459
  Providers that offer a test environment accept `{ environment: 'sandbox' }` at
package/dist/index.cjs CHANGED
@@ -2238,20 +2238,32 @@ function fromRecord(raw) {
2238
2238
  if (raw.type === "MX") {
2239
2239
  const match = /^(\d+)\s+(.+)$/.exec(raw.value);
2240
2240
  if (!match) throw invalid("MX record");
2241
- return { ...common, value: match[2], priority: Number(match[1]) };
2241
+ return { ...common, value: canonicalValue("MX", match[2]), priority: Number(match[1]) };
2242
2242
  }
2243
2243
  if (raw.type === "SRV") {
2244
2244
  const match = /^(\d+)\s+(\d+)\s+(\d+)\s+(.+)$/.exec(raw.value);
2245
2245
  if (!match) throw invalid("SRV record");
2246
2246
  return {
2247
2247
  ...common,
2248
- value: match[4],
2248
+ value: canonicalValue("SRV", match[4]),
2249
2249
  priority: Number(match[1]),
2250
2250
  weight: Number(match[2]),
2251
2251
  port: Number(match[3])
2252
2252
  };
2253
2253
  }
2254
- return { ...common, value: raw.value };
2254
+ return { ...common, value: canonicalValue(raw.type, raw.value) };
2255
+ }
2256
+ function canonicalValue(type, value) {
2257
+ const kind = type.toUpperCase();
2258
+ if (kind === "TXT") return unquoteTxt(value);
2259
+ if (["CNAME", "NS", "MX", "SRV"].includes(kind))
2260
+ return value === "." ? "." : value.replace(/\.$/, "");
2261
+ return value;
2262
+ }
2263
+ function unquoteTxt(value) {
2264
+ const chunk = /"((?:[^"\\]|\\.)*)"/g;
2265
+ if (!/^\s*"(?:[^"\\]|\\.)*"(?:\s+"(?:[^"\\]|\\.)*")*\s*$/.test(value)) return value;
2266
+ return [...value.matchAll(chunk)].map((m) => m[1].replace(/\\(.)/g, "$1")).join("");
2255
2267
  }
2256
2268
  function recordSpec(record, domain, strict = true) {
2257
2269
  const type = record.type.toUpperCase();
@@ -2272,7 +2284,7 @@ function recordSpec(record, domain, strict = true) {
2272
2284
  if (!Number.isInteger(record.priority) || record.priority < 0 || record.priority > 65535)
2273
2285
  reject("MX/SRV priority is required");
2274
2286
  result.priority = record.priority;
2275
- result.target = record.value;
2287
+ result.target = canonicalValue(type, record.value);
2276
2288
  if (type === "SRV") {
2277
2289
  if (![record.weight, record.port].every((n) => Number.isInteger(n) && n >= 0 && n <= 65535))
2278
2290
  reject("SRV weight and port are required");
@@ -2289,7 +2301,7 @@ function recordSpec(record, domain, strict = true) {
2289
2301
  result.tag = match[2];
2290
2302
  result.value = match[3];
2291
2303
  }
2292
- } else result.value = record.value;
2304
+ } else result.value = canonicalValue(type, record.value);
2293
2305
  return result;
2294
2306
  }
2295
2307
  function recordSignature(record) {
@@ -4627,6 +4639,80 @@ function dnsFieldsDiffer(a, b) {
4627
4639
 
4628
4640
  // src/registrars/namebright.ts
4629
4641
  var TOKEN_URL = "https://api.namebright.com/auth/token";
4642
+ var NB_KEYS = new Map(
4643
+ [
4644
+ // domains
4645
+ "DomainName",
4646
+ "Status",
4647
+ "ExpirationDate",
4648
+ "RegistrationDate",
4649
+ "AutoRenew",
4650
+ "Locked",
4651
+ "WhoIsPrivacy",
4652
+ "AuthCode",
4653
+ "Category",
4654
+ "UpgradedDomain",
4655
+ "ResultsTotal",
4656
+ "CurrentPage",
4657
+ "Domains",
4658
+ "NameServers",
4659
+ // contacts
4660
+ "RegistrantContact",
4661
+ "AdministrativeContact",
4662
+ "TechnicalContact",
4663
+ "FirstName",
4664
+ "LastName",
4665
+ "Organization",
4666
+ "Department",
4667
+ "Email",
4668
+ "Address1",
4669
+ "Address2",
4670
+ "City",
4671
+ "Region",
4672
+ "Country",
4673
+ "PostalCode",
4674
+ "PhoneCountry",
4675
+ "Phone",
4676
+ "FaxCountry",
4677
+ "Fax",
4678
+ // host records
4679
+ "ARecords",
4680
+ "AAAARecords",
4681
+ "CNAMERecords",
4682
+ "MXRecords",
4683
+ "TXTRecords",
4684
+ "SRVRecords",
4685
+ "RecordId",
4686
+ "Subdomain",
4687
+ "IPV4Address",
4688
+ "IPV6Address",
4689
+ "RedirectDomain",
4690
+ "MailServer",
4691
+ "Priority",
4692
+ "TextRecord",
4693
+ "Service",
4694
+ "Protocol",
4695
+ "Weight",
4696
+ "Port",
4697
+ "Target",
4698
+ // availability + orders
4699
+ "ProductTypeName",
4700
+ "UnitPrice",
4701
+ "Promotion",
4702
+ "PromotionPrice",
4703
+ "Discount",
4704
+ "Description",
4705
+ "OrderId",
4706
+ "TotalPrice"
4707
+ ].map((k) => [k.toLowerCase(), k])
4708
+ );
4709
+ function canonicalKeys(value) {
4710
+ if (Array.isArray(value)) return value.map(canonicalKeys);
4711
+ if (value === null || typeof value !== "object") return value;
4712
+ return Object.fromEntries(
4713
+ Object.entries(value).map(([k, v]) => [NB_KEYS.get(k.toLowerCase()) ?? k, canonicalKeys(v)])
4714
+ );
4715
+ }
4630
4716
  var NameBrightRegistrar = class extends BaseRegistrar {
4631
4717
  name = "namebright";
4632
4718
  // a REST API: GET and HEAD never change state
@@ -4680,14 +4766,16 @@ var NameBrightRegistrar = class extends BaseRegistrar {
4680
4766
  this.tokenExpiresAt = now + ttlMs - 6e4;
4681
4767
  return this.token;
4682
4768
  }
4683
- // issue an authenticated REST request, attaching a fresh bearer token
4769
+ // issue an authenticated REST request, attaching a fresh bearer token. The
4770
+ // response's keys are normalized to PascalCase (see NB_KEYS).
4684
4771
  async authed(config, opts) {
4685
4772
  const token = await this.getToken(opts);
4686
- return this.http.request({
4773
+ const res = await this.http.request({
4687
4774
  ...config,
4688
4775
  headers: { Authorization: `Bearer ${token}` },
4689
4776
  ...opts
4690
4777
  });
4778
+ return canonicalKeys(res);
4691
4779
  }
4692
4780
  async testConnection(opts) {
4693
4781
  try {
@@ -4708,8 +4796,8 @@ var NameBrightRegistrar = class extends BaseRegistrar {
4708
4796
  { path: "account/domains", query: { page, domainsPerPage: perPage } },
4709
4797
  reqOpts
4710
4798
  );
4711
- const list = Array.isArray(data) ? data : data?.Domains ?? data?.domains ?? [];
4712
- for (const d of list) domains.push(this.toDomain(d));
4799
+ const list = Array.isArray(data) ? data : data?.Domains ?? [];
4800
+ for (const d of list) if (d.DomainName) domains.push(this.toDomain(d));
4713
4801
  hasMore = list.length === perPage;
4714
4802
  page++;
4715
4803
  }
@@ -4746,7 +4834,7 @@ var NameBrightRegistrar = class extends BaseRegistrar {
4746
4834
  opts
4747
4835
  );
4748
4836
  if (Array.isArray(res)) return res.map(String);
4749
- return (res.NameServers ?? res.Nameservers ?? res.nameservers ?? []).map(String);
4837
+ return (res.NameServers ?? []).map(String);
4750
4838
  }
4751
4839
  /**
4752
4840
  * Read the registrant/admin/tech contacts via GET
@@ -4883,10 +4971,10 @@ var NameBrightRegistrar = class extends BaseRegistrar {
4883
4971
  async setDomainFlag(domainName, change, successMessage, opts) {
4884
4972
  try {
4885
4973
  const path = `account/domains/${encodeURIComponent(domainName)}`;
4886
- const current = await this.authed(
4887
- { path },
4888
- opts
4889
- );
4974
+ const current = await this.authed({ path }, opts);
4975
+ if (!current.DomainName) {
4976
+ return { success: false, message: `NameBright: could not read ${domainName} to update it` };
4977
+ }
4890
4978
  const body = {
4891
4979
  DomainName: current.DomainName,
4892
4980
  Status: current.Status,
@@ -4963,7 +5051,7 @@ var NameBrightRegistrar = class extends BaseRegistrar {
4963
5051
  // normalized Domain shape. Neither endpoint returns nameservers.
4964
5052
  toDomain(d) {
4965
5053
  return createDomain({
4966
- domainName: d.DomainName ?? d.domain,
5054
+ domainName: d.DomainName,
4967
5055
  registrar: this.name,
4968
5056
  status: d.Status ?? "ok",
4969
5057
  createdDate: d.RegistrationDate,
@@ -8006,6 +8094,330 @@ function createRegistrar(name, credentials, options) {
8006
8094
  return new RegistrarClass(credentials, options);
8007
8095
  }
8008
8096
 
8097
+ // src/dns-records.ts
8098
+ var DnsSyntaxError = class extends Error {
8099
+ constructor(message) {
8100
+ super(message);
8101
+ this.name = "DnsSyntaxError";
8102
+ }
8103
+ };
8104
+ var NAME_LIMIT = 253;
8105
+ var LABEL_LIMIT = 63;
8106
+ var TTL_LIMIT = 2147483647;
8107
+ var TEXT_OCTETS = 255;
8108
+ var TEXT_TOTAL_OCTETS = 65535 - Math.ceil(65535 / (TEXT_OCTETS + 1));
8109
+ var UINT16_MAX = 65535;
8110
+ var DIGEST_HEX_LENGTH = {
8111
+ 1: 40,
8112
+ 2: 64,
8113
+ 3: 64,
8114
+ 4: 96
8115
+ };
8116
+ var KINDS_ALLOWED_BESIDE_CNAME = /* @__PURE__ */ new Set(["CNAME", "RRSIG", "NSEC", "NSEC3"]);
8117
+ function mapAsciiLabel(label) {
8118
+ const folded = label.toLowerCase();
8119
+ if (folded.length < 1 || folded.length > LABEL_LIMIT) return null;
8120
+ if (!/^[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?$/.test(folded)) return null;
8121
+ return folded;
8122
+ }
8123
+ var labelMapper = mapAsciiLabel;
8124
+ function useLabelMapper(mapper) {
8125
+ labelMapper = mapper;
8126
+ }
8127
+ function resetLabelMapper() {
8128
+ labelMapper = mapAsciiLabel;
8129
+ }
8130
+ function prepareName(input, role = "owner") {
8131
+ const trimmed = input.trim();
8132
+ if (!trimmed) throw new DnsSyntaxError("Name is empty");
8133
+ const bare = trimmed.endsWith(".") ? trimmed.slice(0, -1) : trimmed;
8134
+ if (!bare) throw new DnsSyntaxError("Name is empty");
8135
+ if (bare === "@") {
8136
+ if (role === "target") throw new DnsSyntaxError("Origin token @ cannot be a record target");
8137
+ return "@";
8138
+ }
8139
+ const labels = bare.split(".");
8140
+ if (labels.some((label) => label.length === 0)) {
8141
+ throw new DnsSyntaxError("Name contains an empty label");
8142
+ }
8143
+ const starLabels = labels.filter((label) => label.includes("*"));
8144
+ if (starLabels.length > 0) {
8145
+ if (role !== "owner") {
8146
+ throw new DnsSyntaxError("A wildcard is only valid as an owner name");
8147
+ }
8148
+ const starCount = starLabels.reduce(
8149
+ (count, label) => count + (label.match(/\*/g)?.length ?? 0),
8150
+ 0
8151
+ );
8152
+ if (starCount > 1) throw new DnsSyntaxError("A name can contain only one wildcard");
8153
+ if (labels[0] !== "*") throw new DnsSyntaxError("A wildcard must be its own leftmost label");
8154
+ }
8155
+ const mapped = labels.map((label) => {
8156
+ if (label === "*") return "*";
8157
+ const ascii = labelMapper(label);
8158
+ if (!ascii) throw new DnsSyntaxError(`Label is not a valid DNS label: ${label}`);
8159
+ return ascii.toLowerCase();
8160
+ });
8161
+ const name = mapped.join(".");
8162
+ if (name.length > NAME_LIMIT) throw new DnsSyntaxError("Name is longer than 253 characters");
8163
+ return name;
8164
+ }
8165
+ function isIpv4(text2) {
8166
+ const octets = text2.split(".");
8167
+ if (octets.length !== 4) return false;
8168
+ return octets.every((octet) => {
8169
+ if (!/^\d{1,3}$/.test(octet)) return false;
8170
+ const value = Number(octet);
8171
+ return value >= 0 && value <= 255 && String(value) === octet;
8172
+ });
8173
+ }
8174
+ function isIpv6(text2) {
8175
+ let body = text2;
8176
+ const embedded = text2.match(/^(.+):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
8177
+ if (embedded) {
8178
+ if (!isIpv4(embedded[2])) return false;
8179
+ body = `${embedded[1]}:0:0`;
8180
+ }
8181
+ const compressedAt = body.indexOf("::");
8182
+ if (compressedAt !== -1) {
8183
+ if (body.indexOf("::", compressedAt + 1) !== -1) return false;
8184
+ const groupsOf = (side) => side === "" ? [] : side.split(":");
8185
+ const head = groupsOf(body.slice(0, compressedAt));
8186
+ const tail = groupsOf(body.slice(compressedAt + 2));
8187
+ if (head.length + tail.length > 7) return false;
8188
+ return [...head, ...tail].every((group) => /^[0-9a-fA-F]{1,4}$/.test(group));
8189
+ }
8190
+ const groups = body.split(":");
8191
+ return groups.length === 8 && groups.every((group) => /^[0-9a-fA-F]{1,4}$/.test(group));
8192
+ }
8193
+ function utf8OctetCount(text2) {
8194
+ let count = 0;
8195
+ for (const char of text2) {
8196
+ const code = char.codePointAt(0) ?? 0;
8197
+ if (code <= 127) count += 1;
8198
+ else if (code <= 2047) count += 2;
8199
+ else if (code <= 65535) count += 3;
8200
+ else count += 4;
8201
+ }
8202
+ return count;
8203
+ }
8204
+ function decimalInRange(token, limit) {
8205
+ if (!/^(0|[1-9][0-9]*)$/.test(token)) return null;
8206
+ const value = Number(token);
8207
+ if (value > limit) return null;
8208
+ return value;
8209
+ }
8210
+ function prepareIpv4(text2) {
8211
+ if (!isIpv4(text2)) throw new DnsSyntaxError("IPv4 address is invalid");
8212
+ return text2;
8213
+ }
8214
+ function prepareIpv6(text2) {
8215
+ if (!isIpv6(text2)) throw new DnsSyntaxError("IPv6 address is invalid");
8216
+ return text2.toLowerCase();
8217
+ }
8218
+ function prepareHostTarget(text2, owner, rejectSelf) {
8219
+ const withoutDot = text2.endsWith(".") ? text2.slice(0, -1) : text2;
8220
+ if (isIpv4(withoutDot) || isIpv6(withoutDot)) {
8221
+ throw new DnsSyntaxError("Target must be a domain name, not an address");
8222
+ }
8223
+ const target = prepareName(text2, "target");
8224
+ if (rejectSelf && owner !== void 0 && prepareName(owner, "owner") === target) {
8225
+ throw new DnsSyntaxError("CNAME target is the same name as its owner");
8226
+ }
8227
+ return target;
8228
+ }
8229
+ function prepareMailExchange(text2) {
8230
+ const parts = text2.split(/\s+/);
8231
+ if (parts.length !== 2) {
8232
+ throw new DnsSyntaxError('MX data must be "<priority> <mail host>"');
8233
+ }
8234
+ const priority = decimalInRange(parts[0], UINT16_MAX);
8235
+ if (priority === null) throw new DnsSyntaxError("MX priority must be an integer from 0 to 65535");
8236
+ return `${priority} ${prepareServiceHost(parts[1])}`;
8237
+ }
8238
+ function prepareServiceHost(text2) {
8239
+ return text2 === "." ? "." : prepareHostTarget(text2, void 0, false);
8240
+ }
8241
+ function prepareService(text2) {
8242
+ const parts = text2.split(/\s+/);
8243
+ if (parts.length !== 4) {
8244
+ throw new DnsSyntaxError('SRV data must be "<priority> <weight> <port> <target>"');
8245
+ }
8246
+ const [priority, weight, port] = parts.slice(0, 3).map((part) => decimalInRange(part, UINT16_MAX));
8247
+ if (priority === null)
8248
+ throw new DnsSyntaxError("SRV priority must be an integer from 0 to 65535");
8249
+ if (weight === null) throw new DnsSyntaxError("SRV weight must be an integer from 0 to 65535");
8250
+ if (port === null) throw new DnsSyntaxError("SRV port must be an integer from 0 to 65535");
8251
+ return `${priority} ${weight} ${port} ${prepareServiceHost(parts[3])}`;
8252
+ }
8253
+ function prepareAuthorization(text2) {
8254
+ const match = /^(\S+)\s+(\S+)\s+([\s\S]+)$/.exec(text2);
8255
+ if (!match) throw new DnsSyntaxError('CAA data must be "<flags> <tag> <value>"');
8256
+ const flags = decimalInRange(match[1], 255);
8257
+ if (flags === null) throw new DnsSyntaxError("CAA flags must be an integer from 0 to 255");
8258
+ if (!/^[a-z0-9]{1,15}$/i.test(match[2])) {
8259
+ throw new DnsSyntaxError("CAA tag must be 1 to 15 letters or digits");
8260
+ }
8261
+ const quoted = /^"((?:[^"\\]|\\.)*)"$/.exec(match[3]);
8262
+ const value = quoted ? quoted[1] : match[3];
8263
+ if (!quoted && value.includes('"')) {
8264
+ throw new DnsSyntaxError("CAA value must be one quoted or unquoted string");
8265
+ }
8266
+ return `${flags} ${match[2].toLowerCase()} "${value}"`;
8267
+ }
8268
+ function prepareText(text2) {
8269
+ if (utf8OctetCount(text2) > TEXT_TOTAL_OCTETS) {
8270
+ throw new DnsSyntaxError(`TXT data is longer than ${TEXT_TOTAL_OCTETS} octets`);
8271
+ }
8272
+ return text2;
8273
+ }
8274
+ function readSignerFields(text2) {
8275
+ const parts = text2.trim().split(/\s+/);
8276
+ if (parts.length !== 4)
8277
+ throw new DnsSyntaxError('DS data must be "<key tag> <algorithm> <digest type> <digest>"');
8278
+ const keyTag = decimalInRange(parts[0], UINT16_MAX);
8279
+ const algorithm = decimalInRange(parts[1], 255);
8280
+ const digestType = decimalInRange(parts[2], 255);
8281
+ if (keyTag === null || algorithm === null || digestType === null || !parts[3]) {
8282
+ throw new DnsSyntaxError('DS data must be "<key tag> <algorithm> <digest type> <digest>"');
8283
+ }
8284
+ const digest = parts[3].toLowerCase();
8285
+ if (!/^[0-9a-f]+$/.test(digest)) throw new DnsSyntaxError("DS digest must be hexadecimal");
8286
+ const expectedLength = DIGEST_HEX_LENGTH[digestType];
8287
+ if (expectedLength !== void 0 && digest.length !== expectedLength) {
8288
+ throw new DnsSyntaxError(
8289
+ `DS digest must be ${expectedLength} hex characters for digest type ${digestType}`
8290
+ );
8291
+ }
8292
+ return { keyTag, algorithm, digestType, digest };
8293
+ }
8294
+ function readDelegationSigner(rdata) {
8295
+ return readSignerFields(rdata);
8296
+ }
8297
+ function prepareSignerText(text2) {
8298
+ const signer = readSignerFields(text2);
8299
+ return `${signer.keyTag} ${signer.algorithm} ${signer.digestType} ${signer.digest}`;
8300
+ }
8301
+ function prepareRdata(kind, rdata, owner) {
8302
+ const token = kind.trim().toUpperCase();
8303
+ const text2 = rdata.trim();
8304
+ if (!text2) throw new DnsSyntaxError("Record data is empty");
8305
+ switch (token) {
8306
+ case "A":
8307
+ return prepareIpv4(text2);
8308
+ case "AAAA":
8309
+ return prepareIpv6(text2);
8310
+ case "CNAME":
8311
+ return prepareHostTarget(text2, owner, true);
8312
+ case "NS":
8313
+ return prepareHostTarget(text2, void 0, false);
8314
+ case "MX":
8315
+ return prepareMailExchange(text2);
8316
+ case "TXT":
8317
+ return prepareText(text2);
8318
+ case "SRV":
8319
+ return prepareService(text2);
8320
+ case "CAA":
8321
+ return prepareAuthorization(text2);
8322
+ case "DS":
8323
+ return prepareSignerText(text2);
8324
+ default:
8325
+ return text2;
8326
+ }
8327
+ }
8328
+ function prepareEntry(entry) {
8329
+ if (!Number.isInteger(entry.ttl) || entry.ttl < 0 || entry.ttl > TTL_LIMIT) {
8330
+ throw new DnsSyntaxError("TTL must be an integer from 0 to 2147483647");
8331
+ }
8332
+ const kind = entry.kind.trim().toUpperCase();
8333
+ if (!kind) throw new DnsSyntaxError("Record type is empty");
8334
+ return {
8335
+ owner: prepareName(entry.owner, "owner"),
8336
+ kind,
8337
+ ttl: entry.ttl,
8338
+ rdata: prepareRdata(kind, entry.rdata, entry.owner)
8339
+ };
8340
+ }
8341
+ function syntaxClash(entry, error) {
8342
+ const detail = error instanceof Error ? error.message : "Invalid record";
8343
+ return { owner: entry.owner, kind: entry.kind, detail };
8344
+ }
8345
+ function findCnameConflicts(entries) {
8346
+ const clashes = [];
8347
+ const byOwner = /* @__PURE__ */ new Map();
8348
+ for (const entry of entries) {
8349
+ let owner;
8350
+ let kind;
8351
+ try {
8352
+ owner = prepareName(entry.owner, "owner");
8353
+ kind = entry.kind.trim().toUpperCase();
8354
+ if (!kind) throw new DnsSyntaxError("Record type is empty");
8355
+ } catch (error) {
8356
+ clashes.push(syntaxClash(entry, error));
8357
+ continue;
8358
+ }
8359
+ const group = byOwner.get(owner) ?? [];
8360
+ group.push({ ...entry, owner, kind });
8361
+ byOwner.set(owner, group);
8362
+ }
8363
+ for (const [owner, group] of byOwner) {
8364
+ const aliasCount = group.filter((entry) => entry.kind === "CNAME").length;
8365
+ if (aliasCount === 0) continue;
8366
+ if (aliasCount > 1) {
8367
+ clashes.push({
8368
+ owner,
8369
+ kind: "CNAME",
8370
+ detail: "A name can have only one CNAME target"
8371
+ });
8372
+ }
8373
+ const others = [...new Set(group.map((entry) => entry.kind))].filter(
8374
+ (kind) => !KINDS_ALLOWED_BESIDE_CNAME.has(kind)
8375
+ );
8376
+ if (others.length > 0) {
8377
+ clashes.push({
8378
+ owner,
8379
+ kind: "CNAME",
8380
+ detail: `A CNAME cannot share a name with ${others.join(", ")}`
8381
+ });
8382
+ }
8383
+ }
8384
+ return clashes;
8385
+ }
8386
+ function findRepeatedRecords(entries) {
8387
+ const clashes = [];
8388
+ const seen = /* @__PURE__ */ new Set();
8389
+ for (const entry of entries) {
8390
+ let prepared;
8391
+ try {
8392
+ prepared = prepareEntry(entry);
8393
+ } catch (error) {
8394
+ clashes.push(syntaxClash(entry, error));
8395
+ continue;
8396
+ }
8397
+ if (prepared.kind === "CNAME") continue;
8398
+ const key = `${prepared.owner}\0${prepared.kind}\0${prepared.rdata}`;
8399
+ if (seen.has(key)) {
8400
+ clashes.push({
8401
+ owner: prepared.owner,
8402
+ kind: prepared.kind,
8403
+ detail: `Repeated ${prepared.kind} data at this name`
8404
+ });
8405
+ }
8406
+ seen.add(key);
8407
+ }
8408
+ return clashes;
8409
+ }
8410
+ function toDnsEntry(record, defaultTtl = 3600) {
8411
+ const kind = record.type.trim().toUpperCase();
8412
+ const numbers = kind === "MX" ? [record.priority] : kind === "SRV" ? [record.priority, record.weight, record.port] : [];
8413
+ return {
8414
+ owner: record.name,
8415
+ kind,
8416
+ ttl: record.ttl ?? defaultTtl,
8417
+ rdata: [...numbers.map((n) => n === void 0 ? "" : String(n)), record.value.trim()].join(" ")
8418
+ };
8419
+ }
8420
+
8009
8421
  exports.ALL_FEATURES = ALL_FEATURES;
8010
8422
  exports.AbortError = AbortError;
8011
8423
  exports.AuthenticationError = AuthenticationError;
@@ -8017,6 +8429,7 @@ exports.ConfigurationError = ConfigurationError;
8017
8429
  exports.ConnectionError = ConnectionError;
8018
8430
  exports.ConsentRequiredError = ConsentRequiredError;
8019
8431
  exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
8432
+ exports.DnsSyntaxError = DnsSyntaxError;
8020
8433
  exports.Domain101Registrar = Domain101Registrar;
8021
8434
  exports.DynadotRegistrar = DynadotRegistrar;
8022
8435
  exports.EXTENDED_FEATURES = EXTENDED_FEATURES;
@@ -8050,19 +8463,30 @@ exports.createDomain = createDomain;
8050
8463
  exports.createRegistrar = createRegistrar;
8051
8464
  exports.ensureArray = ensureArray;
8052
8465
  exports.filterDomains = filterDomains;
8466
+ exports.findCnameConflicts = findCnameConflicts;
8467
+ exports.findRepeatedRecords = findRepeatedRecords;
8053
8468
  exports.isCoreFeature = isCoreFeature;
8469
+ exports.isIpv4 = isIpv4;
8470
+ exports.isIpv6 = isIpv6;
8054
8471
  exports.listPortfolio = listPortfolio;
8055
8472
  exports.markNotSent = markNotSent;
8056
8473
  exports.normalizeDomain = normalizeDomain;
8057
8474
  exports.normalizeNameservers = normalizeNameservers;
8058
8475
  exports.parseDate = parseDate;
8059
8476
  exports.parseXml = parseXml;
8477
+ exports.prepareEntry = prepareEntry;
8478
+ exports.prepareName = prepareName;
8479
+ exports.prepareRdata = prepareRdata;
8480
+ exports.readDelegationSigner = readDelegationSigner;
8060
8481
  exports.registrars = registrars;
8061
8482
  exports.requireConsent = requireConsent;
8483
+ exports.resetLabelMapper = resetLabelMapper;
8062
8484
  exports.selectBaseUrl = selectBaseUrl;
8063
8485
  exports.settableForwards = settableForwards;
8064
8486
  exports.sleep = sleep;
8487
+ exports.toDnsEntry = toDnsEntry;
8065
8488
  exports.toRegistrarError = toRegistrarError;
8489
+ exports.useLabelMapper = useLabelMapper;
8066
8490
  exports.wasNotSent = wasNotSent;
8067
8491
  //# sourceMappingURL=index.cjs.map
8068
8492
  //# sourceMappingURL=index.cjs.map