@slicedearth/whoisleuth-cli 1.47.4 → 2.0.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/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
- package/third-party-notices.txt +14 -14
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -255
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/lib/evidence-export.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { compareRdapPublications, compareRegistrySources } from "./registry-comparison.mjs";
|
|
2
2
|
import { buildRegistryInsights } from "./registry-insights.mjs";
|
|
3
|
-
import { buildPortableGeneratorMetadata } from "./portable-generator.mjs";
|
|
3
|
+
import { buildPortableGeneratorMetadata, isUriShapedLabel } from "./portable-generator.mjs";
|
|
4
4
|
import { assertBoundedJsonStructure, isSafeJsonObjectKey } from "./bounded-json.mjs";
|
|
5
|
+
import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "./homepage-metadata-contract.mjs";
|
|
6
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
7
|
+
import { defineSchemaCompatibility } from "../packages/contracts/schema-compatibility.mjs";
|
|
8
|
+
import { isValidAsciiHostname } from "./hostname.mjs";
|
|
5
9
|
export const LOOKUP_EVIDENCE_SCHEMA = 'whoisleuth.lookup-evidence';
|
|
6
|
-
export const
|
|
7
|
-
export const LOOKUP_EVIDENCE_SCHEMA_VERSION =
|
|
10
|
+
export const PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION = 26;
|
|
11
|
+
export const LOOKUP_EVIDENCE_SCHEMA_VERSION = 27;
|
|
8
12
|
export const SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS = Object.freeze([
|
|
9
|
-
|
|
13
|
+
PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
10
14
|
LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
11
15
|
]);
|
|
12
16
|
export const LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES = 5 * 1024 * 1024;
|
|
@@ -15,6 +19,20 @@ export const LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH = 24;
|
|
|
15
19
|
export const LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS = 10_000;
|
|
16
20
|
export const LOOKUP_EVIDENCE_PORTABLE_MAX_KEY_LENGTH = 256;
|
|
17
21
|
export const LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH = 1024 * 1024;
|
|
22
|
+
export const LOOKUP_EVIDENCE_COMPATIBILITY = defineSchemaCompatibility({
|
|
23
|
+
id: 'export.lookup-evidence',
|
|
24
|
+
kind: 'export',
|
|
25
|
+
schema: LOOKUP_EVIDENCE_SCHEMA,
|
|
26
|
+
currentVersion: LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
27
|
+
supportedVersions: SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS,
|
|
28
|
+
acceptsUnversionedLegacy: false,
|
|
29
|
+
futureVersionBehavior: 'reject',
|
|
30
|
+
migration: 'read_only',
|
|
31
|
+
writeSemantics: 'read_only',
|
|
32
|
+
byteBudget: LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES,
|
|
33
|
+
owner: 'lib/evidence-export.mts',
|
|
34
|
+
note: 'Exact v1.47.4 version 26 remains replayable; version 27 is the single v2 successor with bounded homepage and privacy-minimised registration projections.',
|
|
35
|
+
});
|
|
18
36
|
const REGISTRAR_RDAP_STATUSES = new Set([
|
|
19
37
|
'success', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled',
|
|
20
38
|
]);
|
|
@@ -46,7 +64,7 @@ const LOOKUP_TIMING_SOURCES = new Set([
|
|
|
46
64
|
'network_context', 'security_txt', 'external_intelligence',
|
|
47
65
|
'malware_host_intelligence', 'malware_ioc_intelligence',
|
|
48
66
|
]);
|
|
49
|
-
const
|
|
67
|
+
const PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
|
|
50
68
|
'applicable', 'type', 'domain', 'state', 'confidence', 'detail', 'source',
|
|
51
69
|
'rdapServer', 'nameservers', 'statuses', 'registrar', 'registrant', 'abuse',
|
|
52
70
|
'createdDate', 'expiryDate', 'createdDateIso', 'expiryDateIso', 'domainAgeDays',
|
|
@@ -59,6 +77,11 @@ const LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
|
|
|
59
77
|
'securityPosture', 'dns', 'tls', 'hasMx', 'hasNullMx', 'mxHosts', 'hasSpf',
|
|
60
78
|
'hasDmarc', 'bulkComparison', 'limitations',
|
|
61
79
|
]);
|
|
80
|
+
const LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
|
|
81
|
+
...[...PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS]
|
|
82
|
+
.filter((key) => !['registrar', 'registrant', 'abuse'].includes(key)),
|
|
83
|
+
'registryContactsExcluded',
|
|
84
|
+
]);
|
|
62
85
|
const LOOKUP_IDN_ANALYSIS_KEYS = new Set([
|
|
63
86
|
'version', 'mappingVersion', 'asciiDomain', 'unicodeDomain', 'hasIdn',
|
|
64
87
|
'scripts', 'labels', 'mixedScript', 'skeleton', 'referenceMatches', 'findings',
|
|
@@ -166,6 +189,28 @@ const LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS = new Set([
|
|
|
166
189
|
// Bounded cross-row comparison evidence.
|
|
167
190
|
'bulkComparison', 'technology', 'tls', 'ids', 'issuerLabel', 'spkiSha256',
|
|
168
191
|
]);
|
|
192
|
+
const LOOKUP_AVAILABILITY_CREDENTIAL_CATEGORY_KEYS = new Set([
|
|
193
|
+
'password', 'email', 'username', 'one_time_code', 'payment',
|
|
194
|
+
]);
|
|
195
|
+
const LOOKUP_AVAILABILITY_VALUE_PATHS = new Set([
|
|
196
|
+
'dns.records.caa',
|
|
197
|
+
'dns.caaPolicy.records',
|
|
198
|
+
'http.response.bodyHash',
|
|
199
|
+
'pageIdentity.trackingIdentifiers',
|
|
200
|
+
'pageIdentity.fingerprints.exact',
|
|
201
|
+
'pageIdentity.fingerprints.normalizedHtml',
|
|
202
|
+
'pageIdentity.fingerprints.visibleText',
|
|
203
|
+
'pageIdentity.fingerprints.domStructure',
|
|
204
|
+
'pageIdentity.fingerprints.domStructure.similarity',
|
|
205
|
+
'pageIdentity.fingerprints.formStructure',
|
|
206
|
+
'pageIdentity.fingerprints.resourceHosts',
|
|
207
|
+
'pageIdentity.fingerprints.identifiers',
|
|
208
|
+
'pageIdentity.fingerprints.identifiers.values',
|
|
209
|
+
]);
|
|
210
|
+
const LOOKUP_AVAILABILITY_OWNER_PATHS = new Set([
|
|
211
|
+
'dns.records.https',
|
|
212
|
+
'dns.caaPolicy.queriedOwners',
|
|
213
|
+
]);
|
|
169
214
|
function recordOrNull(value) {
|
|
170
215
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
171
216
|
}
|
|
@@ -240,6 +285,12 @@ function projectPortableString(value) {
|
|
|
240
285
|
? sanitized
|
|
241
286
|
: sanitized.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH);
|
|
242
287
|
}
|
|
288
|
+
function projectLookupEvidenceAvailabilityString(value) {
|
|
289
|
+
const sanitized = projectPortableString(value)
|
|
290
|
+
.replace(/[\u0080-\u009f]/gu, ' ')
|
|
291
|
+
.replace(/\p{Default_Ignorable_Code_Point}/gu, '');
|
|
292
|
+
return isUriShapedLabel(value) ? null : sanitized;
|
|
293
|
+
}
|
|
243
294
|
function projectLookupEvidencePrivacySafeTreeValue(value, state, depth) {
|
|
244
295
|
consumePortableProjectionEntry(state, depth);
|
|
245
296
|
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
@@ -272,7 +323,7 @@ function projectLookupEvidencePrivacySafeTreeValue(value, state, depth) {
|
|
|
272
323
|
export function projectLookupEvidencePrivacySafeTree(value) {
|
|
273
324
|
return projectLookupEvidencePrivacySafeTreeValue(value, { entries: 0 }, 0);
|
|
274
325
|
}
|
|
275
|
-
function
|
|
326
|
+
function projectLookupEvidenceAvailabilityPublicValue(value, parentKey, state, depth) {
|
|
276
327
|
consumePortableProjectionEntry(state, depth);
|
|
277
328
|
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
278
329
|
return value;
|
|
@@ -286,7 +337,7 @@ function projectLookupEvidenceAvailabilityValue(value, parentKey, state, depth)
|
|
|
286
337
|
}
|
|
287
338
|
if (Array.isArray(value)) {
|
|
288
339
|
return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
|
|
289
|
-
.map((item) =>
|
|
340
|
+
.map((item) => projectLookupEvidenceAvailabilityPublicValue(item, parentKey, state, depth + 1));
|
|
290
341
|
}
|
|
291
342
|
const source = recordOrNull(value);
|
|
292
343
|
if (!source)
|
|
@@ -296,7 +347,49 @@ function projectLookupEvidenceAvailabilityValue(value, parentKey, state, depth)
|
|
|
296
347
|
if (!LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS.has(key)
|
|
297
348
|
|| privateEvidenceKey(key, item))
|
|
298
349
|
continue;
|
|
299
|
-
output[key] =
|
|
350
|
+
output[key] = projectLookupEvidenceAvailabilityPublicValue(item, key, state, depth + 1);
|
|
351
|
+
}
|
|
352
|
+
return output;
|
|
353
|
+
}
|
|
354
|
+
function projectLookupEvidenceAvailabilityValue(value, path, state, depth) {
|
|
355
|
+
consumePortableProjectionEntry(state, depth);
|
|
356
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
357
|
+
return value;
|
|
358
|
+
if (typeof value === 'string') {
|
|
359
|
+
const normalizedParent = normalizedEvidenceKey(path.at(-1) || '');
|
|
360
|
+
if (PORTABLE_URL_KEYS.has(normalizedParent))
|
|
361
|
+
return portableUri(value);
|
|
362
|
+
if (PORTABLE_ORIGIN_COLLECTION_KEYS.has(normalizedParent))
|
|
363
|
+
return portableOrigin(value);
|
|
364
|
+
return projectLookupEvidenceAvailabilityString(value);
|
|
365
|
+
}
|
|
366
|
+
if (Array.isArray(value)) {
|
|
367
|
+
return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
|
|
368
|
+
.map((item) => projectLookupEvidenceAvailabilityValue(item, path, state, depth + 1));
|
|
369
|
+
}
|
|
370
|
+
const source = recordOrNull(value);
|
|
371
|
+
if (!source)
|
|
372
|
+
return null;
|
|
373
|
+
const output = {};
|
|
374
|
+
for (const [key, item] of Object.entries(source).slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)) {
|
|
375
|
+
if (!LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS.has(key)
|
|
376
|
+
|| privateEvidenceKey(key, item))
|
|
377
|
+
continue;
|
|
378
|
+
const parentPath = path.join('.');
|
|
379
|
+
if (LOOKUP_AVAILABILITY_CREDENTIAL_CATEGORY_KEYS.has(key)
|
|
380
|
+
&& (parentPath !== 'credentialSurfaceProfile.inputs.categories'
|
|
381
|
+
|| !Number.isSafeInteger(item)
|
|
382
|
+
|| Number(item) < 0
|
|
383
|
+
|| Number(item) > LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS))
|
|
384
|
+
continue;
|
|
385
|
+
if (key === 'owner'
|
|
386
|
+
&& (!LOOKUP_AVAILABILITY_OWNER_PATHS.has(parentPath) || typeof item !== 'string'))
|
|
387
|
+
continue;
|
|
388
|
+
if (key === 'value'
|
|
389
|
+
&& (!LOOKUP_AVAILABILITY_VALUE_PATHS.has(parentPath)
|
|
390
|
+
|| (item !== null && typeof item !== 'string')))
|
|
391
|
+
continue;
|
|
392
|
+
output[key] = projectLookupEvidenceAvailabilityValue(item, [...path, key], state, depth + 1);
|
|
300
393
|
}
|
|
301
394
|
return output;
|
|
302
395
|
}
|
|
@@ -306,23 +399,65 @@ function projectLookupEvidenceAvailabilityValue(value, parentKey, state, depth)
|
|
|
306
399
|
* extension cannot become authenticated evidence merely by avoiding a
|
|
307
400
|
* credential-key denylist.
|
|
308
401
|
*/
|
|
309
|
-
|
|
402
|
+
function projectLookupEvidenceAvailabilityWithKeys(value, rootKeys, registryContactsExcluded, currentPrivacyRules) {
|
|
310
403
|
const source = recordOrNull(value);
|
|
311
404
|
if (!source)
|
|
312
405
|
return null;
|
|
313
406
|
const state = { entries: 0 };
|
|
314
407
|
consumePortableProjectionEntry(state, 0);
|
|
315
408
|
const output = {};
|
|
316
|
-
for (const key of
|
|
409
|
+
for (const key of rootKeys) {
|
|
317
410
|
if (!Object.hasOwn(source, key))
|
|
318
411
|
continue;
|
|
319
412
|
const item = source[key];
|
|
320
413
|
if (privateEvidenceKey(key, item))
|
|
321
414
|
continue;
|
|
322
|
-
output[key] =
|
|
415
|
+
output[key] = currentPrivacyRules
|
|
416
|
+
? projectLookupEvidenceAvailabilityValue(item, [key], state, 1)
|
|
417
|
+
: projectLookupEvidenceAvailabilityPublicValue(item, key, state, 1);
|
|
418
|
+
}
|
|
419
|
+
if (registryContactsExcluded)
|
|
420
|
+
output.registryContactsExcluded = true;
|
|
421
|
+
const pageIdentity = recordOrNull(source.pageIdentity);
|
|
422
|
+
const publicationMetadata = pageIdentity?.publicationMetadata;
|
|
423
|
+
if (publicationMetadata !== undefined) {
|
|
424
|
+
if (!['success', 'partial'].includes(String(pageIdentity?.status))
|
|
425
|
+
|| !validPagePublicationMetadata(publicationMetadata)) {
|
|
426
|
+
throw new TypeError('Lookup evidence contains invalid page publication metadata.');
|
|
427
|
+
}
|
|
428
|
+
const projectedPageIdentity = recordOrNull(output.pageIdentity);
|
|
429
|
+
if (projectedPageIdentity) {
|
|
430
|
+
projectedPageIdentity.publicationMetadata = projectLookupEvidencePrivacySafeTree(publicationMetadata);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const httpResponse = recordOrNull(recordOrNull(source.http)?.response);
|
|
434
|
+
const sourceHttp = recordOrNull(source.http);
|
|
435
|
+
const deliveryMetadata = httpResponse?.deliveryMetadata;
|
|
436
|
+
if (deliveryMetadata !== undefined) {
|
|
437
|
+
if (!['success', 'partial'].includes(String(sourceHttp?.status))
|
|
438
|
+
|| !validHttpDeliveryMetadata(deliveryMetadata)) {
|
|
439
|
+
throw new TypeError('Lookup evidence contains invalid HTTP delivery metadata.');
|
|
440
|
+
}
|
|
441
|
+
const projectedHttp = recordOrNull(output.http);
|
|
442
|
+
const projectedResponse = recordOrNull(projectedHttp?.response);
|
|
443
|
+
if (projectedResponse) {
|
|
444
|
+
projectedResponse.deliveryMetadata = projectLookupEvidencePrivacySafeTree(deliveryMetadata);
|
|
445
|
+
}
|
|
323
446
|
}
|
|
324
447
|
return output;
|
|
325
448
|
}
|
|
449
|
+
/** Exact availability projection used only to validate public schema 26. */
|
|
450
|
+
export function projectLookupEvidenceAvailabilityPublic(value) {
|
|
451
|
+
return projectLookupEvidenceAvailabilityWithKeys(value, PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS, false, false);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Current availability projection. Registry-derived registrar, registrant,
|
|
455
|
+
* and abuse contact routes are deliberately excluded at the root boundary;
|
|
456
|
+
* the marker prevents that privacy omission from being read as source absence.
|
|
457
|
+
*/
|
|
458
|
+
export function projectLookupEvidenceAvailability(value) {
|
|
459
|
+
return projectLookupEvidenceAvailabilityWithKeys(value, LOOKUP_AVAILABILITY_ANALYSIS_KEYS, true, true);
|
|
460
|
+
}
|
|
326
461
|
export function assertLookupEvidencePrivacySafeTree(value) {
|
|
327
462
|
assertLookupEvidencePortableTree(value);
|
|
328
463
|
const pending = [value];
|
|
@@ -450,10 +585,7 @@ function boundedHttpStatus(value) {
|
|
|
450
585
|
}
|
|
451
586
|
function boundedTimestamp(value) {
|
|
452
587
|
const text = boundedString(value, 64);
|
|
453
|
-
|
|
454
|
-
return null;
|
|
455
|
-
const time = Date.parse(text);
|
|
456
|
-
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
|
588
|
+
return text ? normalizeExplicitIsoTimestamp(text) : null;
|
|
457
589
|
}
|
|
458
590
|
function boundedEndpoint(value) {
|
|
459
591
|
const text = boundedString(value, 2048);
|
|
@@ -478,11 +610,8 @@ function boundedStringList(value, count, length) {
|
|
|
478
610
|
.filter((item) => item !== null))];
|
|
479
611
|
}
|
|
480
612
|
function boundedHostname(value) {
|
|
481
|
-
const text = boundedString(value, 253)?.
|
|
482
|
-
|
|
483
|
-
return null;
|
|
484
|
-
}
|
|
485
|
-
return text;
|
|
613
|
+
const text = boundedString(value, 253)?.replace(/\.+$/u, '') || null;
|
|
614
|
+
return text && isValidAsciiHostname(text, { requireDot: false }) ? text.toLowerCase() : null;
|
|
486
615
|
}
|
|
487
616
|
function canonicalIpv4(value) {
|
|
488
617
|
const text = boundedString(value, 64);
|
|
@@ -970,6 +1099,353 @@ function boundedSourceError(value, fallback) {
|
|
|
970
1099
|
.trim();
|
|
971
1100
|
return normalized.slice(0, 10_000) || fallback;
|
|
972
1101
|
}
|
|
1102
|
+
function nullableText(maximum) {
|
|
1103
|
+
return (value) => value === null ? null : boundedString(value, maximum) ?? undefined;
|
|
1104
|
+
}
|
|
1105
|
+
function nullableTimestamp(value) {
|
|
1106
|
+
return value === null ? null : boundedTimestamp(value) ?? undefined;
|
|
1107
|
+
}
|
|
1108
|
+
function booleanValue(nullable = false) {
|
|
1109
|
+
return (value) => typeof value === 'boolean' ? value : nullable && value === null ? null : undefined;
|
|
1110
|
+
}
|
|
1111
|
+
function nullableInteger(maximum) {
|
|
1112
|
+
return (value) => value === null ? null : boundedInteger(value, maximum) ?? undefined;
|
|
1113
|
+
}
|
|
1114
|
+
function enumeration(values, nullable = false) {
|
|
1115
|
+
return (value) => typeof value === 'string' && values.has(value)
|
|
1116
|
+
? value
|
|
1117
|
+
: nullable && value === null ? null : undefined;
|
|
1118
|
+
}
|
|
1119
|
+
const LOOKUP_RDAP_PORTABLE_SCALARS = Object.freeze({
|
|
1120
|
+
objectClassName: nullableText(80),
|
|
1121
|
+
language: nullableText(35),
|
|
1122
|
+
conformanceTruncated: booleanValue(),
|
|
1123
|
+
redactionsTruncated: booleanValue(),
|
|
1124
|
+
port43: nullableText(300),
|
|
1125
|
+
parentHandle: nullableText(300),
|
|
1126
|
+
linksTruncated: booleanValue(),
|
|
1127
|
+
noticesTruncated: booleanValue(),
|
|
1128
|
+
remarksTruncated: booleanValue(),
|
|
1129
|
+
serverTruncated: booleanValue(),
|
|
1130
|
+
statusesTruncated: booleanValue(),
|
|
1131
|
+
eventsTruncated: booleanValue(),
|
|
1132
|
+
domain: nullableText(253),
|
|
1133
|
+
unicodeDomain: nullableText(253),
|
|
1134
|
+
handle: nullableText(300),
|
|
1135
|
+
nameserversTruncated: booleanValue(),
|
|
1136
|
+
nameserverAddressesTruncated: booleanValue(),
|
|
1137
|
+
dnssec: enumeration(new Set(['Signed', 'Unsigned', 'Unknown']), true),
|
|
1138
|
+
zoneSigned: booleanValue(true),
|
|
1139
|
+
delegationSigned: booleanValue(true),
|
|
1140
|
+
dsDataTruncated: booleanValue(),
|
|
1141
|
+
variantsTruncated: booleanValue(),
|
|
1142
|
+
registrarIanaId: nullableText(300),
|
|
1143
|
+
entitiesTruncated: booleanValue(),
|
|
1144
|
+
name: nullableText(300),
|
|
1145
|
+
startAddress: nullableText(80),
|
|
1146
|
+
endAddress: nullableText(80),
|
|
1147
|
+
cidrsTruncated: booleanValue(),
|
|
1148
|
+
country: nullableText(2),
|
|
1149
|
+
networkType: nullableText(160),
|
|
1150
|
+
startAutnum: nullableInteger(4_294_967_295),
|
|
1151
|
+
endAutnum: nullableInteger(4_294_967_295),
|
|
1152
|
+
autnumType: nullableText(160),
|
|
1153
|
+
});
|
|
1154
|
+
const LOOKUP_RDAP_PORTABLE_STRING_LISTS = Object.freeze({
|
|
1155
|
+
conformance: [50, 160],
|
|
1156
|
+
serverTruncationReasons: [8, 160],
|
|
1157
|
+
statuses: [100, 160],
|
|
1158
|
+
nameservers: [200, 253],
|
|
1159
|
+
truncatedEntityRoles: [11, 80],
|
|
1160
|
+
cidrs: [200, 160],
|
|
1161
|
+
});
|
|
1162
|
+
const LOOKUP_RDAP_EVENT_SCALARS = Object.freeze({
|
|
1163
|
+
action: nullableText(100),
|
|
1164
|
+
date: nullableText(64),
|
|
1165
|
+
});
|
|
1166
|
+
const LOOKUP_RDAP_LIFECYCLE_SCALARS = Object.freeze({
|
|
1167
|
+
createdDate: nullableText(64),
|
|
1168
|
+
reregistrationDate: nullableText(64),
|
|
1169
|
+
expiryDate: nullableText(64),
|
|
1170
|
+
updatedDate: nullableText(64),
|
|
1171
|
+
transferDate: nullableText(64),
|
|
1172
|
+
deletionDate: nullableText(64),
|
|
1173
|
+
reinstantiationDate: nullableText(64),
|
|
1174
|
+
databaseUpdatedDate: nullableText(64),
|
|
1175
|
+
createdDateIso: nullableTimestamp,
|
|
1176
|
+
reregistrationDateIso: nullableTimestamp,
|
|
1177
|
+
expiryDateIso: nullableTimestamp,
|
|
1178
|
+
updatedDateIso: nullableTimestamp,
|
|
1179
|
+
transferDateIso: nullableTimestamp,
|
|
1180
|
+
deletionDateIso: nullableTimestamp,
|
|
1181
|
+
reinstantiationDateIso: nullableTimestamp,
|
|
1182
|
+
databaseUpdatedDateIso: nullableTimestamp,
|
|
1183
|
+
});
|
|
1184
|
+
const LOOKUP_RDAP_REDACTION_SCALARS = Object.freeze({
|
|
1185
|
+
name: nullableText(300),
|
|
1186
|
+
reason: nullableText(300),
|
|
1187
|
+
method: nullableText(80),
|
|
1188
|
+
pathLanguage: nullableText(80),
|
|
1189
|
+
prePath: nullableText(512),
|
|
1190
|
+
postPath: nullableText(512),
|
|
1191
|
+
replacementPath: nullableText(512),
|
|
1192
|
+
});
|
|
1193
|
+
const LOOKUP_RDAP_REGISTRAR_SCALARS = Object.freeze({
|
|
1194
|
+
handle: nullableText(200),
|
|
1195
|
+
name: nullableText(300),
|
|
1196
|
+
org: nullableText(300),
|
|
1197
|
+
truncated: booleanValue(),
|
|
1198
|
+
});
|
|
1199
|
+
const LOOKUP_RDAP_PUBLIC_ID_SCALARS = Object.freeze({
|
|
1200
|
+
type: nullableText(160),
|
|
1201
|
+
identifier: nullableText(300),
|
|
1202
|
+
});
|
|
1203
|
+
const LOOKUP_RDAP_NAMESERVER_SCALARS = Object.freeze({
|
|
1204
|
+
name: nullableText(253),
|
|
1205
|
+
});
|
|
1206
|
+
const LOOKUP_RDAP_DS_SCALARS = Object.freeze({
|
|
1207
|
+
keyTag: nullableInteger(65_535),
|
|
1208
|
+
algorithm: nullableInteger(255),
|
|
1209
|
+
digestType: nullableInteger(255),
|
|
1210
|
+
digest: nullableText(512),
|
|
1211
|
+
});
|
|
1212
|
+
const LOOKUP_RDAP_VARIANT_SCALARS = Object.freeze({
|
|
1213
|
+
idnTable: nullableText(300),
|
|
1214
|
+
});
|
|
1215
|
+
const LOOKUP_RDAP_VARIANT_NAME_SCALARS = Object.freeze({
|
|
1216
|
+
ldhName: nullableText(253),
|
|
1217
|
+
unicodeName: nullableText(253),
|
|
1218
|
+
});
|
|
1219
|
+
const LOOKUP_WHOIS_PORTABLE_SCALARS = Object.freeze({
|
|
1220
|
+
domainName: nullableText(253),
|
|
1221
|
+
registryDomainId: nullableText(300),
|
|
1222
|
+
registrar: nullableText(300),
|
|
1223
|
+
registrarIanaId: nullableText(300),
|
|
1224
|
+
createdDate: nullableText(100),
|
|
1225
|
+
expiryDate: nullableText(100),
|
|
1226
|
+
updatedDate: nullableText(100),
|
|
1227
|
+
createdDateIso: nullableTimestamp,
|
|
1228
|
+
expiryDateIso: nullableTimestamp,
|
|
1229
|
+
updatedDateIso: nullableTimestamp,
|
|
1230
|
+
statusesTruncated: booleanValue(),
|
|
1231
|
+
nameserversTruncated: booleanValue(),
|
|
1232
|
+
dnssec: nullableText(300),
|
|
1233
|
+
chainStatus: enumeration(new Set(['complete', 'partial'])),
|
|
1234
|
+
authoritativeHop: nullableText(253),
|
|
1235
|
+
failedHop: nullableText(253),
|
|
1236
|
+
conflictingHop: nullableText(253),
|
|
1237
|
+
});
|
|
1238
|
+
const LOOKUP_REGISTRY_INSIGHT_PORTABLE_KEYS = new Set([
|
|
1239
|
+
'version', 'contactDisclosure', 'lifecycle', 'reconciliation', 'publications',
|
|
1240
|
+
'rdapCapabilities',
|
|
1241
|
+
]);
|
|
1242
|
+
function projectKnownArray(value, projectors, maximum) {
|
|
1243
|
+
if (!Array.isArray(value))
|
|
1244
|
+
return [];
|
|
1245
|
+
return value.slice(0, maximum)
|
|
1246
|
+
.map((item) => projectedKnownScalarRecord(item, projectors))
|
|
1247
|
+
.filter((item) => item !== null);
|
|
1248
|
+
}
|
|
1249
|
+
function projectedKnownScalarRecord(value, projectors) {
|
|
1250
|
+
const source = recordOrNull(value);
|
|
1251
|
+
if (!source)
|
|
1252
|
+
return null;
|
|
1253
|
+
const output = {};
|
|
1254
|
+
for (const [key, projector] of Object.entries(projectors)) {
|
|
1255
|
+
if (!Object.hasOwn(source, key))
|
|
1256
|
+
continue;
|
|
1257
|
+
const item = projector(source[key]);
|
|
1258
|
+
if (item !== undefined)
|
|
1259
|
+
output[key] = item;
|
|
1260
|
+
}
|
|
1261
|
+
return output;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Positive portable projection for normalized RDAP. Contact entities, vCards,
|
|
1265
|
+
* entity inventories, arbitrary links/notices/remarks, and raw payloads never
|
|
1266
|
+
* cross this boundary. The marker prevents deliberate omission from becoming
|
|
1267
|
+
* an apparent source assertion that contacts were absent.
|
|
1268
|
+
*/
|
|
1269
|
+
export function projectLookupEvidenceRdapPublication(value) {
|
|
1270
|
+
const source = recordOrNull(value);
|
|
1271
|
+
if (!source)
|
|
1272
|
+
return null;
|
|
1273
|
+
const output = projectedKnownScalarRecord(source, LOOKUP_RDAP_PORTABLE_SCALARS) || {};
|
|
1274
|
+
for (const [key, [count, length]] of Object.entries(LOOKUP_RDAP_PORTABLE_STRING_LISTS)) {
|
|
1275
|
+
if (Object.hasOwn(source, key))
|
|
1276
|
+
output[key] = boundedStringList(source[key], count, length);
|
|
1277
|
+
}
|
|
1278
|
+
if (Object.hasOwn(source, 'events')) {
|
|
1279
|
+
output.events = projectKnownArray(source.events, LOOKUP_RDAP_EVENT_SCALARS, 100)
|
|
1280
|
+
.filter((event) => typeof event.action === 'string' || typeof event.date === 'string');
|
|
1281
|
+
}
|
|
1282
|
+
if (Object.hasOwn(source, 'lifecycle')) {
|
|
1283
|
+
output.lifecycle = projectedKnownScalarRecord(source.lifecycle, LOOKUP_RDAP_LIFECYCLE_SCALARS);
|
|
1284
|
+
}
|
|
1285
|
+
if (Object.hasOwn(source, 'redactions')) {
|
|
1286
|
+
const inputRedactions = Array.isArray(source.redactions) ? source.redactions : [];
|
|
1287
|
+
const redactions = projectKnownArray(inputRedactions, LOOKUP_RDAP_REDACTION_SCALARS, 100)
|
|
1288
|
+
.filter((redaction) => Object.values(redaction).some((item) => item !== null));
|
|
1289
|
+
output.redactions = redactions;
|
|
1290
|
+
output.redactionsTruncated = source.redactionsTruncated === true
|
|
1291
|
+
|| !Array.isArray(source.redactions)
|
|
1292
|
+
|| inputRedactions.length > 100
|
|
1293
|
+
|| redactions.length < Math.min(inputRedactions.length, 100);
|
|
1294
|
+
}
|
|
1295
|
+
if (Object.hasOwn(source, 'nameserverDetails')) {
|
|
1296
|
+
output.nameserverDetails = (Array.isArray(source.nameserverDetails) ? source.nameserverDetails : [])
|
|
1297
|
+
.slice(0, 200)
|
|
1298
|
+
.map((item) => {
|
|
1299
|
+
const nameserver = projectedKnownScalarRecord(item, LOOKUP_RDAP_NAMESERVER_SCALARS);
|
|
1300
|
+
const original = recordOrNull(item);
|
|
1301
|
+
if (!nameserver || !original || typeof nameserver.name !== 'string')
|
|
1302
|
+
return null;
|
|
1303
|
+
nameserver.addresses = boundedStringList(original.addresses, 20, 80);
|
|
1304
|
+
return nameserver;
|
|
1305
|
+
})
|
|
1306
|
+
.filter((item) => item !== null);
|
|
1307
|
+
}
|
|
1308
|
+
if (Object.hasOwn(source, 'dsData')) {
|
|
1309
|
+
output.dsData = projectKnownArray(source.dsData, LOOKUP_RDAP_DS_SCALARS, 50)
|
|
1310
|
+
.filter((item) => Number.isInteger(item.keyTag)
|
|
1311
|
+
&& Number.isInteger(item.algorithm)
|
|
1312
|
+
&& Number.isInteger(item.digestType)
|
|
1313
|
+
&& typeof item.digest === 'string'
|
|
1314
|
+
&& item.digest.length % 2 === 0
|
|
1315
|
+
&& /^[0-9a-f]+$/iu.test(item.digest));
|
|
1316
|
+
}
|
|
1317
|
+
if (Object.hasOwn(source, 'variants') && Array.isArray(source.variants)) {
|
|
1318
|
+
output.variants = source.variants.slice(0, 20).map((item) => {
|
|
1319
|
+
const group = projectedKnownScalarRecord(item, LOOKUP_RDAP_VARIANT_SCALARS);
|
|
1320
|
+
if (!group)
|
|
1321
|
+
return null;
|
|
1322
|
+
const original = recordOrNull(item);
|
|
1323
|
+
const variantNames = projectKnownArray(original?.variantNames, LOOKUP_RDAP_VARIANT_NAME_SCALARS, 50)
|
|
1324
|
+
.filter((name) => typeof name.ldhName === 'string' || typeof name.unicodeName === 'string');
|
|
1325
|
+
const relation = boundedStringList(original?.relation, 20, 100);
|
|
1326
|
+
group.variantNames = variantNames;
|
|
1327
|
+
group.relation = relation;
|
|
1328
|
+
return typeof group.idnTable === 'string' || variantNames.length || relation.length
|
|
1329
|
+
? group
|
|
1330
|
+
: null;
|
|
1331
|
+
}).filter((item) => item !== null);
|
|
1332
|
+
}
|
|
1333
|
+
if (Object.hasOwn(source, 'registrar')) {
|
|
1334
|
+
const registrar = projectedKnownScalarRecord(source.registrar, LOOKUP_RDAP_REGISTRAR_SCALARS);
|
|
1335
|
+
if (registrar) {
|
|
1336
|
+
const original = recordOrNull(source.registrar);
|
|
1337
|
+
registrar.roles = boundedStringList(original?.roles, 12, 80);
|
|
1338
|
+
registrar.publicIds = projectKnownArray(original?.publicIds, LOOKUP_RDAP_PUBLIC_ID_SCALARS, 20)
|
|
1339
|
+
.filter((publicId) => typeof publicId.type === 'string'
|
|
1340
|
+
&& typeof publicId.identifier === 'string');
|
|
1341
|
+
}
|
|
1342
|
+
output.registrar = registrar;
|
|
1343
|
+
}
|
|
1344
|
+
output.contactsExcluded = true;
|
|
1345
|
+
return output;
|
|
1346
|
+
}
|
|
1347
|
+
/** Positive portable projection for normalized WHOIS publication fields. */
|
|
1348
|
+
export function projectLookupEvidenceWhoisPublication(value) {
|
|
1349
|
+
const input = recordOrNull(value);
|
|
1350
|
+
if (!input)
|
|
1351
|
+
return null;
|
|
1352
|
+
const output = projectedKnownScalarRecord(input, LOOKUP_WHOIS_PORTABLE_SCALARS) || {};
|
|
1353
|
+
if (Object.hasOwn(input, 'lifecycle')) {
|
|
1354
|
+
output.lifecycle = projectedKnownScalarRecord(input.lifecycle, LOOKUP_RDAP_LIFECYCLE_SCALARS);
|
|
1355
|
+
}
|
|
1356
|
+
for (const [key, count, length] of [
|
|
1357
|
+
['statuses', 100, 160],
|
|
1358
|
+
['nameservers', 200, 253],
|
|
1359
|
+
['fieldsTruncated', 64, 80],
|
|
1360
|
+
]) {
|
|
1361
|
+
if (Object.hasOwn(input, key))
|
|
1362
|
+
output[key] = boundedStringList(input[key], count, length);
|
|
1363
|
+
}
|
|
1364
|
+
output.contactsExcluded = true;
|
|
1365
|
+
return output;
|
|
1366
|
+
}
|
|
1367
|
+
/** Exact positive projection for one current-schema RDAP source wrapper. */
|
|
1368
|
+
export function projectLookupEvidenceRdapSourcePublication(value) {
|
|
1369
|
+
const source = recordOrNull(value);
|
|
1370
|
+
if (!source)
|
|
1371
|
+
return null;
|
|
1372
|
+
const status = sourceStatus(source.status, RDAP_SOURCE_STATUSES, 'error');
|
|
1373
|
+
const attempts = boundedRdapAttempts(source.attempts, 16);
|
|
1374
|
+
if (status === 'error')
|
|
1375
|
+
return {
|
|
1376
|
+
status,
|
|
1377
|
+
error: boundedSourceError(source.error, 'RDAP source reported an error.'),
|
|
1378
|
+
attempts,
|
|
1379
|
+
};
|
|
1380
|
+
const endpoint = boundedEndpoint(source.endpoint);
|
|
1381
|
+
const hasPublication = status === 'success' || status === 'partial';
|
|
1382
|
+
const parsed = hasPublication ? projectLookupEvidenceRdapPublication(source.parsed) : null;
|
|
1383
|
+
if (status === 'success' && parsed === null)
|
|
1384
|
+
return null;
|
|
1385
|
+
return {
|
|
1386
|
+
status,
|
|
1387
|
+
endpoint,
|
|
1388
|
+
transportSecurity: endpoint ? endpoint.startsWith('https:') ? 'https' : 'http' : null,
|
|
1389
|
+
httpStatus: boundedHttpStatus(source.httpStatus),
|
|
1390
|
+
fetchedAt: boundedTimestamp(source.fetchedAt),
|
|
1391
|
+
attempts,
|
|
1392
|
+
parsed,
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function projectedWhoisChainHop(value) {
|
|
1396
|
+
const hop = recordOrNull(value);
|
|
1397
|
+
if (!hop)
|
|
1398
|
+
return null;
|
|
1399
|
+
const queryProfile = typeof hop.queryProfile === 'string' && WHOIS_QUERY_PROFILES.has(hop.queryProfile)
|
|
1400
|
+
? hop.queryProfile
|
|
1401
|
+
: null;
|
|
1402
|
+
const status = typeof hop.status === 'string'
|
|
1403
|
+
&& ['success', 'error', 'not_issued', 'unknown'].includes(hop.status)
|
|
1404
|
+
? hop.status
|
|
1405
|
+
: 'unknown';
|
|
1406
|
+
return {
|
|
1407
|
+
server: boundedHostname(hop.server),
|
|
1408
|
+
address: boundedString(hop.address, 64),
|
|
1409
|
+
queriedAt: boundedTimestamp(hop.queriedAt),
|
|
1410
|
+
queryProfile,
|
|
1411
|
+
responseEncoding: hop.responseEncoding === 'utf-8' ? 'utf-8' : null,
|
|
1412
|
+
status,
|
|
1413
|
+
detail: boundedString(hop.detail, 240),
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
/** Exact positive projection for one current-schema WHOIS source wrapper. */
|
|
1417
|
+
export function projectLookupEvidenceWhoisSourcePublication(value) {
|
|
1418
|
+
const source = recordOrNull(value);
|
|
1419
|
+
if (!source)
|
|
1420
|
+
return null;
|
|
1421
|
+
const status = sourceStatus(source.status, WHOIS_SOURCE_STATUSES, 'error');
|
|
1422
|
+
if (status === 'error')
|
|
1423
|
+
return {
|
|
1424
|
+
status,
|
|
1425
|
+
error: boundedSourceError(source.error, 'WHOIS source reported an error.'),
|
|
1426
|
+
};
|
|
1427
|
+
const hasPublication = status === 'complete' || status === 'partial';
|
|
1428
|
+
const parsed = hasPublication ? projectLookupEvidenceWhoisPublication(source.parsed) : null;
|
|
1429
|
+
if (status === 'complete' && parsed === null)
|
|
1430
|
+
return null;
|
|
1431
|
+
return {
|
|
1432
|
+
status,
|
|
1433
|
+
queriedAt: boundedTimestamp(source.queriedAt),
|
|
1434
|
+
authoritativeHop: boundedHostname(source.authoritativeHop),
|
|
1435
|
+
failedHop: boundedHostname(source.failedHop),
|
|
1436
|
+
conflictingHop: boundedHostname(source.conflictingHop),
|
|
1437
|
+
parsed,
|
|
1438
|
+
chain: hasPublication
|
|
1439
|
+
? (Array.isArray(source.chain) ? source.chain : [])
|
|
1440
|
+
.slice(0, 16)
|
|
1441
|
+
.map(projectedWhoisChainHop)
|
|
1442
|
+
.filter((hop) => hop !== null)
|
|
1443
|
+
: [],
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
export function projectLookupEvidenceRegistryInsights(value) {
|
|
1447
|
+
return projectedKnownRecord(value, LOOKUP_REGISTRY_INSIGHT_PORTABLE_KEYS);
|
|
1448
|
+
}
|
|
973
1449
|
function sourceStatus(value, allowed, fallback) {
|
|
974
1450
|
return typeof value === 'string' && allowed.has(value) ? value : fallback;
|
|
975
1451
|
}
|
|
@@ -984,19 +1460,15 @@ function rdapSource(rdap, diagnostics) {
|
|
|
984
1460
|
attempts,
|
|
985
1461
|
};
|
|
986
1462
|
const hasPublication = status === 'success' || status === 'partial';
|
|
1463
|
+
const endpoint = boundedEndpoint(source.rdapServer ?? diagnostics?.endpoint);
|
|
987
1464
|
return {
|
|
988
1465
|
status,
|
|
989
|
-
endpoint
|
|
990
|
-
transportSecurity:
|
|
991
|
-
? source.transportSecurity
|
|
992
|
-
: diagnostics?.transportSecurity === 'http' || diagnostics?.transportSecurity === 'https'
|
|
993
|
-
? diagnostics.transportSecurity
|
|
994
|
-
: null,
|
|
1466
|
+
endpoint,
|
|
1467
|
+
transportSecurity: endpoint ? endpoint.startsWith('https:') ? 'https' : 'http' : null,
|
|
995
1468
|
httpStatus: boundedHttpStatus(source.upstreamStatus ?? diagnostics?.httpStatus),
|
|
996
1469
|
fetchedAt: boundedTimestamp(source.fetchedAt ?? diagnostics?.fetchedAt),
|
|
997
1470
|
attempts,
|
|
998
|
-
parsed: hasPublication ?
|
|
999
|
-
raw: hasPublication ? projectLookupEvidencePrivacySafeTree(source.data) : null,
|
|
1471
|
+
parsed: hasPublication ? projectLookupEvidenceRdapPublication(source.parsed) : null,
|
|
1000
1472
|
};
|
|
1001
1473
|
}
|
|
1002
1474
|
function whoisSource(whois, diagnostics) {
|
|
@@ -1017,7 +1489,7 @@ function whoisSource(whois, diagnostics) {
|
|
|
1017
1489
|
authoritativeHop: boundedHostname(parsed?.authoritativeHop ?? diagnostics?.authoritativeHop),
|
|
1018
1490
|
failedHop: boundedHostname(parsed?.failedHop ?? diagnostics?.failedHop),
|
|
1019
1491
|
conflictingHop: boundedHostname(parsed?.conflictingHop ?? diagnostics?.conflictingHop),
|
|
1020
|
-
parsed: hasPublication ?
|
|
1492
|
+
parsed: hasPublication ? projectLookupEvidenceWhoisPublication(parsed) : null,
|
|
1021
1493
|
chain,
|
|
1022
1494
|
};
|
|
1023
1495
|
}
|
|
@@ -1031,7 +1503,7 @@ function registrarPublicationComparison(body, registryParsed) {
|
|
|
1031
1503
|
return null;
|
|
1032
1504
|
const reportedStatus = registrar?.status ?? registrarDiagnostics?.status;
|
|
1033
1505
|
const parsed = ['success', 'partial'].includes(String(reportedStatus))
|
|
1034
|
-
?
|
|
1506
|
+
? projectLookupEvidenceRdapPublication(registrar?.parsed)
|
|
1035
1507
|
: null;
|
|
1036
1508
|
const registrarStatus = typeof reportedStatus === 'string' && REGISTRAR_RDAP_STATUSES.has(reportedStatus)
|
|
1037
1509
|
? (reportedStatus === 'success' && !parsed ? 'partial' : reportedStatus)
|
|
@@ -1043,7 +1515,11 @@ function registrarPublicationComparison(body, registryParsed) {
|
|
|
1043
1515
|
}
|
|
1044
1516
|
export function buildLookupEvidence(response, options = {}) {
|
|
1045
1517
|
assertBoundedJsonStructure(response, 'Lookup response');
|
|
1046
|
-
const { generatedAt = new Date().toISOString(), idnAnalysis = null, applicationVersion = null } = options;
|
|
1518
|
+
const { generatedAt: generatedAtValue = new Date().toISOString(), idnAnalysis = null, applicationVersion = null } = options;
|
|
1519
|
+
const generatedAt = normalizeExplicitIsoTimestamp(generatedAtValue);
|
|
1520
|
+
if (!generatedAt) {
|
|
1521
|
+
throw new TypeError('Lookup evidence generation time must be valid and include an explicit timezone.');
|
|
1522
|
+
}
|
|
1047
1523
|
const body = recordOrNull(response) || {};
|
|
1048
1524
|
const rdap = recordOrNull(body.rdap);
|
|
1049
1525
|
const whois = recordOrNull(body.whois);
|
|
@@ -1051,8 +1527,14 @@ export function buildLookupEvidence(response, options = {}) {
|
|
|
1051
1527
|
const projectedDiagnostics = lookupDiagnostics(diagnostics);
|
|
1052
1528
|
const rdapDiagnostics = recordOrNull(projectedDiagnostics.rdap);
|
|
1053
1529
|
const whoisDiagnostics = recordOrNull(projectedDiagnostics.whois);
|
|
1054
|
-
const projectedRdap = rdapSource(rdap, rdapDiagnostics);
|
|
1055
|
-
const projectedWhois = whoisSource(whois, whoisDiagnostics);
|
|
1530
|
+
const projectedRdap = projectLookupEvidenceRdapSourcePublication(rdapSource(rdap, rdapDiagnostics));
|
|
1531
|
+
const projectedWhois = projectLookupEvidenceWhoisSourcePublication(whoisSource(whois, whoisDiagnostics));
|
|
1532
|
+
if (!projectedRdap) {
|
|
1533
|
+
throw new TypeError('Lookup evidence cannot publish successful RDAP without normalized publication data.');
|
|
1534
|
+
}
|
|
1535
|
+
if (!projectedWhois) {
|
|
1536
|
+
throw new TypeError('Lookup evidence cannot publish complete WHOIS without normalized publication data.');
|
|
1537
|
+
}
|
|
1056
1538
|
const projectedRdapRecord = recordOrNull(projectedRdap) || {};
|
|
1057
1539
|
const projectedWhoisRecord = recordOrNull(projectedWhois) || {};
|
|
1058
1540
|
const rdapParsed = ['success', 'partial'].includes(String(projectedRdapRecord.status))
|
|
@@ -1068,10 +1550,10 @@ export function buildLookupEvidence(response, options = {}) {
|
|
|
1068
1550
|
const registryInsights = buildRegistryInsights({
|
|
1069
1551
|
rdapParsed,
|
|
1070
1552
|
rdapStatus: rdapDiagnostics?.status,
|
|
1071
|
-
rdapFetchedAt:
|
|
1553
|
+
rdapFetchedAt: projectedRdapRecord.fetchedAt,
|
|
1072
1554
|
whoisParsed,
|
|
1073
1555
|
whoisStatus: whoisDiagnostics?.status,
|
|
1074
|
-
whoisQueriedAt:
|
|
1556
|
+
whoisQueriedAt: projectedWhoisRecord.queriedAt,
|
|
1075
1557
|
});
|
|
1076
1558
|
const query = projectLookupEvidenceQuery(body);
|
|
1077
1559
|
const availabilityAnalysis = projectLookupEvidenceAvailability(body.availability);
|
|
@@ -1094,7 +1576,7 @@ export function buildLookupEvidence(response, options = {}) {
|
|
|
1094
1576
|
analysis: {
|
|
1095
1577
|
availability: availabilityAnalysis,
|
|
1096
1578
|
idn,
|
|
1097
|
-
registryInsights:
|
|
1579
|
+
registryInsights: projectLookupEvidenceRegistryInsights(registryInsights),
|
|
1098
1580
|
registryComparison: projectLookupEvidencePrivacySafeTree(compareRegistrySources(rdapParsed, whoisParsed, {
|
|
1099
1581
|
rdapStatus: rdapDiagnostics?.status,
|
|
1100
1582
|
whoisStatus: whoisDiagnostics?.status,
|