tldts-core 7.1.2 → 7.2.1

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/src/factory.ts CHANGED
@@ -8,6 +8,7 @@ import getDomain from './domain';
8
8
  import getDomainWithoutSuffix from './domain-without-suffix';
9
9
  import extractHostname from './extract-hostname';
10
10
  import isIp from './is-ip';
11
+ import isSpecialUse from './is-special-use';
11
12
  import isValidHostname from './is-valid';
12
13
  import { IPublicSuffix, ISuffixLookupOptions } from './lookup/interface';
13
14
  import { IOptions, setDefaults } from './options';
@@ -15,9 +16,10 @@ import getSubdomain from './subdomain';
15
16
 
16
17
  export interface IResult {
17
18
  // `hostname` is either a registered name (including but not limited to a
18
- // hostname), or an IP address. IPv4 addresses must be in dot-decimal
19
- // notation, and IPv6 addresses must be enclosed in brackets ([]). This is
20
- // directly extracted from the input URL.
19
+ // hostname), or an IP address, directly extracted from the input URL. IPv4
20
+ // addresses are in dot-decimal notation. IPv6 is returned without its
21
+ // surrounding brackets; both bracketed (in URLs, e.g. `http://[::1]/`) and
22
+ // bare unbracketed (e.g. `2a01:e35::1`) IPv6 literals are accepted.
21
23
  hostname: string | null;
22
24
 
23
25
  // Is `hostname` an IP? (IPv4 or IPv6)
@@ -32,6 +34,13 @@ export interface IResult {
32
34
  // Specifies if `publicSuffix` comes from the ICANN or PRIVATE section of the list
33
35
  isIcann: boolean | null;
34
36
  isPrivate: boolean | null;
37
+
38
+ // Is `hostname` a special-use domain from the IANA registry (RFC 6761 et al.:
39
+ // e.g. `localhost`, `*.test`, `*.local`, `*.onion`, `home.arpa`)? `isIcann`/
40
+ // `isPrivate` do not identify these (most are not in the Public Suffix List;
41
+ // the few that are appear as ordinary ICANN suffixes). `null` unless the
42
+ // `detectSpecialUse` option is enabled (see is-special-use.ts).
43
+ isSpecialUse: boolean | null;
35
44
  }
36
45
 
37
46
  export function getEmptyResult(): IResult {
@@ -42,6 +51,7 @@ export function getEmptyResult(): IResult {
42
51
  isIcann: null,
43
52
  isIp: null,
44
53
  isPrivate: null,
54
+ isSpecialUse: null,
45
55
  publicSuffix: null,
46
56
  subdomain: null,
47
57
  };
@@ -54,6 +64,7 @@ export function resetResult(result: IResult): void {
54
64
  result.isIcann = null;
55
65
  result.isIp = null;
56
66
  result.isPrivate = null;
67
+ result.isSpecialUse = null;
57
68
  result.publicSuffix = null;
58
69
  result.subdomain = null;
59
70
  }
@@ -143,6 +154,14 @@ export function parseImpl(
143
154
  return result;
144
155
  }
145
156
 
157
+ // Flag special-use domains, only when opted in (`detectSpecialUse`) and only
158
+ // for the full `parse()` result (FLAG.ALL). Computed here, before the
159
+ // public-suffix/domain early-returns below, so single-label names like
160
+ // `localhost` (which have no registrable domain) are still flagged.
161
+ if (step === FLAG.ALL && options.detectSpecialUse) {
162
+ result.isSpecialUse = isSpecialUse(result.hostname);
163
+ }
164
+
146
165
  // Extract public suffix
147
166
  suffixLookup(result.hostname, options, result);
148
167
  if (step === FLAG.PUBLIC_SUFFIX || result.publicSuffix === null) {
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Special-use domain names from the IANA "Special-Use Domain Names" registry:
3
+ * the authoritative list, created by RFC 6761 and maintained as new RFCs add to
4
+ * it: https://www.iana.org/assignments/special-use-domain-names/
5
+ * Snapshot: 2026-05-24. (RFC 6761 is not obsoleted; draft-hoffman-rfc6761bis
6
+ * proposes to retire its prose but keep this registry, so the registry is the
7
+ * source of truth; re-sync this list against it.)
8
+ *
9
+ * These names never correspond to a public registration, yet neither
10
+ * `isIcann` nor `isPrivate` marks one as special-use: most are absent from the
11
+ * Public Suffix List (so `a.test` looks like a registrable domain), and the
12
+ * few that are listed (`onion`, `home.arpa`) appear there as ordinary ICANN
13
+ * suffixes. `isSpecialUse` is the single signal that covers them all.
14
+ *
15
+ * Per the registry and RFC 6761 ("and any names falling within these domains"),
16
+ * the designation covers each listed name AND all of its sub-domains. DNS labels
17
+ * are case-insensitive (RFC 4343); `hostname` is expected to be already
18
+ * lower-cased and trailing-dot-stripped, as produced by `extractHostname`, the
19
+ * same normalization the Public-Suffix-List lookup relies on.
20
+ *
21
+ * Two groups of registry entries are intentionally excluded: the numeric
22
+ * reverse-DNS delegation zones (`10.in-addr.arpa`, the `*.ip6.arpa` ranges, …),
23
+ * which are reverse-DNS PTR zones rather than hostnames and whose parents
24
+ * (`in-addr.arpa`/`ip6.arpa`) are already in the Public Suffix List; and the
25
+ * deprecated `eap-noob.arpa` entry.
26
+ */
27
+ const SPECIAL_USE_DOMAINS: readonly string[] = [
28
+ 'test', // RFC 6761
29
+ 'localhost', // RFC 6761
30
+ 'invalid', // RFC 6761
31
+ 'example', // RFC 6761
32
+ 'example.com', // RFC 6761
33
+ 'example.net', // RFC 6761
34
+ 'example.org', // RFC 6761
35
+ 'local', // RFC 6762 (mDNS)
36
+ 'onion', // RFC 7686 (Tor)
37
+ 'alt', // RFC 9476
38
+ 'home.arpa', // RFC 8375
39
+ 'ipv4only.arpa', // RFC 8880
40
+ 'resolver.arpa', // RFC 9462
41
+ 'service.arpa', // RFC 9665
42
+ '6tisch.arpa', // RFC 9031
43
+ 'eap.arpa', // RFC 9965
44
+ ];
45
+
46
+ /**
47
+ * Return `true` if `hostname` is, or is a sub-domain of, a special-use domain
48
+ * (see the registry note above). Expects an already-normalized `hostname`.
49
+ */
50
+ export default function isSpecialUse(hostname: string): boolean {
51
+ for (const name of SPECIAL_USE_DOMAINS) {
52
+ // Match on a label boundary: `hostname` is either exactly `name` or ends
53
+ // with `.name` (so `latest` is not matched by `test`, nor `myexample.com`
54
+ // by `example.com`).
55
+ if (
56
+ hostname.endsWith(name) &&
57
+ (hostname.length === name.length ||
58
+ hostname.charCodeAt(hostname.length - name.length - 1) === 46) /* '.' */
59
+ ) {
60
+ return true;
61
+ }
62
+ }
63
+
64
+ return false;
65
+ }
package/src/options.ts CHANGED
@@ -2,6 +2,10 @@ export interface IOptions {
2
2
  allowIcannDomains: boolean;
3
3
  allowPrivateDomains: boolean;
4
4
  detectIp: boolean;
5
+ // Detect RFC 6761 / IANA special-use domains and expose the result as
6
+ // `isSpecialUse` on `parse()`. Off by default so the common path stays
7
+ // allocation-free with no extra work; enable it to populate the field.
8
+ detectSpecialUse: boolean;
5
9
  extractHostname: boolean;
6
10
  mixedInputs: boolean;
7
11
  validHosts: string[] | null;
@@ -12,6 +16,7 @@ function setDefaultsImpl({
12
16
  allowIcannDomains = true,
13
17
  allowPrivateDomains = false,
14
18
  detectIp = true,
19
+ detectSpecialUse = false,
15
20
  extractHostname = true,
16
21
  mixedInputs = true,
17
22
  validHosts = null,
@@ -21,6 +26,7 @@ function setDefaultsImpl({
21
26
  allowIcannDomains,
22
27
  allowPrivateDomains,
23
28
  detectIp,
29
+ detectSpecialUse,
24
30
  extractHostname,
25
31
  mixedInputs,
26
32
  validHosts,