@slicedearth/whoisleuth-cli 2.3.0 → 2.5.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 (418) hide show
  1. package/NOTICE +8 -2
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/argument-values.mjs +55 -0
  7. package/cli/arguments.mjs +24 -408
  8. package/cli/artifact-validation/case-response.mjs +65 -44
  9. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  10. package/cli/artifact-validation/lookup-evidence.mjs +15 -9
  11. package/cli/artifact-validation/signed-review.mjs +136 -68
  12. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  13. package/cli/artifact-verify.mjs +92 -24
  14. package/cli/assurance-arguments.mjs +56 -0
  15. package/cli/assurance-command-definitions.mjs +179 -0
  16. package/cli/assurance-command-runner.mjs +40 -5
  17. package/cli/bagit-folder.mjs +57 -0
  18. package/cli/bagit-review.mjs +23 -0
  19. package/cli/bounded-json.mjs +1 -1
  20. package/cli/bulk-checkpoint.mjs +1 -1
  21. package/cli/bulk-command-runner.mjs +4 -2
  22. package/cli/bulk-output.mjs +29 -3
  23. package/cli/bulk.mjs +2 -3
  24. package/cli/case-command.mjs +212 -0
  25. package/cli/case-pack.mjs +50 -32
  26. package/cli/ci-report.mjs +6 -3
  27. package/cli/collection-arguments.mjs +123 -0
  28. package/cli/collection-command-definitions.mjs +330 -0
  29. package/cli/collection-preflight.mjs +8 -1
  30. package/cli/command-argument-grammar.mjs +8 -206
  31. package/cli/command-definition.mjs +329 -0
  32. package/cli/command-reference.mjs +94 -1357
  33. package/cli/command-seeds.mjs +18 -0
  34. package/cli/compare.mjs +1 -1
  35. package/cli/completion.mjs +298 -79
  36. package/cli/config-profile.mjs +8 -6
  37. package/cli/ct-event-intake.mjs +16 -16
  38. package/cli/discover.mjs +5 -4
  39. package/cli/discovery-observation-snapshot.mjs +46 -18
  40. package/cli/discovery-scan-command-runner.mjs +5 -3
  41. package/cli/discovery-scan.mjs +7 -4
  42. package/cli/discovery-snapshot.mjs +2 -2
  43. package/cli/discovery-workflow.mjs +5 -5
  44. package/cli/domain-control-monitor.mjs +50 -20
  45. package/cli/domain-control-observations.mjs +151 -103
  46. package/cli/errors.mjs +37 -6
  47. package/cli/evidence-arguments.mjs +46 -0
  48. package/cli/evidence-command-definitions.mjs +69 -0
  49. package/cli/evidence-command-runner.mjs +18 -8
  50. package/cli/evidence-signing.mjs +5 -4
  51. package/cli/external-observation-mapping.mjs +9 -10
  52. package/cli/formatters/terminal-command-formats.mjs +6 -0
  53. package/cli/formatters/terminal-lookup.mjs +24 -46
  54. package/cli/formatters/terminal-shared.mjs +5 -3
  55. package/cli/formatters/terminal-technology.mjs +114 -0
  56. package/cli/history-arguments.mjs +39 -0
  57. package/cli/history-command-definitions.mjs +88 -0
  58. package/cli/indicator-set-command.mjs +33 -0
  59. package/cli/inline-command-families.mjs +6 -46
  60. package/cli/inline-command-runner.mjs +4 -31
  61. package/cli/input.mjs +2 -2
  62. package/cli/intake-command.mjs +107 -0
  63. package/cli/interactive-launcher.mjs +2 -160
  64. package/cli/interchange-report.mjs +8 -10
  65. package/cli/investigation-artifacts.mjs +176 -0
  66. package/cli/investigation-checkpoint.mjs +10 -0
  67. package/cli/investigation-folder.mjs +114 -0
  68. package/cli/investigation-manifest.mjs +1 -112
  69. package/cli/investigation-package-review.mjs +103 -0
  70. package/cli/investigation-plan.mjs +38 -147
  71. package/cli/investigation-recipes.mjs +177 -0
  72. package/cli/investigation-run.mjs +256 -35
  73. package/cli/local-document-checkpoint.mjs +170 -0
  74. package/cli/local-mmdb-review.mjs +197 -17
  75. package/cli/lookalike-calibration.mjs +1 -1
  76. package/cli/lookup-brief.mjs +2 -2
  77. package/cli/lookup-browser.mjs +5 -12
  78. package/cli/lookup-command-runner.mjs +33 -39
  79. package/cli/lookup-diff.mjs +47 -12
  80. package/cli/lookup-plan.mjs +16 -14
  81. package/cli/lookup-reconcile.mjs +15 -12
  82. package/cli/lookup-timeline.mjs +7 -6
  83. package/cli/mail-header-review.mjs +24 -247
  84. package/cli/mail-review.mjs +75 -21
  85. package/cli/manual.mjs +1 -0
  86. package/cli/network-arguments.mjs +51 -0
  87. package/cli/network-command-definitions.mjs +130 -0
  88. package/cli/network-command-runner.mjs +45 -31
  89. package/cli/offline-evidence-review.mjs +39 -6
  90. package/cli/output-file.mjs +31 -5
  91. package/cli/posture.mjs +1 -1
  92. package/cli/registry-doctor.mjs +3 -1
  93. package/cli/registry-fixture-scaffold.mjs +1 -1
  94. package/cli/registry-support.mjs +1 -1
  95. package/cli/retained-artifact-diff.mjs +10 -9
  96. package/cli/review-arguments.mjs +131 -0
  97. package/cli/review-command-definitions.mjs +273 -0
  98. package/cli/review-command-runner.mjs +104 -149
  99. package/cli/risk-calibration.mjs +8 -8
  100. package/cli/runner.mjs +74 -37
  101. package/cli/saved-lookup.mjs +2 -2
  102. package/cli/selected-input-worker.mjs +63 -0
  103. package/cli/sharing-review.mjs +8 -80
  104. package/cli/signer-trust.mjs +104 -0
  105. package/cli/source-reliability.mjs +75 -84
  106. package/cli/support-arguments.mjs +49 -0
  107. package/cli/support-command-definitions.mjs +209 -0
  108. package/cli/support-command-runner.mjs +2 -2
  109. package/cli/terminal-input.mjs +168 -0
  110. package/cli/workflow-arguments.mjs +86 -0
  111. package/cli/workflow-command-definitions.mjs +78 -0
  112. package/cli/workflow-command-runner.mjs +75 -13
  113. package/docs/cli-reference.md +103 -30
  114. package/docs/cli.md +510 -25
  115. package/docs/contextual-reviews.md +155 -0
  116. package/lib/abort.mjs +4 -1
  117. package/lib/analyst-taxonomy.mjs +2 -40
  118. package/lib/application-version.mjs +1 -1
  119. package/lib/availability.mjs +27 -754
  120. package/lib/bounded-contract-normalizers.mjs +4 -0
  121. package/lib/bounded-file.mjs +3 -1
  122. package/lib/bounded-html-document.mjs +125 -0
  123. package/lib/bounded-json.mjs +2 -290
  124. package/lib/browser-library-profile.mjs +99 -130
  125. package/lib/browser-library-worker.mjs +112 -0
  126. package/lib/candidate-provenance-bounds.mjs +2 -0
  127. package/lib/classify.mjs +1 -1
  128. package/lib/client-behavior-profile.mjs +3 -3
  129. package/lib/ct-query.mjs +2 -27
  130. package/lib/ct-response-bounds.mjs +2 -9
  131. package/lib/ct-search.mjs +89 -46
  132. package/lib/dmarc-discovery.mjs +62 -0
  133. package/lib/dns-convergence-review.mjs +21 -8
  134. package/lib/dns-delegation-health.mjs +36 -22
  135. package/lib/dns-inheritance-review.mjs +207 -0
  136. package/lib/dns-intelligence.mjs +44 -29
  137. package/lib/dns-mx.mjs +1 -1
  138. package/lib/dns-query-coverage.mjs +40 -0
  139. package/lib/dnssec-evidence-validation.mjs +11 -3
  140. package/lib/domain-assurance.mjs +1 -1
  141. package/lib/domain-change-packet.mjs +18 -14
  142. package/lib/domain-change-review.mjs +40 -16
  143. package/lib/domain-control-flight-recorder.mjs +301 -124
  144. package/lib/domain-control-manifest.mjs +126 -58
  145. package/lib/domain-posture-analysis.mjs +41 -11
  146. package/lib/domain-posture-parsers.mjs +88 -6
  147. package/lib/domain-posture.mjs +120 -32
  148. package/lib/domain-registration.mjs +392 -0
  149. package/lib/evidence-export-privacy.mjs +568 -0
  150. package/lib/evidence-export.mjs +12 -593
  151. package/lib/evidence-report.mjs +7 -2
  152. package/lib/external-intelligence-risk.mjs +2 -124
  153. package/lib/favicon.mjs +117 -41
  154. package/lib/generated/cisa-kev-catalog.mjs +50 -4
  155. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  156. package/lib/generated/retire-browser-catalog.mjs +2742 -1034
  157. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  158. package/lib/html-signals.mjs +87 -85
  159. package/lib/http-evidence-bounds.mjs +2 -7
  160. package/lib/http-intelligence.mjs +52 -27
  161. package/lib/idn-confusable-policy.mjs +6 -4
  162. package/lib/idn-confusables.mjs +1 -1
  163. package/lib/interchange-fidelity-registry.mjs +10 -0
  164. package/lib/lookup-cache.mjs +34 -11
  165. package/lib/lookup-child-profile-contract.mjs +24 -15
  166. package/lib/lookup-contract-primitives.mjs +1 -1
  167. package/lib/lookup-diagnostics.mjs +2 -1
  168. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  169. package/lib/lookup-progress.mjs +79 -13
  170. package/lib/lookup-response-contract.mjs +1077 -0
  171. package/lib/lookup-response.mjs +11 -24
  172. package/lib/lookup-source-progress.mjs +24 -35
  173. package/lib/lookup.mjs +49 -28
  174. package/lib/observed-network-context.mjs +43 -27
  175. package/lib/opportunity-scoring.mjs +2 -107
  176. package/lib/outbound-identity.mjs +1 -1
  177. package/lib/outbound-request-bounds.mjs +4 -1
  178. package/lib/page-fingerprints.mjs +87 -134
  179. package/lib/page-role-profile.mjs +12 -8
  180. package/lib/perceptual-hash-comparison.mjs +2 -34
  181. package/lib/perceptual-hash.mjs +79 -24
  182. package/lib/portable-generator.mjs +2 -29
  183. package/lib/project-metadata.mjs +2 -8
  184. package/lib/rdap-bootstrap.mjs +72 -22
  185. package/lib/rdap-client.mjs +26 -14
  186. package/lib/rdap-normalization.mjs +33 -18
  187. package/lib/rdap-search-workbench.mjs +1 -1
  188. package/lib/rdap-transport.mjs +47 -20
  189. package/lib/rdap-types.mjs +2 -1
  190. package/lib/rdap.mjs +18 -3
  191. package/lib/registrable-domain.mjs +2 -36
  192. package/lib/registrar-standing-contract.mjs +2 -1
  193. package/lib/registrar-standing.mjs +9 -13
  194. package/lib/registry-capabilities.mjs +7 -3
  195. package/lib/registry-capability-catalogue.mjs +29 -20
  196. package/lib/registry-dates.mjs +2 -190
  197. package/lib/registry-insights.mjs +56 -18
  198. package/lib/response-policy.mjs +86 -48
  199. package/lib/risk-calibration-summary.mjs +2 -338
  200. package/lib/risk-scoring.mjs +2 -345
  201. package/lib/rpki-evidence.mjs +8 -5
  202. package/lib/safe-fetch.mjs +54 -47
  203. package/lib/scoring-evidence-quality.mjs +2 -107
  204. package/lib/security-txt.mjs +10 -7
  205. package/lib/semantic-version.mjs +2 -54
  206. package/lib/service-binding-dns.mjs +35 -0
  207. package/lib/smtp-transport-review.mjs +75 -34
  208. package/lib/sslbl-certificates.generated.mjs +141 -138
  209. package/lib/static-html-analysis.mjs +117 -146
  210. package/lib/structured-data-identity.mjs +2 -1
  211. package/lib/technology-markup.mjs +100 -0
  212. package/lib/threat-intelligence-manifests.mjs +11 -139
  213. package/lib/threat-intelligence-runtime.mjs +24 -52
  214. package/lib/threat-intelligence-targets.mjs +6 -34
  215. package/lib/threat-intelligence-types.mjs +2 -36
  216. package/lib/typosquat-generator.mjs +29 -16
  217. package/lib/urlhaus-intelligence.mjs +8 -3
  218. package/lib/urlscan-intelligence.mjs +1 -1
  219. package/lib/website-enrichment.mjs +388 -0
  220. package/lib/website-security-posture.mjs +30 -6
  221. package/lib/website-technology.mjs +111 -71
  222. package/lib/whois-authority.mjs +55 -5
  223. package/lib/whois-chain.mjs +9 -2
  224. package/lib/whois-common-formats.mjs +5 -5
  225. package/lib/whois-contracts.mjs +2 -1
  226. package/lib/whois-dialects.mjs +2 -2
  227. package/lib/whois-normalization.mjs +3 -1
  228. package/lib/whois-parser-support.mjs +8 -1
  229. package/lib/whois-parser.mjs +5 -3
  230. package/lib/whois-transport.mjs +23 -7
  231. package/package.json +14 -14
  232. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  233. package/packages/analysis/bounded-json.mjs +330 -0
  234. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  235. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  236. package/packages/analysis/ct-query.mjs +27 -0
  237. package/packages/analysis/ct-response-bounds.mjs +9 -0
  238. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  239. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  240. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  241. package/packages/analysis/opportunity-scoring.mjs +107 -0
  242. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  243. package/packages/analysis/portable-generator.mjs +29 -0
  244. package/packages/analysis/project-metadata.mjs +8 -0
  245. package/packages/analysis/registrable-domain.mjs +56 -0
  246. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  247. package/packages/analysis/risk-scoring.mjs +345 -0
  248. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  249. package/packages/analysis/semantic-version.mjs +54 -0
  250. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  251. package/packages/cases/case-attachment-model.mjs +83 -0
  252. package/packages/cases/case-evidence-links.mjs +135 -0
  253. package/packages/cases/case-evidence-model.mjs +229 -69
  254. package/packages/cases/case-evidence-quality.mjs +5 -0
  255. package/packages/cases/case-export-input.mjs +55 -0
  256. package/packages/cases/case-incident-context.mjs +52 -0
  257. package/packages/cases/case-markdown.mjs +11 -0
  258. package/packages/cases/case-migration-model.mjs +98 -48
  259. package/packages/cases/case-model.mjs +1 -0
  260. package/packages/cases/case-recheck-model.mjs +149 -0
  261. package/packages/cases/case-record-contracts.mjs +1 -1
  262. package/packages/cases/case-record-core.mjs +4 -3
  263. package/packages/cases/case-record-operations.mjs +123 -133
  264. package/packages/cases/case-record-projection.mjs +30 -20
  265. package/packages/cases/case-record-version-input.mjs +19 -0
  266. package/packages/cases/case-report.mjs +114 -67
  267. package/packages/cases/case-requested-evidence.mjs +146 -0
  268. package/packages/cases/case-response-actions.mjs +756 -0
  269. package/packages/cases/case-response-model.mjs +49 -1129
  270. package/packages/cases/case-response-outcomes.mjs +338 -0
  271. package/packages/cases/case-response-packet-render.mjs +157 -0
  272. package/packages/cases/case-response-packet-types.mjs +1 -0
  273. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  274. package/packages/cases/case-response-packet.mjs +58 -593
  275. package/packages/cases/case-response-records.mjs +93 -0
  276. package/packages/cases/case-response-review-inputs.mjs +382 -0
  277. package/packages/cases/case-response-values.mjs +85 -0
  278. package/packages/cases/case-review-package.mjs +45 -0
  279. package/packages/cases/case-selection.mjs +46 -0
  280. package/packages/cases/case-storage-model.mjs +26 -1
  281. package/packages/cases/case-workflow-metadata.mjs +225 -0
  282. package/packages/cases/case-workflow-migration.mjs +43 -0
  283. package/packages/cases/http-summary.mjs +6 -1
  284. package/packages/cases/response-route-freshness.mjs +16 -0
  285. package/packages/comparison/capture-context.mjs +61 -0
  286. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  287. package/packages/comparison/favicon-similarity.mjs +1 -1
  288. package/packages/comparison/page-similarity.mjs +3 -2
  289. package/packages/comparison/relationship-evidence.mjs +103 -14
  290. package/packages/comparison/relationship-provenance.mjs +99 -0
  291. package/packages/contracts/analyst-interchange.mjs +234 -0
  292. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  293. package/packages/contracts/analyst-review-state.mjs +5 -4
  294. package/packages/contracts/case-portability.mjs +370 -114
  295. package/packages/contracts/case-views-contract.mjs +12 -0
  296. package/packages/contracts/case-views.mjs +26 -0
  297. package/packages/contracts/cli-command-semantics.mjs +3 -0
  298. package/packages/contracts/cli-grammar.mjs +6 -0
  299. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  300. package/packages/contracts/cli-lookup.mjs +0 -1
  301. package/packages/contracts/context-review.mjs +24 -0
  302. package/packages/contracts/document-intake.mjs +11 -0
  303. package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
  304. package/packages/contracts/domain-control-manifest.mjs +128 -74
  305. package/packages/contracts/domain-control-monitor.mjs +59 -45
  306. package/packages/contracts/domain-control-review.mjs +200 -121
  307. package/packages/contracts/domain-name.mjs +21 -0
  308. package/packages/contracts/external-observation-interchange.mjs +2 -1
  309. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  310. package/packages/contracts/har-review.mjs +2 -0
  311. package/packages/contracts/http-url.mjs +2 -0
  312. package/packages/contracts/identity-events.mjs +4 -0
  313. package/packages/contracts/investigation-package-limits.mjs +24 -0
  314. package/packages/contracts/investigation-portability.mjs +56 -27
  315. package/packages/contracts/investigation-run.mjs +18 -0
  316. package/packages/contracts/ip-address.mjs +44 -0
  317. package/packages/contracts/lookup-evidence.mjs +35 -0
  318. package/packages/contracts/mail-authentication.mjs +5 -0
  319. package/packages/contracts/message-intake.mjs +31 -0
  320. package/packages/contracts/offline-comparison.mjs +4 -2
  321. package/packages/contracts/page-fingerprints.mjs +15 -0
  322. package/packages/contracts/passive-mail.mjs +10 -0
  323. package/packages/contracts/risk-calibration.mjs +0 -1
  324. package/packages/contracts/schema-compatibility.mjs +1 -1
  325. package/packages/contracts/schema-lifecycle.mjs +83 -153
  326. package/packages/contracts/selected-file-limits.mjs +6 -0
  327. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  328. package/packages/contracts/web-capture.mjs +10 -0
  329. package/packages/contracts/workspace-portability.mjs +219 -238
  330. package/packages/evidence/artifact-integrity.mjs +8 -2
  331. package/packages/evidence/artifact-structure.mjs +105 -0
  332. package/packages/evidence/collection-quality.mjs +67 -0
  333. package/packages/evidence/decision-fact.mjs +4 -1
  334. package/packages/evidence/dns-query-outcome.mjs +72 -0
  335. package/packages/evidence/domain-control-runtime.mjs +155 -27
  336. package/packages/evidence/domain-posture-context.mjs +79 -0
  337. package/packages/evidence/email-recipient.mjs +18 -0
  338. package/packages/evidence/image-regions.mjs +56 -0
  339. package/packages/evidence/latest-observations.mjs +28 -0
  340. package/packages/evidence/lookup-target.mjs +108 -0
  341. package/packages/evidence/observation.mjs +18 -1
  342. package/packages/evidence/passphrase-encryption.mjs +44 -0
  343. package/packages/evidence/png-pixels.mjs +117 -0
  344. package/packages/evidence/public-address-policy.mjs +83 -0
  345. package/packages/evidence/record-identity.mjs +72 -0
  346. package/packages/evidence/registry-dates.mjs +190 -0
  347. package/packages/evidence/retained-file.mjs +69 -0
  348. package/packages/evidence/review-age.mjs +14 -0
  349. package/packages/interchange/bagit.mjs +342 -0
  350. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  351. package/packages/interchange/crc32.mjs +17 -0
  352. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  353. package/packages/interchange/external-findings-import.mjs +93 -61
  354. package/packages/interchange/managed-indicator-export.mjs +35 -0
  355. package/packages/interchange/managed-indicator-set.mjs +210 -0
  356. package/packages/interchange/misp-indicator-export.mjs +93 -0
  357. package/packages/interchange/sharing-policy.mjs +56 -0
  358. package/packages/interchange/stix-indicator-export.mjs +129 -0
  359. package/packages/interchange/web-capture-import.mjs +389 -0
  360. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  361. package/packages/investigation/bulk-mail-exposure.mjs +9 -14
  362. package/packages/investigation/capture-coverage.mjs +57 -0
  363. package/packages/investigation/connector-provenance-review.mjs +100 -0
  364. package/packages/investigation/context-review.mjs +35 -0
  365. package/packages/investigation/document-intake.mjs +81 -0
  366. package/packages/investigation/docx-intake.mjs +133 -0
  367. package/packages/investigation/domain-history-review.mjs +100 -0
  368. package/packages/investigation/domain-transition-review.mjs +47 -0
  369. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  370. package/packages/investigation/har-intake.mjs +68 -0
  371. package/packages/investigation/identity-event-intake.mjs +131 -0
  372. package/packages/investigation/identity-incident-review.mjs +39 -0
  373. package/packages/investigation/incident-sequence-review.mjs +58 -0
  374. package/packages/investigation/intake-report.mjs +19 -0
  375. package/packages/investigation/investigation-bagit.mjs +14 -0
  376. package/packages/investigation/investigation-capsule.mjs +33 -5
  377. package/packages/investigation/investigation-manifest.mjs +200 -0
  378. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  379. package/packages/investigation/investigation-package.mjs +200 -0
  380. package/packages/investigation/link-intake.mjs +179 -0
  381. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  382. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  383. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  384. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  385. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  386. package/packages/investigation/mail-authentication-review.mjs +222 -0
  387. package/packages/investigation/mail-header-identity.mjs +209 -0
  388. package/packages/investigation/message-intake.mjs +195 -0
  389. package/packages/investigation/page-behaviour.mjs +113 -0
  390. package/packages/investigation/pdf-intake.mjs +155 -0
  391. package/packages/investigation/platform-continuity-review.mjs +60 -0
  392. package/packages/investigation/qr-intake.mjs +56 -0
  393. package/packages/investigation/requested-action-hints.mjs +11 -0
  394. package/packages/investigation/selected-input-review.mjs +17 -0
  395. package/packages/investigation/storefront-review.mjs +70 -0
  396. package/packages/monitoring/analyst-review-state.mjs +38 -90
  397. package/packages/workspace/brand-profile-model.mjs +160 -22
  398. package/packages/workspace/bulk-columns.mjs +17 -0
  399. package/packages/workspace/bulk-review-model.mjs +39 -31
  400. package/packages/workspace/bulk-session-model.mjs +112 -27
  401. package/packages/workspace/bulk-sort.mjs +1 -0
  402. package/packages/workspace/campaign-model.mjs +1 -1
  403. package/packages/workspace/case-views.mjs +102 -0
  404. package/packages/workspace/detection-rule-model.mjs +1 -1
  405. package/packages/workspace/hostile-input.mjs +29 -8
  406. package/packages/workspace/investigation-guide.mjs +2 -2
  407. package/packages/workspace/investigation-template-model.mjs +45 -6
  408. package/packages/workspace/page-baseline.mjs +20 -17
  409. package/packages/workspace/relationship-observation-model.mjs +54 -35
  410. package/packages/workspace/shortlist-model.mjs +14 -5
  411. package/packages/workspace/watchlist-history.mjs +30 -14
  412. package/packages/workspace/watchlist-store.mjs +24 -6
  413. package/packages/workspace/website-snapshot-model.mjs +98 -33
  414. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  415. package/packages/workspace/workspace-archive.mjs +90 -25
  416. package/third-party-notices.txt +577 -17
  417. package/lib/hostname.mjs +0 -38
  418. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -8,18 +8,23 @@
8
8
  // contains raw RDAP/WHOIS material. This module deliberately excludes all raw
9
9
  // registry/web responses, contacts, cookies, screenshots, and authentication
10
10
  // data. Reports contain only the normalized case record.
11
- import { caseEvidenceIncomparableReasons, compareCaseEvidence, latestCaseEvidence } from "./case-model.mjs";
11
+ import { caseEvidenceTimeline, compareCaseEvidence, currentCaseEvidence, publishedCaseEvidenceTimelineForVerification } from "./case-evidence-model.mjs";
12
12
  import { httpSecurityHeaderLabel } from "./http-summary.mjs";
13
- import { analystInteroperabilityTags } from "../../lib/analyst-taxonomy.mjs";
14
- import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../../lib/portable-generator.mjs";
13
+ import { webCollectionScoreLimitation } from "../evidence/collection-quality.mjs";
14
+ import { analystInteroperabilityTags } from "../analysis/analyst-taxonomy.mjs";
15
+ import { caseEvidenceLinkIssues } from "./case-evidence-links.mjs";
16
+ import { caseTypeSummary } from "./case-workflow-metadata.mjs";
17
+ import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../analysis/portable-generator.mjs";
15
18
  import { buildCaseResponseLifecycleSummary, CASE_EVIDENCE_RELATION_STANCES } from "./case-response-model.mjs";
16
19
  import { normalizeCaseBrandProfileIds } from "./case-brand-profile-references.mjs";
17
- import { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
20
+ import { CASE_RECHECK_CONDITIONS } from "./case-recheck-model.mjs";
21
+ import { escapeCaseMarkdownInline as escapeMarkdownInline } from "./case-markdown.mjs";
22
+ import { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION, LATEST_PUBLIC_CASE_REPORT_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_REPORT_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
18
23
  // ---------------------------------------------------------------------------
19
24
  // Constants
20
25
  // ---------------------------------------------------------------------------
21
26
  export { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION };
22
- const LIMITATIONS_TEXT = [
27
+ const PUBLISHED_V2_3_LIMITATIONS = Object.freeze([
23
28
  'This report contains normalised browser-local observations from WHOISleuth analyst cases.',
24
29
  'It is not a live lookup and does not contain raw WHOIS, RDAP, DNS, HTML, or responses collected during website checks.',
25
30
  'Absence of a signal (e.g. no MX record observed) does not prove nonexistence. It may not have been evaluated.',
@@ -28,17 +33,12 @@ const LIMITATIONS_TEXT = [
28
33
  'Brand Profile references record an explicit analyst-selected association only; they do not establish ownership, attribution, intent, safety, or maliciousness.',
29
34
  'Provider workflow outcomes and independently observed technical effects remain separately attributed. A provider outcome does not establish independent remediation, absence, or safety; neither evidence family establishes legal sufficiency or provider performance.',
30
35
  'Generated locally in the browser. Review the package before sharing it.',
36
+ ]);
37
+ const LIMITATIONS_TEXT = [
38
+ ...PUBLISHED_V2_3_LIMITATIONS.slice(0, 2),
39
+ 'Snapshot hostnames are excluded; supporting DNS, TLS and web observations need not concern the Case registration domain.',
40
+ ...PUBLISHED_V2_3_LIMITATIONS.slice(2),
31
41
  ].join(' ');
32
- function escapeMarkdownInline(text) {
33
- return String(text)
34
- .replace(/[\r\n\u2028\u2029]+/g, ' ')
35
- .replace(/([\\`*_{}\[\]<>()#+!|~])/g, '\\$1')
36
- // GFM autolinks bare URLs independently of ordinary link syntax. Break
37
- // the scheme or the first `www.` separator without making normal domains,
38
- // dates, and prose noisy in the report source.
39
- .replace(/\b([a-z][a-z0-9+.-]{1,31}):(?=\/\/)/gi, '$1\\:')
40
- .replace(/\bwww\.(?=\S)/gi, 'www\\.');
41
- }
42
42
  /**
43
43
  * Escapes a multiline note body for safe inclusion in a Markdown blockquote.
44
44
  * Each line is individually escaped and prefixed with `> `.
@@ -84,6 +84,12 @@ function formatReportValue(value, field) {
84
84
  return String(value);
85
85
  return String(value);
86
86
  }
87
+ function readableRiskScore(snapshot) {
88
+ const value = escapeMarkdownInline(formatReportValue(snapshot.riskScore));
89
+ const limitation = snapshot.riskScore === null ? null
90
+ : webCollectionScoreLimitation(snapshot.webCollectionQuality, snapshot.scanDepth);
91
+ return limitation ? `${value} — ${escapeMarkdownInline(limitation)}` : value;
92
+ }
87
93
  /**
88
94
  * Returns a shallow clone of a snapshot containing only known schema fields.
89
95
  * Unknown/imported keys are never included.
@@ -100,10 +106,12 @@ function pickKnownSnapshotFields(snapshot) {
100
106
  return {
101
107
  id: snapshot.id,
102
108
  fingerprint: snapshot.fingerprint,
109
+ ...(snapshot.factorOrder ? { factorOrder: snapshot.factorOrder } : {}),
103
110
  firstCapturedAt: snapshot.firstCapturedAt,
104
111
  capturedAt: snapshot.capturedAt,
105
112
  source: snapshot.source,
106
113
  scanDepth: snapshot.scanDepth,
114
+ ...(snapshot.webCollectionQuality ? { webCollectionQuality: { ...snapshot.webCollectionQuality } } : {}),
107
115
  availability: snapshot.availability,
108
116
  confidence: snapshot.confidence,
109
117
  riskModelVersion: snapshot.riskModelVersion,
@@ -165,51 +173,10 @@ export function buildCaseReport(caseRecord, options = {}) {
165
173
  const now = generatedAt || new Date().toISOString();
166
174
  const generator = buildPortableGeneratorMetadata(options.applicationVersion);
167
175
  // --- Build JSON report ---
168
- const timelineEntries = [];
169
- if (Array.isArray(caseRecord.evidenceHistory) && caseRecord.evidenceHistory.length > 0) {
170
- const chronological = [...caseRecord.evidenceHistory];
171
- for (let i = 0; i < chronological.length; i++) {
172
- const snapshot = chronological[i];
173
- if (!snapshot)
174
- continue;
175
- const isBaseline = i === 0;
176
- const hasRepeatedObservation = snapshot.firstCapturedAt !== snapshot.capturedAt;
177
- let changes = null;
178
- let hasIncomparableChange = false;
179
- let incomparableReasons = [];
180
- if (!isBaseline) {
181
- const previous = chronological[i - 1];
182
- if (!previous)
183
- continue;
184
- const rawChanges = compareCaseEvidence(previous, snapshot);
185
- incomparableReasons = caseEvidenceIncomparableReasons(previous, snapshot);
186
- const hostnameContextChanged = incomparableReasons.includes('observation-context');
187
- if (rawChanges.length > 0 && !hostnameContextChanged) {
188
- changes = rawChanges.map((change) => ({
189
- field: change.field,
190
- label: change.label,
191
- before: change.before,
192
- after: change.after,
193
- tone: change.tone,
194
- }));
195
- }
196
- else if (snapshot.fingerprint !== previous.fingerprint
197
- && incomparableReasons.length === 0) {
198
- incomparableReasons = ['other'];
199
- }
200
- hasIncomparableChange = incomparableReasons.length > 0;
201
- }
202
- timelineEntries.push({
203
- snapshot: pickKnownSnapshotFields(snapshot),
204
- isBaseline,
205
- hasRepeatedObservation,
206
- changes,
207
- hasIncomparableChange,
208
- incomparableReasons,
209
- });
210
- }
211
- }
212
- const latest = latestCaseEvidence({ evidenceHistory: caseRecord.evidenceHistory ?? undefined });
176
+ const timeline = caseEvidenceTimeline(caseRecord.evidenceHistory);
177
+ const timelineEntries = projectReportTimeline(timeline);
178
+ const selection = currentCaseEvidence(caseRecord);
179
+ const latest = selection.snapshot;
213
180
  const currentAssessment = latest ? pickKnownSnapshotFields(latest) : null;
214
181
  const responseLifecycle = buildCaseResponseLifecycleSummary(caseRecord);
215
182
  const observedEffects = caseRecord.observedEffects ?? {
@@ -231,6 +198,7 @@ export function buildCaseReport(caseRecord, options = {}) {
231
198
  application: generator,
232
199
  case: {
233
200
  id: caseRecord.id,
201
+ title: caseRecord.title ?? '',
234
202
  domain: caseRecord.domain,
235
203
  status: caseRecord.status,
236
204
  disposition: caseRecord.disposition,
@@ -247,7 +215,9 @@ export function buildCaseReport(caseRecord, options = {}) {
247
215
  currentAssessment,
248
216
  evidenceTimeline: timelineEntries,
249
217
  analystResponse: {
218
+ ...(caseRecord.workflowMetadata === undefined ? {} : { workflowMetadata: structuredClone(caseRecord.workflowMetadata) }),
250
219
  evidencePins: caseRecord.evidencePins.map((item) => ({ ...item, limitations: [...item.limitations] })),
220
+ ...(caseRecord.evidenceLinks === undefined ? {} : { evidenceLinks: structuredClone(caseRecord.evidenceLinks) }),
251
221
  decisions: caseRecord.decisions.map((item) => ({ ...item, evidencePinIds: [...item.evidencePinIds] })),
252
222
  actions: caseRecord.actions.map((item) => ({
253
223
  ...item,
@@ -290,12 +260,42 @@ export function buildCaseReport(caseRecord, options = {}) {
290
260
  })),
291
261
  },
292
262
  responseLifecycle,
293
- limitations: LIMITATIONS_TEXT,
263
+ limitations: [LIMITATIONS_TEXT, selection.limitation, ...new Set(timeline.map((entry) => entry.orderingLimitation))].filter(Boolean).join(' '),
294
264
  };
295
265
  // --- Build Markdown ---
296
266
  const md = buildMarkdown(json, options.includeAttribution !== false);
297
267
  return { json, markdown: md };
298
268
  }
269
+ function projectReportTimeline(timeline) {
270
+ return timeline.map(entry => ({
271
+ snapshot: pickKnownSnapshotFields(entry.snapshot), isBaseline: entry.isBaseline,
272
+ hasRepeatedObservation: entry.hasRepeatedObservation,
273
+ changes: entry.changes?.map(({ field, label, before, after, tone }) => ({ field, label, before, after, tone })) ?? null,
274
+ hasIncomparableChange: entry.hasIncomparableChange, incomparableReasons: [...entry.incomparableReasons],
275
+ }));
276
+ }
277
+ /** Expected strict reader projection; historical disclosure wording is immutable. */
278
+ export function buildCaseReportVerificationProjection(caseRecord, options, schemaVersion) {
279
+ const generated = buildCaseReport(caseRecord, options).json;
280
+ if (schemaVersion === CASE_REPORT_SCHEMA_VERSION)
281
+ return generated;
282
+ if (caseRecord.evidenceHistory.some(snapshot => snapshot.webCollectionQuality !== undefined)) {
283
+ throw new TypeError('Published report formats cannot declare newer collection-quality fields.');
284
+ }
285
+ const current = { ...generated, evidenceTimeline: projectReportTimeline(publishedCaseEvidenceTimelineForVerification(caseRecord.evidenceHistory)) };
286
+ if (schemaVersion === LATEST_PUBLIC_CASE_REPORT_SCHEMA_VERSION)
287
+ return { ...current, schemaVersion };
288
+ if (schemaVersion !== PUBLISHED_V2_3_CASE_REPORT_SCHEMA_VERSION) {
289
+ throw new TypeError('No strict Case report projection is defined for this version.');
290
+ }
291
+ const { title: _title, ...publishedCase } = current.case;
292
+ return {
293
+ ...current,
294
+ case: publishedCase,
295
+ schemaVersion,
296
+ limitations: PUBLISHED_V2_3_LIMITATIONS.join(' ') + current.limitations.slice(LIMITATIONS_TEXT.length),
297
+ };
298
+ }
299
299
  // ---------------------------------------------------------------------------
300
300
  // Markdown generation
301
301
  // ---------------------------------------------------------------------------
@@ -310,6 +310,11 @@ function buildMarkdown(report, includeAttribution) {
310
310
  const domain = escapeMarkdownInline(report.case.domain || 'unknown');
311
311
  lines.push(`# Case Report: ${domain}`);
312
312
  lines.push('');
313
+ if (report.case.title) {
314
+ lines.push(`**Incident:** ${escapeMarkdownInline(report.case.title)}`);
315
+ lines.push('');
316
+ }
317
+ lines.push(`**Case ID:** ${escapeMarkdownInline(report.case.id)}`);
313
318
  // Metadata
314
319
  lines.push(`**Generated:** ${escapeMarkdownInline(report.generatedAt)}`);
315
320
  lines.push(`**Report schema:** ${escapeMarkdownInline(report.schema)} v${report.schemaVersion}`);
@@ -330,6 +335,17 @@ function buildMarkdown(report, includeAttribution) {
330
335
  if (report.case.tags.length > 0) {
331
336
  lines.push(`| Tags | ${escapeMarkdownInline(report.case.tags.join(', '))} |`);
332
337
  }
338
+ const caseTypes = caseTypeSummary(report.analystResponse.workflowMetadata ? { workflowMetadata: report.analystResponse.workflowMetadata } : {});
339
+ if (caseTypes)
340
+ lines.push(`| Case types | ${escapeMarkdownInline(caseTypes)} |`);
341
+ const context = report.analystResponse.workflowMetadata?.investigationContext;
342
+ if (context) {
343
+ lines.push(`| Investigation objective | ${escapeMarkdownInline(context.objective)} |`);
344
+ lines.push(`| Incident URL (${context.urlRetention === 'exact' ? 'exact' : 'origin only'}) | ${escapeMarkdownInline(context.incidentUrl)} |`);
345
+ }
346
+ for (const target of report.analystResponse.workflowMetadata?.incidentTargets ?? []) {
347
+ lines.push(`| Incident link (${target.state}) | ${escapeMarkdownInline(target.url)} |`);
348
+ }
333
349
  lines.push(`| Notes included | ${report.case.notesIncluded ? 'Yes' : 'No'} |`);
334
350
  lines.push('');
335
351
  // Current assessment
@@ -338,7 +354,7 @@ function buildMarkdown(report, includeAttribution) {
338
354
  if (report.currentAssessment) {
339
355
  const a = report.currentAssessment;
340
356
  lines.push(`- **Availability:** ${escapeMarkdownInline(formatReportValue(a.availability))}`);
341
- lines.push(`- **Risk score:** ${escapeMarkdownInline(formatReportValue(a.riskScore))}`);
357
+ lines.push(`- **Risk score:** ${readableRiskScore(a)}`);
342
358
  lines.push(`- **Risk model:** ${a.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(a.riskModelVersion))}`}`);
343
359
  lines.push(`- **Brand Profile context:** ${escapeMarkdownInline(formatReportValue(a.profileContextState))}`);
344
360
  if (a.profileContextLimitation)
@@ -355,7 +371,9 @@ function buildMarkdown(report, includeAttribution) {
355
371
  lines.push(`- **Source:** ${escapeMarkdownInline(formatReportValue(a.source))}`);
356
372
  }
357
373
  else {
358
- lines.push('No evidence captured.');
374
+ lines.push(report.evidenceTimeline.length
375
+ ? 'Retained snapshots have no unique latest assessment. Review the timeline and limitations.'
376
+ : 'No evidence captured.');
359
377
  }
360
378
  lines.push('');
361
379
  // Evidence timeline
@@ -391,7 +409,7 @@ function buildMarkdown(report, includeAttribution) {
391
409
  lines.push('**Key evidence:**');
392
410
  lines.push('');
393
411
  lines.push(`- Availability: ${escapeMarkdownInline(formatReportValue(snap.availability))}`);
394
- lines.push(`- Risk score: ${escapeMarkdownInline(formatReportValue(snap.riskScore))}`);
412
+ lines.push(`- Risk score: ${readableRiskScore(snap)}`);
395
413
  lines.push(`- Risk model: ${snap.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(snap.riskModelVersion))}`}`);
396
414
  lines.push(`- Brand Profile context: ${escapeMarkdownInline(formatReportValue(snap.profileContextState))}`);
397
415
  if (snap.profileContextLimitation)
@@ -458,6 +476,8 @@ function buildMarkdown(report, includeAttribution) {
458
476
  lines.push('> Opportunity scores and factors use different or unversioned models, so their numeric difference is not treated as a domain change.');
459
477
  if (reasons.includes('scan-depth'))
460
478
  lines.push('> Capture depths differ, so unevaluated deep signals are not treated as additions or removals.');
479
+ if (reasons.includes('collection-quality'))
480
+ lines.push('> Missing or incomplete observations prevent comparison of affected registration, page, favicon or score fields.');
461
481
  if (reasons.length === 0 || reasons.includes('other'))
462
482
  lines.push('> The observations differ materially, but no reliable field-level comparison is available.');
463
483
  lines.push('');
@@ -467,7 +487,7 @@ function buildMarkdown(report, includeAttribution) {
467
487
  lines.push('## Analyst Decision Packet');
468
488
  lines.push('');
469
489
  const response = report.analystResponse;
470
- if (!response.evidencePins.length && !response.sightings.length && !response.decisions.length && !response.actions.length && !response.assertions.length && !response.manualTrail.length && !response.observedEffects.reviews.length && !response.closures.records.length && !response.branches.length) {
490
+ if (!response.evidencePins.length && !response.evidenceLinks?.length && !response.sightings.length && !response.decisions.length && !response.actions.length && !response.assertions.length && !response.manualTrail.length && !response.observedEffects.reviews.length && !response.closures.records.length && !response.branches.length) {
471
491
  lines.push('No evidence pins, source-qualified sightings, structured assertions, decision records, response actions, independent effect reviews, closures, or manual investigation steps recorded.');
472
492
  lines.push('');
473
493
  }
@@ -478,7 +498,7 @@ function buildMarkdown(report, includeAttribution) {
478
498
  lines.push('No evidence pins recorded.');
479
499
  for (const pin of response.evidencePins) {
480
500
  lines.push(`- **${escapeMarkdownInline(pin.label)}:** ${escapeMarkdownInline(pin.value)}`);
481
- lines.push(` Source: ${escapeMarkdownInline(pin.source)}; observed ${escapeMarkdownInline(pin.observedAt)}; completeness ${escapeMarkdownInline(pin.completeness)}.`);
501
+ lines.push(` Source: ${escapeMarkdownInline(pin.source)}; observed ${escapeMarkdownInline(pin.observedAt ?? 'Time unavailable')}; completeness ${escapeMarkdownInline(pin.completeness)}.`);
482
502
  if (pin.limitations.length)
483
503
  lines.push(` Limitations: ${escapeMarkdownInline(pin.limitations.join('; '))}`);
484
504
  }
@@ -488,7 +508,7 @@ function buildMarkdown(report, includeAttribution) {
488
508
  if (!response.sightings.length)
489
509
  lines.push('No source-qualified sightings recorded.');
490
510
  for (const sighting of response.sightings) {
491
- lines.push(`- **${escapeMarkdownInline(sighting.state.replaceAll('_', ' '))}:** ${escapeMarkdownInline(sighting.category)} (${escapeMarkdownInline(sighting.observedAt)})`);
511
+ lines.push(`- **${escapeMarkdownInline(sighting.state.replaceAll('_', ' '))}:** ${escapeMarkdownInline(sighting.category)} (${escapeMarkdownInline(sighting.observedAt ?? 'Observation time unavailable')})`);
492
512
  lines.push(` Source: ${escapeMarkdownInline(sighting.source)}; class ${escapeMarkdownInline(sighting.sourceClass)}; completeness ${escapeMarkdownInline(sighting.completeness)}.`);
493
513
  if (sighting.evidencePinId)
494
514
  lines.push(` Evidence pin: ${escapeMarkdownInline(sighting.evidencePinId)}`);
@@ -512,6 +532,11 @@ function buildMarkdown(report, includeAttribution) {
512
532
  lines.push(`- **${escapeMarkdownInline(assertion.state)}:** ${escapeMarkdownInline(assertion.statement)}`);
513
533
  if (assertion.rationale)
514
534
  lines.push(` ${escapeMarkdownInline(assertion.rationale)}`);
535
+ if (assertion.recheck) {
536
+ lines.push(` Recheck target: ${escapeMarkdownInline(assertion.recheck.targetHostname)}; comparison conditions: ${escapeMarkdownInline(assertion.recheck.conditions)}`);
537
+ if (assertion.recheck.baselinePinId)
538
+ lines.push(` Baseline evidence pin: ${escapeMarkdownInline(assertion.recheck.baselinePinId)}`);
539
+ }
515
540
  if (assertion.provenance) {
516
541
  lines.push(` External provenance: ${escapeMarkdownInline(assertion.provenance.format.toUpperCase())}; source ${escapeMarkdownInline(assertion.provenance.sourceName)}; file SHA-256 ${escapeMarkdownInline(assertion.provenance.sourceDigestSha256)}.`);
517
542
  if (assertion.provenance.publisher)
@@ -537,6 +562,12 @@ function buildMarkdown(report, includeAttribution) {
537
562
  }
538
563
  lines.push('');
539
564
  }
565
+ if (response.evidenceLinks?.length) {
566
+ lines.push('### Analyst-declared evidence relationships', '');
567
+ for (const { link, missingPinIds, cyclic } of caseEvidenceLinkIssues(response.evidenceLinks, response.evidencePins))
568
+ lines.push(`- ${escapeMarkdownInline(link.fromPinId)} ${link.kind === 'derived_from' ? 'derived from' : 'shares a source with'} ${escapeMarkdownInline(link.toPinId)} · recorded ${link.createdAt}`, ` Basis: ${escapeMarkdownInline(link.basis)}`, ...(link.withdrawal ? [` Withdrawn ${link.withdrawal.at}: ${escapeMarkdownInline(link.withdrawal.reason)}`] : []), ...(missingPinIds.length ? [` Referenced pins not retained: ${missingPinIds.map(escapeMarkdownInline).join(', ')}`] : []), ...(cyclic ? [' Conflicting imported derivation cycle; no order is inferred.'] : []));
569
+ lines.push('These relationships record analyst attribution, not independent corroboration or added confidence.', '');
570
+ }
540
571
  lines.push('### Decisions');
541
572
  lines.push('');
542
573
  if (!response.decisions.length)
@@ -562,6 +593,13 @@ function buildMarkdown(report, includeAttribution) {
562
593
  lines.push(` Route review due: ${escapeMarkdownInline(action.routeReviewAfter)}`);
563
594
  if (action.originActionId)
564
595
  lines.push(` Originating action: ${escapeMarkdownInline(action.originActionId)}`);
596
+ if (action.amendment)
597
+ lines.push(` Amendment of submitted packet SHA-256: ${action.amendment.packetDigestSha256}; request events: ${action.amendment.requestEventIds.map(escapeMarkdownInline).join(', ')}`);
598
+ for (const event of action.history)
599
+ if (event.evidenceRequest) {
600
+ const request = event.evidenceRequest;
601
+ lines.push(` Requested evidence [${request.state}]: ${escapeMarkdownInline(request.summary)}; deadline: ${request.dueAt ?? 'not stated'}; original packet: ${request.packetDigestSha256}${event.applied ? '' : ' (retained conflict)'}`, ` Prepared pins: ${request.evidencePinIds.map(escapeMarkdownInline).join(', ') || 'none'}${request.rationale ? `; ${escapeMarkdownInline(request.rationale)}` : ''}`);
602
+ }
565
603
  if (action.dueAt)
566
604
  lines.push(` Due: ${escapeMarkdownInline(action.dueAt)}`);
567
605
  if (action.followUpAt)
@@ -623,10 +661,19 @@ function buildMarkdown(report, includeAttribution) {
623
661
  : `- Latest independently observed change time: Withheld because the independent change state is ${escapeMarkdownInline(report.responseLifecycle.observedChangeState)}.`);
624
662
  if (observedEffect)
625
663
  lines.push(`- Latest independent review: ${escapeMarkdownInline(observedEffect.state.replaceAll('_', ' '))} at ${escapeMarkdownInline(observedEffect.observedAt)} from ${escapeMarkdownInline(observedEffect.source)}.`);
664
+ else if (response.observedEffects.reviews.length)
665
+ lines.push('- A single latest independent review cannot be selected from the retained observation times. Review the individual records below.');
626
666
  if (!response.observedEffects.reviews.length)
627
667
  lines.push('- No independent observed-effect review recorded.');
628
668
  for (const review of response.observedEffects.reviews) {
629
669
  lines.push(`- **${escapeMarkdownInline(review.state.replaceAll('_', ' '))}** (${escapeMarkdownInline(review.observedAt)}): ${escapeMarkdownInline(review.source)}; class ${escapeMarkdownInline(review.sourceClass)}; completeness ${escapeMarkdownInline(review.completeness)}.`);
670
+ if (review.recheck) {
671
+ lines.push(` Question ${escapeMarkdownInline(review.recheck.questionId)}: ${escapeMarkdownInline(review.recheck.question)}`);
672
+ lines.push(` Recheck target: ${escapeMarkdownInline(review.recheck.targetHostname)}; retained comparison conditions: ${escapeMarkdownInline(review.recheck.conditions)}`);
673
+ lines.push(` Condition comparison: ${escapeMarkdownInline(CASE_RECHECK_CONDITIONS[review.recheck.conditionsMatch])}`);
674
+ if (review.recheck.baselinePinId)
675
+ lines.push(` Baseline evidence pin: ${escapeMarkdownInline(review.recheck.baselinePinId)}`);
676
+ }
630
677
  if (review.evidencePinId)
631
678
  lines.push(` Evidence pin: ${escapeMarkdownInline(review.evidencePinId)}`);
632
679
  if (review.sightingId)
@@ -0,0 +1,146 @@
1
+ import { array, enumeration, exact, HEX_DIGEST_RE, iso, text } from "../evidence/artifact-structure.mjs";
2
+ import { assertWorkspaceInputGraph } from "../workspace/hostile-input.mjs";
3
+ import { MAX_DECISION_PIN_REFERENCES, MAX_RESPONSE_RATIONALE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../contracts/case-portability.mjs";
4
+ import { SAFE_ID_RE } from "./case-response-values.mjs";
5
+ function identifier(value, label) {
6
+ const result = text(value, label, 64);
7
+ if (!SAFE_ID_RE.test(result))
8
+ throw new TypeError(`${label} is invalid.`);
9
+ return result;
10
+ }
11
+ function identifiers(value, label) {
12
+ const result = array(value, label, MAX_DECISION_PIN_REFERENCES).map(item => identifier(item, label));
13
+ if (new Set(result).size !== result.length)
14
+ throw new TypeError(`${label} contains duplicate identities.`);
15
+ return result;
16
+ }
17
+ function packetDigest(value) {
18
+ if (typeof value !== 'string' || !HEX_DIGEST_RE.test(value))
19
+ throw new TypeError('Select a recorded submitted-packet digest.');
20
+ return value;
21
+ }
22
+ export function readCaseEvidenceRequest(raw) {
23
+ if (raw === undefined)
24
+ return undefined;
25
+ assertWorkspaceInputGraph(raw, 'Requested evidence');
26
+ const row = exact(raw, ['id', 'packetDigestSha256', 'summary', 'dueAt', 'state', 'evidencePinIds', 'rationale', 'previousEventIds'], 'Requested evidence');
27
+ iso(row.dueAt, 'Requested evidence deadline', true);
28
+ const summary = text(row.summary, 'Requested evidence summary', MAX_RESPONSE_VALUE_LENGTH).trim();
29
+ const rationale = text(row.rationale, 'Requested evidence rationale', MAX_RESPONSE_RATIONALE_LENGTH, true).trim();
30
+ const state = enumeration(row.state, ['requested', 'prepared', 'unavailable'], 'Requested evidence state');
31
+ const evidencePinIds = identifiers(row.evidencePinIds, 'Requested evidence pins');
32
+ if (!summary || (state === 'prepared' && !evidencePinIds.length) || (state === 'unavailable' && !rationale)) {
33
+ throw new TypeError('Prepared evidence needs retained pins; unavailable evidence needs an explanation.');
34
+ }
35
+ return { id: identifier(row.id, 'Evidence request identity'), packetDigestSha256: packetDigest(row.packetDigestSha256),
36
+ summary, dueAt: row.dueAt, state, evidencePinIds, rationale,
37
+ previousEventIds: identifiers(row.previousEventIds, 'Prior evidence request events') };
38
+ }
39
+ export function readCasePacketAmendment(raw) {
40
+ if (raw === undefined)
41
+ return undefined;
42
+ assertWorkspaceInputGraph(raw, 'Packet amendment');
43
+ const row = exact(raw, ['packetDigestSha256', 'requestEventIds'], 'Packet amendment');
44
+ const requestEventIds = identifiers(row.requestEventIds, 'Amendment request events');
45
+ if (!requestEventIds.length)
46
+ throw new TypeError('Select at least one prepared evidence request.');
47
+ return { packetDigestSha256: packetDigest(row.packetDigestSha256), requestEventIds };
48
+ }
49
+ export function assertEvidenceRequestEvent(request, event) {
50
+ if (request && (event.nextState !== 'acknowledged' || !['submitted', 'acknowledged'].includes(String(event.previousState))
51
+ || (request.state === 'requested'
52
+ ? event.sourceClass !== 'provider' || event.providerOutcome !== 'more_information_requested' || request.evidencePinIds.length > 0 || Boolean(request.rationale) || request.previousEventIds.length > 0
53
+ : event.sourceClass !== 'analyst' || request.previousEventIds.length === 0)))
54
+ throw new TypeError('Requested evidence has incompatible event provenance.');
55
+ }
56
+ /** A digest belongs to the explicit delivery event, never the latest mutable reference. */
57
+ export function submittedPacketReceipts(action) {
58
+ return action.history.flatMap(event => {
59
+ const match = /^response-packet-sha256:([a-f0-9]{64})$/u.exec(event.reference ?? '');
60
+ return event.applied && event.previousState === 'authorised' && event.nextState === 'submitted'
61
+ && event.sourceClass === 'analyst' && match
62
+ ? [{ eventId: event.id, digestSha256: match[1], occurredAt: event.occurredAt }] : [];
63
+ });
64
+ }
65
+ export function latestEvidenceRequests(action) {
66
+ const events = action.history.filter((event) => event.applied && Boolean(event.evidenceRequest));
67
+ const replaced = new Set(events.flatMap(event => event.evidenceRequest.previousEventIds
68
+ .filter(id => events.some(parent => parent.id === id && parent.evidenceRequest.id === event.evidenceRequest.id))));
69
+ return events.filter(event => !replaced.has(event.id));
70
+ }
71
+ /** Causal links order same-time reviews and retain concurrent branches for explicit reconciliation. */
72
+ export function assertEvidenceRequestHistory(events, missingHistory) {
73
+ const byId = new Map(events.map(event => [event.id, event]));
74
+ const complete = new Set(), visiting = new Set();
75
+ const visit = (id) => {
76
+ if (complete.has(id))
77
+ return;
78
+ if (visiting.has(id))
79
+ throw new TypeError('Requested evidence history contains a cycle.');
80
+ visiting.add(id);
81
+ const request = byId.get(id)?.evidenceRequest;
82
+ for (const parentId of request?.previousEventIds ?? []) {
83
+ const parent = byId.get(parentId);
84
+ if (!parent && missingHistory)
85
+ continue;
86
+ if (!parent?.evidenceRequest || parent.evidenceRequest.id !== request.id
87
+ || parent.evidenceRequest.summary !== request.summary || parent.evidenceRequest.dueAt !== request.dueAt
88
+ || parent.evidenceRequest.packetDigestSha256 !== request.packetDigestSha256)
89
+ throw new TypeError('Requested evidence history does not preserve its original request.');
90
+ visit(parentId);
91
+ }
92
+ visiting.delete(id);
93
+ complete.add(id);
94
+ };
95
+ for (const event of events)
96
+ visit(event.id);
97
+ }
98
+ export function assertEvidenceRequestTransition(action, request, sourceClass, providerOutcome, validPinIds) {
99
+ if (!['submitted', 'acknowledged'].includes(action.state)
100
+ || !submittedPacketReceipts(action).some(receipt => receipt.digestSha256 === request.packetDigestSha256)) {
101
+ throw new Error('Requested evidence must refer to an explicitly recorded packet delivery on an open action.');
102
+ }
103
+ const previousEvents = latestEvidenceRequests(action).filter(event => event.evidenceRequest.id === request.id);
104
+ if (!previousEvents.length) {
105
+ if (request.state !== 'requested' || sourceClass !== 'provider' || providerOutcome !== 'more_information_requested'
106
+ || request.evidencePinIds.length || request.rationale || request.previousEventIds.length)
107
+ throw new Error('A new evidence request must record the provider request before preparation.');
108
+ }
109
+ else if (sourceClass !== 'analyst' || request.state === 'requested'
110
+ || previousEvents.some(({ evidenceRequest: previous }) => request.summary !== previous.summary
111
+ || request.dueAt !== previous.dueAt || request.packetDigestSha256 !== previous.packetDigestSha256)) {
112
+ throw new Error('Keep the original provider request unchanged; append a separate request for changed requirements.');
113
+ }
114
+ if (JSON.stringify(request.previousEventIds.slice().sort()) !== JSON.stringify(previousEvents.map(event => event.id).sort())) {
115
+ throw new Error('The evidence preparation changed. Review all current request events before saving.');
116
+ }
117
+ if (validPinIds && request.evidencePinIds.some(id => !validPinIds.has(id)))
118
+ throw new Error('Requested evidence refers to a pin no longer retained in this Case.');
119
+ }
120
+ export function assertPacketAmendment(actions, originActionId, amendment) {
121
+ const original = actions.find(action => action.id === originActionId);
122
+ if (!original || !submittedPacketReceipts(original).some(receipt => receipt.digestSha256 === amendment.packetDigestSha256)) {
123
+ throw new Error('An amendment requires the exact recorded packet delivery from its originating action.');
124
+ }
125
+ const latest = latestEvidenceRequests(original);
126
+ if (amendment.requestEventIds.some(id => !latest.some(event => event.id === id
127
+ && latest.filter(other => other.evidenceRequest.id === event.evidenceRequest.id).length === 1
128
+ && event.evidenceRequest.state === 'prepared' && event.evidenceRequest.packetDigestSha256 === amendment.packetDigestSha256))) {
129
+ throw new Error('Review the current prepared evidence requests before creating or submitting an amendment.');
130
+ }
131
+ }
132
+ export function assertPacketAmendmentSelection(actions, selectedActionId, selectedPinIds) {
133
+ const action = actions.find(item => item.id === selectedActionId);
134
+ if (!action?.amendment)
135
+ return;
136
+ assertPacketAmendment(actions, action.originActionId, action.amendment);
137
+ const original = actions.find(item => item.id === action.originActionId);
138
+ const required = latestEvidenceRequests(original).filter(event => action.amendment.requestEventIds.includes(event.id))
139
+ .flatMap(event => event.evidenceRequest.evidencePinIds);
140
+ if (required.some(id => !selectedPinIds.has(id)))
141
+ throw new TypeError('Select every prepared request pin for this amendment; the original packet is not regenerated.');
142
+ }
143
+ export function evidenceRequestDelivery(actions, actionId, eventId) {
144
+ return actions.flatMap(action => action.originActionId === actionId && action.amendment?.requestEventIds.includes(eventId)
145
+ ? submittedPacketReceipts(action).map(receipt => ({ actionId: action.id, ...receipt })) : []);
146
+ }