@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) 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/project-metadata.mjs +1 -1
  100. package/lib/provider-policy-admission.mjs +3 -1
  101. package/lib/rdap-bootstrap.mjs +76 -16
  102. package/lib/rdap-client.mjs +20 -12
  103. package/lib/rdap-normalization.mjs +306 -75
  104. package/lib/rdap-transport.mjs +2 -2
  105. package/lib/rdap-validation.mjs +65 -14
  106. package/lib/rdap.mjs +2 -2
  107. package/lib/registrable-domain.mjs +36 -0
  108. package/lib/registry-insights.mjs +7 -0
  109. package/lib/response-policy.mjs +27 -4
  110. package/lib/risk-calibration-summary.mjs +35 -33
  111. package/lib/risk-scoring.mjs +4 -3
  112. package/lib/safe-fetch.mjs +62 -40
  113. package/lib/security-txt.mjs +1 -1
  114. package/lib/smtp-transport-review.mjs +8 -5
  115. package/lib/sslbl-intelligence.mjs +6 -8
  116. package/lib/static-html-analysis.mjs +301 -4
  117. package/lib/structured-data-identity.mjs +51 -19
  118. package/lib/threat-intelligence-fixtures.mjs +2 -4
  119. package/lib/threat-intelligence-manifests.mjs +2 -7
  120. package/lib/threat-intelligence-runtime.mjs +3 -5
  121. package/lib/threat-intelligence-types.mjs +3 -0
  122. package/lib/threatfox-intelligence.mjs +7 -3
  123. package/lib/tls-intelligence.mjs +4 -4
  124. package/lib/urlhaus-intelligence.mjs +4 -2
  125. package/lib/urlscan-intelligence.mjs +6 -5
  126. package/lib/website-security-posture.mjs +1 -1
  127. package/lib/website-technology.mjs +1 -1
  128. package/lib/whois-authority.mjs +43 -8
  129. package/lib/whois-chain.mjs +25 -3
  130. package/lib/whois-normalization.mjs +3 -1
  131. package/lib/whois-transport.mjs +12 -3
  132. package/lib/zone-intent-review.mjs +1 -1
  133. package/package.json +13 -5
  134. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  135. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  136. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  137. package/packages/cases/case-migration-model.mjs +476 -0
  138. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  139. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  140. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  141. package/packages/cases/case-record-model.mjs +9 -0
  142. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  143. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  144. package/packages/cases/case-response-model.mjs +1604 -0
  145. package/packages/cases/case-response-packet.mjs +1312 -0
  146. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  147. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  148. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  149. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  150. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  151. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  152. package/packages/comparison/favicon-similarity.mjs +78 -0
  153. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  154. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  155. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  156. package/packages/contracts/analyst-review-state.mjs +50 -0
  157. package/packages/contracts/case-portability.mjs +1310 -0
  158. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  159. package/packages/contracts/cli-lookup.mjs +394 -0
  160. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  161. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  162. package/packages/contracts/domain-control-monitor.mjs +310 -0
  163. package/packages/contracts/domain-control-review.mjs +828 -0
  164. package/packages/contracts/domain-name.mjs +4 -0
  165. package/packages/contracts/external-observation-interchange.mjs +224 -0
  166. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  167. package/packages/contracts/investigation-portability.mjs +331 -0
  168. package/packages/contracts/offline-comparison.mjs +160 -0
  169. package/packages/contracts/risk-calibration.mjs +1288 -0
  170. package/packages/contracts/schema-compatibility.mjs +71 -0
  171. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  172. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  173. package/packages/contracts/workspace-portability.mjs +1347 -0
  174. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  175. package/packages/evidence/decision-fact.mjs +735 -0
  176. package/packages/evidence/domain-control-runtime.mjs +547 -0
  177. package/packages/evidence/domain-name.mjs +41 -0
  178. package/packages/evidence/observation.mjs +384 -0
  179. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  180. package/packages/interchange/retained-text.mjs +8 -0
  181. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  182. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  183. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  184. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  185. package/packages/investigation/bulk-review-policy.mjs +1 -0
  186. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  187. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  188. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  189. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  190. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  191. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  192. package/packages/monitoring/analyst-review-state.mjs +680 -0
  193. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +71 -48
  194. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  195. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  196. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  197. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  198. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  199. package/packages/workspace/hostile-input.mjs +172 -0
  200. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  201. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  202. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  203. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  204. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  205. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  206. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  207. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  208. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  209. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  210. package/third-party-notices.txt +14 -14
  211. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  212. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  213. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  214. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  215. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  216. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  217. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  218. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  219. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  220. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  221. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  222. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  223. package/frontend/src/lib/analysis/scoring.js +0 -55
  224. package/frontend/src/lib/analysis/utils.js +0 -255
  225. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  226. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  227. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  228. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  229. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  230. package/lib/bounded-json-response.mjs +0 -170
  231. package/lib/lookup-response-contract.mjs +0 -1034
  232. package/lib/observation.mjs +0 -115
  233. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  236. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,21 +1,26 @@
1
- import { ACQUISITION_DECISIONS, ACQUISITION_MANUAL_CHECKS, ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION, } from "../frontend/src/lib/analysis/acquisition-decision-packet.js";
2
- import { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, } from "../frontend/src/lib/analysis/bulk-domain-comparison.js";
3
- import { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, MAX_BULK_MAIL_EXPOSURE_ROWS, } from "../frontend/src/lib/analysis/bulk-mail-exposure.js";
4
- import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, } from "../frontend/src/lib/analysis/bulk-review-export.js";
5
- import { CASE_RESPONSE_PACKET_SCHEMA, CASE_RESPONSE_PACKET_VERSION, LEGACY_CASE_RESPONSE_PACKET_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, } from "../frontend/src/lib/analysis/case-response-packet.js";
6
- import { CASE_ACTION_STATES, CASE_ACTION_TYPES, MAX_DECISION_PIN_REFERENCES, MAX_CASE_ACTIONS, MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_RECIPIENT_LENGTH, } from "../frontend/src/lib/analysis/case-response-model.js";
7
- import { INVESTIGATION_CAPSULE_SCHEMA, INVESTIGATION_CAPSULE_VERSION, LEGACY_INVESTIGATION_CAPSULE_VERSION, } from "../frontend/src/lib/analysis/investigation-capsule.js";
8
- import { LOOKUP_INVESTIGATION_BRIEF_SCHEMA } from "../frontend/src/lib/analysis/lookup-investigation-brief.js";
9
- import { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_TARGET_TYPES, LOOKUP_CLAIM_PASSPORT_VERSION, MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS, } from "../frontend/src/lib/analysis/lookup-claim-passport.js";
10
- import { LOOKUP_CLAIM_IDS, LOOKUP_CLAIM_READINESS_VERSION, LOOKUP_CLAIM_REQUIREMENT_IDS, } from "../frontend/src/lib/analysis/lookup-claim-readiness.js";
11
- import { normalizeDomainControlPassportDocument } from "../frontend/src/lib/analysis/domain-control-manifest-core.js";
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalArtifactJsonV2 } from "../packages/evidence/artifact-integrity.mjs";
3
+ import { ACQUISITION_DECISIONS, ACQUISITION_MANUAL_CHECKS, ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION, } from "../packages/investigation/acquisition-decision-packet.mjs";
4
+ import { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, } from "../packages/investigation/bulk-domain-comparison.mjs";
5
+ import { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, MAX_BULK_MAIL_EXPOSURE_ROWS, } from "../packages/investigation/bulk-mail-exposure.mjs";
6
+ import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, } from "../packages/investigation/bulk-review-export.mjs";
7
+ import { CASE_RESPONSE_PACKET_SCHEMA, CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../packages/cases/case-response-packet.mjs";
8
+ import { CASE_ACTION_EVENT_SOURCE_CLASSES, CASE_ACTION_STATES, CASE_ACTION_TYPES, CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, CASE_PROVIDER_OUTCOMES, isLegalCaseActionTransition, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_DECISION_PIN_REFERENCES, MAX_CASE_ACTIONS, MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_RECIPIENT_LENGTH, } from "../packages/cases/case-response-model.mjs";
9
+ import { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, INVESTIGATION_CAPSULE_SCHEMA, INVESTIGATION_CAPSULE_VERSION, PUBLIC_INVESTIGATION_CAPSULE_VERSION, } from "../packages/investigation/investigation-capsule.mjs";
10
+ import { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION, } from "../packages/investigation/lookup-asset-graph.mjs";
11
+ import { PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION, LOOKUP_INVESTIGATION_BRIEF_SCHEMA, LOOKUP_INVESTIGATION_BRIEF_VERSION, MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES, } from "../packages/investigation/lookup-investigation-brief.mjs";
12
+ import { DECISION_FACT_CONSISTENCY_STATES, DECISION_FACT_EVIDENCE_STATES, DECISION_FACT_FRESHNESS_STATES, DECISION_FACT_PROVENANCE_STATES, DECISION_FACT_PROJECTION_VERSION, DECISION_FACT_VERSION, MAX_DECISION_FACT_CONTRIBUTORS, MAX_DECISION_FACT_CONTRADICTIONS, MAX_DECISION_FACT_LIMITATIONS, MAX_DECISION_FACT_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_CONTRADICTIONS, MAX_DECISION_FACT_PROJECTION_BYTES, MAX_DECISION_FACT_PROJECTION_FACTS, MAX_DECISION_FACT_PROJECTION_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_REFERENCES, MAX_DECISION_FACT_PROJECTION_SOURCES, MAX_DECISION_FACT_PROJECTION_SOURCE_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_SOURCE_REFERENCES, MAX_DECISION_FACT_REFERENCES, MAX_DECISION_FACTS, decisionFactCompleteness, } from "../packages/evidence/decision-fact.mjs";
13
+ import { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_TARGET_TYPES, LOOKUP_CLAIM_PASSPORT_VERSION, MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS, } from "../packages/investigation/lookup-claim-passport.mjs";
14
+ import { LOOKUP_CLAIM_IDS, LOOKUP_CLAIM_READINESS_VERSION, LOOKUP_CLAIM_REQUIREMENT_IDS, } from "../packages/investigation/lookup-claim-readiness.mjs";
15
+ import { normalizeDomainControlManifestDocument } from "../packages/evidence/domain-control-runtime.mjs";
12
16
  import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_VERSION } from "../lib/domain-change-packet.mjs";
13
17
  import { DOMAIN_CONTROL_MANIFEST_SCHEMA } from "../lib/domain-control-manifest.mjs";
14
18
  import { INVESTIGATION_MANIFEST_SCHEMA, INVESTIGATION_MANIFEST_VERSION, MAX_INVESTIGATION_MANIFEST_ARTIFACTS, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, } from "./investigation-manifest.mjs";
15
- import { assertLookupEvidencePrivacySafeTree, assertLookupEvidencePortableTree, LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION, LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, projectLookupEvidenceAvailability, projectLookupEvidenceQuery, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../lib/evidence-export.mjs";
19
+ import { assertLookupEvidencePrivacySafeTree, assertLookupEvidencePortableTree, LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, projectLookupEvidenceAvailability, projectLookupEvidenceAvailabilityPublic, projectLookupEvidenceQuery, projectLookupEvidenceRdapPublication, projectLookupEvidenceRegistryInsights, projectLookupEvidenceWhoisPublication, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../lib/evidence-export.mjs";
16
20
  import { compareRegistrySources } from "../lib/registry-comparison.mjs";
17
21
  import { buildRegistryInsights } from "../lib/registry-insights.mjs";
18
22
  import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../lib/project-metadata.mjs";
23
+ import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "../lib/homepage-metadata-contract.mjs";
19
24
  import { isDeepStrictEqual } from 'node:util';
20
25
  const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
21
26
  const DIGEST_RE = /^sha256:[a-f0-9]{64}$/u;
@@ -124,7 +129,7 @@ const LOOKUP_TIMING_SOURCES = [
124
129
  'network_context', 'security_txt', 'external_intelligence',
125
130
  'malware_host_intelligence', 'malware_ioc_intelligence',
126
131
  ];
127
- const LOOKUP_AVAILABILITY_ANALYSIS_KEYS = [
132
+ const PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS = [
128
133
  'applicable', 'type', 'domain', 'state', 'confidence', 'detail', 'source',
129
134
  'rdapServer', 'nameservers', 'statuses', 'registrar', 'registrant', 'abuse',
130
135
  'createdDate', 'expiryDate', 'createdDateIso', 'expiryDateIso', 'domainAgeDays',
@@ -137,11 +142,40 @@ const LOOKUP_AVAILABILITY_ANALYSIS_KEYS = [
137
142
  'securityPosture', 'dns', 'tls', 'hasMx', 'hasNullMx', 'mxHosts', 'hasSpf',
138
143
  'hasDmarc', 'bulkComparison', 'limitations',
139
144
  ];
145
+ const LOOKUP_AVAILABILITY_ANALYSIS_KEYS = PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS.filter((key) => !['registrar', 'registrant', 'abuse'].includes(key));
140
146
  const LOOKUP_IDN_ANALYSIS_KEYS = [
141
147
  'version', 'mappingVersion', 'asciiDomain', 'unicodeDomain', 'hasIdn',
142
148
  'scripts', 'labels', 'mixedScript', 'skeleton', 'referenceMatches', 'findings',
143
149
  'truncated', 'limitations',
144
150
  ];
151
+ function validateLookupEvidenceHomepageMetadata(availability, version) {
152
+ const pageIdentity = availability.pageIdentity === null || availability.pageIdentity === undefined
153
+ ? null
154
+ : record(availability.pageIdentity, 'Lookup evidence page identity');
155
+ const http = availability.http === null || availability.http === undefined
156
+ ? null
157
+ : record(availability.http, 'Lookup evidence HTTP analysis');
158
+ const response = http?.response === null || http?.response === undefined
159
+ ? null
160
+ : record(http.response, 'Lookup evidence HTTP response');
161
+ const publicationPresent = Boolean(pageIdentity && Object.hasOwn(pageIdentity, 'publicationMetadata'));
162
+ const deliveryPresent = Boolean(response && Object.hasOwn(response, 'deliveryMetadata'));
163
+ if (version < LOOKUP_EVIDENCE_SCHEMA_VERSION && (publicationPresent || deliveryPresent)) {
164
+ fail('Lookup evidence homepage metadata epoch');
165
+ }
166
+ if (publicationPresent && !validPagePublicationMetadata(pageIdentity?.publicationMetadata)) {
167
+ fail('Lookup evidence page publication metadata');
168
+ }
169
+ if (publicationPresent && !['success', 'partial'].includes(String(pageIdentity?.status))) {
170
+ fail('Lookup evidence page publication metadata source state');
171
+ }
172
+ if (deliveryPresent && !validHttpDeliveryMetadata(response?.deliveryMetadata)) {
173
+ fail('Lookup evidence HTTP delivery metadata');
174
+ }
175
+ if (deliveryPresent && !['success', 'partial'].includes(String(http?.status))) {
176
+ fail('Lookup evidence HTTP delivery metadata source state');
177
+ }
178
+ }
145
179
  function validateLookupEvidenceRdapAttempt(value, label) {
146
180
  const attempt = exact(value, ['endpoint', 'transportSecurity', 'status', 'outcome', 'detail', 'selected'], label);
147
181
  let endpoint = null;
@@ -351,45 +385,40 @@ function validateLookupEvidenceRdap(value, version) {
351
385
  const error = exact(source, ['status', 'error', 'attempts'], 'Lookup evidence RDAP source');
352
386
  text(error.error, 'Lookup evidence RDAP error', 10_000);
353
387
  array(error.attempts, 'Lookup evidence RDAP attempts', 16).forEach((item, index) => {
354
- if (version === LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION)
355
- record(item, `Lookup evidence legacy RDAP attempt ${index + 1}`);
356
- else
357
- validateLookupEvidenceRdapAttempt(item, `Lookup evidence RDAP attempt ${index + 1}`);
388
+ validateLookupEvidenceRdapAttempt(item, `Lookup evidence RDAP attempt ${index + 1}`);
358
389
  });
359
390
  return 'error';
360
391
  }
361
- const complete = exact(source, ['status', 'endpoint', 'transportSecurity', 'httpStatus', 'fetchedAt', 'attempts', 'parsed', 'raw'], 'Lookup evidence RDAP source');
362
- const status = enumeration(complete.status, version === LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION
363
- ? ['success', 'not_found']
364
- : ['success', 'partial', 'not_found', 'unsupported', 'skipped', 'disabled'], 'Lookup evidence RDAP status');
392
+ const complete = exact(source, version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
393
+ ? ['status', 'endpoint', 'transportSecurity', 'httpStatus', 'fetchedAt', 'attempts', 'parsed']
394
+ : ['status', 'endpoint', 'transportSecurity', 'httpStatus', 'fetchedAt', 'attempts', 'parsed', 'raw'], 'Lookup evidence RDAP source');
395
+ const status = enumeration(complete.status, ['success', 'partial', 'not_found', 'unsupported', 'skipped', 'disabled'], 'Lookup evidence RDAP status');
365
396
  let endpoint = null;
366
397
  if (complete.endpoint !== null)
367
398
  endpoint = absoluteUrl(complete.endpoint, 'Lookup evidence RDAP endpoint', ['http:', 'https:']);
368
399
  if (complete.transportSecurity !== null)
369
400
  enumeration(complete.transportSecurity, ['http', 'https'], 'Lookup evidence RDAP transport');
370
- if (version !== LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION) {
371
- if (endpoint && complete.transportSecurity !== endpoint.protocol.slice(0, -1))
372
- fail('Lookup evidence RDAP transport');
373
- if (!endpoint && complete.transportSecurity !== null)
374
- fail('Lookup evidence RDAP transport');
375
- }
401
+ if (endpoint && complete.transportSecurity !== endpoint.protocol.slice(0, -1))
402
+ fail('Lookup evidence RDAP transport');
403
+ if (!endpoint && complete.transportSecurity !== null)
404
+ fail('Lookup evidence RDAP transport');
376
405
  if (complete.httpStatus !== null)
377
406
  integer(complete.httpStatus, 'Lookup evidence RDAP HTTP status', 100, 599);
378
407
  iso(complete.fetchedAt, 'Lookup evidence RDAP fetchedAt', true);
379
408
  array(complete.attempts, 'Lookup evidence RDAP attempts', 16).forEach((item, index) => {
380
- if (version === LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION)
381
- record(item, `Lookup evidence legacy RDAP attempt ${index + 1}`);
382
- else
383
- validateLookupEvidenceRdapAttempt(item, `Lookup evidence RDAP attempt ${index + 1}`);
409
+ validateLookupEvidenceRdapAttempt(item, `Lookup evidence RDAP attempt ${index + 1}`);
384
410
  });
385
411
  nullableRecord(complete.parsed, 'Lookup evidence RDAP parsed data');
386
- nullableRecord(complete.raw, 'Lookup evidence RDAP raw data');
387
- if (version !== LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION
388
- && status === 'success' && complete.parsed === null)
412
+ if (version < LOOKUP_EVIDENCE_SCHEMA_VERSION)
413
+ nullableRecord(complete.raw, 'Lookup evidence RDAP raw data');
414
+ if (version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
415
+ && !isDeepStrictEqual(complete.parsed, projectLookupEvidenceRdapPublication(complete.parsed))) {
416
+ fail('Lookup evidence RDAP portable publication');
417
+ }
418
+ if (status === 'success' && complete.parsed === null)
389
419
  fail('Lookup evidence RDAP publication');
390
- if (version !== LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION
391
- && !['success', 'partial'].includes(status)
392
- && (complete.parsed !== null || complete.raw !== null))
420
+ if (!['success', 'partial'].includes(status)
421
+ && (complete.parsed !== null || (version < LOOKUP_EVIDENCE_SCHEMA_VERSION && complete.raw !== null)))
393
422
  fail('Lookup evidence RDAP unavailable publication');
394
423
  return status;
395
424
  }
@@ -401,24 +430,22 @@ function validateLookupEvidenceWhois(value, version) {
401
430
  return 'error';
402
431
  }
403
432
  const complete = exact(source, ['status', 'queriedAt', 'authoritativeHop', 'failedHop', 'conflictingHop', 'parsed', 'chain'], 'Lookup evidence WHOIS source');
404
- const status = enumeration(complete.status, version === LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION
405
- ? ['complete', 'partial', 'unknown']
406
- : ['complete', 'partial', 'not_found', 'unsupported', 'skipped', 'disabled'], 'Lookup evidence WHOIS status');
433
+ const status = enumeration(complete.status, ['complete', 'partial', 'not_found', 'unsupported', 'skipped', 'disabled'], 'Lookup evidence WHOIS status');
407
434
  iso(complete.queriedAt, 'Lookup evidence WHOIS queriedAt', true);
408
435
  optionalText(complete.authoritativeHop, 'Lookup evidence WHOIS authoritative hop', 253);
409
436
  optionalText(complete.failedHop, 'Lookup evidence WHOIS failed hop', 253);
410
437
  optionalText(complete.conflictingHop, 'Lookup evidence WHOIS conflicting hop', 253);
411
438
  nullableRecord(complete.parsed, 'Lookup evidence WHOIS parsed data');
439
+ if (version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
440
+ && !isDeepStrictEqual(complete.parsed, projectLookupEvidenceWhoisPublication(complete.parsed))) {
441
+ fail('Lookup evidence WHOIS portable publication');
442
+ }
412
443
  array(complete.chain, 'Lookup evidence WHOIS chain', 16).forEach((item, index) => {
413
- if (version === LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION)
414
- record(item, `Lookup evidence legacy WHOIS chain item ${index + 1}`);
415
- else
416
- validateLookupEvidenceWhoisHop(item, `Lookup evidence WHOIS chain item ${index + 1}`);
444
+ validateLookupEvidenceWhoisHop(item, `Lookup evidence WHOIS chain item ${index + 1}`);
417
445
  });
418
446
  if (status === 'complete' && complete.parsed === null)
419
447
  fail('Lookup evidence WHOIS publication');
420
- if (version !== LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION
421
- && !['complete', 'partial'].includes(status)
448
+ if (!['complete', 'partial'].includes(status)
422
449
  && (complete.parsed !== null || complete.chain.length !== 0))
423
450
  fail('Lookup evidence WHOIS unavailable publication');
424
451
  return status;
@@ -687,7 +714,7 @@ export function validateLookupEvidenceArtifactStructure(value) {
687
714
  if (root.schema !== LOOKUP_EVIDENCE_SCHEMA
688
715
  || !SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS.includes(Number(root.schemaVersion)))
689
716
  fail('Lookup evidence');
690
- const version = integer(root.schemaVersion, 'Lookup evidence version', LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION, LOOKUP_EVIDENCE_SCHEMA_VERSION);
717
+ const version = integer(root.schemaVersion, 'Lookup evidence version', PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, LOOKUP_EVIDENCE_SCHEMA_VERSION);
691
718
  iso(root.generatedAt, 'Lookup evidence generatedAt');
692
719
  const application = exact(root.application, ['name', 'version', 'projectUrl'], 'Lookup evidence application');
693
720
  if (application.name !== 'WHOISleuth' || application.projectUrl !== WHOISLEUTH_SOURCE_REPOSITORY_URL)
@@ -707,79 +734,45 @@ export function validateLookupEvidenceArtifactStructure(value) {
707
734
  }
708
735
  else if (query.inputHostname !== null || query.registrableDomain !== null || query.isSubdomain !== false)
709
736
  fail('Lookup evidence query');
710
- if (version !== LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION
711
- && !isDeepStrictEqual(query, projectLookupEvidenceQuery(query)))
737
+ if (!isDeepStrictEqual(query, projectLookupEvidenceQuery(query)))
712
738
  fail('Lookup evidence query');
713
739
  let diagnostics;
714
740
  let rdapDiagnosticStatus;
715
741
  let whoisDiagnosticStatus;
716
- let currentDiagnosticStates = null;
717
- if (version === LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION) {
718
- diagnostics = exactOptional(root.diagnostics, ['rdap', 'whois'], ['version', 'availability', 'timing', 'registryAccess', 'reverseDns', 'network', 'securityTxt', 'sslbl'], 'Lookup evidence diagnostics');
719
- if (diagnostics.version !== undefined)
720
- integer(diagnostics.version, 'Lookup evidence diagnostics version', 1, 10);
721
- const rdapDiagnostics = record(diagnostics.rdap, 'Lookup evidence rdap diagnostics');
722
- const whoisDiagnostics = record(diagnostics.whois, 'Lookup evidence whois diagnostics');
723
- rdapDiagnosticStatus = enumeration(rdapDiagnostics.status, ['success', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled'], 'Lookup evidence RDAP diagnostic status');
724
- whoisDiagnosticStatus = enumeration(whoisDiagnostics.status, ['complete', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled'], 'Lookup evidence WHOIS diagnostic status');
725
- for (const id of ['availability', 'timing', 'registryAccess', 'reverseDns', 'network', 'securityTxt', 'sslbl']) {
726
- if (diagnostics[id] !== undefined)
727
- record(diagnostics[id], `Lookup evidence ${id} diagnostics`);
728
- }
729
- }
730
- else {
731
- diagnostics = record(root.diagnostics, 'Lookup evidence diagnostics');
732
- currentDiagnosticStates = validateLookupEvidenceCurrentDiagnostics(diagnostics);
733
- rdapDiagnosticStatus = currentDiagnosticStates.rdap;
734
- whoisDiagnosticStatus = currentDiagnosticStates.whois;
735
- }
742
+ diagnostics = record(root.diagnostics, 'Lookup evidence diagnostics');
743
+ const currentDiagnosticStates = validateLookupEvidenceCurrentDiagnostics(diagnostics);
744
+ rdapDiagnosticStatus = currentDiagnosticStates.rdap;
745
+ whoisDiagnosticStatus = currentDiagnosticStates.whois;
736
746
  const sources = exact(root.sources, ['rdap', 'whois', 'reverseDns', 'network', 'securityTxt', 'sslbl'], 'Lookup evidence sources');
737
747
  const rdapSourceStatus = validateLookupEvidenceRdap(sources.rdap, version);
738
748
  const whoisSourceStatus = validateLookupEvidenceWhois(sources.whois, version);
739
- if (version !== LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION) {
740
- if (rdapSourceStatus !== rdapDiagnosticStatus)
741
- fail('Lookup evidence RDAP source state');
742
- if (whoisSourceStatus !== whoisDiagnosticStatus)
743
- fail('Lookup evidence WHOIS source state');
744
- }
745
- else {
746
- const rdapSource = record(sources.rdap, 'Lookup evidence legacy RDAP source');
747
- const whoisSource = record(sources.whois, 'Lookup evidence legacy WHOIS source');
748
- if (rdapDiagnosticStatus === 'success'
749
- && (rdapSourceStatus !== 'success' || rdapSource.parsed === null))
750
- fail('Lookup evidence legacy RDAP source state');
751
- if (rdapDiagnosticStatus === 'not_found' && rdapSourceStatus !== 'not_found')
752
- fail('Lookup evidence legacy RDAP source state');
753
- if (rdapDiagnosticStatus === 'error' && rdapSourceStatus !== 'error')
754
- fail('Lookup evidence legacy RDAP source state');
755
- if (whoisDiagnosticStatus === 'complete'
756
- && (whoisSourceStatus !== 'complete' || whoisSource.parsed === null))
757
- fail('Lookup evidence legacy WHOIS source state');
758
- if (whoisDiagnosticStatus === 'partial'
759
- && (whoisSourceStatus !== 'partial' || whoisSource.parsed === null))
760
- fail('Lookup evidence legacy WHOIS source state');
761
- if (whoisDiagnosticStatus === 'error' && whoisSourceStatus !== 'error')
762
- fail('Lookup evidence legacy WHOIS source state');
763
- }
749
+ if (rdapSourceStatus !== rdapDiagnosticStatus)
750
+ fail('Lookup evidence RDAP source state');
751
+ if (whoisSourceStatus !== whoisDiagnosticStatus)
752
+ fail('Lookup evidence WHOIS source state');
764
753
  const reverseDnsState = validateLookupEvidenceReverseDns(sources.reverseDns);
765
754
  const networkState = validateLookupEvidenceNetwork(sources.network);
766
- if (currentDiagnosticStates) {
767
- if (currentDiagnosticStates.reverseDns
768
- && !isDeepStrictEqual(reverseDnsState, currentDiagnosticStates.reverseDns)) {
769
- fail('Lookup evidence reverse DNS source state');
770
- }
771
- if (currentDiagnosticStates.network !== null
772
- && (networkState?.status ?? null) !== currentDiagnosticStates.network) {
773
- fail('Lookup evidence network source state');
774
- }
755
+ if (currentDiagnosticStates.reverseDns
756
+ && !isDeepStrictEqual(reverseDnsState, currentDiagnosticStates.reverseDns)) {
757
+ fail('Lookup evidence reverse DNS source state');
758
+ }
759
+ if (currentDiagnosticStates.network !== null
760
+ && (networkState?.status ?? null) !== currentDiagnosticStates.network) {
761
+ fail('Lookup evidence network source state');
775
762
  }
776
763
  validateLookupEvidenceSecurityTxt(sources.securityTxt);
777
764
  validateLookupEvidenceSslbl(sources.sslbl);
778
765
  const analysis = exact(root.analysis, ['availability', 'idn', 'registryInsights', 'registryComparison', 'registrarPublicationComparison'], 'Lookup evidence analysis');
779
766
  if (analysis.availability !== null) {
780
- exactOptional(analysis.availability, [], LOOKUP_AVAILABILITY_ANALYSIS_KEYS, 'Lookup evidence availability analysis');
781
- if (version !== LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION
782
- && !isDeepStrictEqual(analysis.availability, projectLookupEvidenceAvailability(analysis.availability)))
767
+ const currentAvailability = version >= LOOKUP_EVIDENCE_SCHEMA_VERSION;
768
+ const availability = exactOptional(analysis.availability, currentAvailability ? ['registryContactsExcluded'] : [], currentAvailability ? LOOKUP_AVAILABILITY_ANALYSIS_KEYS : PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS, 'Lookup evidence availability analysis');
769
+ if (currentAvailability && availability.registryContactsExcluded !== true) {
770
+ fail('Lookup evidence availability contact exclusion');
771
+ }
772
+ validateLookupEvidenceHomepageMetadata(availability, version);
773
+ if (!isDeepStrictEqual(analysis.availability, currentAvailability
774
+ ? projectLookupEvidenceAvailability(analysis.availability)
775
+ : projectLookupEvidenceAvailabilityPublic(analysis.availability)))
783
776
  fail('Lookup evidence availability analysis');
784
777
  }
785
778
  if (analysis.idn !== null) {
@@ -788,7 +781,7 @@ export function validateLookupEvidenceArtifactStructure(value) {
788
781
  const registryInsights = record(analysis.registryInsights, 'Lookup evidence registry insights');
789
782
  const registryComparison = record(analysis.registryComparison, 'Lookup evidence registry comparison');
790
783
  validateLookupEvidenceRegistrarComparison(analysis.registrarPublicationComparison);
791
- if (version !== LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION) {
784
+ {
792
785
  const rdap = record(sources.rdap, 'Lookup evidence RDAP source');
793
786
  const whois = record(sources.whois, 'Lookup evidence WHOIS source');
794
787
  const rdapParsed = ['success', 'partial'].includes(rdapDiagnosticStatus) ? rdap.parsed : null;
@@ -799,7 +792,7 @@ export function validateLookupEvidenceArtifactStructure(value) {
799
792
  });
800
793
  if (!isDeepStrictEqual(registryComparison, expectedComparison))
801
794
  fail('Lookup evidence registry comparison derivation');
802
- const expectedInsights = buildRegistryInsights({
795
+ const rebuiltInsights = buildRegistryInsights({
803
796
  rdapParsed,
804
797
  rdapStatus: rdapDiagnosticStatus,
805
798
  rdapFetchedAt: rdap.fetchedAt,
@@ -807,6 +800,9 @@ export function validateLookupEvidenceArtifactStructure(value) {
807
800
  whoisStatus: whoisDiagnosticStatus,
808
801
  whoisQueriedAt: whois.queriedAt,
809
802
  });
803
+ const expectedInsights = version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
804
+ ? projectLookupEvidenceRegistryInsights(rebuiltInsights)
805
+ : rebuiltInsights;
810
806
  if (!isDeepStrictEqual(registryInsights, expectedInsights))
811
807
  fail('Lookup evidence registry insight derivation');
812
808
  try {
@@ -817,16 +813,11 @@ export function validateLookupEvidenceArtifactStructure(value) {
817
813
  }
818
814
  }
819
815
  }
820
- function validateIntegrity(value, label, version, legacyVersion, currentVersion, legacyExplicit = false) {
821
- const explicit = version === currentVersion || (version === legacyVersion && legacyExplicit);
822
- if (version !== legacyVersion && version !== currentVersion)
816
+ function validateIntegrity(value, label, version, currentVersion) {
817
+ if (version !== currentVersion)
823
818
  fail(label);
824
- const integrity = exact(value, explicit
825
- ? ['algorithm', 'canonicalization', 'digestSha256']
826
- : ['algorithm', 'digestSha256'], label);
827
- if (integrity.algorithm !== 'SHA-256')
828
- fail(label);
829
- if (explicit && integrity.canonicalization !== (version === currentVersion ? 'sorted-json-v2' : 'sorted-json-v1'))
819
+ const integrity = exact(value, ['algorithm', 'canonicalization', 'digestSha256'], label);
820
+ if (integrity.algorithm !== 'SHA-256' || integrity.canonicalization !== 'sorted-json-v2')
830
821
  fail(label);
831
822
  digest(integrity.digestSha256, label);
832
823
  }
@@ -877,7 +868,7 @@ function validateAcquisition(value) {
877
868
  strings(evidence.nextSteps, 'Acquisition next steps', 6, 600);
878
869
  strings(evidence.limitations, 'Acquisition evidence limitations', 12, 600);
879
870
  strings(root.limitations, 'Acquisition limitations', 8, 600);
880
- validateIntegrity(root.integrity, 'Acquisition integrity', root.version, 1, ACQUISITION_DECISION_PACKET_VERSION);
871
+ validateIntegrity(root.integrity, 'Acquisition integrity', root.version, ACQUISITION_DECISION_PACKET_VERSION);
881
872
  }
882
873
  const CLAIM_PASSPORT_SOURCE_STATES = ['complete', 'not_found', 'partial', 'skipped', 'unavailable', 'unknown', 'unsupported'];
883
874
  function validateClaimPassportTarget(type, value) {
@@ -971,7 +962,7 @@ function validateLookupClaimPassport(value) {
971
962
  if (models.risk !== null)
972
963
  integer(models.risk, 'Lookup claim passport Risk model', 1, 1_000);
973
964
  strings(root.limitations, 'Lookup claim passport limitations', MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, 600);
974
- validateIntegrity(root.integrity, 'Lookup claim passport integrity', root.version, LOOKUP_CLAIM_PASSPORT_VERSION, LOOKUP_CLAIM_PASSPORT_VERSION);
965
+ validateIntegrity(root.integrity, 'Lookup claim passport integrity', root.version, LOOKUP_CLAIM_PASSPORT_VERSION);
975
966
  }
976
967
  const COMPARISON_STATES = ['conflicting', 'different', 'equal', 'missing', 'not_recorded', 'unavailable'];
977
968
  const SOURCE_STATES = ['complete', 'error', 'not_found', 'not_recorded', 'partial', 'skipped', 'unavailable', 'unsupported'];
@@ -1026,7 +1017,7 @@ function validateDomainComparison(value) {
1026
1017
  fail('Domain comparison counts');
1027
1018
  }
1028
1019
  strings(comparison.limitations, 'Domain comparison limitations', 12, 600);
1029
- validateIntegrity(root.integrity, 'Domain comparison integrity', root.version, 3, BULK_DOMAIN_COMPARISON_EXPORT_VERSION);
1020
+ validateIntegrity(root.integrity, 'Domain comparison integrity', root.version, BULK_DOMAIN_COMPARISON_EXPORT_VERSION);
1030
1021
  }
1031
1022
  const MAIL_STATES = ['authenticated_mail', 'evidence_incomplete', 'mail_auth_gap', 'mail_auth_incomplete', 'no_explicit_mx', 'null_mx'];
1032
1023
  function validateSourceCoverage(value, label) {
@@ -1086,7 +1077,7 @@ function validateMailExposure(value) {
1086
1077
  if (declaredUnevaluated < unevaluated)
1087
1078
  fail('Bulk mail exposure unevaluated count');
1088
1079
  strings(report.limitations, 'Bulk mail exposure limitations', 12, 600);
1089
- validateIntegrity(root.integrity, 'Bulk mail exposure integrity', root.version, 1, BULK_MAIL_EXPOSURE_EXPORT_VERSION);
1080
+ validateIntegrity(root.integrity, 'Bulk mail exposure integrity', root.version, BULK_MAIL_EXPOSURE_EXPORT_VERSION);
1090
1081
  }
1091
1082
  function validateBulkView(value, label) {
1092
1083
  const view = exact(value, ['primaryFilter', 'mutationFilter', 'signalFilters', 'sourceFilter', 'lifecycleFilter', 'ageFilter', 'mailFilter', 'registrarFilter', 'caseDispositionFilter', 'reviewStateFilter', 'groupBy', 'sortKey', 'sortDirection'], label);
@@ -1131,7 +1122,7 @@ function validateBulkReviewManifest(value) {
1131
1122
  if (rows.length !== domains.length)
1132
1123
  fail('Bulk review manifest selection');
1133
1124
  strings(root.limitations, 'Bulk review manifest limitations', 8, 600);
1134
- validateIntegrity(root.integrity, 'Bulk review manifest integrity', root.version, 1, BULK_REVIEW_MANIFEST_VERSION);
1125
+ validateIntegrity(root.integrity, 'Bulk review manifest integrity', root.version, BULK_REVIEW_MANIFEST_VERSION);
1135
1126
  }
1136
1127
  function validateInvestigationManifest(value) {
1137
1128
  const root = exact(value, ['schema', 'version', 'generatedAt', 'application', 'workflow', 'configuration', 'artifacts', 'steps', 'summary', 'limitations', 'integrity'], 'Investigation manifest');
@@ -1168,9 +1159,21 @@ function validateInvestigationManifest(value) {
1168
1159
  || integer(summary.totalBytes, 'Investigation manifest total bytes', 1, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES) !== totalBytes)
1169
1160
  fail('Investigation manifest summary');
1170
1161
  strings(root.limitations, 'Investigation manifest limitations', 8, 600);
1171
- validateIntegrity(root.integrity, 'Investigation manifest integrity', root.version, 1, INVESTIGATION_MANIFEST_VERSION);
1162
+ validateIntegrity(root.integrity, 'Investigation manifest integrity', root.version, INVESTIGATION_MANIFEST_VERSION);
1172
1163
  }
1173
- function validateActionSummary(value, label) {
1164
+ const CASE_RESPONSE_PREFLIGHT_IDS = [
1165
+ 'required_incident_fields',
1166
+ 'evidence_pins',
1167
+ 'analyst_decision',
1168
+ 'recipient_route',
1169
+ 'profile_recipient',
1170
+ 'case_disposition',
1171
+ 'evidence_freshness',
1172
+ 'contradictory_evidence',
1173
+ 'action_tracking',
1174
+ ];
1175
+ const PUBLIC_CASE_ACTION_STATES = ['planned', 'ready_for_review', 'submitted', 'acknowledged', 'resolved', 'closed'];
1176
+ function validatePublicActionSummary(value, label) {
1174
1177
  const summary = exact(value, ['total', 'active', 'submitted', 'acknowledged', 'resolved', 'closed', 'overdue', 'followUpDue', 'withOutcome', 'latestOutcomes'], label);
1175
1178
  const total = integer(summary.total, label, 0, MAX_CASE_ACTIONS);
1176
1179
  for (const key of ['active', 'submitted', 'acknowledged', 'resolved', 'closed', 'overdue', 'followUpDue', 'withOutcome'])
@@ -1182,22 +1185,11 @@ function validateActionSummary(value, label) {
1182
1185
  const outcome = exact(candidate, ['actionId', 'recipient', 'state', 'outcome', 'updatedAt'], label);
1183
1186
  text(outcome.actionId, label, 64);
1184
1187
  text(outcome.recipient, label, 320);
1185
- enumeration(outcome.state, CASE_ACTION_STATES, label);
1188
+ enumeration(outcome.state, PUBLIC_CASE_ACTION_STATES, label);
1186
1189
  text(outcome.outcome, label, 2_000);
1187
1190
  iso(outcome.updatedAt, label);
1188
1191
  }
1189
1192
  }
1190
- const CASE_RESPONSE_PREFLIGHT_IDS = [
1191
- 'required_incident_fields',
1192
- 'evidence_pins',
1193
- 'analyst_decision',
1194
- 'recipient_route',
1195
- 'profile_recipient',
1196
- 'case_disposition',
1197
- 'evidence_freshness',
1198
- 'contradictory_evidence',
1199
- 'action_tracking',
1200
- ];
1201
1193
  function expectedObservationAge(observedAt, generatedAt) {
1202
1194
  const ageSeconds = Math.floor((Date.parse(generatedAt) - Date.parse(observedAt)) / 1_000);
1203
1195
  if (ageSeconds < -300)
@@ -1208,10 +1200,10 @@ function expectedObservationAge(observedAt, generatedAt) {
1208
1200
  return { ageSeconds, band: 'one_to_seven_days', refreshRecommended: false };
1209
1201
  return { ageSeconds, band: 'over_seven_days', refreshRecommended: true };
1210
1202
  }
1211
- function validateCaseResponsePacket(value) {
1203
+ function validateCaseResponsePacketV6(value) {
1212
1204
  const root = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'reviewRequired', 'submissionPerformed', 'profile', 'case', 'incident', 'contacts', 'preflight', 'escalationHistory', 'provenance', 'integrity'], 'Case-response packet');
1213
- if (root.schemaVersion !== LEGACY_CASE_RESPONSE_PACKET_VERSION && root.schemaVersion !== CASE_RESPONSE_PACKET_VERSION)
1214
- fail('Case-response packet');
1205
+ if (root.schemaVersion !== PUBLIC_CASE_RESPONSE_PACKET_VERSION)
1206
+ fail('Case-response packet v6');
1215
1207
  iso(root.generatedAt, 'Case-response packet generatedAt');
1216
1208
  if (root.reviewRequired !== true || root.submissionPerformed !== false)
1217
1209
  fail('Case-response packet review state');
@@ -1304,14 +1296,14 @@ function validateCaseResponsePacket(value) {
1304
1296
  const expectedStatus = actualCounts.block ? 'needs_input' : actualCounts.caution ? 'review_cautions' : 'ready_for_review';
1305
1297
  if (preflight.status !== expectedStatus || preflight.canExport !== (actualCounts.block === 0))
1306
1298
  fail('Case-response preflight status');
1307
- validateActionSummary(preflight.actionSummary, 'Case-response action summary');
1299
+ validatePublicActionSummary(preflight.actionSummary, 'Case-response action summary');
1308
1300
  const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
1309
1301
  for (const candidate of history) {
1310
1302
  const action = exact(candidate, ['type', 'recipient', 'contactSource', 'state', 'reference', 'outcome', 'createdAt', 'updatedAt'], 'Case-response escalation action');
1311
1303
  enumeration(action.type, CASE_ACTION_TYPES, 'Case-response action type');
1312
1304
  text(action.recipient, 'Case-response action recipient', 320, true);
1313
1305
  text(action.contactSource, 'Case-response action source', 120, true);
1314
- enumeration(action.state, CASE_ACTION_STATES, 'Case-response action state');
1306
+ enumeration(action.state, PUBLIC_CASE_ACTION_STATES, 'Case-response action state');
1315
1307
  optionalText(action.reference, 'Case-response action reference', 500);
1316
1308
  optionalText(action.outcome, 'Case-response action outcome', 2_000);
1317
1309
  iso(action.createdAt, 'Case-response action createdAt');
@@ -1333,12 +1325,509 @@ function validateCaseResponsePacket(value) {
1333
1325
  strings(provenance.limitations, 'Case-response limitations', 8, 600);
1334
1326
  const integrity = exact(root.integrity, ['algorithm', 'canonicalization', 'scope', 'digestSha256'], 'Case-response integrity');
1335
1327
  if (integrity.algorithm !== 'SHA-256'
1336
- || integrity.canonicalization !== (root.schemaVersion === CASE_RESPONSE_PACKET_VERSION ? 'sorted-json-v2' : 'sorted-json-v1')
1328
+ || integrity.canonicalization !== 'sorted-json-v2'
1337
1329
  || integrity.scope !== 'packet excluding integrity')
1338
1330
  fail('Case-response integrity');
1339
1331
  if (typeof integrity.digestSha256 !== 'string' || !HEX_DIGEST_RE.test(integrity.digestSha256))
1340
1332
  fail('Case-response integrity');
1341
1333
  }
1334
+ function validateCurrentActionSummary(value, label) {
1335
+ const summary = exact(value, [
1336
+ 'total', 'active', 'drafting', 'readyForReview', 'reviewed', 'authorised', 'submitted',
1337
+ 'acknowledged', 'terminal', 'overdue', 'followUpDue', 'withProviderOutcome', 'latestOutcomes',
1338
+ ], label);
1339
+ const total = integer(summary.total, label, 0, MAX_CASE_ACTIONS);
1340
+ const drafting = integer(summary.drafting, label, 0, total);
1341
+ const ready = integer(summary.readyForReview, label, 0, total);
1342
+ const reviewed = integer(summary.reviewed, label, 0, total);
1343
+ const authorised = integer(summary.authorised, label, 0, total);
1344
+ const submitted = integer(summary.submitted, label, 0, total);
1345
+ const acknowledged = integer(summary.acknowledged, label, 0, total);
1346
+ const terminal = integer(summary.terminal, label, 0, total);
1347
+ const active = integer(summary.active, label, 0, total);
1348
+ if (drafting + ready + reviewed + authorised + submitted + acknowledged + terminal !== total || active + terminal !== total)
1349
+ fail(label);
1350
+ for (const key of ['overdue', 'followUpDue', 'withProviderOutcome'])
1351
+ integer(summary[key], label, 0, total);
1352
+ const outcomes = array(summary.latestOutcomes, label, 5);
1353
+ for (const candidate of outcomes) {
1354
+ const outcome = exact(candidate, ['actionId', 'recipient', 'state', 'providerOutcome', 'outcomeDetail', 'occurredAt'], label);
1355
+ text(outcome.actionId, label, 64);
1356
+ text(outcome.recipient, label, 320);
1357
+ enumeration(outcome.state, CASE_ACTION_STATES, label);
1358
+ enumeration(outcome.providerOutcome, CASE_PROVIDER_OUTCOMES, label);
1359
+ optionalText(outcome.outcomeDetail, label, 2_000);
1360
+ iso(outcome.occurredAt, label);
1361
+ }
1362
+ return total;
1363
+ }
1364
+ function validateCaseResponsePacketV7(value) {
1365
+ const root = exact(value, [
1366
+ 'schema', 'schemaVersion', 'generatedAt', 'reviewRequired', 'submissionPerformed', 'profile', 'case',
1367
+ 'incident', 'contacts', 'selectedEvidence', 'contradictions', 'readiness', 'artefactReferences',
1368
+ 'authorisation', 'preflight', 'escalationHistory', 'escalationHistoryOmitted',
1369
+ 'escalationHistoryLimitations', 'responseLifecycle', 'provenance', 'integrity',
1370
+ ], 'Case-response packet v7');
1371
+ if (root.schemaVersion !== CASE_RESPONSE_PACKET_VERSION)
1372
+ fail('Case-response packet v7');
1373
+ iso(root.generatedAt, 'Case-response packet generatedAt');
1374
+ if (root.reviewRequired !== true || root.submissionPerformed !== false)
1375
+ fail('Case-response packet review state');
1376
+ const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
1377
+ const profileId = enumeration(profile.id, RESPONSE_PACKET_PROFILE_IDS, 'Case-response profile id');
1378
+ text(profile.label, 'Case-response profile label', 200);
1379
+ text(profile.audience, 'Case-response profile audience', 300);
1380
+ text(profile.subject, 'Case-response profile subject', 500);
1381
+ for (const key of ['checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'])
1382
+ strings(profile[key], `Case-response profile ${key}`, 24, 500);
1383
+ const selectedProfile = RESPONSE_PACKET_PROFILES.find((candidate) => candidate.id === profileId);
1384
+ if (!selectedProfile
1385
+ || profile.label !== selectedProfile.label
1386
+ || profile.audience !== selectedProfile.audience
1387
+ || !sameValues(profile.checklist, selectedProfile.checklist)
1388
+ || !sameValues(profile.evidenceOrder, selectedProfile.evidenceOrder)
1389
+ || !sameValues(profile.includedEvidence, selectedProfile.includedEvidence)
1390
+ || !sameValues(profile.excludedEvidence, selectedProfile.excludedEvidence)
1391
+ || !sameValues(profile.redactions, selectedProfile.redactions)
1392
+ || !sameValues(profile.attachments, selectedProfile.attachments)
1393
+ || !sameValues(profile.followUpFields, selectedProfile.followUpFields))
1394
+ fail('Case-response profile');
1395
+ const caseRecord = exact(root.case, ['id', 'domain', 'status', 'disposition', 'updatedAt'], 'Case-response case');
1396
+ text(caseRecord.id, 'Case-response case id', 128);
1397
+ domain(caseRecord.domain, 'Case-response case domain');
1398
+ text(caseRecord.status, 'Case-response case status', 80);
1399
+ text(caseRecord.disposition, 'Case-response case disposition', 80);
1400
+ iso(caseRecord.updatedAt, 'Case-response case updatedAt');
1401
+ const incident = exact(root.incident, ['category', 'affectedParty', 'abusiveUrls', 'observedHarm', 'observedAt'], 'Case-response incident');
1402
+ text(incident.category, 'Case-response category', 80);
1403
+ text(incident.affectedParty, 'Case-response affected party', 200);
1404
+ const urls = strings(incident.abusiveUrls, 'Case-response abusive URLs', MAX_ABUSIVE_URLS, 2_048);
1405
+ if (!urls.length || urls.some((item) => {
1406
+ try {
1407
+ const parsed = new URL(item);
1408
+ return !['http:', 'https:'].includes(parsed.protocol) || Boolean(parsed.username || parsed.password) || parsed.toString() !== item;
1409
+ }
1410
+ catch {
1411
+ return true;
1412
+ }
1413
+ }) || new Set(urls).size !== urls.length)
1414
+ fail('Case-response abusive URLs');
1415
+ text(incident.observedHarm, 'Case-response observed harm', 2_000);
1416
+ iso(incident.observedAt, 'Case-response observedAt');
1417
+ if (profile.subject !== `${selectedProfile?.subjectPrefix}: ${caseRecord.domain} (${incident.category})`)
1418
+ fail('Case-response profile subject');
1419
+ const contacts = array(root.contacts, 'Case-response contacts', MAX_RESPONSE_CONTACTS);
1420
+ const contactKeys = new Set();
1421
+ for (const candidate of contacts) {
1422
+ const contact = exact(candidate, ['kind', 'contact', 'source', 'observedAt', 'freshness', 'limitations'], 'Case-response contact');
1423
+ const kind = enumeration(contact.kind, RESPONSE_CONTACT_KINDS, 'Case-response contact kind');
1424
+ const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
1425
+ const key = `${kind}\u0000${contactValue.toLowerCase()}`;
1426
+ if (contactKeys.has(key))
1427
+ fail('Case-response contacts');
1428
+ contactKeys.add(key);
1429
+ text(contact.source, 'Case-response contact source', 120);
1430
+ iso(contact.observedAt, 'Case-response contact observedAt', true);
1431
+ const freshness = enumeration(contact.freshness, ['current', 'stale', 'unknown'], 'Case-response contact freshness');
1432
+ const routeAge = contact.observedAt === null
1433
+ ? null
1434
+ : Date.parse(root.generatedAt) - Date.parse(contact.observedAt);
1435
+ const expectedFreshness = routeAge === null
1436
+ ? 'unknown'
1437
+ : routeAge < -300_000 || routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000
1438
+ ? 'stale'
1439
+ : 'current';
1440
+ if (freshness !== expectedFreshness)
1441
+ fail('Case-response contact freshness');
1442
+ strings(contact.limitations, 'Case-response contact limitations', MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH);
1443
+ }
1444
+ const evidence = array(root.selectedEvidence, 'Case-response selected evidence', MAX_RESPONSE_SELECTED_EVIDENCE);
1445
+ const evidenceIds = new Set();
1446
+ for (const candidate of evidence) {
1447
+ const item = exact(candidate, ['id', 'label', 'source', 'observedAt', 'completeness', 'limitations'], 'Case-response selected evidence');
1448
+ const id = text(item.id, 'Case-response evidence id', 64);
1449
+ if (evidenceIds.has(id))
1450
+ fail('Case-response selected evidence');
1451
+ evidenceIds.add(id);
1452
+ text(item.label, 'Case-response evidence label', 80);
1453
+ text(item.source, 'Case-response evidence source', 120);
1454
+ iso(item.observedAt, 'Case-response evidence observedAt');
1455
+ enumeration(item.completeness, ['complete', 'partial', 'inconclusive', 'unknown'], 'Case-response evidence completeness');
1456
+ strings(item.limitations, 'Case-response evidence limitations', 8, 240);
1457
+ }
1458
+ const contradictions = array(root.contradictions, 'Case-response contradictions', MAX_RESPONSE_CONTRADICTIONS);
1459
+ const contradictionIds = new Set();
1460
+ for (const candidate of contradictions) {
1461
+ const item = exact(candidate, ['id', 'statement', 'state', 'limitations'], 'Case-response contradiction');
1462
+ const id = text(item.id, 'Case-response contradiction id', 64);
1463
+ if (contradictionIds.has(id))
1464
+ fail('Case-response contradictions');
1465
+ contradictionIds.add(id);
1466
+ text(item.statement, 'Case-response contradiction statement', 2_000);
1467
+ enumeration(item.state, ['open', 'resolved'], 'Case-response contradiction state');
1468
+ strings(item.limitations, 'Case-response contradiction limitations', 8, 240);
1469
+ }
1470
+ const readiness = exact(root.readiness, ['profileId', 'rows', 'counts', 'limitations'], 'Case-response readiness');
1471
+ if (readiness.profileId !== profileId)
1472
+ fail('Case-response readiness profile');
1473
+ const readinessRows = array(readiness.rows, 'Case-response readiness rows', RESPONSE_READINESS_ROW_IDS.length, RESPONSE_READINESS_ROW_IDS.length);
1474
+ const actualReadiness = Object.fromEntries(RESPONSE_READINESS_STATES.map((state) => [state, 0]));
1475
+ const requiredReadiness = new Set([
1476
+ 'observed_behaviour', 'exact_url', 'observation_time', 'capture_provenance', 'authority_review',
1477
+ 'selected_evidence', 'contradictions', 'source_limitations',
1478
+ ...(profileId === 'internal_soc' ? [] : ['recipient_route']),
1479
+ ...(['registrar', 'registry', 'network_hosting'].includes(profileId) ? ['infrastructure_responsibility'] : []),
1480
+ ]);
1481
+ const requiredContact = selectedProfile?.requiredContactKind
1482
+ ? contacts.find((candidate) => candidate.kind === selectedProfile.requiredContactKind)
1483
+ : contacts[0];
1484
+ const expectedReadinessStates = {
1485
+ observed_behaviour: 'complete',
1486
+ exact_url: 'complete',
1487
+ observation_time: expectedObservationAge(incident.observedAt, root.generatedAt).refreshRecommended ? 'stale' : 'complete',
1488
+ capture_provenance: evidence.length ? 'complete' : 'unavailable',
1489
+ recipient_route: !requiredContact
1490
+ ? 'not_provided'
1491
+ : requiredContact.freshness === 'stale'
1492
+ ? 'stale'
1493
+ : requiredContact.freshness === 'current' && requiredContact.source ? 'complete' : 'partial',
1494
+ selected_evidence: !evidence.length
1495
+ ? 'not_provided'
1496
+ : evidence.every((candidate) => candidate.completeness === 'complete') ? 'complete' : 'partial',
1497
+ ...(contradictions.length ? {
1498
+ contradictions: contradictions.some((candidate) => candidate.state === 'open') ? 'partial' : 'complete',
1499
+ } : {}),
1500
+ };
1501
+ for (const [index, candidate] of readinessRows.entries()) {
1502
+ const row = exact(candidate, ['id', 'label', 'state', 'detail', 'requiredForAuthorisation', 'limitations'], 'Case-response readiness row');
1503
+ if (row.id !== RESPONSE_READINESS_ROW_IDS[index])
1504
+ fail('Case-response readiness row order');
1505
+ text(row.label, 'Case-response readiness label', 200);
1506
+ const state = enumeration(row.state, RESPONSE_READINESS_STATES, 'Case-response readiness state');
1507
+ actualReadiness[state] = (actualReadiness[state] ?? 0) + 1;
1508
+ text(row.detail, 'Case-response readiness detail', 500);
1509
+ boolean(row.requiredForAuthorisation, 'Case-response readiness requirement');
1510
+ if (row.requiredForAuthorisation !== requiredReadiness.has(row.id)
1511
+ || (expectedReadinessStates[row.id] !== undefined
1512
+ && row.state !== expectedReadinessStates[row.id]))
1513
+ fail('Case-response readiness projection');
1514
+ strings(row.limitations, 'Case-response readiness limitations', 8, 240);
1515
+ }
1516
+ const readinessCounts = exact(readiness.counts, [...RESPONSE_READINESS_STATES], 'Case-response readiness counts');
1517
+ for (const state of RESPONSE_READINESS_STATES)
1518
+ if (integer(readinessCounts[state], 'Case-response readiness count', 0, readinessRows.length) !== actualReadiness[state])
1519
+ fail('Case-response readiness counts');
1520
+ strings(readiness.limitations, 'Case-response readiness limitations', 8, 600);
1521
+ const artefacts = array(root.artefactReferences, 'Case-response artefact references', MAX_RESPONSE_ARTEFACT_REFERENCES);
1522
+ const artefactIds = new Set();
1523
+ for (const candidate of artefacts) {
1524
+ const item = exact(candidate, ['id', 'label', 'mediaType', 'capturedAt', 'source', 'digestSha256', 'byteLength', 'limitations'], 'Case-response artefact reference');
1525
+ const id = text(item.id, 'Case-response artefact id', 64);
1526
+ if (artefactIds.has(id))
1527
+ fail('Case-response artefact references');
1528
+ artefactIds.add(id);
1529
+ text(item.label, 'Case-response artefact label', 120);
1530
+ text(item.mediaType, 'Case-response artefact media type', 120);
1531
+ iso(item.capturedAt, 'Case-response artefact capturedAt');
1532
+ text(item.source, 'Case-response artefact source', 120);
1533
+ if (typeof item.digestSha256 !== 'string' || !HEX_DIGEST_RE.test(item.digestSha256))
1534
+ fail('Case-response artefact digest');
1535
+ if (item.byteLength !== null)
1536
+ integer(item.byteLength, 'Case-response artefact byte length', 0, 100 * 1024 * 1024);
1537
+ strings(item.limitations, 'Case-response artefact limitations', 8, 240);
1538
+ }
1539
+ const authorisation = exact(root.authorisation, ['status', 'reviewedInputDigestSha256', 'suppliedReviewDigestSha256', 'digestMatches', 'confirmedAt', 'confirmations', 'missingConfirmations', 'limitations'], 'Case-response authorisation');
1540
+ const authorisationStatus = enumeration(authorisation.status, ['draft', 'authorised'], 'Case-response authorisation status');
1541
+ if (typeof authorisation.reviewedInputDigestSha256 !== 'string' || !HEX_DIGEST_RE.test(authorisation.reviewedInputDigestSha256))
1542
+ fail('Case-response review digest');
1543
+ if (authorisation.suppliedReviewDigestSha256 !== null && (typeof authorisation.suppliedReviewDigestSha256 !== 'string' || !HEX_DIGEST_RE.test(authorisation.suppliedReviewDigestSha256)))
1544
+ fail('Case-response supplied review digest');
1545
+ boolean(authorisation.digestMatches, 'Case-response authorisation digest match');
1546
+ iso(authorisation.confirmedAt, 'Case-response authorisation confirmedAt', true);
1547
+ if (authorisation.confirmedAt !== null
1548
+ && Date.parse(authorisation.confirmedAt) > Date.parse(root.generatedAt) + MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS) {
1549
+ fail('Case-response authorisation confirmedAt');
1550
+ }
1551
+ const confirmations = exact(authorisation.confirmations, [...RESPONSE_AUTHORISATION_CONFIRMATION_IDS], 'Case-response confirmations');
1552
+ for (const id of RESPONSE_AUTHORISATION_CONFIRMATION_IDS)
1553
+ boolean(confirmations[id], `Case-response confirmation ${id}`);
1554
+ const missing = strings(authorisation.missingConfirmations, 'Case-response missing confirmations', RESPONSE_AUTHORISATION_CONFIRMATION_IDS.length, 80);
1555
+ if (missing.some((id) => !RESPONSE_AUTHORISATION_CONFIRMATION_IDS.includes(id)))
1556
+ fail('Case-response missing confirmations');
1557
+ strings(authorisation.limitations, 'Case-response authorisation limitations', 8, 600);
1558
+ const expectedMissing = RESPONSE_AUTHORISATION_CONFIRMATION_IDS.filter((id) => confirmations[id] !== true);
1559
+ const expectedDigestMatches = authorisation.suppliedReviewDigestSha256 === authorisation.reviewedInputDigestSha256;
1560
+ const missingRequiredReadiness = readinessRows.some((candidate) => {
1561
+ const row = candidate;
1562
+ return row.requiredForAuthorisation === true && (row.state === 'not_provided' || row.state === 'unavailable');
1563
+ });
1564
+ const authorityReady = readinessRows.some((candidate) => {
1565
+ const row = candidate;
1566
+ return row.id === 'authority_review' && row.state === 'complete';
1567
+ });
1568
+ if (authorisation.digestMatches !== expectedDigestMatches
1569
+ || !sameValues(missing, expectedMissing)
1570
+ || (authorisationStatus === 'authorised' && (authorisation.digestMatches !== true || authorisation.confirmedAt === null || missing.length || missingRequiredReadiness || !authorityReady))
1571
+ || (authorisationStatus === 'draft' && authorisation.confirmedAt !== null))
1572
+ fail('Case-response authorisation');
1573
+ const preflight = exact(root.preflight, ['version', 'status', 'canExport', 'counts', 'checks', 'actionSummary'], 'Case-response preflight');
1574
+ if (preflight.version !== 2)
1575
+ fail('Case-response preflight');
1576
+ const checks = array(preflight.checks, 'Case-response preflight checks', CASE_RESPONSE_PREFLIGHT_IDS.length, CASE_RESPONSE_PREFLIGHT_IDS.length);
1577
+ const actualCounts = { block: 0, caution: 0, pass: 0 };
1578
+ for (const [index, candidate] of checks.entries()) {
1579
+ const check = exact(candidate, ['id', 'label', 'state', 'detail'], 'Case-response preflight check');
1580
+ if (check.id !== CASE_RESPONSE_PREFLIGHT_IDS[index])
1581
+ fail('Case-response preflight check order');
1582
+ text(check.label, 'Case-response preflight label', 200);
1583
+ const state = enumeration(check.state, ['block', 'caution', 'pass'], 'Case-response preflight state');
1584
+ actualCounts[state] += 1;
1585
+ text(check.detail, 'Case-response preflight detail', 1_000);
1586
+ }
1587
+ const counts = exact(preflight.counts, ['block', 'caution', 'pass'], 'Case-response preflight counts');
1588
+ for (const state of ['block', 'caution', 'pass'])
1589
+ if (integer(counts[state], 'Case-response preflight count', 0, checks.length) !== actualCounts[state])
1590
+ fail('Case-response preflight counts');
1591
+ const expectedStatus = actualCounts.block ? 'needs_input' : actualCounts.caution ? 'review_cautions' : 'ready_for_review';
1592
+ if (preflight.status !== expectedStatus || preflight.canExport !== (actualCounts.block === 0))
1593
+ fail('Case-response preflight status');
1594
+ const actionTotal = validateCurrentActionSummary(preflight.actionSummary, 'Case-response action summary');
1595
+ const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
1596
+ const escalationHistoryOmitted = integer(root.escalationHistoryOmitted, 'Case-response omitted actions', 0, MAX_CASE_ACTIONS);
1597
+ const escalationHistoryLimitations = strings(root.escalationHistoryLimitations, 'Case-response history limitations', 4, 600);
1598
+ if (history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY)
1599
+ || escalationHistoryOmitted !== actionTotal - history.length
1600
+ || (escalationHistoryOmitted > 0) !== (escalationHistoryLimitations.length > 0))
1601
+ fail('Case-response action-history bounds');
1602
+ const actionIds = new Set();
1603
+ const providerEvents = [];
1604
+ for (const candidate of history) {
1605
+ const action = exact(candidate, ['actionId', 'type', 'recipient', 'contactSource', 'state', 'reference', 'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted', 'historyLimitations', 'transitions', 'createdAt', 'updatedAt'], 'Case-response escalation action');
1606
+ const actionId = text(action.actionId, 'Case-response action id', 64);
1607
+ if (actionIds.has(actionId))
1608
+ fail('Case-response action identity');
1609
+ actionIds.add(actionId);
1610
+ enumeration(action.type, CASE_ACTION_TYPES, 'Case-response action type');
1611
+ text(action.recipient, 'Case-response action recipient', 320);
1612
+ text(action.contactSource, 'Case-response action source', 120);
1613
+ enumeration(action.state, CASE_ACTION_STATES, 'Case-response action state');
1614
+ optionalText(action.reference, 'Case-response action reference', 500);
1615
+ if (action.providerOutcome !== null)
1616
+ enumeration(action.providerOutcome, CASE_PROVIDER_OUTCOMES, 'Case-response provider outcome');
1617
+ optionalText(action.outcomeDetail, 'Case-response action outcome detail', 2_000);
1618
+ optionalText(action.originActionId, 'Case-response action origin', 64);
1619
+ integer(action.historyOmitted, 'Case-response action omitted history', 0, 1_000_000);
1620
+ strings(action.historyLimitations, 'Case-response action history limitations', 8, 240);
1621
+ const transitions = array(action.transitions, 'Case-response action transitions', MAX_CASE_ACTION_EVENTS_PER_ACTION, 1);
1622
+ const eventIds = new Set();
1623
+ let projectedState = null;
1624
+ let latestReference = null;
1625
+ let latestProviderOutcome = null;
1626
+ let latestProviderOutcomeDetail = null;
1627
+ let latestOutcomeDetail = null;
1628
+ let previousTime = Number.NEGATIVE_INFINITY;
1629
+ let previousEventId = '';
1630
+ for (const eventCandidate of transitions) {
1631
+ const event = exact(eventCandidate, ['id', 'previousState', 'nextState', 'occurredAt', 'sourceClass', 'provenance', 'reference', 'evidencePinId', 'limitations', 'providerOutcome', 'outcomeDetail', 'originActionId', 'applied'], 'Case-response action transition');
1632
+ const eventId = text(event.id, 'Case-response action event id', 64);
1633
+ if (eventIds.has(eventId))
1634
+ fail('Case-response action event identity');
1635
+ eventIds.add(eventId);
1636
+ const previousState = event.previousState === null ? null : enumeration(event.previousState, CASE_ACTION_STATES, 'Case-response previous state');
1637
+ const nextState = enumeration(event.nextState, CASE_ACTION_STATES, 'Case-response next state');
1638
+ iso(event.occurredAt, 'Case-response action event time');
1639
+ const eventTime = Date.parse(event.occurredAt);
1640
+ if (eventTime < previousTime || (eventTime === previousTime && eventId < previousEventId))
1641
+ fail('Case-response action event order');
1642
+ previousTime = eventTime;
1643
+ previousEventId = eventId;
1644
+ const sourceClass = enumeration(event.sourceClass, CASE_ACTION_EVENT_SOURCE_CLASSES, 'Case-response action event source class');
1645
+ text(event.provenance, 'Case-response action event provenance', 80);
1646
+ optionalText(event.reference, 'Case-response action event reference', 500);
1647
+ optionalText(event.evidencePinId, 'Case-response action event evidence pin', 64);
1648
+ strings(event.limitations, 'Case-response action event limitations', 8, 240);
1649
+ const providerOutcome = event.providerOutcome === null ? null : enumeration(event.providerOutcome, CASE_PROVIDER_OUTCOMES, 'Case-response action event provider outcome');
1650
+ if (providerOutcome !== null && !['submitted', 'acknowledged', 'terminal'].includes(nextState) && sourceClass !== 'migration')
1651
+ fail('Case-response action event provider outcome');
1652
+ const migrationSnapshot = sourceClass === 'migration' && previousState === null;
1653
+ if (providerOutcome !== null && previousState === 'authorised' && nextState === 'submitted' && !migrationSnapshot)
1654
+ fail('Case-response submission event outcome');
1655
+ if (providerOutcome === 'no_response' && sourceClass === 'provider')
1656
+ fail('Case-response no-response event source');
1657
+ if (['ready_for_review', 'reviewed', 'authorised', 'submitted'].includes(nextState)
1658
+ && sourceClass !== 'analyst' && !migrationSnapshot)
1659
+ fail('Case-response explicit analyst transition');
1660
+ if (nextState === 'terminal' && previousState !== null
1661
+ && ['drafting', 'ready_for_review', 'reviewed', 'authorised'].includes(previousState)
1662
+ && (sourceClass !== 'analyst' || providerOutcome !== 'withdrawn'))
1663
+ fail('Case-response early terminal transition');
1664
+ if (providerOutcome === 'withdrawn'
1665
+ && (nextState !== 'terminal' || (sourceClass !== 'analyst' && !migrationSnapshot)))
1666
+ fail('Case-response withdrawn transition');
1667
+ optionalText(event.outcomeDetail, 'Case-response action event outcome detail', 2_000);
1668
+ optionalText(event.originActionId, 'Case-response action event origin', 64);
1669
+ if (event.originActionId === actionId)
1670
+ fail('Case-response action event origin');
1671
+ boolean(event.applied, 'Case-response action event projection');
1672
+ const applied = event.applied;
1673
+ if (providerOutcome !== null) {
1674
+ providerEvents.push({
1675
+ actionId,
1676
+ eventId,
1677
+ outcome: providerOutcome,
1678
+ occurredAt: event.occurredAt,
1679
+ reference: event.reference,
1680
+ applied,
1681
+ });
1682
+ }
1683
+ if (projectedState === null && Number(action.historyOmitted) > 0 && previousState !== null)
1684
+ projectedState = previousState;
1685
+ const expectedApplied = previousState === projectedState
1686
+ && isLegalCaseActionTransition(previousState, nextState, sourceClass);
1687
+ if (applied !== expectedApplied)
1688
+ fail('Case-response action event projection');
1689
+ if (applied) {
1690
+ projectedState = nextState;
1691
+ if (event.reference !== null)
1692
+ latestReference = event.reference;
1693
+ if (event.outcomeDetail !== null)
1694
+ latestOutcomeDetail = event.outcomeDetail;
1695
+ if (providerOutcome !== null) {
1696
+ latestProviderOutcome = providerOutcome;
1697
+ latestProviderOutcomeDetail = event.outcomeDetail;
1698
+ }
1699
+ }
1700
+ }
1701
+ if ((projectedState ?? 'drafting') !== action.state
1702
+ || latestReference !== action.reference
1703
+ || latestProviderOutcome !== action.providerOutcome
1704
+ || (latestProviderOutcomeDetail ?? latestOutcomeDetail) !== action.outcomeDetail)
1705
+ fail('Case-response action projection');
1706
+ if (action.originActionId === actionId)
1707
+ fail('Case-response action origin');
1708
+ iso(action.createdAt, 'Case-response action createdAt');
1709
+ iso(action.updatedAt, 'Case-response action updatedAt');
1710
+ }
1711
+ const lifecycle = exact(root.responseLifecycle, ['providerOutcomeState', 'latestProviderOutcome', 'observedChangeState', 'latestObservedEffect', 'latestObservedChangeAt', 'closure', 'limitations'], 'Case-response lifecycle');
1712
+ const providerOutcomeState = enumeration(lifecycle.providerOutcomeState, ['available', 'missing', 'ambiguous'], 'Case-response lifecycle provider state');
1713
+ let actualLatestProvider = null;
1714
+ if (lifecycle.latestProviderOutcome !== null) {
1715
+ const outcome = exact(lifecycle.latestProviderOutcome, ['actionId', 'eventId', 'outcome', 'occurredAt', 'reference'], 'Case-response lifecycle provider outcome');
1716
+ const actionId = text(outcome.actionId, 'Case-response lifecycle action id', 64);
1717
+ const eventId = text(outcome.eventId, 'Case-response lifecycle event id', 64);
1718
+ const providerOutcome = enumeration(outcome.outcome, CASE_PROVIDER_OUTCOMES, 'Case-response lifecycle provider outcome');
1719
+ iso(outcome.occurredAt, 'Case-response lifecycle provider time');
1720
+ const occurredAt = outcome.occurredAt;
1721
+ optionalText(outcome.reference, 'Case-response lifecycle reference', 500);
1722
+ actualLatestProvider = {
1723
+ actionId,
1724
+ eventId,
1725
+ outcome: providerOutcome,
1726
+ occurredAt,
1727
+ reference: outcome.reference,
1728
+ };
1729
+ }
1730
+ providerEvents.sort((left, right) => Date.parse(right.occurredAt) - Date.parse(left.occurredAt)
1731
+ || (right.eventId < left.eventId ? -1 : right.eventId > left.eventId ? 1 : 0));
1732
+ const latestProviderTime = providerEvents[0]?.occurredAt ?? null;
1733
+ const latestProviderEvents = latestProviderTime === null
1734
+ ? []
1735
+ : providerEvents.filter((event) => event.occurredAt === latestProviderTime);
1736
+ const expectedLatestProvider = escalationHistoryOmitted === 0
1737
+ && latestProviderEvents.length === 1 && latestProviderEvents[0].applied
1738
+ ? latestProviderEvents[0]
1739
+ : null;
1740
+ const expectedProviderState = escalationHistoryOmitted > 0
1741
+ ? 'ambiguous'
1742
+ : providerEvents.length === 0 ? 'missing' : expectedLatestProvider ? 'available' : 'ambiguous';
1743
+ if (providerOutcomeState !== expectedProviderState
1744
+ || (actualLatestProvider === null) !== (expectedLatestProvider === null)
1745
+ || (actualLatestProvider && expectedLatestProvider && (actualLatestProvider.actionId !== expectedLatestProvider.actionId
1746
+ || actualLatestProvider.eventId !== expectedLatestProvider.eventId
1747
+ || actualLatestProvider.outcome !== expectedLatestProvider.outcome
1748
+ || actualLatestProvider.occurredAt !== expectedLatestProvider.occurredAt
1749
+ || actualLatestProvider.reference !== expectedLatestProvider.reference)))
1750
+ fail('Case-response lifecycle provider state');
1751
+ const observedChangeState = enumeration(lifecycle.observedChangeState, ['available', 'missing', 'ambiguous'], 'Case-response lifecycle observed-change state');
1752
+ if (lifecycle.latestObservedEffect !== null) {
1753
+ const effect = exact(lifecycle.latestObservedEffect, ['reviewId', 'state', 'observedAt', 'sourceClass', 'source'], 'Case-response lifecycle observed effect');
1754
+ text(effect.reviewId, 'Case-response effect review id', 64);
1755
+ enumeration(effect.state, CASE_OBSERVED_EFFECT_STATES, 'Case-response effect state');
1756
+ iso(effect.observedAt, 'Case-response effect observedAt');
1757
+ enumeration(effect.sourceClass, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, 'Case-response effect source class');
1758
+ text(effect.source, 'Case-response effect source', 80);
1759
+ }
1760
+ iso(lifecycle.latestObservedChangeAt, 'Case-response observed change time', true);
1761
+ if ((observedChangeState === 'available') !== (lifecycle.latestObservedChangeAt !== null))
1762
+ fail('Case-response lifecycle observed-change state');
1763
+ if (lifecycle.closure !== null) {
1764
+ const closure = exact(lifecycle.closure, ['id', 'reason', 'createdAt', 'limitations'], 'Case-response closure');
1765
+ text(closure.id, 'Case-response closure id', 64);
1766
+ enumeration(closure.reason, CASE_CLOSURE_REASONS, 'Case-response closure reason');
1767
+ iso(closure.createdAt, 'Case-response closure time');
1768
+ strings(closure.limitations, 'Case-response closure limitations', 8, 240);
1769
+ }
1770
+ strings(lifecycle.limitations, 'Case-response lifecycle limitations', 8, 600);
1771
+ const reviewedInputs = {
1772
+ contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
1773
+ version: CASE_RESPONSE_REVIEW_INPUTS_VERSION,
1774
+ profile: {
1775
+ id: profile.id,
1776
+ label: profile.label,
1777
+ audience: profile.audience,
1778
+ subject: profile.subject,
1779
+ checklist: profile.checklist,
1780
+ includedEvidence: profile.includedEvidence,
1781
+ excludedEvidence: profile.excludedEvidence,
1782
+ redactions: profile.redactions,
1783
+ },
1784
+ case: root.case,
1785
+ incident: root.incident,
1786
+ contacts: root.contacts,
1787
+ selectedEvidence: root.selectedEvidence,
1788
+ contradictions: root.contradictions,
1789
+ readiness: root.readiness,
1790
+ artefactReferences: root.artefactReferences,
1791
+ escalationHistory: root.escalationHistory,
1792
+ escalationHistoryOmitted: root.escalationHistoryOmitted,
1793
+ escalationHistoryLimitations: root.escalationHistoryLimitations,
1794
+ responseLifecycle: root.responseLifecycle,
1795
+ };
1796
+ const expectedReviewDigest = createHash('sha256')
1797
+ .update(canonicalArtifactJsonV2(reviewedInputs))
1798
+ .digest('hex');
1799
+ if (authorisation.reviewedInputDigestSha256 !== expectedReviewDigest)
1800
+ fail('Case-response reviewed-input digest');
1801
+ const provenance = exact(root.provenance, ['latestEvidenceCapturedAt', 'evidencePinCount', 'decisionCount', 'assertionCount', 'observationAge', 'limitations'], 'Case-response provenance');
1802
+ iso(provenance.latestEvidenceCapturedAt, 'Case-response evidence time', true);
1803
+ integer(provenance.evidencePinCount, 'Case-response evidence pin count', 0, MAX_CASE_EVIDENCE_PINS);
1804
+ integer(provenance.decisionCount, 'Case-response decision count', 0, MAX_CASE_DECISIONS);
1805
+ integer(provenance.assertionCount, 'Case-response assertion count', 0, MAX_CASE_ASSERTIONS);
1806
+ const age = exact(provenance.observationAge, ['ageSeconds', 'band', 'refreshRecommended'], 'Case-response observation age');
1807
+ integer(age.ageSeconds, 'Case-response observation age seconds', Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
1808
+ enumeration(age.band, ['future_or_clock_skew', 'one_to_seven_days', 'over_seven_days', 'under_24_hours'], 'Case-response observation age band');
1809
+ boolean(age.refreshRecommended, 'Case-response refresh recommendation');
1810
+ const expectedAge = expectedObservationAge(incident.observedAt, root.generatedAt);
1811
+ if (age.ageSeconds !== expectedAge.ageSeconds || age.band !== expectedAge.band || age.refreshRecommended !== expectedAge.refreshRecommended)
1812
+ fail('Case-response observation age');
1813
+ strings(provenance.limitations, 'Case-response limitations', 8, 600);
1814
+ const integrity = exact(root.integrity, ['algorithm', 'canonicalization', 'scope', 'digestSha256'], 'Case-response integrity');
1815
+ if (integrity.algorithm !== 'SHA-256' || integrity.canonicalization !== 'sorted-json-v2' || integrity.scope !== 'packet excluding integrity')
1816
+ fail('Case-response integrity');
1817
+ if (typeof integrity.digestSha256 !== 'string' || !HEX_DIGEST_RE.test(integrity.digestSha256))
1818
+ fail('Case-response integrity');
1819
+ }
1820
+ function validateCaseResponsePacket(value) {
1821
+ if (value.schemaVersion === PUBLIC_CASE_RESPONSE_PACKET_VERSION)
1822
+ return validateCaseResponsePacketV6(value);
1823
+ if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion < CASE_RESPONSE_PACKET_VERSION) {
1824
+ throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is not part of the public compatibility boundary; no data was changed.`);
1825
+ }
1826
+ if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion > CASE_RESPONSE_PACKET_VERSION) {
1827
+ throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is newer than the supported version ${CASE_RESPONSE_PACKET_VERSION}; no data was changed.`);
1828
+ }
1829
+ validateCaseResponsePacketV7(value);
1830
+ }
1342
1831
  function validateBriefFact(value, label) {
1343
1832
  const fact = exact(value, ['label', 'value', 'detail', 'provenance'], label);
1344
1833
  text(fact.label, label, 320);
@@ -1365,9 +1854,10 @@ function validateDecisionEntry(value, label) {
1365
1854
  if (typeof entry.href !== 'string' || !/^#[^\u0000-\u001f\u007f]{1,160}$/u.test(entry.href))
1366
1855
  fail(label);
1367
1856
  }
1368
- function validateBrief(value) {
1857
+ function validatePublicBrief(value) {
1369
1858
  const brief = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'target', 'targetType', 'task', 'taskLabel', 'question', 'summary', 'observation', 'verifiedFacts', 'contradictions', 'unknowns', 'nextActions', 'relationships', 'limitations'], 'Investigation capsule brief');
1370
- if (brief.schema !== LOOKUP_INVESTIGATION_BRIEF_SCHEMA || brief.schemaVersion !== 1)
1859
+ if (brief.schema !== LOOKUP_INVESTIGATION_BRIEF_SCHEMA
1860
+ || brief.schemaVersion !== PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION)
1371
1861
  fail('Investigation capsule brief');
1372
1862
  iso(brief.generatedAt, 'Investigation capsule brief generatedAt');
1373
1863
  text(brief.target, 'Investigation capsule brief target', 253);
@@ -1411,9 +1901,191 @@ function validateBrief(value) {
1411
1901
  strings(relationships.kinds, 'Investigation capsule relationship kinds', 12, 320);
1412
1902
  strings(brief.limitations, 'Investigation capsule brief limitations', 20, 320);
1413
1903
  }
1904
+ function validateProjectionCollection(value, label, totalMaximum, displayedMaximum, validateItem) {
1905
+ const collection = exact(value, ['total', 'displayed', 'omitted', 'items'], label);
1906
+ const total = integer(collection.total, `${label} total`, 0, totalMaximum);
1907
+ const displayed = integer(collection.displayed, `${label} displayed`, 0, displayedMaximum);
1908
+ const omitted = integer(collection.omitted, `${label} omitted`, 0, totalMaximum);
1909
+ const items = array(collection.items, `${label} items`, displayedMaximum);
1910
+ if (displayed > total || omitted !== total - displayed || items.length !== displayed)
1911
+ fail(label);
1912
+ items.forEach(validateItem);
1913
+ return { total, displayed, omitted, items };
1914
+ }
1915
+ function validateSortedUnique(values, label) {
1916
+ for (let index = 0; index < values.length; index += 1) {
1917
+ if (index > 0 && values[index - 1] >= values[index])
1918
+ fail(label);
1919
+ }
1920
+ }
1921
+ function validateDecisionIdentifier(value, label) {
1922
+ const identifier = text(value, label, 200);
1923
+ if (!/^[a-z0-9](?:[a-z0-9._:-]{0,199})$/u.test(identifier))
1924
+ fail(label);
1925
+ return identifier;
1926
+ }
1927
+ function validateProjectedStringCollection(value, label, totalMaximum, displayedMaximum, textMaximum) {
1928
+ const projection = validateProjectionCollection(value, label, totalMaximum, displayedMaximum, (item) => { text(item, `${label} item`, textMaximum); });
1929
+ validateSortedUnique(projection.items, label);
1930
+ return projection;
1931
+ }
1932
+ function validateDecisionFactSource(value, label) {
1933
+ const source = exact(value, [
1934
+ 'id',
1935
+ 'label',
1936
+ 'provenance',
1937
+ 'evidenceState',
1938
+ 'observedAt',
1939
+ 'references',
1940
+ 'limitations',
1941
+ ], label);
1942
+ const id = validateDecisionIdentifier(source.id, `${label} id`);
1943
+ text(source.label, `${label} label`, 160);
1944
+ enumeration(source.provenance, DECISION_FACT_PROVENANCE_STATES, `${label} provenance`);
1945
+ enumeration(source.evidenceState, DECISION_FACT_EVIDENCE_STATES, `${label} evidence state`);
1946
+ iso(source.observedAt, `${label} observation time`, true);
1947
+ validateProjectedStringCollection(source.references, `${label} references`, MAX_DECISION_FACT_REFERENCES, MAX_DECISION_FACT_PROJECTION_SOURCE_REFERENCES, 200);
1948
+ validateProjectedStringCollection(source.limitations, `${label} limitations`, MAX_DECISION_FACT_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_SOURCE_LIMITATIONS, 280);
1949
+ return id;
1950
+ }
1951
+ function validateProjectedDecisionFact(value, label) {
1952
+ const fact = exact(value, [
1953
+ 'version',
1954
+ 'id',
1955
+ 'question',
1956
+ 'conclusion',
1957
+ 'importance',
1958
+ 'evidenceState',
1959
+ 'completeness',
1960
+ 'freshness',
1961
+ 'consistency',
1962
+ 'dependencies',
1963
+ 'sourceReferences',
1964
+ 'sources',
1965
+ 'contradictions',
1966
+ 'limitations',
1967
+ 'safeNextActions',
1968
+ ], label);
1969
+ if (fact.version !== DECISION_FACT_VERSION)
1970
+ fail(`${label} version`);
1971
+ const id = validateDecisionIdentifier(fact.id, `${label} id`);
1972
+ text(fact.question, `${label} question`, 320);
1973
+ text(fact.conclusion, `${label} conclusion`, 640);
1974
+ enumeration(fact.importance, ['high', 'medium', 'low'], `${label} importance`);
1975
+ const evidenceState = enumeration(fact.evidenceState, DECISION_FACT_EVIDENCE_STATES, `${label} evidence state`);
1976
+ if (fact.completeness !== decisionFactCompleteness(evidenceState))
1977
+ fail(`${label} completeness`);
1978
+ enumeration(fact.freshness, DECISION_FACT_FRESHNESS_STATES, `${label} freshness`);
1979
+ const consistency = enumeration(fact.consistency, DECISION_FACT_CONSISTENCY_STATES, `${label} consistency`);
1980
+ const dependencies = validateProjectedStringCollection(fact.dependencies, `${label} dependencies`, MAX_DECISION_FACT_CONTRIBUTORS, MAX_DECISION_FACT_PROJECTION_SOURCES, 200);
1981
+ dependencies.items.forEach((item) => validateDecisionIdentifier(item, `${label} dependency`));
1982
+ validateProjectedStringCollection(fact.sourceReferences, `${label} source references`, MAX_DECISION_FACT_REFERENCES, MAX_DECISION_FACT_PROJECTION_REFERENCES, 200);
1983
+ const sourceIds = [];
1984
+ const sources = validateProjectionCollection(fact.sources, `${label} sources`, MAX_DECISION_FACT_CONTRIBUTORS, MAX_DECISION_FACT_PROJECTION_SOURCES, (item, index) => { sourceIds.push(validateDecisionFactSource(item, `${label} source ${index + 1}`)); });
1985
+ validateSortedUnique(sourceIds, `${label} sources`);
1986
+ if (dependencies.total !== sources.total
1987
+ || dependencies.displayed !== sources.displayed
1988
+ || dependencies.omitted !== sources.omitted
1989
+ || !sameValues(dependencies.items, sourceIds))
1990
+ fail(`${label} dependencies`);
1991
+ validateProjectedStringCollection(fact.contradictions, `${label} contradictions`, MAX_DECISION_FACT_CONTRADICTIONS, MAX_DECISION_FACT_PROJECTION_CONTRADICTIONS, 640);
1992
+ validateProjectedStringCollection(fact.limitations, `${label} limitations`, MAX_DECISION_FACT_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_LIMITATIONS, 280);
1993
+ const actionIds = [];
1994
+ validateProjectionCollection(fact.safeNextActions, `${label} safe next actions`, MAX_DECISION_FACT_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_NEXT_ACTIONS, (candidate, index) => {
1995
+ const action = exact(candidate, ['id', 'label', 'reason', 'expectedOutcome', 'href', 'importance'], `${label} safe next action ${index + 1}`);
1996
+ actionIds.push(validateDecisionIdentifier(action.id, `${label} safe next action id`));
1997
+ text(action.label, `${label} safe next action label`, 160);
1998
+ text(action.reason, `${label} safe next action reason`, 320);
1999
+ text(action.expectedOutcome, `${label} safe next action outcome`, 320);
2000
+ if (typeof action.href !== 'string' || !/^#[a-z0-9](?:[a-z0-9._:-]{0,159})$/u.test(action.href))
2001
+ fail(`${label} safe next action href`);
2002
+ enumeration(action.importance, ['high', 'medium', 'low'], `${label} safe next action importance`);
2003
+ });
2004
+ validateSortedUnique(actionIds, `${label} safe next actions`);
2005
+ return { id, consistency };
2006
+ }
2007
+ function validateDecisionFactProjection(value) {
2008
+ const projection = exact(value, [
2009
+ 'version',
2010
+ 'total',
2011
+ 'displayed',
2012
+ 'omitted',
2013
+ 'contradictory',
2014
+ 'unresolved',
2015
+ 'facts',
2016
+ ], 'Investigation capsule Decision Fact projection');
2017
+ if (projection.version !== DECISION_FACT_PROJECTION_VERSION)
2018
+ fail('Investigation capsule Decision Fact projection');
2019
+ const total = integer(projection.total, 'Investigation capsule Decision Fact total', 0, MAX_DECISION_FACTS);
2020
+ const displayed = integer(projection.displayed, 'Investigation capsule Decision Fact displayed', 0, MAX_DECISION_FACT_PROJECTION_FACTS);
2021
+ const omitted = integer(projection.omitted, 'Investigation capsule Decision Fact omitted', 0, MAX_DECISION_FACTS);
2022
+ const contradictory = integer(projection.contradictory, 'Investigation capsule Decision Fact contradictory', 0, total);
2023
+ const unresolved = integer(projection.unresolved, 'Investigation capsule Decision Fact unresolved', 0, total);
2024
+ const facts = array(projection.facts, 'Investigation capsule Decision Facts', MAX_DECISION_FACT_PROJECTION_FACTS);
2025
+ if (displayed > total || omitted !== total - displayed || facts.length !== displayed
2026
+ || contradictory + unresolved > total)
2027
+ fail('Investigation capsule Decision Fact projection');
2028
+ const factIds = [];
2029
+ let displayedContradictory = 0;
2030
+ let displayedUnresolved = 0;
2031
+ for (const [index, candidate] of facts.entries()) {
2032
+ const validated = validateProjectedDecisionFact(candidate, `Investigation capsule Decision Fact ${index + 1}`);
2033
+ factIds.push(validated.id);
2034
+ if (validated.consistency === 'contradictory')
2035
+ displayedContradictory += 1;
2036
+ if (validated.consistency === 'unknown')
2037
+ displayedUnresolved += 1;
2038
+ }
2039
+ validateSortedUnique(factIds, 'Investigation capsule Decision Facts');
2040
+ if (contradictory < displayedContradictory || unresolved < displayedUnresolved
2041
+ || (omitted === 0 && (contradictory !== displayedContradictory || unresolved !== displayedUnresolved))) {
2042
+ fail('Investigation capsule Decision Fact state counts');
2043
+ }
2044
+ if (new TextEncoder().encode(JSON.stringify(projection)).byteLength > MAX_DECISION_FACT_PROJECTION_BYTES) {
2045
+ fail('Investigation capsule Decision Fact projection byte limit');
2046
+ }
2047
+ }
2048
+ function validateCurrentBrief(value) {
2049
+ const brief = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'target', 'targetType', 'task', 'taskLabel', 'question', 'summary', 'observation', 'decisionFacts', 'relationships', 'limitations'], 'Investigation capsule brief');
2050
+ if (brief.schema !== LOOKUP_INVESTIGATION_BRIEF_SCHEMA
2051
+ || brief.schemaVersion !== LOOKUP_INVESTIGATION_BRIEF_VERSION)
2052
+ fail('Investigation capsule brief');
2053
+ iso(brief.generatedAt, 'Investigation capsule brief generatedAt');
2054
+ text(brief.target, 'Investigation capsule brief target', 253);
2055
+ text(brief.targetType, 'Investigation capsule brief target type', 40);
2056
+ enumeration(brief.task, ['general', 'acquisition', 'brand', 'incident', 'owned'], 'Investigation capsule brief task');
2057
+ text(brief.taskLabel, 'Investigation capsule brief task label', 320);
2058
+ text(brief.question, 'Investigation capsule brief question', 320);
2059
+ text(brief.summary, 'Investigation capsule brief summary', 500);
2060
+ const observation = exact(brief.observation, ['observedAt', 'evidenceAgeDays', 'completeSources', 'limitedSources', 'freshnessPolicy'], 'Investigation capsule observation');
2061
+ iso(observation.observedAt, 'Investigation capsule observation time', true);
2062
+ if (observation.evidenceAgeDays !== null)
2063
+ integer(observation.evidenceAgeDays, 'Investigation capsule evidence age', 0, 1_000_000);
2064
+ integer(observation.completeSources, 'Investigation capsule complete sources', 0, 100);
2065
+ integer(observation.limitedSources, 'Investigation capsule limited sources', 0, 100);
2066
+ const policy = exact(observation.freshnessPolicy, ['version', 'id', 'task', 'thresholdsDays'], 'Investigation capsule freshness policy');
2067
+ if (policy.version !== 1)
2068
+ fail('Investigation capsule freshness policy');
2069
+ enumeration(policy.id, ['task-default', 'analyst-custom'], 'Investigation capsule freshness policy');
2070
+ if (policy.task !== brief.task)
2071
+ fail('Investigation capsule freshness policy');
2072
+ const thresholds = exact(policy.thresholdsDays, ['registration', 'network', 'web'], 'Investigation capsule freshness thresholds');
2073
+ for (const key of ['registration', 'network', 'web'])
2074
+ integer(thresholds[key], 'Investigation capsule freshness threshold', 0, 3650);
2075
+ validateDecisionFactProjection(brief.decisionFacts);
2076
+ const relationships = exact(brief.relationships, ['nodes', 'edges', 'truncated', 'kinds'], 'Investigation capsule relationship summary');
2077
+ integer(relationships.nodes, 'Investigation capsule relationship node count', 0, 72);
2078
+ integer(relationships.edges, 'Investigation capsule relationship edge count', 0, 120);
2079
+ boolean(relationships.truncated, 'Investigation capsule relationship truncation');
2080
+ strings(relationships.kinds, 'Investigation capsule relationship kinds', 12, 320);
2081
+ strings(brief.limitations, 'Investigation capsule brief limitations', 20, 320);
2082
+ if (new TextEncoder().encode(JSON.stringify(brief)).byteLength > MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES) {
2083
+ fail('Investigation capsule brief byte limit');
2084
+ }
2085
+ }
1414
2086
  function validateGraph(value) {
1415
2087
  const graph = exact(value, ['version', 'targetId', 'nodes', 'edges', 'sources', 'truncated', 'limitations'], 'Investigation capsule graph');
1416
- if (graph.version !== 2)
2088
+ if (graph.version !== LOOKUP_ASSET_GRAPH_VERSION)
1417
2089
  fail('Investigation capsule graph');
1418
2090
  text(graph.targetId, 'Investigation capsule graph target', 160);
1419
2091
  const nodes = array(graph.nodes, 'Investigation capsule graph nodes', 72, 1);
@@ -1501,7 +2173,8 @@ function validateAnalystRecords(value) {
1501
2173
  }
1502
2174
  export function validateInvestigationCapsuleStructure(value) {
1503
2175
  const root = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'application', 'target', 'sourceContracts', 'investigationBrief', 'graphSnapshot', 'analystRecords', 'integrity', 'limitations'], 'Investigation capsule');
1504
- if (root.schemaVersion !== LEGACY_INVESTIGATION_CAPSULE_VERSION && root.schemaVersion !== INVESTIGATION_CAPSULE_VERSION)
2176
+ if (root.schemaVersion !== PUBLIC_INVESTIGATION_CAPSULE_VERSION
2177
+ && root.schemaVersion !== INVESTIGATION_CAPSULE_VERSION)
1505
2178
  fail('Investigation capsule');
1506
2179
  iso(root.generatedAt, 'Investigation capsule generatedAt');
1507
2180
  const application = exact(root.application, ['name', 'version'], 'Investigation capsule application');
@@ -1527,21 +2200,27 @@ export function validateInvestigationCapsuleStructure(value) {
1527
2200
  const expectedContractIds = root.analystRecords === null
1528
2201
  ? ['lookup-evidence', 'investigation-brief', 'asset-graph']
1529
2202
  : ['lookup-evidence', 'investigation-brief', 'asset-graph', 'analyst-records'];
2203
+ const expectedBriefVersion = root.schemaVersion === INVESTIGATION_CAPSULE_VERSION
2204
+ ? LOOKUP_INVESTIGATION_BRIEF_VERSION
2205
+ : PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION;
1530
2206
  if (!sameValues(contracts.map((candidate) => candidate.id), expectedContractIds))
1531
2207
  fail('Investigation capsule source contracts');
1532
2208
  if (!byId.has('lookup-evidence') || !byId.has('investigation-brief') || !byId.has('asset-graph')
1533
2209
  || byId.get('lookup-evidence')?.embedded !== false
1534
2210
  || byId.get('investigation-brief')?.schema !== LOOKUP_INVESTIGATION_BRIEF_SCHEMA
1535
- || byId.get('investigation-brief')?.version !== 1
2211
+ || byId.get('investigation-brief')?.version !== expectedBriefVersion
1536
2212
  || byId.get('investigation-brief')?.embedded !== true
1537
- || byId.get('asset-graph')?.schema !== 'whoisleuth.lookup-asset-graph'
1538
- || byId.get('asset-graph')?.version !== 2
2213
+ || byId.get('asset-graph')?.schema !== LOOKUP_ASSET_GRAPH_SCHEMA
2214
+ || byId.get('asset-graph')?.version !== LOOKUP_ASSET_GRAPH_VERSION
1539
2215
  || byId.get('asset-graph')?.embedded !== true
1540
- || (byId.has('analyst-records') && (byId.get('analyst-records')?.schema !== 'whoisleuth.case-analyst-records'
1541
- || byId.get('analyst-records')?.version !== 1
2216
+ || (byId.has('analyst-records') && (byId.get('analyst-records')?.schema !== INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA
2217
+ || byId.get('analyst-records')?.version !== INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION
1542
2218
  || byId.get('analyst-records')?.embedded !== true)))
1543
2219
  fail('Investigation capsule source contracts');
1544
- validateBrief(root.investigationBrief);
2220
+ if (root.schemaVersion === INVESTIGATION_CAPSULE_VERSION)
2221
+ validateCurrentBrief(root.investigationBrief);
2222
+ else
2223
+ validatePublicBrief(root.investigationBrief);
1545
2224
  validateGraph(root.graphSnapshot);
1546
2225
  validateAnalystRecords(root.analystRecords);
1547
2226
  const brief = root.investigationBrief;
@@ -1551,17 +2230,12 @@ export function validateInvestigationCapsuleStructure(value) {
1551
2230
  || brief.relationships.nodes !== graph.nodes.length
1552
2231
  || brief.relationships.edges !== graph.edges.length)
1553
2232
  fail('Investigation capsule projection linkage');
1554
- const current = root.schemaVersion === INVESTIGATION_CAPSULE_VERSION;
1555
- const integrity = exact(root.integrity, current
1556
- ? ['algorithm', 'canonicalization', 'scope', 'briefDigest', 'graphDigest', 'analystRecordsDigest', 'digestSha256']
1557
- : ['algorithm', 'briefDigest', 'graphDigest', 'analystRecordsDigest'], 'Investigation capsule integrity');
2233
+ const integrity = exact(root.integrity, ['algorithm', 'canonicalization', 'scope', 'briefDigest', 'graphDigest', 'analystRecordsDigest', 'digestSha256'], 'Investigation capsule integrity');
1558
2234
  if (integrity.algorithm !== 'SHA-256')
1559
2235
  fail('Investigation capsule integrity');
1560
- if (current) {
1561
- if (integrity.canonicalization !== 'sorted-json-v2' || integrity.scope !== 'capsule excluding integrity')
1562
- fail('Investigation capsule integrity');
1563
- digest(integrity.digestSha256, 'Investigation capsule digest');
1564
- }
2236
+ if (integrity.canonicalization !== 'sorted-json-v2' || integrity.scope !== 'capsule excluding integrity')
2237
+ fail('Investigation capsule integrity');
2238
+ digest(integrity.digestSha256, 'Investigation capsule digest');
1565
2239
  digest(integrity.briefDigest, 'Investigation capsule brief digest');
1566
2240
  digest(integrity.graphDigest, 'Investigation capsule graph digest');
1567
2241
  if (integrity.analystRecordsDigest !== null)
@@ -1791,7 +2465,7 @@ function validateDomainChangePacket(value) {
1791
2465
  }))
1792
2466
  fail('Domain change packet summary');
1793
2467
  strings(root.limitations, 'Domain change packet limitations', 8, 600);
1794
- validateIntegrity(root.integrity, 'Domain change packet integrity', root.version, 1, DOMAIN_CHANGE_PACKET_VERSION);
2468
+ validateIntegrity(root.integrity, 'Domain change packet integrity', root.version, DOMAIN_CHANGE_PACKET_VERSION);
1795
2469
  }
1796
2470
  export function validateSignedDigestArtifactStructure(schema, value) {
1797
2471
  if (value.schema !== schema)
@@ -1808,7 +2482,7 @@ export function validateSignedDigestArtifactStructure(schema, value) {
1808
2482
  validateBulkReviewManifest(value);
1809
2483
  else if (schema === DOMAIN_CONTROL_MANIFEST_SCHEMA) {
1810
2484
  try {
1811
- normalizeDomainControlPassportDocument(value);
2485
+ normalizeDomainControlManifestDocument(value);
1812
2486
  }
1813
2487
  catch {
1814
2488
  fail('Domain control manifest');