@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.
Files changed (235) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/provider-policy-admission.mjs +3 -1
  100. package/lib/rdap-bootstrap.mjs +76 -16
  101. package/lib/rdap-client.mjs +20 -12
  102. package/lib/rdap-normalization.mjs +306 -75
  103. package/lib/rdap-transport.mjs +2 -2
  104. package/lib/rdap-validation.mjs +65 -14
  105. package/lib/rdap.mjs +2 -2
  106. package/lib/registrable-domain.mjs +36 -0
  107. package/lib/registry-insights.mjs +7 -0
  108. package/lib/response-policy.mjs +27 -4
  109. package/lib/risk-calibration-summary.mjs +35 -33
  110. package/lib/risk-scoring.mjs +4 -3
  111. package/lib/safe-fetch.mjs +62 -40
  112. package/lib/security-txt.mjs +1 -1
  113. package/lib/smtp-transport-review.mjs +8 -5
  114. package/lib/sslbl-intelligence.mjs +6 -8
  115. package/lib/static-html-analysis.mjs +301 -4
  116. package/lib/structured-data-identity.mjs +51 -19
  117. package/lib/threat-intelligence-fixtures.mjs +2 -4
  118. package/lib/threat-intelligence-manifests.mjs +2 -7
  119. package/lib/threat-intelligence-runtime.mjs +3 -5
  120. package/lib/threat-intelligence-types.mjs +3 -0
  121. package/lib/threatfox-intelligence.mjs +7 -3
  122. package/lib/tls-intelligence.mjs +4 -4
  123. package/lib/urlhaus-intelligence.mjs +4 -2
  124. package/lib/urlscan-intelligence.mjs +6 -5
  125. package/lib/website-security-posture.mjs +1 -1
  126. package/lib/website-technology.mjs +1 -1
  127. package/lib/whois-authority.mjs +43 -8
  128. package/lib/whois-chain.mjs +25 -3
  129. package/lib/whois-normalization.mjs +3 -1
  130. package/lib/whois-transport.mjs +12 -3
  131. package/lib/zone-intent-review.mjs +1 -1
  132. package/package.json +12 -4
  133. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  134. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  135. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  136. package/packages/cases/case-migration-model.mjs +476 -0
  137. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  138. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  139. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  140. package/packages/cases/case-record-model.mjs +9 -0
  141. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  142. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  143. package/packages/cases/case-response-model.mjs +1604 -0
  144. package/packages/cases/case-response-packet.mjs +1312 -0
  145. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  146. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  147. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  148. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  149. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  150. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  151. package/packages/comparison/favicon-similarity.mjs +78 -0
  152. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  153. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  154. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  155. package/packages/contracts/analyst-review-state.mjs +50 -0
  156. package/packages/contracts/case-portability.mjs +1310 -0
  157. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  158. package/packages/contracts/cli-lookup.mjs +394 -0
  159. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  160. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  161. package/packages/contracts/domain-control-monitor.mjs +310 -0
  162. package/packages/contracts/domain-control-review.mjs +828 -0
  163. package/packages/contracts/domain-name.mjs +4 -0
  164. package/packages/contracts/external-observation-interchange.mjs +224 -0
  165. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  166. package/packages/contracts/investigation-portability.mjs +331 -0
  167. package/packages/contracts/offline-comparison.mjs +160 -0
  168. package/packages/contracts/risk-calibration.mjs +1288 -0
  169. package/packages/contracts/schema-compatibility.mjs +71 -0
  170. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  171. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  172. package/packages/contracts/workspace-portability.mjs +1347 -0
  173. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  174. package/packages/evidence/decision-fact.mjs +735 -0
  175. package/packages/evidence/domain-control-runtime.mjs +547 -0
  176. package/packages/evidence/domain-name.mjs +41 -0
  177. package/packages/evidence/observation.mjs +384 -0
  178. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  179. package/packages/interchange/retained-text.mjs +8 -0
  180. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  181. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  182. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  183. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  184. package/packages/investigation/bulk-review-policy.mjs +1 -0
  185. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  186. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  187. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  188. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  189. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  190. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  191. package/packages/monitoring/analyst-review-state.mjs +680 -0
  192. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
  193. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  194. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  195. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  196. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  197. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  198. package/packages/workspace/hostile-input.mjs +172 -0
  199. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  200. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  201. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  202. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  203. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  204. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  205. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  206. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  207. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  208. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  209. package/third-party-notices.txt +14 -14
  210. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  211. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  212. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  213. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  214. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  215. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  216. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  217. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  218. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  219. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  220. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  221. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  222. package/frontend/src/lib/analysis/scoring.js +0 -55
  223. package/frontend/src/lib/analysis/utils.js +0 -255
  224. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  225. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  226. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  227. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  228. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  229. package/lib/bounded-json-response.mjs +0 -170
  230. package/lib/lookup-response-contract.mjs +0 -1034
  231. package/lib/observation.mjs +0 -115
  232. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  233. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -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 LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION = 25;
7
- export const LOOKUP_EVIDENCE_SCHEMA_VERSION = 26;
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
- LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION,
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 LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
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 projectLookupEvidenceAvailabilityValue(value, parentKey, state, depth) {
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) => projectLookupEvidenceAvailabilityValue(item, parentKey, state, depth + 1));
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] = projectLookupEvidenceAvailabilityValue(item, key, state, depth + 1);
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
- export function projectLookupEvidenceAvailability(value) {
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 LOOKUP_AVAILABILITY_ANALYSIS_KEYS) {
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] = projectLookupEvidenceAvailabilityValue(item, key, state, 1);
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
- if (!text)
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)?.toLowerCase().replace(/\.+$/u, '') || null;
482
- if (!text || !text.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/iu.test(label))) {
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: boundedEndpoint(source.rdapServer ?? diagnostics?.endpoint),
990
- transportSecurity: source.transportSecurity === 'http' || source.transportSecurity === 'https'
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 ? projectLookupEvidencePrivacySafeTree(source.parsed) : null,
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 ? projectLookupEvidencePrivacySafeTree(parsed) : null,
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
- ? recordOrNull(projectLookupEvidencePrivacySafeTree(registrar?.parsed))
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: rdap?.fetchedAt,
1553
+ rdapFetchedAt: projectedRdapRecord.fetchedAt,
1072
1554
  whoisParsed,
1073
1555
  whoisStatus: whoisDiagnostics?.status,
1074
- whoisQueriedAt: recordOrNull(Array.isArray(whois?.chain) ? whois.chain[0] : null)?.queriedAt,
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: projectLookupEvidencePrivacySafeTree(registryInsights),
1579
+ registryInsights: projectLookupEvidenceRegistryInsights(registryInsights),
1098
1580
  registryComparison: projectLookupEvidencePrivacySafeTree(compareRegistrySources(rdapParsed, whoisParsed, {
1099
1581
  rdapStatus: rdapDiagnostics?.status,
1100
1582
  whoisStatus: whoisDiagnostics?.status,