@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -8,14 +8,22 @@
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 { caseEvidenceTimeline, compareCaseEvidence, currentCaseEvidence } 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 { caseClosureHistoryQualification } from "./case-response-outcomes.mjs";
18
+ import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../analysis/portable-generator.mjs";
15
19
  import { buildCaseResponseLifecycleSummary, CASE_EVIDENCE_RELATION_STANCES } from "./case-response-model.mjs";
16
20
  import { normalizeCaseBrandProfileIds } from "./case-brand-profile-references.mjs";
21
+ import { EVIDENCE_FOLLOW_UP_CASE_REPORT_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
22
+ import { OBJECT_RESPONSE_CASE_REPORT_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
23
+ import { CASE_DELIVERY_RECEIPT_LIMITATION } from "./case-packet-correction.mjs";
17
24
  import { CASE_RECHECK_CONDITIONS } from "./case-recheck-model.mjs";
18
- import { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_REPORT_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
25
+ import { escapeCaseMarkdownInline as escapeMarkdownInline } from "./case-markdown.mjs";
26
+ 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";
19
27
  // ---------------------------------------------------------------------------
20
28
  // Constants
21
29
  // ---------------------------------------------------------------------------
@@ -35,16 +43,6 @@ const LIMITATIONS_TEXT = [
35
43
  'Snapshot hostnames are excluded; supporting DNS, TLS and web observations need not concern the Case registration domain.',
36
44
  ...PUBLISHED_V2_3_LIMITATIONS.slice(2),
37
45
  ].join(' ');
38
- function escapeMarkdownInline(text) {
39
- return String(text)
40
- .replace(/[\r\n\u2028\u2029]+/g, ' ')
41
- .replace(/([\\`*_{}\[\]<>()#+!|~])/g, '\\$1')
42
- // GFM autolinks bare URLs independently of ordinary link syntax. Break
43
- // the scheme or the first `www.` separator without making normal domains,
44
- // dates, and prose noisy in the report source.
45
- .replace(/\b([a-z][a-z0-9+.-]{1,31}):(?=\/\/)/gi, '$1\\:')
46
- .replace(/\bwww\.(?=\S)/gi, 'www\\.');
47
- }
48
46
  /**
49
47
  * Escapes a multiline note body for safe inclusion in a Markdown blockquote.
50
48
  * Each line is individually escaped and prefixed with `> `.
@@ -90,6 +88,12 @@ function formatReportValue(value, field) {
90
88
  return String(value);
91
89
  return String(value);
92
90
  }
91
+ function readableRiskScore(snapshot) {
92
+ const value = escapeMarkdownInline(formatReportValue(snapshot.riskScore));
93
+ const limitation = snapshot.riskScore === null ? null
94
+ : webCollectionScoreLimitation(snapshot.webCollectionQuality, snapshot.scanDepth);
95
+ return limitation ? `${value} — ${escapeMarkdownInline(limitation)}` : value;
96
+ }
93
97
  /**
94
98
  * Returns a shallow clone of a snapshot containing only known schema fields.
95
99
  * Unknown/imported keys are never included.
@@ -111,6 +115,7 @@ function pickKnownSnapshotFields(snapshot) {
111
115
  capturedAt: snapshot.capturedAt,
112
116
  source: snapshot.source,
113
117
  scanDepth: snapshot.scanDepth,
118
+ ...(snapshot.webCollectionQuality ? { webCollectionQuality: { ...snapshot.webCollectionQuality } } : {}),
114
119
  availability: snapshot.availability,
115
120
  confidence: snapshot.confidence,
116
121
  riskModelVersion: snapshot.riskModelVersion,
@@ -144,6 +149,7 @@ function pickKnownSnapshotFields(snapshot) {
144
149
  reusesOfficialAssets: snapshot.reusesOfficialAssets,
145
150
  hasPasswordField: snapshot.hasPasswordField,
146
151
  hasExternalFormAction: snapshot.hasExternalFormAction,
152
+ ...(snapshot.hasExternalPasswordForm === undefined ? {} : { hasExternalPasswordForm: snapshot.hasExternalPasswordForm }),
147
153
  phishingLanguageMatch: snapshot.phishingLanguageMatch,
148
154
  privacyProtected: snapshot.privacyProtected ?? null,
149
155
  idnReferenceMatch: snapshot.idnReferenceMatch ?? null,
@@ -173,14 +179,7 @@ export function buildCaseReport(caseRecord, options = {}) {
173
179
  const generator = buildPortableGeneratorMetadata(options.applicationVersion);
174
180
  // --- Build JSON report ---
175
181
  const timeline = caseEvidenceTimeline(caseRecord.evidenceHistory);
176
- const timelineEntries = timeline.map((entry) => ({
177
- snapshot: pickKnownSnapshotFields(entry.snapshot),
178
- isBaseline: entry.isBaseline,
179
- hasRepeatedObservation: entry.hasRepeatedObservation,
180
- changes: entry.changes?.map(({ field, label, before, after, tone }) => ({ field, label, before, after, tone })) ?? null,
181
- hasIncomparableChange: entry.hasIncomparableChange,
182
- incomparableReasons: [...entry.incomparableReasons],
183
- }));
182
+ const timelineEntries = projectReportTimeline(timeline);
184
183
  const selection = currentCaseEvidence(caseRecord);
185
184
  const latest = selection.snapshot;
186
185
  const currentAssessment = latest ? pickKnownSnapshotFields(latest) : null;
@@ -221,7 +220,9 @@ export function buildCaseReport(caseRecord, options = {}) {
221
220
  currentAssessment,
222
221
  evidenceTimeline: timelineEntries,
223
222
  analystResponse: {
223
+ ...(caseRecord.workflowMetadata === undefined ? {} : { workflowMetadata: structuredClone(caseRecord.workflowMetadata) }),
224
224
  evidencePins: caseRecord.evidencePins.map((item) => ({ ...item, limitations: [...item.limitations] })),
225
+ ...(caseRecord.evidenceLinks === undefined ? {} : { evidenceLinks: structuredClone(caseRecord.evidenceLinks) }),
225
226
  decisions: caseRecord.decisions.map((item) => ({ ...item, evidencePinIds: [...item.evidencePinIds] })),
226
227
  actions: caseRecord.actions.map((item) => ({
227
228
  ...item,
@@ -270,11 +271,29 @@ export function buildCaseReport(caseRecord, options = {}) {
270
271
  const md = buildMarkdown(json, options.includeAttribution !== false);
271
272
  return { json, markdown: md };
272
273
  }
274
+ function projectReportTimeline(timeline) {
275
+ return timeline.map(entry => ({
276
+ snapshot: pickKnownSnapshotFields(entry.snapshot), isBaseline: entry.isBaseline,
277
+ hasRepeatedObservation: entry.hasRepeatedObservation,
278
+ changes: entry.changes?.map(({ field, label, before, after, tone }) => ({ field, label, before, after, tone })) ?? null,
279
+ hasIncomparableChange: entry.hasIncomparableChange, incomparableReasons: [...entry.incomparableReasons],
280
+ }));
281
+ }
273
282
  /** Expected strict reader projection; historical disclosure wording is immutable. */
274
283
  export function buildCaseReportVerificationProjection(caseRecord, options, schemaVersion) {
275
- const current = buildCaseReport(caseRecord, options).json;
284
+ const generated = buildCaseReport(caseRecord, options).json;
276
285
  if (schemaVersion === CASE_REPORT_SCHEMA_VERSION)
277
- return current;
286
+ return generated;
287
+ if (schemaVersion === OBJECT_RESPONSE_CASE_REPORT_SCHEMA_VERSION)
288
+ return { ...generated, schemaVersion };
289
+ if (schemaVersion === EVIDENCE_FOLLOW_UP_CASE_REPORT_SCHEMA_VERSION)
290
+ return { ...generated, schemaVersion };
291
+ if (caseRecord.evidenceHistory.some(snapshot => snapshot.webCollectionQuality !== undefined)) {
292
+ throw new TypeError('Published report formats cannot declare newer collection-quality fields.');
293
+ }
294
+ const current = { ...generated, evidenceTimeline: projectReportTimeline(publishedCaseEvidenceTimelineForVerification(caseRecord.evidenceHistory)) };
295
+ if (schemaVersion === LATEST_PUBLIC_CASE_REPORT_SCHEMA_VERSION)
296
+ return { ...current, schemaVersion };
278
297
  if (schemaVersion !== PUBLISHED_V2_3_CASE_REPORT_SCHEMA_VERSION) {
279
298
  throw new TypeError('No strict Case report projection is defined for this version.');
280
299
  }
@@ -325,6 +344,17 @@ function buildMarkdown(report, includeAttribution) {
325
344
  if (report.case.tags.length > 0) {
326
345
  lines.push(`| Tags | ${escapeMarkdownInline(report.case.tags.join(', '))} |`);
327
346
  }
347
+ const caseTypes = caseTypeSummary(report.analystResponse.workflowMetadata ? { workflowMetadata: report.analystResponse.workflowMetadata } : {});
348
+ if (caseTypes)
349
+ lines.push(`| Case types | ${escapeMarkdownInline(caseTypes)} |`);
350
+ const context = report.analystResponse.workflowMetadata?.investigationContext;
351
+ if (context) {
352
+ lines.push(`| Investigation objective | ${escapeMarkdownInline(context.objective)} |`);
353
+ lines.push(`| Incident URL (${context.urlRetention === 'exact' ? 'exact' : 'origin only'}) | ${escapeMarkdownInline(context.incidentUrl)} |`);
354
+ }
355
+ for (const target of report.analystResponse.workflowMetadata?.incidentTargets ?? []) {
356
+ lines.push(`| Incident link (${target.state}) | ${escapeMarkdownInline(target.url)} |`);
357
+ }
328
358
  lines.push(`| Notes included | ${report.case.notesIncluded ? 'Yes' : 'No'} |`);
329
359
  lines.push('');
330
360
  // Current assessment
@@ -333,7 +363,7 @@ function buildMarkdown(report, includeAttribution) {
333
363
  if (report.currentAssessment) {
334
364
  const a = report.currentAssessment;
335
365
  lines.push(`- **Availability:** ${escapeMarkdownInline(formatReportValue(a.availability))}`);
336
- lines.push(`- **Risk score:** ${escapeMarkdownInline(formatReportValue(a.riskScore))}`);
366
+ lines.push(`- **Risk score:** ${readableRiskScore(a)}`);
337
367
  lines.push(`- **Risk model:** ${a.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(a.riskModelVersion))}`}`);
338
368
  lines.push(`- **Brand Profile context:** ${escapeMarkdownInline(formatReportValue(a.profileContextState))}`);
339
369
  if (a.profileContextLimitation)
@@ -388,7 +418,7 @@ function buildMarkdown(report, includeAttribution) {
388
418
  lines.push('**Key evidence:**');
389
419
  lines.push('');
390
420
  lines.push(`- Availability: ${escapeMarkdownInline(formatReportValue(snap.availability))}`);
391
- lines.push(`- Risk score: ${escapeMarkdownInline(formatReportValue(snap.riskScore))}`);
421
+ lines.push(`- Risk score: ${readableRiskScore(snap)}`);
392
422
  lines.push(`- Risk model: ${snap.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(snap.riskModelVersion))}`}`);
393
423
  lines.push(`- Brand Profile context: ${escapeMarkdownInline(formatReportValue(snap.profileContextState))}`);
394
424
  if (snap.profileContextLimitation)
@@ -455,6 +485,8 @@ function buildMarkdown(report, includeAttribution) {
455
485
  lines.push('> Opportunity scores and factors use different or unversioned models, so their numeric difference is not treated as a domain change.');
456
486
  if (reasons.includes('scan-depth'))
457
487
  lines.push('> Capture depths differ, so unevaluated deep signals are not treated as additions or removals.');
488
+ if (reasons.includes('collection-quality'))
489
+ lines.push('> Missing or incomplete observations prevent comparison of affected registration, page, favicon or score fields.');
458
490
  if (reasons.length === 0 || reasons.includes('other'))
459
491
  lines.push('> The observations differ materially, but no reliable field-level comparison is available.');
460
492
  lines.push('');
@@ -464,7 +496,7 @@ function buildMarkdown(report, includeAttribution) {
464
496
  lines.push('## Analyst Decision Packet');
465
497
  lines.push('');
466
498
  const response = report.analystResponse;
467
- 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) {
499
+ 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) {
468
500
  lines.push('No evidence pins, source-qualified sightings, structured assertions, decision records, response actions, independent effect reviews, closures, or manual investigation steps recorded.');
469
501
  lines.push('');
470
502
  }
@@ -476,6 +508,8 @@ function buildMarkdown(report, includeAttribution) {
476
508
  for (const pin of response.evidencePins) {
477
509
  lines.push(`- **${escapeMarkdownInline(pin.label)}:** ${escapeMarkdownInline(pin.value)}`);
478
510
  lines.push(` Source: ${escapeMarkdownInline(pin.source)}; observed ${escapeMarkdownInline(pin.observedAt ?? 'Time unavailable')}; completeness ${escapeMarkdownInline(pin.completeness)}.`);
511
+ if (pin.responseObject)
512
+ lines.push(` Exact object: ${escapeMarkdownInline(pin.responseObject.kind)} · ${escapeMarkdownInline(pin.responseObject.identifier)}`);
479
513
  if (pin.limitations.length)
480
514
  lines.push(` Limitations: ${escapeMarkdownInline(pin.limitations.join('; '))}`);
481
515
  }
@@ -539,6 +573,12 @@ function buildMarkdown(report, includeAttribution) {
539
573
  }
540
574
  lines.push('');
541
575
  }
576
+ if (response.evidenceLinks?.length) {
577
+ lines.push('### Analyst-declared evidence relationships', '');
578
+ for (const { link, missingPinIds, cyclic } of caseEvidenceLinkIssues(response.evidenceLinks, response.evidencePins))
579
+ 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.'] : []));
580
+ lines.push('These relationships record analyst attribution, not independent corroboration or added confidence.', '');
581
+ }
542
582
  lines.push('### Decisions');
543
583
  lines.push('');
544
584
  if (!response.decisions.length)
@@ -558,12 +598,22 @@ function buildMarkdown(report, includeAttribution) {
558
598
  for (const action of response.actions) {
559
599
  lines.push(`- **${escapeMarkdownInline(action.type.replaceAll('_', ' '))}:** ${escapeMarkdownInline(action.recipient)} (${escapeMarkdownInline(action.state)})`);
560
600
  lines.push(` Contact source: ${escapeMarkdownInline(action.contactSource)}; updated ${escapeMarkdownInline(action.updatedAt)}.`);
601
+ lines.push(` Object binding: ${action.responseObjects?.length ? action.responseObjects.map(object => `${escapeMarkdownInline(object.kind)} · ${escapeMarkdownInline(object.identifier)}`).join('; ') : 'Unknown; historical or unbound actions do not establish exact-object coverage.'}`);
561
602
  if (action.routeObservedAt)
562
603
  lines.push(` Route reviewed: ${escapeMarkdownInline(action.routeObservedAt)}`);
563
604
  if (action.routeReviewAfter)
564
605
  lines.push(` Route review due: ${escapeMarkdownInline(action.routeReviewAfter)}`);
565
606
  if (action.originActionId)
566
607
  lines.push(` Originating action: ${escapeMarkdownInline(action.originActionId)}`);
608
+ if (action.amendment)
609
+ lines.push(` Amendment of submitted packet SHA-256: ${action.amendment.packetDigestSha256}; request events: ${action.amendment.requestEventIds.map(escapeMarkdownInline).join(', ')}`);
610
+ if (action.correction)
611
+ lines.push(` ${action.correction.purpose === 'correction' ? 'Correction request' : 'Retraction request'} for delivery event ${escapeMarkdownInline(action.correction.deliveryEventId)}; packet v${action.correction.packetVersion} SHA-256: ${action.correction.packetDigestSha256}`, ` Reason: ${escapeMarkdownInline(action.correction.reason)}`, ` Previous statement: ${escapeMarkdownInline(action.correction.previousStatement)}`, ` Corrected statement: ${escapeMarkdownInline(action.correction.correctedStatement || 'Request to retract the previous statement')}`, ` Corrected evidence pins: ${action.correction.evidencePinIds.map(escapeMarkdownInline).join(', ')}`, ' Preparation is not delivery, recipient acceptance, restoration or independent recheck.');
612
+ for (const event of action.history)
613
+ if (event.evidenceRequest) {
614
+ const request = event.evidenceRequest;
615
+ 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)}` : ''}`);
616
+ }
567
617
  if (action.dueAt)
568
618
  lines.push(` Due: ${escapeMarkdownInline(action.dueAt)}`);
569
619
  if (action.followUpAt)
@@ -580,8 +630,14 @@ function buildMarkdown(report, includeAttribution) {
580
630
  lines.push(` - ${escapeMarkdownInline(event.occurredAt)} · ${escapeMarkdownInline(event.previousState ?? 'none')} → ${escapeMarkdownInline(event.nextState)} · ${escapeMarkdownInline(event.sourceClass)} · ${event.applied ? 'applied' : 'retained conflict'}`);
581
631
  if (event.providerOutcome)
582
632
  lines.push(` Provider outcome at this time: ${escapeMarkdownInline(event.providerOutcome.replaceAll('_', ' '))}`);
633
+ if (event.responseObjects?.length)
634
+ lines.push(` Objects at this event: ${event.responseObjects.map(object => `${escapeMarkdownInline(object.kind)} · ${escapeMarkdownInline(object.identifier)}`).join('; ')}`);
635
+ if (event.objectOutcome)
636
+ lines.push(` Source-reported object outcome: ${escapeMarkdownInline(event.objectOutcome)}; not independent verification or causation.`);
583
637
  if (event.reference)
584
638
  lines.push(` Event reference: ${escapeMarkdownInline(event.reference)}`);
639
+ if (event.packetReceipt)
640
+ lines.push(` Retained packet v${event.packetReceipt.packetVersion}: ${event.packetReceipt.packetDigestSha256}; recipient ${escapeMarkdownInline(event.packetReceipt.recipient)}; audience ${escapeMarkdownInline(event.packetReceipt.profile)}; generated ${event.packetReceipt.packetGeneratedAt}`, ` ${CASE_DELIVERY_RECEIPT_LIMITATION}`);
585
641
  if (event.evidencePinId)
586
642
  lines.push(` Evidence pin: ${escapeMarkdownInline(event.evidencePinId)}`);
587
643
  if (event.limitations.length)
@@ -631,6 +687,10 @@ function buildMarkdown(report, includeAttribution) {
631
687
  lines.push('- No independent observed-effect review recorded.');
632
688
  for (const review of response.observedEffects.reviews) {
633
689
  lines.push(`- **${escapeMarkdownInline(review.state.replaceAll('_', ' '))}** (${escapeMarkdownInline(review.observedAt)}): ${escapeMarkdownInline(review.source)}; class ${escapeMarkdownInline(review.sourceClass)}; completeness ${escapeMarkdownInline(review.completeness)}.`);
690
+ if (review.responseObject)
691
+ lines.push(` Exact object: ${escapeMarkdownInline(review.responseObject.kind)} · ${escapeMarkdownInline(review.responseObject.identifier)}`);
692
+ if (review.objectOutcome)
693
+ lines.push(` Independently recorded object outcome: ${escapeMarkdownInline(review.objectOutcome)}; point-in-time observation, not report causation or recurrence.`);
634
694
  if (review.recheck) {
635
695
  lines.push(` Question ${escapeMarkdownInline(review.recheck.questionId)}: ${escapeMarkdownInline(review.recheck.question)}`);
636
696
  lines.push(` Recheck target: ${escapeMarkdownInline(review.recheck.targetHostname)}; retained comparison conditions: ${escapeMarkdownInline(review.recheck.conditions)}`);
@@ -653,13 +713,19 @@ function buildMarkdown(report, includeAttribution) {
653
713
  lines.push('- No deliberate analyst closure recorded.');
654
714
  for (const closure of response.closures.records) {
655
715
  lines.push(`- **Closure · ${escapeMarkdownInline(closure.reason.replaceAll('_', ' '))}** (${escapeMarkdownInline(closure.createdAt)}): ${escapeMarkdownInline(closure.summary)}`);
716
+ lines.push(` Scope: ${closure.responseObject ? `${escapeMarkdownInline(closure.responseObject.kind)} · ${escapeMarkdownInline(closure.responseObject.identifier)}; other objects and the Case remain independent.` : 'Whole Case analyst decision.'}`);
656
717
  if (closure.observedEffectReviewId)
657
718
  lines.push(` Independent review: ${escapeMarkdownInline(closure.observedEffectReviewId)}`);
658
719
  if (closure.actionId)
659
720
  lines.push(` Provider action: ${escapeMarkdownInline(closure.actionId)}`);
721
+ const qualification = caseClosureHistoryQualification(closure, response.actions);
722
+ if (qualification)
723
+ lines.push(` History qualification: ${escapeMarkdownInline(qualification)}`);
660
724
  if (closure.limitations.length)
661
725
  lines.push(` Limitations: ${escapeMarkdownInline(closure.limitations.join('; '))}`);
662
726
  }
727
+ if (response.closures.omitted)
728
+ lines.push(`- Closure records not retained: ${response.closures.omitted}. ${escapeMarkdownInline(response.closures.limitations.join('; '))}`);
663
729
  lines.push('');
664
730
  // Analyst notes (only when included)
665
731
  if (report.case.notesIncluded && Array.isArray(report.case.notes) && report.case.notes.length > 0) {
@@ -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 && ['authorised', 'submitted'].includes(event.previousState ?? '') && 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
+ }