@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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/NOTICE +6 -0
- package/README.md +20 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +454 -1748
- package/cli/artifact-validation/case-response.mjs +66 -35
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +112 -72
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +66 -111
- package/cli/command-argument-grammar.mjs +215 -0
- package/cli/command-reference.mjs +342 -285
- package/cli/completion.mjs +303 -80
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +59 -18
- package/cli/discovery-scan-command-runner.mjs +21 -0
- package/cli/discovery-scan.mjs +13 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +2 -0
- package/cli/input.mjs +116 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +18 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-header-review.mjs +467 -0
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +70 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +62 -7
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +77 -24
- package/docs/cli.md +300 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +51 -8
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +73 -26
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +49 -16
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +221 -117
- package/lib/domain-control-manifest.mjs +129 -69
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +201 -8
- package/lib/domain-posture.mjs +147 -29
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -543
- package/lib/evidence-report.mjs +5 -3
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +111 -108
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +8 -10
- package/lib/lookup-source-progress.mjs +40 -14
- package/lib/lookup.mjs +48 -27
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +85 -34
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +18 -6
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +42 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +178 -151
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +153 -45
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +127 -44
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +12 -22
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +95 -31
- package/packages/cases/case-record-projection.mjs +221 -0
- package/packages/cases/case-report.mjs +70 -54
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +64 -1115
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +74 -572
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +31 -2
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
- package/packages/comparison/page-similarity.mjs +5 -2
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +366 -83
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +94 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +15 -11
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +183 -211
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +201 -33
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +92 -51
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +193 -165
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +276 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +23 -11
- package/packages/workspace/bulk-session-model.mjs +100 -24
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -23
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +164 -26
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +87 -21
- package/third-party-notices.txt +29 -2
- package/cli/evidence-command-arguments.mjs +0 -156
- package/packages/investigation/bulk-review-export.mjs +0 -63
package/lib/rdap-bootstrap.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// IANA RDAP bootstrap retrieval, bounded stale caching, and authority
|
|
2
2
|
// selection for domain, IP, and ASN queries.
|
|
3
3
|
import net from 'node:net';
|
|
4
|
+
import { abortable } from "./abort.mjs";
|
|
4
5
|
import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
|
|
5
6
|
const BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
|
|
6
7
|
const BOOTSTRAP_STALE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -116,21 +117,17 @@ function validBootstrap(data) {
|
|
|
116
117
|
&& Array.isArray(service[1]) && service[1].length > 0
|
|
117
118
|
&& service[1].some((url) => typeof url === 'string' && /^https?:\/\//i.test(url))));
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
if (cached && now() - cached.fetchedAt < BOOTSTRAP_TTL_MS)
|
|
126
|
-
return cached.data;
|
|
127
|
-
const inflight = bootstrapInflight.get(kind);
|
|
128
|
-
if (inflight)
|
|
129
|
-
return inflight;
|
|
130
|
-
const request = (async () => {
|
|
120
|
+
function startBootstrap(kind, options) {
|
|
121
|
+
const now = options.now ?? Date.now;
|
|
122
|
+
const fetchUpstream = options.fetchUpstream ?? fetchRdapDetailedWithTimeout;
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const { signal } = controller;
|
|
125
|
+
const request = abortable(async () => {
|
|
131
126
|
try {
|
|
127
|
+
signal.throwIfAborted();
|
|
132
128
|
const requestedEndpoint = `https://data.iana.org/rdap/${kind}.json`;
|
|
133
|
-
const response = await fetchUpstream(requestedEndpoint, {}, BOOTSTRAP_FETCH_TIMEOUT_MS);
|
|
129
|
+
const response = await fetchUpstream(requestedEndpoint, { signal }, BOOTSTRAP_FETCH_TIMEOUT_MS);
|
|
130
|
+
signal.throwIfAborted();
|
|
134
131
|
const finalEndpoint = admitRdapEndpoint(response.finalUrl ?? requestedEndpoint);
|
|
135
132
|
if (finalEndpoint !== requestedEndpoint) {
|
|
136
133
|
throw new Error(`IANA bootstrap redirected outside its fixed source endpoint for ${kind}`);
|
|
@@ -152,21 +149,74 @@ async function fetchBootstrap(kind, options = {}) {
|
|
|
152
149
|
return data;
|
|
153
150
|
}
|
|
154
151
|
catch (cause) {
|
|
152
|
+
signal.throwIfAborted();
|
|
155
153
|
const fallback = bootstrapCache.get(kind);
|
|
156
154
|
if (fallback && now() - fallback.fetchedAt <= BOOTSTRAP_STALE_TTL_MS) {
|
|
157
155
|
return fallback.data;
|
|
158
156
|
}
|
|
159
157
|
throw cause;
|
|
160
158
|
}
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
}, signal);
|
|
160
|
+
const flight = {
|
|
161
|
+
controller, readers: 0, settled: false,
|
|
162
|
+
request: request.finally(() => {
|
|
163
|
+
flight.settled = true;
|
|
164
|
+
if (bootstrapInflight.get(kind) === flight)
|
|
165
|
+
bootstrapInflight.delete(kind);
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
bootstrapInflight.set(kind, flight);
|
|
169
|
+
return flight;
|
|
170
|
+
}
|
|
171
|
+
async function fetchBootstrap(kind, options = {}) {
|
|
172
|
+
if (!BOOTSTRAP_KINDS.has(kind))
|
|
173
|
+
throw new Error(`Unsupported RDAP bootstrap kind: ${kind}`);
|
|
174
|
+
options.signal?.throwIfAborted();
|
|
175
|
+
const now = options.now ?? Date.now;
|
|
176
|
+
const cached = bootstrapCache.get(kind);
|
|
177
|
+
if (cached && now() - cached.fetchedAt < BOOTSTRAP_TTL_MS)
|
|
178
|
+
return cached.data;
|
|
179
|
+
const flight = bootstrapInflight.get(kind) ?? startBootstrap(kind, options);
|
|
180
|
+
flight.readers++;
|
|
181
|
+
let released = false;
|
|
182
|
+
const release = () => {
|
|
183
|
+
if (released)
|
|
184
|
+
return;
|
|
185
|
+
released = true;
|
|
186
|
+
// One deadline releases only its reader. The fixed-source transport lives
|
|
187
|
+
// until the last reader leaves, and never caches a cancelled late response.
|
|
188
|
+
flight.readers--;
|
|
189
|
+
if (!flight.readers && !flight.settled) {
|
|
190
|
+
flight.controller.abort();
|
|
191
|
+
if (bootstrapInflight.get(kind) === flight)
|
|
192
|
+
bootstrapInflight.delete(kind);
|
|
163
193
|
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
194
|
+
};
|
|
195
|
+
const { signal } = options;
|
|
196
|
+
let cancel = () => { };
|
|
197
|
+
const result = new Promise((resolve, reject) => {
|
|
198
|
+
// Attach before observing cancellation so even an abandoned queued flight
|
|
199
|
+
// retains a rejection handler. Release synchronously to avoid starting it.
|
|
200
|
+
flight.request.then(resolve, reject);
|
|
201
|
+
cancel = () => { release(); reject(signal?.reason); };
|
|
202
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
203
|
+
if (signal?.aborted)
|
|
204
|
+
cancel();
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
return await result;
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
signal?.removeEventListener('abort', cancel);
|
|
211
|
+
release();
|
|
212
|
+
if (flight.controller.signal.aborted)
|
|
213
|
+
await flight.request.catch(() => { });
|
|
214
|
+
}
|
|
167
215
|
}
|
|
168
216
|
function clearRdapBootstrapCache() {
|
|
169
217
|
bootstrapCache.clear();
|
|
218
|
+
for (const flight of bootstrapInflight.values())
|
|
219
|
+
flight.controller.abort();
|
|
170
220
|
bootstrapInflight.clear();
|
|
171
221
|
}
|
|
172
222
|
function uniqueRdapBases(urls) {
|
|
@@ -185,9 +235,9 @@ function uniqueRdapBases(urls) {
|
|
|
185
235
|
return true;
|
|
186
236
|
});
|
|
187
237
|
}
|
|
188
|
-
async function findRdapBases(type, value) {
|
|
238
|
+
async function findRdapBases(type, value, options = {}) {
|
|
189
239
|
if (type === 'domain') {
|
|
190
|
-
const bootstrap = await fetchBootstrap('dns');
|
|
240
|
+
const bootstrap = await fetchBootstrap('dns', options);
|
|
191
241
|
const tld = value.split('.').pop()?.toLowerCase() || '';
|
|
192
242
|
for (const [tlds, urls] of bootstrap.services) {
|
|
193
243
|
if (tlds.some((entry) => entry.toLowerCase() === tld))
|
|
@@ -196,7 +246,7 @@ async function findRdapBases(type, value) {
|
|
|
196
246
|
return [];
|
|
197
247
|
}
|
|
198
248
|
if (type === 'ipv4' || type === 'ipv6') {
|
|
199
|
-
const bootstrap = await fetchBootstrap(type);
|
|
249
|
+
const bootstrap = await fetchBootstrap(type, options);
|
|
200
250
|
const matcher = type === 'ipv4' ? ipInCidrV4 : ipInCidrV6;
|
|
201
251
|
let best = null;
|
|
202
252
|
let bestPrefix = -1;
|
|
@@ -215,7 +265,7 @@ async function findRdapBases(type, value) {
|
|
|
215
265
|
return best || [];
|
|
216
266
|
}
|
|
217
267
|
if (type === 'asn') {
|
|
218
|
-
const bootstrap = await fetchBootstrap('asn');
|
|
268
|
+
const bootstrap = await fetchBootstrap('asn', options);
|
|
219
269
|
const number = parseInt(value.replace(/^AS/i, ''), 10);
|
|
220
270
|
for (const [ranges, urls] of bootstrap.services) {
|
|
221
271
|
for (const range of ranges) {
|
|
@@ -245,7 +245,7 @@ export function inspectRdapCapabilities(parsedValue, status) {
|
|
|
245
245
|
break;
|
|
246
246
|
}
|
|
247
247
|
const declarations = [...unique].sort().map((value) => {
|
|
248
|
-
const entry = EXTENSION_CATALOG[value];
|
|
248
|
+
const entry = Object.hasOwn(EXTENSION_CATALOG, value) ? EXTENSION_CATALOG[value] : undefined;
|
|
249
249
|
return entry
|
|
250
250
|
? {
|
|
251
251
|
identifier: value,
|
package/lib/rdap-client.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cached } from "./lookup-cache.mjs";
|
|
2
2
|
import { findRdapBases, uniqueRdapBases, } from "./rdap-bootstrap.mjs";
|
|
3
3
|
import { rdapAttempt, rdapFailure } from "./rdap-attempts.mjs";
|
|
4
|
-
import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
|
|
4
|
+
import { fetchRdapDetailedWithTimeout, RdapBodyAdmissionError, } from "./rdap-transport.mjs";
|
|
5
5
|
import { admitRdapObjectEndpoint, validateRdapResponse, } from "./rdap-validation.mjs";
|
|
6
6
|
const UPSTREAM_TIMEOUT_MS = 7_000;
|
|
7
7
|
const UPSTREAM_TOTAL_DEADLINE_MS = 12_000;
|
|
@@ -24,20 +24,23 @@ function rdapPathFor(type, value) {
|
|
|
24
24
|
* Query the bounded bootstrap candidate set while retaining every attempted
|
|
25
25
|
* endpoint as separately attributed transport evidence.
|
|
26
26
|
*/
|
|
27
|
-
export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapDetailedWithTimeout) {
|
|
27
|
+
export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapDetailedWithTimeout, signal) {
|
|
28
|
+
signal?.throwIfAborted();
|
|
28
29
|
const candidates = uniqueRdapBases(bases).slice(0, MAX_RDAP_ENDPOINTS);
|
|
29
30
|
if (candidates.length === 0)
|
|
30
31
|
return null;
|
|
31
32
|
const startedAt = Date.now();
|
|
32
33
|
const attempts = [];
|
|
33
34
|
for (const base of candidates) {
|
|
35
|
+
signal?.throwIfAborted();
|
|
34
36
|
const elapsed = Date.now() - startedAt;
|
|
35
37
|
const remaining = UPSTREAM_TOTAL_DEADLINE_MS - elapsed;
|
|
36
38
|
if (remaining <= 0)
|
|
37
39
|
break;
|
|
38
40
|
const url = `${base.replace(/\/$/, '')}/${rdapPathFor(type, value)}`;
|
|
39
41
|
try {
|
|
40
|
-
const upstream = await fetchUpstream(url, { headers: { Accept: 'application/rdap+json' } }, Math.min(UPSTREAM_TIMEOUT_MS, remaining));
|
|
42
|
+
const upstream = await fetchUpstream(url, { headers: { Accept: 'application/rdap+json' }, ...(signal ? { signal } : {}) }, Math.min(UPSTREAM_TIMEOUT_MS, remaining));
|
|
43
|
+
signal?.throwIfAborted();
|
|
41
44
|
const selectedEndpoint = admitRdapObjectEndpoint(type, value, upstream.finalUrl ?? url);
|
|
42
45
|
if (!selectedEndpoint) {
|
|
43
46
|
attempts.push(rdapAttempt(url, 'invalid_response', {
|
|
@@ -110,6 +113,11 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
|
|
|
110
113
|
};
|
|
111
114
|
}
|
|
112
115
|
catch (cause) {
|
|
116
|
+
signal?.throwIfAborted();
|
|
117
|
+
if (cause instanceof RdapBodyAdmissionError) {
|
|
118
|
+
attempts.push(rdapAttempt(url, 'invalid_response', { status: cause.status, detail: cause.message }));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
113
121
|
const detail = String(errorProperty(cause, 'message') || 'request failed');
|
|
114
122
|
const outcome = errorProperty(cause, 'name') === 'AbortError' ||
|
|
115
123
|
/timed? out|time limit/i.test(detail)
|
|
@@ -125,9 +133,9 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
|
|
|
125
133
|
: 'the total upstream deadline expired';
|
|
126
134
|
throw Object.assign(new Error(`RDAP lookup failed across ${candidates.length} endpoint(s): ${detail}`), { attempts });
|
|
127
135
|
}
|
|
128
|
-
export async function fetchRdapRecordWithParser(type, value, parseRdap) {
|
|
136
|
+
export async function fetchRdapRecordWithParser(type, value, parseRdap, options = {}) {
|
|
129
137
|
return cached(`rdap:${type}:${value}`, async () => {
|
|
130
|
-
const bases = await findRdapBases(type, value);
|
|
131
|
-
return fetchRdapFromBasesWithParser(type, value, bases, parseRdap);
|
|
132
|
-
});
|
|
138
|
+
const bases = await findRdapBases(type, value, options);
|
|
139
|
+
return fetchRdapFromBasesWithParser(type, value, bases, parseRdap, options.fetchUpstream, options.signal);
|
|
140
|
+
}, options.signal);
|
|
133
141
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Bounded normalization of untrusted RDAP response objects.
|
|
2
2
|
import net from 'node:net';
|
|
3
3
|
import { registryDateIso } from "./registry-dates.mjs";
|
|
4
|
-
import {} from "./rdap-types.mjs";
|
|
4
|
+
import { MAX_RDAP_STATUSES, MAX_RDAP_REDACTIONS, } from "./rdap-types.mjs";
|
|
5
5
|
const MAX_RDAP_ENTITIES = 100;
|
|
6
|
+
const MAX_RDAP_EVENTS = 100;
|
|
6
7
|
const MAX_RDAP_ENTITY_DEPTH = 6;
|
|
7
8
|
const MAX_ENTITIES_PER_ROLE = 5;
|
|
8
9
|
const MAX_VCARD_ENTRIES = 100;
|
|
@@ -10,7 +11,6 @@ const MAX_ENTITY_ROLES = 12;
|
|
|
10
11
|
const MAX_CONTACT_VALUES = 8;
|
|
11
12
|
const MAX_ENTITY_LINKS = 10;
|
|
12
13
|
const MAX_RDAP_LINKS = 20;
|
|
13
|
-
const MAX_RDAP_REDACTIONS = 100;
|
|
14
14
|
const MAX_RDAP_VARIANT_GROUPS = 20;
|
|
15
15
|
const MAX_RDAP_VARIANT_NAMES = 50;
|
|
16
16
|
const MAX_RDAP_SERVER_TRUNCATION_REASONS = 8;
|
|
@@ -166,8 +166,10 @@ function contactValuesTruncated(vcardArray) {
|
|
|
166
166
|
return rawPart.length > 300 || /[\u0000-\u001f\u007f]/u.test(rawPart) || rawPart.trim() !== '';
|
|
167
167
|
});
|
|
168
168
|
const retainedParts = parts.filter((part) => part !== null);
|
|
169
|
+
// Empty structured components publish no address; they are not omitted
|
|
170
|
+
// evidence. Supplied non-blank values still pass every validation bound.
|
|
169
171
|
if (rejectedNonBlankPart
|
|
170
|
-
|| boundedString(retainedParts.join(', '), 1000) === null)
|
|
172
|
+
|| (retainedParts.length > 0 && boundedString(retainedParts.join(', '), 1000) === null))
|
|
171
173
|
return true;
|
|
172
174
|
}
|
|
173
175
|
return false;
|
|
@@ -506,18 +508,33 @@ function boundedEventString(value, maxLength) {
|
|
|
506
508
|
return null;
|
|
507
509
|
return value.trim() || null;
|
|
508
510
|
}
|
|
509
|
-
function
|
|
511
|
+
function normalizedEventInventory(value) {
|
|
510
512
|
if (!Array.isArray(value))
|
|
511
|
-
return [];
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
513
|
+
return { items: [], truncated: value !== undefined };
|
|
514
|
+
let truncated = value.length > MAX_RDAP_EVENTS;
|
|
515
|
+
const items = [];
|
|
516
|
+
for (const event of value.slice(0, MAX_RDAP_EVENTS)) {
|
|
517
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
518
|
+
truncated = true;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
515
521
|
const record = event;
|
|
516
522
|
const action = boundedEventString(record.eventAction, 100)?.toLowerCase().replace(/\s+/g, ' ') || null;
|
|
517
523
|
const date = boundedEventString(record.eventDate, 64);
|
|
518
524
|
const actor = boundedEventString(record.eventActor, 160);
|
|
519
|
-
|
|
520
|
-
|
|
525
|
+
if (record.eventAction !== undefined && action === null
|
|
526
|
+
|| record.eventDate !== undefined && date === null
|
|
527
|
+
|| record.eventActor !== undefined && actor === null)
|
|
528
|
+
truncated = true;
|
|
529
|
+
if (action || date)
|
|
530
|
+
items.push({ action, date, actor });
|
|
531
|
+
else
|
|
532
|
+
truncated = true;
|
|
533
|
+
}
|
|
534
|
+
return { items, truncated };
|
|
535
|
+
}
|
|
536
|
+
function normalizeRdapEvents(value) {
|
|
537
|
+
return normalizedEventInventory(value).items;
|
|
521
538
|
}
|
|
522
539
|
function lifecycleDate(events, action, newest) {
|
|
523
540
|
let selected = null;
|
|
@@ -697,7 +714,8 @@ function entityInventory(entities) {
|
|
|
697
714
|
function parseRdapObject(type, data) {
|
|
698
715
|
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
699
716
|
return null;
|
|
700
|
-
const
|
|
717
|
+
const eventInfo = normalizedEventInventory(data.events);
|
|
718
|
+
const events = eventInfo.items;
|
|
701
719
|
const redactionInfo = normalizeRedactions(data.redacted);
|
|
702
720
|
const noticesInfo = summarizeTextBlocks(data.notices);
|
|
703
721
|
const remarksInfo = summarizeTextBlocks(data.remarks);
|
|
@@ -708,12 +726,11 @@ function parseRdapObject(type, data) {
|
|
|
708
726
|
// to every RDAP object type. Unlike set-like fields, status order and
|
|
709
727
|
// repetition remain as published for backwards compatibility.
|
|
710
728
|
const statuses = Array.isArray(data.status)
|
|
711
|
-
? data.status.slice(0,
|
|
729
|
+
? data.status.slice(0, MAX_RDAP_STATUSES)
|
|
712
730
|
.map((status) => boundedString(status, 160))
|
|
713
731
|
.filter((status) => status !== null)
|
|
714
732
|
: [];
|
|
715
|
-
const inspectedStatusCount = Array.isArray(data.status) ? Math.min(data.status.length,
|
|
716
|
-
const inspectedEventCount = Array.isArray(data.events) ? Math.min(data.events.length, 100) : 0;
|
|
733
|
+
const inspectedStatusCount = Array.isArray(data.status) ? Math.min(data.status.length, MAX_RDAP_STATUSES) : 0;
|
|
717
734
|
const common = {
|
|
718
735
|
objectClassName: boundedString(data.objectClassName, 80, { lower: true }),
|
|
719
736
|
language: boundedString(data.lang, 35, { lower: true }),
|
|
@@ -732,12 +749,10 @@ function parseRdapObject(type, data) {
|
|
|
732
749
|
serverTruncationReasons,
|
|
733
750
|
statuses,
|
|
734
751
|
statusesTruncated: data.status !== undefined && (!Array.isArray(data.status)
|
|
735
|
-
|| data.status.length >
|
|
752
|
+
|| data.status.length > MAX_RDAP_STATUSES
|
|
736
753
|
|| statuses.length < inspectedStatusCount),
|
|
737
754
|
events,
|
|
738
|
-
eventsTruncated:
|
|
739
|
-
|| data.events.length > 100
|
|
740
|
-
|| events.length < inspectedEventCount),
|
|
755
|
+
eventsTruncated: eventInfo.truncated,
|
|
741
756
|
lifecycle: summarizeLifecycle(events),
|
|
742
757
|
};
|
|
743
758
|
if (type === 'domain') {
|
|
@@ -213,7 +213,7 @@ function inspectRdapReverseSearchResponse(value, requestedProperties = []) {
|
|
|
213
213
|
mappings.push(Object.freeze({
|
|
214
214
|
property,
|
|
215
215
|
propertyPath,
|
|
216
|
-
state: REGISTERED_PROPERTY_PATHS[property]?.includes(propertyPath)
|
|
216
|
+
state: Object.hasOwn(REGISTERED_PROPERTY_PATHS, property) && REGISTERED_PROPERTY_PATHS[property]?.includes(propertyPath)
|
|
217
217
|
? 'registered'
|
|
218
218
|
: 'unrecognized',
|
|
219
219
|
}));
|
package/lib/rdap-transport.mjs
CHANGED
|
@@ -2,38 +2,65 @@
|
|
|
2
2
|
// normalization, and registrar enrichment remain separate concerns in
|
|
3
3
|
// lib/rdap.mts.
|
|
4
4
|
import { readTextCapped, safeFetch, safeFetchDetailed } from "./safe-fetch.mjs";
|
|
5
|
+
import { abortable } from "./abort.mjs";
|
|
5
6
|
const MAX_RDAP_BYTES = 2000000;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
finally {
|
|
17
|
-
clearTimeout(timeout);
|
|
7
|
+
export class RdapBodyAdmissionError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
kind;
|
|
10
|
+
constructor(status, kind) {
|
|
11
|
+
super(kind === 'too_large' ? `RDAP response exceeded ${MAX_RDAP_BYTES} bytes.`
|
|
12
|
+
: kind === 'invalid_encoding' ? 'RDAP response contains invalid UTF-8 encoding.'
|
|
13
|
+
: 'RDAP response body could not be read completely.');
|
|
14
|
+
this.name = 'RdapBodyAdmissionError';
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.kind = kind;
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
|
-
async function
|
|
19
|
+
async function fetchRdapTransport(url, options, timeoutMs, fetchResponse, readText) {
|
|
20
|
+
options.signal?.throwIfAborted();
|
|
21
21
|
const controller = new AbortController();
|
|
22
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
22
23
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
24
|
+
let response;
|
|
25
|
+
const cancelBody = () => { void response?.body?.cancel().catch(() => { }); };
|
|
23
26
|
try {
|
|
24
|
-
const result = await (
|
|
25
|
-
|
|
27
|
+
const result = await abortable(async () => {
|
|
28
|
+
const result = await fetchResponse(url, { ...options, signal });
|
|
29
|
+
response = result.response;
|
|
30
|
+
if (signal.aborted) {
|
|
31
|
+
cancelBody();
|
|
32
|
+
signal.throwIfAborted();
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}, signal);
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = await abortable(() => readText(result.response, MAX_RDAP_BYTES, { fatalUtf8: true }), signal);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
const invalidEncoding = error instanceof TypeError && 'code' in error
|
|
43
|
+
&& error.code === 'ERR_ENCODING_INVALID_ENCODED_DATA';
|
|
44
|
+
throw new RdapBodyAdmissionError(result.response.status, invalidEncoding ? 'invalid_encoding' : 'unreadable');
|
|
45
|
+
}
|
|
46
|
+
const { text, truncated } = body;
|
|
26
47
|
if (truncated)
|
|
27
|
-
throw new
|
|
48
|
+
throw new RdapBodyAdmissionError(result.response.status, 'too_large');
|
|
28
49
|
return {
|
|
29
|
-
status: result.response.status,
|
|
30
|
-
|
|
31
|
-
text,
|
|
32
|
-
finalUrl: result.finalUrl,
|
|
50
|
+
status: result.response.status, ok: result.response.ok, text,
|
|
51
|
+
...(result.finalUrl === undefined ? {} : { finalUrl: result.finalUrl }),
|
|
33
52
|
};
|
|
34
53
|
}
|
|
35
54
|
finally {
|
|
36
55
|
clearTimeout(timeout);
|
|
56
|
+
if (signal.aborted)
|
|
57
|
+
cancelBody();
|
|
37
58
|
}
|
|
38
59
|
}
|
|
60
|
+
async function fetchRdapWithTimeout(url, options, timeoutMs, dependencies = {}) {
|
|
61
|
+
return fetchRdapTransport(url, options, timeoutMs, async (target, request) => ({ response: await (dependencies.fetch ?? safeFetch)(target, request) }), dependencies.readText ?? readTextCapped);
|
|
62
|
+
}
|
|
63
|
+
async function fetchRdapDetailedWithTimeout(url, options, timeoutMs, dependencies = {}) {
|
|
64
|
+
return fetchRdapTransport(url, options, timeoutMs, dependencies.fetchDetailed ?? safeFetchDetailed, dependencies.readText ?? readTextCapped);
|
|
65
|
+
}
|
|
39
66
|
export { fetchRdapDetailedWithTimeout, fetchRdapWithTimeout, };
|
package/lib/rdap-types.mjs
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const MAX_RDAP_STATUSES = 100;
|
|
2
|
+
export const MAX_RDAP_REDACTIONS = 100;
|
package/lib/rdap.mjs
CHANGED
|
@@ -4,18 +4,33 @@ import { fetchRdapFromBasesWithParser, fetchRdapRecordWithParser, } from "./rdap
|
|
|
4
4
|
import { normalizeRdapEvents, parseRdap, summarizeLifecycle, } from "./rdap-normalization.mjs";
|
|
5
5
|
import { fetchRegistrarRdapRecordWithParser, selectRegistrarRdapLink, } from "./rdap-registrar.mjs";
|
|
6
6
|
import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
|
|
7
|
-
import { registryServiceAdmissionFor } from "./registry-capabilities.mjs";
|
|
7
|
+
import { REGISTRY_CAPABILITIES_VERSION, registryServiceAdmissionFor } from "./registry-capabilities.mjs";
|
|
8
|
+
// A null record does not establish domain absence. Keep the HTTP explanation
|
|
9
|
+
// aligned with the same admission decision used before bootstrap discovery.
|
|
10
|
+
function rdapUnavailableResponse(type, value) {
|
|
11
|
+
const admission = type === 'domain' ? registryServiceAdmissionFor(value, 'rdap') : null;
|
|
12
|
+
if (admission?.allowed === false) {
|
|
13
|
+
return {
|
|
14
|
+
error: 'RDAP collection was not attempted because the retained registry capability policy does not admit this service.',
|
|
15
|
+
source: 'retained_registry_capability_policy',
|
|
16
|
+
capabilityVersion: REGISTRY_CAPABILITIES_VERSION,
|
|
17
|
+
limitation: admission.limitation,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return { error: `No RDAP registry found for "${value}" via IANA bootstrap` };
|
|
21
|
+
}
|
|
8
22
|
async function fetchRdapFromBases(type, value, bases, fetchUpstream = fetchRdapDetailedWithTimeout) {
|
|
9
23
|
return fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream);
|
|
10
24
|
}
|
|
11
25
|
async function fetchRdapRecord(type, value, options = {}) {
|
|
26
|
+
options.signal?.throwIfAborted();
|
|
12
27
|
if (type === 'domain' && registryServiceAdmissionFor(value, 'rdap')?.allowed === false) {
|
|
13
28
|
return null;
|
|
14
29
|
}
|
|
15
30
|
const fetchRecord = options.fetchRecord ?? fetchRdapRecordWithParser;
|
|
16
|
-
return fetchRecord(type, value, parseRdap);
|
|
31
|
+
return fetchRecord(type, value, parseRdap, options.signal ? { signal: options.signal } : {});
|
|
17
32
|
}
|
|
18
33
|
async function fetchRegistrarRdapRecord(domain, registryRecord, options = {}) {
|
|
19
34
|
return fetchRegistrarRdapRecordWithParser(domain, registryRecord, parseRdap, options);
|
|
20
35
|
}
|
|
21
|
-
export { BOOTSTRAP_TTL_MS, BOOTSTRAP_STALE_TTL_MS, fetchBootstrap, clearRdapBootstrapCache, fetchRdapRecord, fetchRdapFromBases, fetchRegistrarRdapRecord, selectRegistrarRdapLink, uniqueBases, parseRdap, normalizeRdapEvents, summarizeLifecycle, };
|
|
36
|
+
export { BOOTSTRAP_TTL_MS, BOOTSTRAP_STALE_TTL_MS, fetchBootstrap, clearRdapBootstrapCache, fetchRdapRecord, rdapUnavailableResponse, fetchRdapFromBases, fetchRegistrarRdapRecord, selectRegistrarRdapLink, uniqueBases, parseRdap, normalizeRdapEvents, summarizeLifecycle, };
|
|
@@ -1,5 +1,25 @@
|
|
|
1
|
-
import { getDomain } from 'tldts';
|
|
1
|
+
import { getDomain, getPublicSuffix } from 'tldts';
|
|
2
2
|
import { isValidAsciiDomainName } from "./hostname.mjs";
|
|
3
|
+
import { MAX_DOMAIN_NAME_LENGTH, MAX_DOMAIN_LABEL_LENGTH } from "../packages/contracts/domain-name.mjs";
|
|
4
|
+
// Reserve the shortest generated name and its separating dot.
|
|
5
|
+
export const MAX_DISCOVERY_SUFFIX_LENGTH = MAX_DOMAIN_NAME_LENGTH - 2;
|
|
6
|
+
/** ICANN suffix rules for a plain ASCII hostname; private suffixes do not change identity. */
|
|
7
|
+
export function publicSuffixForAsciiHostname(value) {
|
|
8
|
+
if (!isValidAsciiDomainName(value, { requireDot: false, requireLowercase: true }))
|
|
9
|
+
return null;
|
|
10
|
+
return getPublicSuffix(value);
|
|
11
|
+
}
|
|
12
|
+
/** Preserve reserved single labels; compound suffixes must match the local ICANN suffix rules. */
|
|
13
|
+
export function normalizeDiscoverySuffix(raw) {
|
|
14
|
+
if (typeof raw !== 'string' || raw.length > MAX_DISCOVERY_SUFFIX_LENGTH + 1 || /[\u0000-\u001f\u007f]/u.test(raw))
|
|
15
|
+
return null;
|
|
16
|
+
const value = raw.trim().toLowerCase().replace(/^\./u, '');
|
|
17
|
+
if (!value || value.length > MAX_DISCOVERY_SUFFIX_LENGTH)
|
|
18
|
+
return null;
|
|
19
|
+
if (value.length <= MAX_DOMAIN_LABEL_LENGTH && /^[a-z]{2,}$/u.test(value))
|
|
20
|
+
return value;
|
|
21
|
+
return value.includes('.') && publicSuffixForAsciiHostname(value) === value ? value : null;
|
|
22
|
+
}
|
|
3
23
|
// Returns one canonical registrable-domain identity for an already public DNS
|
|
4
24
|
// hostname. Callers retain the original hostname separately when subdomain
|
|
5
25
|
// context matters; authority, evidence binding and scoring use this value.
|
|
@@ -101,6 +101,7 @@ function validNullableTimestamp(value) {
|
|
|
101
101
|
function validNullableDate(value) {
|
|
102
102
|
return value === null || (typeof value === 'string'
|
|
103
103
|
&& ISO_DATE_RE.test(value)
|
|
104
|
+
&& Number.isFinite(Date.parse(`${value}T00:00:00.000Z`))
|
|
104
105
|
&& new Date(`${value}T00:00:00.000Z`).toISOString().startsWith(value));
|
|
105
106
|
}
|
|
106
107
|
function validNullableText(value, maximum = MAX_REGISTRAR_STANDING_TEXT_LENGTH) {
|
|
@@ -98,7 +98,11 @@ function registryServiceAdmissionFor(value, service) {
|
|
|
98
98
|
if (!capability || (service !== 'rdap' && service !== 'whois'))
|
|
99
99
|
return null;
|
|
100
100
|
if (service === 'rdap') {
|
|
101
|
-
|
|
101
|
+
// A retained absence is a discovery hint, not an access restriction.
|
|
102
|
+
// Both current profiles permit bounded bootstrap discovery; unknown or
|
|
103
|
+
// future restriction profiles remain denied until explicitly handled.
|
|
104
|
+
const allowed = capability.rdapAccessProfile === 'iana-bootstrap'
|
|
105
|
+
|| capability.rdapAccessProfile === 'no-iana-service';
|
|
102
106
|
return {
|
|
103
107
|
service,
|
|
104
108
|
state: allowed ? 'allowed' : 'unsupported',
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Registry compatibility metadata. IANA bootstrap and referral discovery
|
|
2
2
|
// remain authoritative. Explicit query profiles may alter only the first
|
|
3
3
|
// referred registry query after a fixture-backed adapter is integrated.
|
|
4
|
+
export const REGISTRY_STANDARDS_COVERAGE_SCHEMA = 'whoisleuth.registry-standards-coverage';
|
|
4
5
|
const REGISTRY_CAPABILITIES_VERSION = 29;
|
|
5
6
|
const MAX_CAPABILITY_INPUT_LENGTH = 253;
|
|
6
7
|
const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
|
|
7
|
-
schema:
|
|
8
|
+
schema: REGISTRY_STANDARDS_COVERAGE_SCHEMA,
|
|
8
9
|
version: 1,
|
|
9
10
|
verifiedAt: '2026-08-03',
|
|
10
11
|
sources: Object.freeze({
|