@slicedearth/whoisleuth-cli 1.32.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.
Files changed (189) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
  3. package/NOTICE +76 -0
  4. package/README.md +84 -0
  5. package/TRADEMARKS.md +41 -0
  6. package/bin/whoisleuth.mjs +35 -0
  7. package/cli/archive-inspect.mjs +198 -0
  8. package/cli/arguments.mjs +1022 -0
  9. package/cli/artifact-verify.mjs +210 -0
  10. package/cli/bulk-checkpoint.mjs +218 -0
  11. package/cli/bulk-command-runner.mjs +109 -0
  12. package/cli/bulk-output.mjs +112 -0
  13. package/cli/bulk.mjs +133 -0
  14. package/cli/compare.mjs +238 -0
  15. package/cli/completion.mjs +175 -0
  16. package/cli/csv.mjs +1 -0
  17. package/cli/discover.mjs +50 -0
  18. package/cli/discovery-command-runner.mjs +36 -0
  19. package/cli/discovery-observation-snapshot.mjs +315 -0
  20. package/cli/discovery-scan-command-runner.mjs +141 -0
  21. package/cli/discovery-scan.mjs +325 -0
  22. package/cli/discovery-snapshot.mjs +133 -0
  23. package/cli/discovery-workflow.mjs +73 -0
  24. package/cli/doctor.mjs +137 -0
  25. package/cli/errors.mjs +21 -0
  26. package/cli/evidence-command-arguments.mjs +145 -0
  27. package/cli/evidence-command-runner.mjs +106 -0
  28. package/cli/evidence-signing.mjs +215 -0
  29. package/cli/exit-codes.mjs +9 -0
  30. package/cli/export-evidence.mjs +28 -0
  31. package/cli/formatters/evidence-report.mjs +3 -0
  32. package/cli/formatters/html.mjs +69 -0
  33. package/cli/formatters/json.mjs +135 -0
  34. package/cli/formatters/markdown.mjs +3 -0
  35. package/cli/formatters/terminal.mjs +658 -0
  36. package/cli/http.mjs +27 -0
  37. package/cli/lookup-command-runner.mjs +87 -0
  38. package/cli/lookup-diff.mjs +171 -0
  39. package/cli/mail-review.mjs +204 -0
  40. package/cli/manual.mjs +34 -0
  41. package/cli/network-command-runner.mjs +88 -0
  42. package/cli/output-file.mjs +97 -0
  43. package/cli/page-compare.mjs +123 -0
  44. package/cli/posture.mjs +20 -0
  45. package/cli/progress-events.mjs +53 -0
  46. package/cli/progress.mjs +56 -0
  47. package/cli/registry-access.mjs +3 -0
  48. package/cli/registry-support.mjs +120 -0
  49. package/cli/risk-calibration.mjs +294 -0
  50. package/cli/runner-types.mjs +1 -0
  51. package/cli/runner.mjs +726 -0
  52. package/cli/saved-lookup.mjs +87 -0
  53. package/cli/source-reliability.mjs +727 -0
  54. package/cli/strict-exit.mjs +55 -0
  55. package/cli/terminal-presentation.mjs +147 -0
  56. package/docs/cli.md +827 -0
  57. package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
  58. package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
  59. package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
  60. package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
  61. package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
  62. package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
  63. package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
  64. package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
  65. package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
  66. package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
  67. package/frontend/src/lib/analysis/bulk-sort.js +49 -0
  68. package/frontend/src/lib/analysis/campaign-model.js +265 -0
  69. package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
  70. package/frontend/src/lib/analysis/case-migration-model.js +248 -0
  71. package/frontend/src/lib/analysis/case-model.js +5 -0
  72. package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
  73. package/frontend/src/lib/analysis/case-record-core.js +232 -0
  74. package/frontend/src/lib/analysis/case-record-model.js +7 -0
  75. package/frontend/src/lib/analysis/case-record-operations.js +224 -0
  76. package/frontend/src/lib/analysis/case-response-model.js +640 -0
  77. package/frontend/src/lib/analysis/case-response-packet.js +573 -0
  78. package/frontend/src/lib/analysis/case-storage-model.js +93 -0
  79. package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
  80. package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
  81. package/frontend/src/lib/analysis/http-summary.js +165 -0
  82. package/frontend/src/lib/analysis/investigation-guide.js +500 -0
  83. package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
  84. package/frontend/src/lib/analysis/page-baseline.js +272 -0
  85. package/frontend/src/lib/analysis/page-similarity.js +179 -0
  86. package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
  87. package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
  88. package/frontend/src/lib/analysis/scoring.js +135 -0
  89. package/frontend/src/lib/analysis/shortlist-model.js +209 -0
  90. package/frontend/src/lib/analysis/utils.js +265 -0
  91. package/frontend/src/lib/analysis/watchlist-history.js +643 -0
  92. package/frontend/src/lib/analysis/watchlist-store.js +127 -0
  93. package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
  94. package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
  95. package/frontend/src/lib/analysis/workspace-archive.js +472 -0
  96. package/lib/abort.mjs +26 -0
  97. package/lib/abusech-auth.mjs +19 -0
  98. package/lib/availability.mjs +716 -0
  99. package/lib/bounded-contract-normalizers.mjs +61 -0
  100. package/lib/browser-library-profile.mjs +310 -0
  101. package/lib/bulk-comparison-evidence.mjs +83 -0
  102. package/lib/classify.mjs +93 -0
  103. package/lib/client-behavior-profile.mjs +83 -0
  104. package/lib/credential-surface-profile.mjs +66 -0
  105. package/lib/csv.mjs +23 -0
  106. package/lib/ct-query.mjs +27 -0
  107. package/lib/ct-search.mjs +393 -0
  108. package/lib/distributed-operation-budget.mjs +461 -0
  109. package/lib/dns-delegation-health.mjs +344 -0
  110. package/lib/dns-intelligence.mjs +739 -0
  111. package/lib/dns-mx.mjs +24 -0
  112. package/lib/dns-resolver-selection.mjs +33 -0
  113. package/lib/domain-posture-analysis.mjs +254 -0
  114. package/lib/domain-posture-parsers.mjs +358 -0
  115. package/lib/domain-posture.mjs +745 -0
  116. package/lib/error-detail.mjs +10 -0
  117. package/lib/evidence-export.mjs +431 -0
  118. package/lib/evidence-report-markdown.mjs +71 -0
  119. package/lib/evidence-report.mjs +391 -0
  120. package/lib/external-intelligence-risk.mjs +108 -0
  121. package/lib/favicon.mjs +146 -0
  122. package/lib/feature-policy.mjs +70 -0
  123. package/lib/generated/retire-browser-catalog.mjs +8724 -0
  124. package/lib/generated/unicode-confusables-17.mjs +93 -0
  125. package/lib/hostname.mjs +38 -0
  126. package/lib/html-signals.mjs +680 -0
  127. package/lib/http-intelligence.mjs +232 -0
  128. package/lib/idn-confusable-policy.mjs +86 -0
  129. package/lib/idn-confusables.mjs +465 -0
  130. package/lib/lookup-cache.mjs +130 -0
  131. package/lib/lookup-progress.mjs +291 -0
  132. package/lib/lookup-source-progress.mjs +109 -0
  133. package/lib/lookup-threat-provider-inventory.mjs +13 -0
  134. package/lib/lookup.mjs +592 -0
  135. package/lib/observation.mjs +114 -0
  136. package/lib/observed-network-context.mjs +355 -0
  137. package/lib/operation-budget.mjs +489 -0
  138. package/lib/page-fingerprints.mjs +481 -0
  139. package/lib/page-role-profile.mjs +117 -0
  140. package/lib/perceptual-hash.mjs +451 -0
  141. package/lib/rdap-attempts.mjs +34 -0
  142. package/lib/rdap-bootstrap.mjs +173 -0
  143. package/lib/rdap-capabilities.mjs +292 -0
  144. package/lib/rdap-client.mjs +125 -0
  145. package/lib/rdap-normalization.mjs +691 -0
  146. package/lib/rdap-registrar.mjs +196 -0
  147. package/lib/rdap-transport.mjs +39 -0
  148. package/lib/rdap-types.mjs +1 -0
  149. package/lib/rdap-validation.mjs +94 -0
  150. package/lib/rdap.mjs +16 -0
  151. package/lib/registry-access.mjs +32 -0
  152. package/lib/registry-capabilities.mjs +117 -0
  153. package/lib/registry-capability-catalogue.mjs +1206 -0
  154. package/lib/registry-comparison.mjs +318 -0
  155. package/lib/registry-dates.mjs +190 -0
  156. package/lib/registry-insights.mjs +325 -0
  157. package/lib/response-policy.mjs +256 -0
  158. package/lib/risk-scoring.mjs +164 -0
  159. package/lib/safe-fetch.mjs +446 -0
  160. package/lib/security-txt.mjs +359 -0
  161. package/lib/service-binding-dns.mjs +608 -0
  162. package/lib/sslbl-certificates.generated.mjs +143 -0
  163. package/lib/sslbl-intelligence.mjs +241 -0
  164. package/lib/static-html-analysis.mjs +340 -0
  165. package/lib/structured-data-identity.mjs +323 -0
  166. package/lib/threat-intelligence-contract.mjs +5 -0
  167. package/lib/threat-intelligence-definition-registry.mjs +34 -0
  168. package/lib/threat-intelligence-fixtures.mjs +82 -0
  169. package/lib/threat-intelligence-manifests.mjs +512 -0
  170. package/lib/threat-intelligence-runtime.mjs +662 -0
  171. package/lib/threat-intelligence-targets.mjs +168 -0
  172. package/lib/threat-intelligence-types.mjs +3 -0
  173. package/lib/threatfox-intelligence.mjs +307 -0
  174. package/lib/tls-intelligence.mjs +814 -0
  175. package/lib/typosquat-generator.mjs +884 -0
  176. package/lib/urlhaus-intelligence.mjs +361 -0
  177. package/lib/urlscan-intelligence.mjs +349 -0
  178. package/lib/website-security-posture.mjs +325 -0
  179. package/lib/website-technology.mjs +437 -0
  180. package/lib/whois-authority.mjs +98 -0
  181. package/lib/whois-chain.mjs +261 -0
  182. package/lib/whois-contacts.mjs +163 -0
  183. package/lib/whois-contracts.mjs +3 -0
  184. package/lib/whois-normalization.mjs +27 -0
  185. package/lib/whois-transport.mjs +100 -0
  186. package/lib/whois-values.mjs +92 -0
  187. package/lib/whois.mjs +1187 -0
  188. package/package.json +55 -0
  189. package/third-party-notices.txt +5571 -0
@@ -0,0 +1,196 @@
1
+ import net from 'node:net';
2
+ import { cached } from "./lookup-cache.mjs";
3
+ import { rdapAttempt } from "./rdap-attempts.mjs";
4
+ import { MAX_RDAP_ENDPOINT_LENGTH } from "./rdap-bootstrap.mjs";
5
+ import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
6
+ import { canonicalRdapDomain, validateRdapResponse, } from "./rdap-validation.mjs";
7
+ const REGISTRAR_RDAP_TIMEOUT_MS = 7_000;
8
+ function recordOrNull(value) {
9
+ return value && typeof value === 'object' && !Array.isArray(value)
10
+ ? value
11
+ : null;
12
+ }
13
+ function registrarRdapError(endpoint, outcome, options = {}) {
14
+ const attempt = rdapAttempt(endpoint, outcome, options);
15
+ const detail = attempt.detail || 'The registrar RDAP request failed.';
16
+ return Object.assign(new Error(detail), {
17
+ registrarRdap: {
18
+ status: 'error',
19
+ detail,
20
+ endpoint: attempt.endpoint,
21
+ transportSecurity: attempt.transportSecurity,
22
+ upstreamStatus: attempt.status,
23
+ fetchedAt: null,
24
+ attempt,
25
+ },
26
+ });
27
+ }
28
+ function domainEndpointIdentity(raw, domain) {
29
+ try {
30
+ const url = new URL(raw);
31
+ const canonical = canonicalRdapDomain(domain);
32
+ const path = url.pathname.replace(/\/+$/, '');
33
+ const encodedDomain = path.match(/\/domain\/([^/]+)$/i)?.[1];
34
+ if (!canonical || !encodedDomain)
35
+ return null;
36
+ const pathDomain = canonicalRdapDomain(decodeURIComponent(encodedDomain));
37
+ if (pathDomain !== canonical)
38
+ return null;
39
+ return `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}/domain/${canonical}`;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ export function selectRegistrarRdapLink(domain, links, registryEndpoint = null) {
46
+ const canonical = canonicalRdapDomain(domain);
47
+ if (!canonical || !Array.isArray(links))
48
+ return null;
49
+ const registryIdentity = registryEndpoint
50
+ ? domainEndpointIdentity(registryEndpoint, canonical)
51
+ : null;
52
+ for (const linkValue of links) {
53
+ const link = linkValue;
54
+ if (!link || typeof link !== 'object' || Array.isArray(link))
55
+ continue;
56
+ if (link.rel !== 'related' || typeof link.href !== 'string')
57
+ continue;
58
+ if (link.href.length > MAX_RDAP_ENDPOINT_LENGTH ||
59
+ /[\u0000-\u001f\u007f]/.test(link.href)) {
60
+ continue;
61
+ }
62
+ let url;
63
+ try {
64
+ url = new URL(link.href);
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ if (url.protocol !== 'https:' || url.username || url.password)
70
+ continue;
71
+ if (url.port || url.search || url.hash)
72
+ continue;
73
+ const hostname = url.hostname.replace(/^\[|\]$/g, '');
74
+ if (net.isIP(hostname))
75
+ continue;
76
+ const type = typeof link.type === 'string'
77
+ ? (link.type.split(';', 1)[0] ?? '').trim().toLowerCase()
78
+ : '';
79
+ if (type && type !== 'application/rdap+json')
80
+ continue;
81
+ const identity = domainEndpointIdentity(url.href, canonical);
82
+ if (!identity || identity === registryIdentity)
83
+ continue;
84
+ return url.href;
85
+ }
86
+ return null;
87
+ }
88
+ export async function fetchRegistrarRdapRecordWithParser(domain, registryRecord, parseRdap, options = {}) {
89
+ const canonical = canonicalRdapDomain(domain);
90
+ if (!canonical)
91
+ throw new Error('A valid domain is required for registrar RDAP.');
92
+ const fetchUpstream = options.fetchUpstream || fetchRdapDetailedWithTimeout;
93
+ return cached(`rdap-registrar:domain:${canonical}`, async () => {
94
+ const registryParsed = recordOrNull(registryRecord?.parsed);
95
+ const registryEndpoint = typeof registryRecord?.rdapServer === 'string' ? registryRecord.rdapServer : null;
96
+ const endpoint = selectRegistrarRdapLink(canonical, registryParsed?.links, registryEndpoint);
97
+ if (!endpoint) {
98
+ return {
99
+ status: 'unsupported',
100
+ detail: 'The registry did not publish a registrar RDAP link for this domain.',
101
+ endpoint: null,
102
+ transportSecurity: null,
103
+ upstreamStatus: null,
104
+ fetchedAt: null,
105
+ attempt: null,
106
+ };
107
+ }
108
+ let upstream;
109
+ try {
110
+ upstream = await fetchUpstream(endpoint, { headers: { Accept: 'application/rdap+json' } }, REGISTRAR_RDAP_TIMEOUT_MS);
111
+ }
112
+ catch (cause) {
113
+ const error = cause && typeof cause === 'object' && !Array.isArray(cause)
114
+ ? cause
115
+ : {};
116
+ const detail = typeof error.message === 'string' ? error.message : 'request failed';
117
+ const outcome = error.name === 'AbortError' || /timed? out|time limit/i.test(detail)
118
+ ? 'timeout'
119
+ : /exceeded \d+ bytes/i.test(detail)
120
+ ? 'invalid_response'
121
+ : 'network_error';
122
+ throw registrarRdapError(endpoint, outcome, { detail });
123
+ }
124
+ const selectedEndpoint = upstream.finalUrl || endpoint;
125
+ if (selectRegistrarRdapLink(canonical, [{ rel: 'related', href: selectedEndpoint }], registryEndpoint) !== selectedEndpoint) {
126
+ throw registrarRdapError(selectedEndpoint, 'invalid_response', {
127
+ status: upstream.status,
128
+ detail: 'The registrar endpoint redirected outside the eligible HTTPS domain-object URL boundary.',
129
+ });
130
+ }
131
+ if (upstream.status !== 404 && !upstream.ok) {
132
+ const outcome = upstream.status === 429
133
+ ? 'rate_limited'
134
+ : upstream.status >= 500
135
+ ? 'server_error'
136
+ : 'client_error';
137
+ throw registrarRdapError(selectedEndpoint, outcome, {
138
+ status: upstream.status,
139
+ detail: `The registrar endpoint returned HTTP ${upstream.status}.`,
140
+ });
141
+ }
142
+ let data;
143
+ try {
144
+ data = JSON.parse(upstream.text);
145
+ }
146
+ catch {
147
+ throw registrarRdapError(selectedEndpoint, 'invalid_json', {
148
+ status: upstream.status,
149
+ detail: 'The registrar endpoint returned invalid JSON.',
150
+ });
151
+ }
152
+ const fetchedAt = new Date().toISOString();
153
+ if (upstream.status === 404) {
154
+ const attempt = rdapAttempt(selectedEndpoint, 'not_found', {
155
+ status: upstream.status,
156
+ detail: 'The registrar endpoint reported no matching object.',
157
+ selected: true,
158
+ });
159
+ return {
160
+ status: 'not_found',
161
+ detail: 'The registrar RDAP service reported no matching object.',
162
+ endpoint: selectedEndpoint,
163
+ transportSecurity: 'https',
164
+ upstreamStatus: upstream.status,
165
+ fetchedAt,
166
+ data,
167
+ parsed: null,
168
+ attempt,
169
+ };
170
+ }
171
+ const parsed = parseRdap('domain', data);
172
+ const validation = validateRdapResponse('domain', canonical, parsed);
173
+ if (!validation.valid) {
174
+ throw registrarRdapError(selectedEndpoint, 'invalid_response', {
175
+ status: upstream.status,
176
+ detail: validation.detail,
177
+ });
178
+ }
179
+ const attempt = rdapAttempt(selectedEndpoint, 'success', {
180
+ status: upstream.status,
181
+ detail: 'The registrar endpoint returned the requested RDAP object.',
182
+ selected: true,
183
+ });
184
+ return {
185
+ status: 'success',
186
+ detail: null,
187
+ endpoint: selectedEndpoint,
188
+ transportSecurity: 'https',
189
+ upstreamStatus: upstream.status,
190
+ fetchedAt,
191
+ data,
192
+ parsed,
193
+ attempt,
194
+ };
195
+ });
196
+ }
@@ -0,0 +1,39 @@
1
+ // Bounded RDAP HTTP transport. Bootstrap selection, response validation,
2
+ // normalization, and registrar enrichment remain separate concerns in
3
+ // lib/rdap.mts.
4
+ import { readTextCapped, safeFetch, safeFetchDetailed } from "./safe-fetch.mjs";
5
+ const MAX_RDAP_BYTES = 2000000;
6
+ async function fetchRdapWithTimeout(url, options, timeoutMs) {
7
+ const controller = new AbortController();
8
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
9
+ try {
10
+ const response = await safeFetch(url, { ...options, signal: controller.signal });
11
+ const { text, truncated } = await readTextCapped(response, MAX_RDAP_BYTES);
12
+ if (truncated)
13
+ throw new Error(`Response from ${url} exceeded ${MAX_RDAP_BYTES} bytes`);
14
+ return { status: response.status, ok: response.ok, text };
15
+ }
16
+ finally {
17
+ clearTimeout(timeout);
18
+ }
19
+ }
20
+ async function fetchRdapDetailedWithTimeout(url, options, timeoutMs) {
21
+ const controller = new AbortController();
22
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
23
+ try {
24
+ const result = await safeFetchDetailed(url, { ...options, signal: controller.signal });
25
+ const { text, truncated } = await readTextCapped(result.response, MAX_RDAP_BYTES);
26
+ if (truncated)
27
+ throw new Error(`Response from ${url} exceeded ${MAX_RDAP_BYTES} bytes`);
28
+ return {
29
+ status: result.response.status,
30
+ ok: result.response.ok,
31
+ text,
32
+ finalUrl: result.finalUrl,
33
+ };
34
+ }
35
+ finally {
36
+ clearTimeout(timeout);
37
+ }
38
+ }
39
+ export { fetchRdapDetailedWithTimeout, fetchRdapWithTimeout, };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import net from 'node:net';
2
+ import { domainToASCII } from 'node:url';
3
+ import { ipv4ToLong, ipv6ToBigInt } from "./rdap-bootstrap.mjs";
4
+ export function canonicalRdapDomain(value) {
5
+ if (typeof value !== 'string')
6
+ return null;
7
+ const ascii = domainToASCII(value.trim().replace(/\.+$/, ''));
8
+ return ascii ? ascii.toLowerCase() : null;
9
+ }
10
+ function ipv6ToComparableBigInt(ip) {
11
+ let normalized = ip;
12
+ if (ip.includes('.')) {
13
+ const lastColon = ip.lastIndexOf(':');
14
+ const embedded = ip.slice(lastColon + 1);
15
+ if (net.isIP(embedded) !== 4)
16
+ return null;
17
+ const value = ipv4ToLong(embedded);
18
+ normalized = `${ip.slice(0, lastColon)}:${(value >>> 16).toString(16)}:${(value & 0xffff).toString(16)}`;
19
+ }
20
+ try {
21
+ return ipv6ToBigInt(normalized);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ export function validateRdapResponse(type, requestedValue, parsed) {
28
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
29
+ return { valid: false, detail: 'The response did not contain a usable RDAP object.' };
30
+ }
31
+ const expectedClass = type === 'domain' ? 'domain' : type === 'asn' ? 'autnum' : 'ip network';
32
+ if (parsed.objectClassName && parsed.objectClassName !== expectedClass) {
33
+ return {
34
+ valid: false,
35
+ detail: `Expected object class ${expectedClass}, received ${parsed.objectClassName}.`,
36
+ };
37
+ }
38
+ if (type === 'domain') {
39
+ if (!('domain' in parsed)) {
40
+ return { valid: false, detail: 'The response did not contain a domain RDAP object.' };
41
+ }
42
+ const requested = canonicalRdapDomain(requestedValue);
43
+ const returned = canonicalRdapDomain(parsed.domain);
44
+ if (!requested || !returned || requested !== returned) {
45
+ return { valid: false, detail: 'The response domain did not match the requested domain.' };
46
+ }
47
+ return { valid: true, detail: null };
48
+ }
49
+ if (type === 'asn') {
50
+ if (!('startAutnum' in parsed)) {
51
+ return { valid: false, detail: 'The response did not contain an autnum RDAP object.' };
52
+ }
53
+ const requested = Number(String(requestedValue).replace(/^AS/i, ''));
54
+ const startAutnum = parsed.startAutnum;
55
+ const endAutnum = parsed.endAutnum;
56
+ if (!Number.isSafeInteger(requested) ||
57
+ typeof startAutnum !== 'number' ||
58
+ !Number.isSafeInteger(startAutnum) ||
59
+ typeof endAutnum !== 'number' ||
60
+ !Number.isSafeInteger(endAutnum) ||
61
+ startAutnum > endAutnum ||
62
+ requested < startAutnum ||
63
+ requested > endAutnum) {
64
+ return { valid: false, detail: 'The response AS range did not cover the requested ASN.' };
65
+ }
66
+ return { valid: true, detail: null };
67
+ }
68
+ if (!('startAddress' in parsed)) {
69
+ return { valid: false, detail: 'The response did not contain an IP network RDAP object.' };
70
+ }
71
+ const version = type === 'ipv4' ? 4 : 6;
72
+ const startAddress = parsed.startAddress;
73
+ const endAddress = parsed.endAddress;
74
+ if (net.isIP(requestedValue) !== version ||
75
+ typeof startAddress !== 'string' ||
76
+ net.isIP(startAddress) !== version ||
77
+ typeof endAddress !== 'string' ||
78
+ net.isIP(endAddress) !== version) {
79
+ return { valid: false, detail: 'The response did not contain a compatible IP range.' };
80
+ }
81
+ const convert = version === 4 ? ipv4ToLong : ipv6ToComparableBigInt;
82
+ const requested = convert(requestedValue);
83
+ const start = convert(startAddress);
84
+ const end = convert(endAddress);
85
+ if (requested === null ||
86
+ start === null ||
87
+ end === null ||
88
+ start > end ||
89
+ requested < start ||
90
+ requested > end) {
91
+ return { valid: false, detail: 'The response IP range did not cover the requested address.' };
92
+ }
93
+ return { valid: true, detail: null };
94
+ }
package/lib/rdap.mjs ADDED
@@ -0,0 +1,16 @@
1
+ // Public RDAP facade shared by the Express server and Netlify Functions.
2
+ import { BOOTSTRAP_STALE_TTL_MS, BOOTSTRAP_TTL_MS, clearRdapBootstrapCache, fetchBootstrap, uniqueRdapBases as uniqueBases, } from "./rdap-bootstrap.mjs";
3
+ import { fetchRdapFromBasesWithParser, fetchRdapRecordWithParser, } from "./rdap-client.mjs";
4
+ import { normalizeRdapEvents, parseRdap, summarizeLifecycle, } from "./rdap-normalization.mjs";
5
+ import { fetchRegistrarRdapRecordWithParser, selectRegistrarRdapLink, } from "./rdap-registrar.mjs";
6
+ import { fetchRdapWithTimeout, } from "./rdap-transport.mjs";
7
+ async function fetchRdapFromBases(type, value, bases, fetchUpstream = fetchRdapWithTimeout) {
8
+ return fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream);
9
+ }
10
+ async function fetchRdapRecord(type, value) {
11
+ return fetchRdapRecordWithParser(type, value, parseRdap);
12
+ }
13
+ async function fetchRegistrarRdapRecord(domain, registryRecord, options = {}) {
14
+ return fetchRegistrarRdapRecordWithParser(domain, registryRecord, parseRdap, options);
15
+ }
16
+ export { BOOTSTRAP_TTL_MS, BOOTSTRAP_STALE_TTL_MS, fetchBootstrap, clearRdapBootstrapCache, fetchRdapRecord, fetchRdapFromBases, fetchRegistrarRdapRecord, selectRegistrarRdapLink, uniqueBases, parseRdap, normalizeRdapEvents, summarizeLifecycle, };
@@ -0,0 +1,32 @@
1
+ const REGISTRY_ACCESS_PROFILE_LABELS = Object.freeze({
2
+ 'iana-bootstrap': 'IANA bootstrap discovery',
3
+ 'iana-referral': 'IANA referral discovery',
4
+ 'no-iana-service': 'No service published by IANA',
5
+ 'registry-policy-restricted': 'Registry policy restricted',
6
+ 'source-ip-authorization-required': 'Source-IP authorization required',
7
+ });
8
+ const WHOIS_REGISTRY_ACCESS_PROFILES = new Set([
9
+ 'iana-referral',
10
+ 'no-iana-service',
11
+ 'registry-policy-restricted',
12
+ 'source-ip-authorization-required',
13
+ ]);
14
+ const RDAP_REGISTRY_ACCESS_PROFILES = new Set([
15
+ 'iana-bootstrap',
16
+ 'no-iana-service',
17
+ ]);
18
+ function isRegistryAccessProfile(value) {
19
+ return typeof value === 'string' && Object.hasOwn(REGISTRY_ACCESS_PROFILE_LABELS, value);
20
+ }
21
+ function isWhoisRegistryAccessProfile(value) {
22
+ return typeof value === 'string' && WHOIS_REGISTRY_ACCESS_PROFILES.has(value);
23
+ }
24
+ function isRdapRegistryAccessProfile(value) {
25
+ return typeof value === 'string' && RDAP_REGISTRY_ACCESS_PROFILES.has(value);
26
+ }
27
+ function registryAccessProfileLabel(value) {
28
+ return isRegistryAccessProfile(value)
29
+ ? REGISTRY_ACCESS_PROFILE_LABELS[value] ?? 'Unknown'
30
+ : 'Unknown';
31
+ }
32
+ export { isRdapRegistryAccessProfile, isWhoisRegistryAccessProfile, registryAccessProfileLabel, };
@@ -0,0 +1,117 @@
1
+ // Registry capability evaluation over the immutable reviewed catalogue.
2
+ import { CAPABILITY_BY_SUFFIX, DEFAULT_CAPABILITY, EXPLICIT_CAPABILITIES, MAX_CAPABILITY_INPUT_LENGTH, REGISTRY_CAPABILITIES_VERSION, REGISTRY_STANDARDS_COVERAGE_SNAPSHOT, VERSION_26_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, } from "./registry-capability-catalogue.mjs";
3
+ const IANA_TLD_RECORD_PREFIX = 'https://www.iana.org/domains/root/db/';
4
+ function documentationUrlsForSuffix(capability, suffixes) {
5
+ if (suffixes.length !== 1 || capability.suffixes.length <= 1) {
6
+ return [...capability.documentationUrls];
7
+ }
8
+ const suffix = suffixes[0];
9
+ const matchingIanaRecord = suffix ? `${IANA_TLD_RECORD_PREFIX}${suffix}.html` : '';
10
+ return capability.documentationUrls.filter((url) => (!url.startsWith(IANA_TLD_RECORD_PREFIX) || url === matchingIanaRecord));
11
+ }
12
+ function cloneCapability(capability, { suffixes = capability.suffixes } = {}) {
13
+ return {
14
+ ...capability,
15
+ suffixes: [...suffixes],
16
+ fixtureScenarios: [...capability.fixtureScenarios],
17
+ verificationFiles: [...capability.verificationFiles],
18
+ documentationUrls: documentationUrlsForSuffix(capability, suffixes),
19
+ };
20
+ }
21
+ function canonicalSuffix(value) {
22
+ if (typeof value !== 'string' ||
23
+ value.length > MAX_CAPABILITY_INPUT_LENGTH ||
24
+ /[\u0000-\u001f\u007f]/.test(value)) {
25
+ return null;
26
+ }
27
+ let trimmed = value.trim();
28
+ if (trimmed.startsWith('.'))
29
+ trimmed = trimmed.slice(1);
30
+ if (trimmed.endsWith('.'))
31
+ trimmed = trimmed.slice(0, -1);
32
+ if (trimmed.startsWith('.') || trimmed.endsWith('.'))
33
+ return null;
34
+ if (!trimmed || /[\s\\/%@:?#]/u.test(trimmed))
35
+ return null;
36
+ let ascii = '';
37
+ try {
38
+ const parsed = new URL(`http://${trimmed}`);
39
+ if (parsed.username ||
40
+ parsed.password ||
41
+ parsed.port ||
42
+ parsed.pathname !== '/' ||
43
+ parsed.search ||
44
+ parsed.hash) {
45
+ return null;
46
+ }
47
+ ascii = parsed.hostname;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ if (!ascii || ascii.length > MAX_CAPABILITY_INPUT_LENGTH)
53
+ return null;
54
+ const labels = ascii.toLowerCase().split('.');
55
+ if (labels.some((label) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))) {
56
+ return null;
57
+ }
58
+ const suffix = labels.at(-1) || null;
59
+ return suffix && /[a-z]/.test(suffix) ? suffix : null;
60
+ }
61
+ function registryCapabilityFor(value) {
62
+ const suffix = canonicalSuffix(value);
63
+ if (!suffix)
64
+ return null;
65
+ const capability = CAPABILITY_BY_SUFFIX.get(suffix);
66
+ if (capability) {
67
+ return {
68
+ ...cloneCapability(capability, { suffixes: [suffix] }),
69
+ explicitSuffixProfile: true,
70
+ };
71
+ }
72
+ return {
73
+ ...cloneCapability(DEFAULT_CAPABILITY, { suffixes: [suffix] }),
74
+ explicitSuffixProfile: false,
75
+ };
76
+ }
77
+ function registryAccessDiagnosticFor(value) {
78
+ const capability = registryCapabilityFor(value);
79
+ if (!capability ||
80
+ (capability.whoisAccessProfile === 'iana-referral' &&
81
+ capability.rdapAccessProfile === 'iana-bootstrap')) {
82
+ return null;
83
+ }
84
+ return {
85
+ suffix: capability.suffixes[0],
86
+ coverageState: capability.coverageState,
87
+ whoisAccessProfile: capability.whoisAccessProfile,
88
+ rdapAccessProfile: capability.rdapAccessProfile,
89
+ limitation: capability.limitation,
90
+ authority: 'context_only',
91
+ };
92
+ }
93
+ function listRegistryCapabilities() {
94
+ return EXPLICIT_CAPABILITIES.map((capability) => cloneCapability(capability));
95
+ }
96
+ function registryStandardsCoverageSnapshot() {
97
+ return {
98
+ ...REGISTRY_STANDARDS_COVERAGE_SNAPSHOT,
99
+ sources: {
100
+ ...REGISTRY_STANDARDS_COVERAGE_SNAPSHOT.sources,
101
+ urls: [...REGISTRY_STANDARDS_COVERAGE_SNAPSHOT.sources.urls],
102
+ },
103
+ counts: { ...REGISTRY_STANDARDS_COVERAGE_SNAPSHOT.counts },
104
+ exceptions: REGISTRY_STANDARDS_COVERAGE_SNAPSHOT.exceptions.map((entry) => ({
105
+ ...entry,
106
+ })),
107
+ };
108
+ }
109
+ function registryCompatibilityMatrix() {
110
+ return listRegistryCapabilities()
111
+ .flatMap((capability) => capability.suffixes.map((suffix) => ({
112
+ ...cloneCapability(capability, { suffixes: [suffix] }),
113
+ explicitSuffixProfile: true,
114
+ })))
115
+ .sort((left, right) => (left.suffixes[0] ?? '').localeCompare(right.suffixes[0] ?? ''));
116
+ }
117
+ export { REGISTRY_CAPABILITIES_VERSION, VERSION_26_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, registryCapabilityFor, registryCompatibilityMatrix, listRegistryCapabilities, registryAccessDiagnosticFor, registryStandardsCoverageSnapshot, };