@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
@@ -1,13 +1,18 @@
1
1
  // Pure, framework-neutral analyst-case records, evidence histories, bounded
2
2
  // record normalization, and analyst updates.
3
- import { canonicalRegistrableDomain } from "../../lib/registrable-domain.mjs";
4
- import { parseCredentialFreeHttpUrl } from "../evidence/lookup-target.mjs";
3
+ import { normalizeCaseObjective, parseIncidentUrlContext, } from "./case-incident-context.mjs";
5
4
  import { selectExistingCase } from "./case-selection.mjs";
5
+ import { emptyCaseWorkflowMetadata, updateCaseWorkflowMetadata } from "./case-workflow-metadata.mjs";
6
+ import { assertCaseResponseObject, readCaseResponseObject, readCaseResponseObjects, sameCaseResponseObject } from "./case-response-object.mjs";
7
+ import { correctionDelivery, readCasePacketCorrection, readCaseDeliveryPacketReceipt } from "./case-packet-correction.mjs";
8
+ import { readCaseWorkflowFields } from "./case-workflow-migration.mjs";
6
9
  import { appendCaseAction, appendCaseAssertion, appendCaseClosure, appendCaseDecision, appendCaseEvidencePin, appendCaseEvidencePins, appendCaseManualTrailEvent, appendCaseObservedEffectReview, appendCaseSighting, buildCaseClosureLinkContext, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseClosureHistory, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseObservedEffectHistory, normalizeCaseSightings, updateCaseAction, updateCaseAssertion, } from "./case-response-model.mjs";
7
10
  import { MAX_CASES, MAX_NOTES_PER_CASE, } from "./case-record-contracts.mjs";
8
- import { PUBLISHED_V2_3_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, MAX_CASE_OBJECTIVE_LENGTH } from "../contracts/case-portability.mjs";
11
+ import { MAX_CASE_OBJECTIVE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../contracts/case-portability.mjs";
12
+ import { caseRecordVersionInput } from "./case-record-version-input.mjs";
9
13
  import { readCaseAttachments } from "./case-attachment-model.mjs";
10
- import { assertCurrentRecheckQuestion, assertRecheckNonReproduction, readCaseRecheckAnswerContext } from "./case-recheck-model.mjs";
14
+ import { readCaseEvidenceLinks, appendCaseEvidenceLink, withdrawCaseEvidenceLink } from "./case-evidence-links.mjs";
15
+ import { assertCurrentRecheckQuestion, assertRecheckNonReproduction, assertCaseObjectObservationOutcome, readCaseRecheckAnswerContext } from "./case-recheck-model.mjs";
11
16
  import { caseDispositionSupportsDefensiveResponse, caseStatusIsClosed, caseStatusOptionsForDirectEdit, caseStatusRequiresClosure, isReviewedCaseDisposition, } from "./case-record-decisions.mjs";
12
17
  import { DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_SET, caseTimestampOrNull, deterministicId, makeId, normalizeDisposition, normalizeDomain, normalizeNoteBody, normalizeReviewReasonCode, normalizeNotes, normalizeSource, normalizeStatus, normalizeTags, objectRecord, safeId, } from "./case-record-core.mjs";
13
18
  import { assertCaseBrandProfileIds, normalizeCaseBrandProfileIds, } from "./case-brand-profile-references.mjs";
@@ -15,84 +20,7 @@ import { normalizeEvidenceHistory, } from "./case-evidence-model.mjs";
15
20
  import { appendCaseInvestigationBranch, caseInvestigationBranchReferences, normalizeCaseInvestigationBranches, updateCaseInvestigationBranch, } from "./case-investigation-branch-model.mjs";
16
21
  export { caseDispositionSupportsDefensiveResponse, caseStatusIsClosed, caseStatusOptionsForDirectEdit, caseStatusRequiresClosure, isReviewedCaseDisposition, };
17
22
  export { MAX_CASE_OBJECTIVE_LENGTH };
18
- export const MAX_CASE_INCIDENT_URL_LENGTH = 1_850;
19
- export const INCIDENT_CONTEXT_STATEMENT_PREFIX = 'Investigate incident URL: ';
20
- const OBJECTIVE_PREFIX = 'Objective: ';
21
- const RETENTION_SEPARATOR = ' | URL retained: ';
22
- function boundedContextText(value, maximum) {
23
- return typeof value === 'string'
24
- ? value.replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
25
- : '';
26
- }
27
- export function normalizeCaseObjective(value) {
28
- return boundedContextText(value, MAX_CASE_OBJECTIVE_LENGTH);
29
- }
30
- export function parseIncidentUrlContext(value) {
31
- const parsed = parseCredentialFreeHttpUrl(value, MAX_CASE_INCIDENT_URL_LENGTH);
32
- if (!parsed)
33
- return null;
34
- const hostname = parsed.hostname.toLowerCase().replace(/\.$/u, '');
35
- const registrableDomain = canonicalRegistrableDomain(hostname);
36
- if (!registrableDomain)
37
- return null;
38
- const exactUrl = parsed.toString();
39
- if (exactUrl.length > MAX_CASE_INCIDENT_URL_LENGTH)
40
- return null;
41
- return Object.freeze({
42
- exactUrl,
43
- hostname,
44
- registrableDomain,
45
- originUrl: parsed.origin,
46
- hasPath: parsed.pathname !== '/',
47
- hasQuery: Boolean(parsed.search),
48
- hasFragment: Boolean(parsed.hash),
49
- });
50
- }
51
- export function caseInvestigationContext(record) {
52
- if (!record)
53
- return null;
54
- for (const assertion of [...record.assertions].reverse()) {
55
- if (assertion.kind !== 'next_step' || assertion.state !== 'open'
56
- || !assertion.statement.startsWith(INCIDENT_CONTEXT_STATEMENT_PREFIX))
57
- continue;
58
- const incidentUrl = assertion.statement.slice(INCIDENT_CONTEXT_STATEMENT_PREFIX.length);
59
- const parsed = parseIncidentUrlContext(incidentUrl);
60
- if (!parsed || parsed.registrableDomain !== record.domain)
61
- continue;
62
- const rationale = assertion.rationale ?? '';
63
- const separatorIndex = rationale.lastIndexOf(RETENTION_SEPARATOR);
64
- const objective = separatorIndex > OBJECTIVE_PREFIX.length && rationale.startsWith(OBJECTIVE_PREFIX)
65
- ? normalizeCaseObjective(rationale.slice(OBJECTIVE_PREFIX.length, separatorIndex))
66
- : '';
67
- const retention = separatorIndex >= 0 ? rationale.slice(separatorIndex + RETENTION_SEPARATOR.length) : '';
68
- if (!objective || (retention !== 'exact' && retention !== 'origin_only'))
69
- continue;
70
- return Object.freeze({
71
- objective,
72
- incidentUrl: parsed.exactUrl,
73
- urlRetention: retention,
74
- assertionId: assertion.id,
75
- updatedAt: assertion.updatedAt,
76
- });
77
- }
78
- return null;
79
- }
80
- export function caseInvestigationContextAssertion(input) {
81
- const objective = normalizeCaseObjective(input.objective);
82
- if (!objective)
83
- throw new Error('Enter the investigation objective before retaining Incident context.');
84
- const parsed = parseIncidentUrlContext(input.incidentUrl);
85
- if (!parsed)
86
- throw new Error(`Enter one absolute HTTP(S) Incident URL of at most ${MAX_CASE_INCIDENT_URL_LENGTH} characters without credentials.`);
87
- const retention = input.retainExactUrl ? 'exact' : 'origin_only';
88
- const retainedUrl = input.retainExactUrl ? parsed.exactUrl : parsed.originUrl;
89
- return Object.freeze({
90
- statement: `${INCIDENT_CONTEXT_STATEMENT_PREFIX}${retainedUrl}`,
91
- rationale: `${OBJECTIVE_PREFIX}${objective}${RETENTION_SEPARATOR}${retention}`,
92
- retainedUrl,
93
- retention,
94
- });
95
- }
23
+ export * from "./case-incident-context.mjs";
96
24
  // ---------------------------------------------------------------------------
97
25
  // Case normalization
98
26
  // ---------------------------------------------------------------------------
@@ -130,39 +58,42 @@ function normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, source
130
58
  */
131
59
  export function normalizeCase(raw, existing, nowIso, sourceVersion) {
132
60
  const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
133
- const record = objectRecord(raw);
134
- const domain = normalizeDomain(existing ? existing.domain : record.domain);
61
+ const input = objectRecord(raw);
62
+ const domain = normalizeDomain(existing ? existing.domain : input.domain);
135
63
  if (!domain)
136
64
  return null;
65
+ const { record, timestampOptions } = caseRecordVersionInput(input, sourceVersion);
137
66
  const createdAt = existing ? existing.createdAt : caseTimestampOrNull(record.createdAt, sourceVersion) || now;
138
67
  const updatedAt = caseTimestampOrNull(record.updatedAt, sourceVersion) || createdAt;
139
- const timestampOptions = {
140
- legacyTimestamps: sourceVersion != null && sourceVersion < PUBLISHED_V2_3_CASE_SCHEMA_VERSION,
141
- ...(sourceVersion === undefined ? {} : { sourceVersion }),
142
- };
143
68
  const evidencePins = normalizeCaseEvidencePins(record.evidencePins, updatedAt, timestampOptions);
144
69
  const pinIds = new Set(evidencePins.map((item) => item.id));
145
70
  const actions = normalizeCaseActions(record.actions, updatedAt, { ...timestampOptions, validEvidencePinIds: pinIds });
71
+ for (const action of actions)
72
+ for (const event of action.history)
73
+ if (event.packetReceipt
74
+ && (event.packetReceipt.caseId !== record.id || event.packetReceipt.target !== domain))
75
+ throw new TypeError('The packet receipt belongs to a different Case or target.');
146
76
  const assertions = normalizeCaseAssertions(record.assertions, updatedAt, pinIds, timestampOptions);
147
77
  const sightings = normalizeCaseSightings(record.sightings, updatedAt, pinIds, timestampOptions);
148
78
  const sightingIds = new Set(sightings.map((item) => item.id));
149
- const observedEffects = normalizeCaseObservedEffectHistory(sourceVersion != null && sourceVersion < 13 ? undefined : record.observedEffects, updatedAt, pinIds, sightingIds, timestampOptions);
150
- const closures = normalizeCaseClosureHistory(sourceVersion != null && sourceVersion < 13 ? undefined : record.closures, updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), timestampOptions, buildCaseClosureLinkContext(observedEffects, actions));
79
+ const observedEffects = normalizeCaseObservedEffectHistory(record.observedEffects, updatedAt, pinIds, sightingIds, timestampOptions);
80
+ const closures = normalizeCaseClosureHistory(record.closures, updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), timestampOptions, buildCaseClosureLinkContext(observedEffects, actions));
151
81
  const normalizedStatus = normalizeStatus(record.status);
152
82
  const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
153
83
  const attachments = readCaseAttachments(record.attachments);
84
+ const evidenceLinks = readCaseEvidenceLinks(record.evidenceLinks);
154
85
  return {
155
86
  id: existing ? existing.id : safeId(record.id) || deterministicId(domain),
156
87
  domain,
157
- title: sourceVersion != null && sourceVersion < INCIDENT_CASE_SCHEMA_VERSION ? '' : normalizeCaseObjective(record.title),
88
+ title: normalizeCaseObjective(record.title),
158
89
  status: caseStatusRequiresClosure(normalizedStatus)
159
- && closures.records.length === 0 && !closures.preV13HistoryUnavailable
90
+ && !closures.records.some(closure => closure.responseObject === undefined) && !closures.preV13HistoryUnavailable
160
91
  ? 'reviewing'
161
92
  : normalizedStatus,
162
93
  disposition: normalizeDisposition(record.disposition),
163
94
  reviewReasonCode: normalizeReviewReasonCode(record.reviewReasonCode),
164
95
  brandProfileIds: normalizeCaseBrandProfileIds(record.brandProfileIds),
165
- tags: normalizeTags(record.tags),
96
+ ...readCaseWorkflowFields(record, domain, assertions, sourceVersion),
166
97
  notes: normalizeNotes(record.notes, now, sourceVersion),
167
98
  source: normalizeSource(record.source),
168
99
  evidenceHistory: normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, sourceVersion),
@@ -176,6 +107,7 @@ export function normalizeCase(raw, existing, nowIso, sourceVersion) {
176
107
  closures,
177
108
  branches: normalizeCaseInvestigationBranches(record.branches, updatedAt, branchReferences, timestampOptions),
178
109
  ...(attachments === undefined ? {} : { attachments }),
110
+ ...(evidenceLinks === undefined ? {} : { evidenceLinks }),
179
111
  createdAt,
180
112
  updatedAt,
181
113
  };
@@ -192,6 +124,13 @@ export function createCase(input, nowIso) {
192
124
  throw new Error('A valid domain is required to open a case.');
193
125
  const noteBody = normalizeNoteBody(input.note);
194
126
  const source = normalizeSource(input.source);
127
+ const workflowMetadata = updateCaseWorkflowMetadata(emptyCaseWorkflowMetadata(), input, domain, now);
128
+ const scopedCase = { domain, workflowMetadata };
129
+ for (const raw of [input.evidencePin, ...(Array.isArray(input.evidencePins) ? input.evidencePins : []), input.observedEffectReview, input.closure])
130
+ assertCaseResponseObject(readCaseResponseObject(objectRecord(raw).responseObject), scopedCase);
131
+ for (const object of readCaseResponseObjects(objectRecord(input.action).responseObjects) ?? [])
132
+ assertCaseResponseObject(object, scopedCase);
133
+ assertCaseResponseObject(readCaseResponseObject(objectRecord(objectRecord(input.assertion).recheck).responseObject), scopedCase);
195
134
  const evidencePins = input.evidencePins !== undefined
196
135
  ? appendCaseEvidencePins([], input.evidencePins, now)
197
136
  : input.evidencePin !== undefined
@@ -212,13 +151,19 @@ export function createCase(input, nowIso) {
212
151
  ? appendCaseObservedEffectReview(normalizeCaseObservedEffectHistory(undefined, now), input.observedEffectReview, now, pinIds, new Set(sightings.map((item) => item.id)))
213
152
  : normalizeCaseObservedEffectHistory(undefined, now);
214
153
  for (const review of observedEffects.reviews) {
154
+ const pin = evidencePins.find(item => item.id === review.evidencePinId);
155
+ if (review.responseObject && pin && !sameCaseResponseObject(review.responseObject, pin.responseObject))
156
+ throw new TypeError('The selected observation evidence does not explicitly concern this exact object.');
157
+ if (review.responseObject && review.state === 'not_reproduced' && !review.recheck)
158
+ throw new TypeError('Object-specific non-reproduction requires a saved baseline question and comparable evidence.');
159
+ assertCaseObjectObservationOutcome(review.objectOutcome, review.recheck, review.completeness, evidencePins, pin, review.observedAt);
215
160
  if (!review.recheck)
216
161
  continue;
217
162
  assertCurrentRecheckQuestion(review.recheck, assertions);
218
163
  assertRecheckNonReproduction(review.state, review.recheck, review.completeness, evidencePins, evidencePins.find(pin => pin.id === review.evidencePinId), review.observedAt);
219
164
  }
220
165
  const closures = input.closure !== undefined
221
- ? appendCaseClosure(normalizeCaseClosureHistory(undefined, now), input.closure, now, observedEffects, actions)
166
+ ? appendCaseClosure(normalizeCaseClosureHistory(undefined, now), input.closure, now, observedEffects, actions, evidencePins)
222
167
  : normalizeCaseClosureHistory(undefined, now);
223
168
  if (caseStatusRequiresClosure(normalizeStatus(input.status)) && input.closure === undefined) {
224
169
  throw new Error('Opening a resolved case requires a deliberate closure reason and its linked review context.');
@@ -227,11 +172,12 @@ export function createCase(input, nowIso) {
227
172
  id: makeId(),
228
173
  domain,
229
174
  title: normalizeCaseObjective(input.title),
230
- status: input.closure !== undefined ? 'resolved' : normalizeStatus(input.status),
175
+ status: input.closure !== undefined && objectRecord(input.closure).responseObject === undefined ? 'resolved' : normalizeStatus(input.status),
231
176
  disposition: normalizeDisposition(input.disposition),
232
177
  reviewReasonCode: normalizeReviewReasonCode(input.reviewReasonCode),
233
178
  brandProfileIds: input.brandProfileIds === undefined ? [] : assertCaseBrandProfileIds(input.brandProfileIds),
234
179
  tags: normalizeTags(input.tags),
180
+ workflowMetadata,
235
181
  notes: noteBody ? [{ id: makeId(), body: noteBody, createdAt: now }] : [],
236
182
  source,
237
183
  evidenceHistory: normalizeEvidenceHistory(input.evidence ? [input.evidence] : [], {
@@ -243,6 +189,7 @@ export function createCase(input, nowIso) {
243
189
  decisions: input.decision !== undefined
244
190
  ? appendCaseDecision([], input.decision, now)
245
191
  : [],
192
+ ...(input.evidenceLink === undefined ? {} : { evidenceLinks: appendCaseEvidenceLink([], input.evidenceLink, evidencePins, now) }),
246
193
  actions,
247
194
  assertions,
248
195
  manualTrail: input.trailEvent !== undefined
@@ -319,9 +266,36 @@ export function updateCase(cases, id, patch, nowIso) {
319
266
  const current = cases[index];
320
267
  if (!current)
321
268
  throw new Error('That case no longer exists.');
269
+ if (patch.expectedResponseContext !== undefined && patch.expectedResponseContext !== JSON.stringify(current))
270
+ throw new TypeError('The Case response context changed after preview. Review the current delivery, scope and evidence before creating this draft.');
271
+ const workflowMetadata = updateCaseWorkflowMetadata(current.workflowMetadata
272
+ ?? readCaseWorkflowFields(current, current.domain, current.assertions).workflowMetadata, patch, current.domain, now);
273
+ const scopedCase = { ...current, workflowMetadata };
274
+ for (const raw of [patch.evidencePin, ...(Array.isArray(patch.evidencePins) ? patch.evidencePins : []), patch.observedEffectReview, patch.closure]) {
275
+ assertCaseResponseObject(readCaseResponseObject(objectRecord(raw).responseObject), scopedCase);
276
+ }
277
+ for (const raw of [patch.action, patch.actionUpdate, objectRecord(patch.actionUpdate).transition]) {
278
+ for (const object of readCaseResponseObjects(objectRecord(raw).responseObjects) ?? [])
279
+ assertCaseResponseObject(object, scopedCase);
280
+ }
281
+ for (const raw of [patch.assertion, patch.assertionUpdate, patch.observedEffectReview]) {
282
+ assertCaseResponseObject(readCaseResponseObject(objectRecord(objectRecord(raw).recheck).responseObject), scopedCase);
283
+ }
284
+ if (patch.evidenceLink !== undefined && patch.evidenceLinkWithdrawal !== undefined)
285
+ throw new TypeError('Record or withdraw one evidence relationship at a time.');
322
286
  if (patch.title !== undefined && patch.expectedTitle !== undefined && patch.expectedTitle !== (current.title ?? '')) {
323
287
  throw new Error('The incident title changed after this draft was started. Reload and review the current title before saving; your draft has not been applied.');
324
288
  }
289
+ for (const [value, expected, actual, label] of [
290
+ [patch.status, patch.expectedStatus, current.status, 'status'],
291
+ [patch.disposition, patch.expectedDisposition, current.disposition, 'disposition'],
292
+ [patch.reviewReasonCode, patch.expectedReviewReasonCode, current.reviewReasonCode ?? null, 'review reason'],
293
+ [patch.tags, patch.expectedTags, current.tags, 'tags'],
294
+ ]) {
295
+ if (value !== undefined && expected !== undefined && JSON.stringify(expected) !== JSON.stringify(actual)) {
296
+ throw new Error(`The Case ${label} changed after this edit was started. Reopen the Case to review the current value; your edit has not been applied.`);
297
+ }
298
+ }
325
299
  let notes = current.notes;
326
300
  if (patch.note !== undefined) {
327
301
  const body = normalizeNoteBody(patch.note);
@@ -345,6 +319,17 @@ export function updateCase(cases, id, patch, nowIso) {
345
319
  evidencePins = appendCaseEvidencePin(evidencePins, patch.evidencePin, now);
346
320
  }
347
321
  const pinIds = new Set(evidencePins.map((item) => item.id));
322
+ const correctionInput = patch.action !== undefined ? objectRecord(patch.action) : patch.actionUpdate !== undefined
323
+ ? { ...current.actions.find(action => action.id === objectRecord(patch.actionUpdate).id), ...objectRecord(patch.actionUpdate) } : null;
324
+ if (correctionInput?.correction !== undefined)
325
+ correctionDelivery(current.actions, {
326
+ originActionId: typeof correctionInput.originActionId === 'string' ? correctionInput.originActionId : null,
327
+ recipient: typeof correctionInput.recipient === 'string' ? correctionInput.recipient : '',
328
+ responseObjects: readCaseResponseObjects(correctionInput.responseObjects), correction: readCasePacketCorrection(correctionInput.correction),
329
+ }, { caseId: current.id, target: current.domain, selectedPinIds: pinIds });
330
+ const transitionReceipt = readCaseDeliveryPacketReceipt(objectRecord(objectRecord(patch.actionUpdate).transition).packetReceipt);
331
+ if (transitionReceipt && (transitionReceipt.caseId !== current.id || transitionReceipt.target !== current.domain))
332
+ throw new TypeError('The packet receipt belongs to a different Case or target.');
348
333
  let decisions = current.decisions;
349
334
  if (patch.decision !== undefined) {
350
335
  decisions = appendCaseDecision(current.decisions, patch.decision, now, pinIds);
@@ -356,6 +341,16 @@ export function updateCase(cases, id, patch, nowIso) {
356
341
  if (patch.actionUpdate !== undefined) {
357
342
  actions = updateCaseAction(actions, patch.actionUpdate, now, pinIds);
358
343
  }
344
+ if (correctionInput?.correction !== undefined || transitionReceipt) {
345
+ for (const original of current.actions) {
346
+ const retained = actions.find(action => action.id === original.id);
347
+ if (!retained || original.history.some(event => !retained.history.some(next => JSON.stringify(next) === JSON.stringify(event)))
348
+ || (patch.action !== undefined && JSON.stringify(retained) !== JSON.stringify(original)))
349
+ throw new TypeError('This linked operation would omit or change historical delivery records within the Case bounds. No correction or receipt was retained.');
350
+ }
351
+ if (transitionReceipt && !actions.find(action => action.id === transitionReceipt.actionId)?.history.some(event => JSON.stringify(event.packetReceipt) === JSON.stringify(transitionReceipt)))
352
+ throw new TypeError('The exact delivery receipt exceeds the retained history bounds. No delivery was recorded.');
353
+ }
359
354
  let assertions = current.assertions;
360
355
  if (patch.assertion !== undefined) {
361
356
  assertions = appendCaseAssertion(current.assertions, patch.assertion, now, pinIds);
@@ -387,20 +382,28 @@ export function updateCase(cases, id, patch, nowIso) {
387
382
  assertCurrentRecheckQuestion(context, assertions);
388
383
  assertRecheckNonReproduction(review.state, context, String(review.completeness), evidencePins, evidencePins.find(pin => pin.id === review.evidencePinId), typeof review.observedAt === 'string' ? review.observedAt : null);
389
384
  }
385
+ const responseObject = readCaseResponseObject(review.responseObject);
386
+ const pin = evidencePins.find(item => item.id === review.evidencePinId);
387
+ assertCaseObjectObservationOutcome(review.objectOutcome, context, String(review.completeness), evidencePins, pin, typeof review.observedAt === 'string' ? review.observedAt : null);
388
+ if (responseObject && pin && !sameCaseResponseObject(responseObject, pin.responseObject))
389
+ throw new TypeError('The selected observation evidence does not explicitly concern this exact object.');
390
+ if (responseObject && review.state === 'not_reproduced' && !context)
391
+ throw new TypeError('Object-specific non-reproduction requires a saved baseline question and comparable evidence.');
390
392
  observedEffects = appendCaseObservedEffectReview(observedEffects, patch.observedEffectReview, now, pinIds, new Set(sightings.map((item) => item.id)));
391
393
  }
392
394
  let closures = normalizeCaseClosureHistory(current.closures, current.updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), {}, buildCaseClosureLinkContext(observedEffects, actions));
393
395
  if (patch.closure !== undefined) {
394
- closures = appendCaseClosure(closures, patch.closure, now, observedEffects, actions);
396
+ closures = appendCaseClosure(closures, patch.closure, now, observedEffects, actions, evidencePins);
395
397
  }
396
398
  if (caseStatusRequiresClosure(patch.status) && patch.closure === undefined) {
397
399
  throw new Error('Resolve this case through the deliberate closure review so the reason and evidence state remain explicit.');
398
400
  }
399
401
  const record = {
400
402
  ...current,
403
+ workflowMetadata,
401
404
  title: patch.title === undefined ? current.title ?? '' : normalizeCaseObjective(patch.title),
402
405
  status: patch.closure !== undefined
403
- ? 'resolved'
406
+ ? objectRecord(patch.closure).responseObject !== undefined ? current.status : 'resolved'
404
407
  : patch.status !== undefined ? normalizeStatus(patch.status) : current.status,
405
408
  disposition: patch.disposition !== undefined ? normalizeDisposition(patch.disposition) : current.disposition,
406
409
  reviewReasonCode: patch.reviewReasonCode !== undefined
@@ -413,6 +416,11 @@ export function updateCase(cases, id, patch, nowIso) {
413
416
  source,
414
417
  evidenceHistory,
415
418
  evidencePins,
419
+ ...((patch.evidenceLink !== undefined || patch.evidenceLinkWithdrawal !== undefined) ? {
420
+ evidenceLinks: patch.evidenceLink !== undefined
421
+ ? appendCaseEvidenceLink(current.evidenceLinks ?? [], patch.evidenceLink, evidencePins, now)
422
+ : withdrawCaseEvidenceLink(current.evidenceLinks ?? [], patch.evidenceLinkWithdrawal, now),
423
+ } : {}),
416
424
  decisions,
417
425
  actions,
418
426
  assertions,
@@ -537,26 +545,7 @@ export function recordCaseInvestigationContext(cases, id, input, nowIso) {
537
545
  if (!parsed || parsed.registrableDomain !== current.domain) {
538
546
  throw new Error(`The Incident URL must belong to the Case domain ${current.domain}.`);
539
547
  }
540
- const context = caseInvestigationContextAssertion(input);
541
- const existing = caseInvestigationContext(current);
542
- return updateCase(cases, id, existing
543
- ? {
544
- assertionUpdate: {
545
- id: existing.assertionId,
546
- statement: context.statement,
547
- rationale: context.rationale,
548
- state: 'open',
549
- },
550
- }
551
- : {
552
- assertion: {
553
- kind: 'next_step',
554
- statement: context.statement,
555
- rationale: context.rationale,
556
- evidenceRelations: [],
557
- state: 'open',
558
- },
559
- }, nowIso);
548
+ return updateCase(cases, id, { investigationContext: input }, nowIso);
560
549
  }
561
550
  export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
562
551
  const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
@@ -577,8 +566,10 @@ export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
577
566
  observedAt: input.observedAt,
578
567
  collectionDepth: input.collectionDepth,
579
568
  observationHostname: input.observationHostname,
569
+ ...(input.responseObject ? { responseObject: input.responseObject } : {}),
580
570
  completeness: input.completeness,
581
- truncated: false,
571
+ truncated: input.comparisonTruncated === true
572
+ || (typeof input.comparisonSummary === 'string' && input.comparisonSummary.length > MAX_RESPONSE_VALUE_LENGTH),
582
573
  limitations: input.limitations,
583
574
  },
584
575
  }, now);
@@ -596,6 +587,8 @@ export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
596
587
  followUpAt: input.followUpAt,
597
588
  limitations: input.limitations,
598
589
  ...(input.recheck ? { recheck: input.recheck } : {}),
590
+ ...(input.responseObject ? { responseObject: input.responseObject } : {}),
591
+ ...(input.objectOutcome ? { objectOutcome: input.objectOutcome } : {}),
599
592
  },
600
593
  }, now);
601
594
  }
@@ -1,4 +1,4 @@
1
- import { PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
1
+ import { PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
2
2
  import { caseStatusIsClosed } from "./case-record-decisions.mjs";
3
3
  const PRESERVE = Object.freeze({
4
4
  durable: 'preserve', internal: 'preserve', trusted: 'preserve', public: 'preserve',
@@ -86,6 +86,12 @@ const CASE_FIELD_RULES = Object.freeze({
86
86
  audienceExclusions: { public: { label: 'Brand Profile references', order: 2 } },
87
87
  }),
88
88
  tags: fieldRule('tags', PRESERVE, (record) => [...record.tags]),
89
+ workflowMetadata: fieldRule('workflowMetadata', PUBLIC_REDACT, (record, profile) => (record.workflowMetadata === undefined ? undefined : profile === 'public'
90
+ ? { types: [...record.workflowMetadata.types], incidentTargets: [], investigationContext: null }
91
+ : structuredClone(record.workflowMetadata)), {
92
+ nestedSensitiveFields: ['incidentTargets', 'investigationContext'],
93
+ audienceExclusions: { public: { label: 'Private incident links and investigation context', order: 4, sinceVersion: EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } },
94
+ }),
89
95
  notes: fieldRule('notes', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public'
90
96
  ? []
91
97
  : record.notes.map((item) => ({ ...item }))), {
@@ -96,14 +102,26 @@ const CASE_FIELD_RULES = Object.freeze({
96
102
  }),
97
103
  source: preservedField('source'),
98
104
  evidenceHistory: fieldRule('evidenceHistory', PRESERVE, (record) => structuredClone(record.evidenceHistory)),
99
- evidencePins: fieldRule('evidencePins', PRESERVE, (record) => structuredClone(record.evidencePins)),
105
+ evidencePins: fieldRule('evidencePins', PUBLIC_REDACT, (record, profile) => structuredClone(record.evidencePins).map(pin => {
106
+ if (profile !== 'public')
107
+ return pin;
108
+ const { responseObject: _scope, infrastructureObservation: _infrastructure, ...publicPin } = pin;
109
+ return publicPin;
110
+ }), { nestedSensitiveFields: ['responseObject', 'infrastructureObservation'], audienceExclusions: { public: { label: 'Exact-object evidence scope and infrastructure observations', order: 4, sinceVersion: 18 } } }),
111
+ evidenceLinks: fieldRule('evidenceLinks', PUBLIC_EXCLUDE, (record, profile) => (profile === 'public' ? undefined : structuredClone(record.evidenceLinks)), { audienceExclusions: { public: { label: 'Analyst evidence relationships', order: 4, sinceVersion: EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } } }),
100
112
  decisions: fieldRule('decisions', PRESERVE, (record) => structuredClone(record.decisions)),
101
113
  actions: fieldRule('actions', TRUSTED_REDACT_PUBLIC_EXCLUDE, (record, profile) => (profile === 'public'
102
114
  ? []
103
115
  : profile === 'trusted'
104
- ? structuredClone(record.actions).map((item) => ({ ...item, recipient: '[redacted]' }))
116
+ ? structuredClone(record.actions).map((item) => {
117
+ const { correction: _privateCorrection, ...shared } = item;
118
+ return { ...shared, recipient: '[redacted]', history: shared.history.map(event => {
119
+ const { packetReceipt: _privateReceipt, ...sharedEvent } = event;
120
+ return sharedEvent;
121
+ }) };
122
+ })
105
123
  : structuredClone(record.actions)), {
106
- nestedSensitiveFields: ['recipient'],
124
+ nestedSensitiveFields: ['recipient', 'responseObjects', 'correction', 'packetReceipt'],
107
125
  audienceExclusions: {
108
126
  trusted: { label: 'Recipient values', order: 2 },
109
127
  public: { label: 'Actions and recipient values', order: 3 },
@@ -184,6 +202,7 @@ const OUTSIDE_SCHEMA_EXCLUSIONS = Object.freeze({
184
202
  });
185
203
  function currentAudienceExclusions(audience, caseVersion) {
186
204
  const entries = [
205
+ ...(audience === 'trusted' && caseVersion >= 19 ? [{ label: 'Private correction content and exact delivery receipts', order: 2 }] : []),
187
206
  ...Object.values(CASE_FIELD_RULES).flatMap((rule) => (rule.audienceExclusions[audience] ? [rule.audienceExclusions[audience]] : [])),
188
207
  OUTSIDE_SCHEMA_EXCLUSIONS[audience],
189
208
  ];
@@ -0,0 +1,41 @@
1
+ import { EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, PUBLISHED_V2_CASE_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_SCHEMA_VERSION, OBJECT_RESPONSE_CASE_SCHEMA_VERSION, DELIVERY_CORRECTION_CASE_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
2
+ /** Adapt declared historical fields before ordinary current-record recovery. */
3
+ export function caseRecordVersionInput(record, sourceVersion) {
4
+ const declared = sourceVersion != null;
5
+ if (declared && sourceVersion < DELIVERY_CORRECTION_CASE_SCHEMA_VERSION && Array.isArray(record.actions)
6
+ && record.actions.some(action => action && typeof action === 'object' && (action.correction !== undefined
7
+ || Array.isArray(action.history) && action.history.some((event) => event?.packetReceipt !== undefined)))) {
8
+ throw new TypeError('Delivery receipts and corrections require Case schema 19; historical deliveries were not reinterpreted.');
9
+ }
10
+ if (declared && sourceVersion < OBJECT_RESPONSE_CASE_SCHEMA_VERSION) {
11
+ const items = (value) => Array.isArray(value) ? value.filter((item) => !!item && typeof item === 'object' && !Array.isArray(item)) : [];
12
+ const object = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
13
+ const pins = items(record.evidencePins);
14
+ const actions = items(record.actions);
15
+ const reviews = items(object(record.observedEffects).reviews);
16
+ const closures = items(object(record.closures).records);
17
+ const questions = [...items(record.assertions), ...reviews].map(item => object(item.recheck));
18
+ const events = actions.flatMap(action => items(action.history));
19
+ if (pins.some(pin => pin.responseObject !== undefined || pin.infrastructureObservation !== undefined)
20
+ || actions.some(action => action.responseObjects !== undefined)
21
+ || events.some(event => event.responseObjects !== undefined || event.objectOutcome !== undefined)
22
+ || [...reviews, ...closures, ...questions].some(item => item.responseObject !== undefined || item.objectOutcome !== undefined)
23
+ || [...items(record.evidenceHistory), object(record.evidence)].some(snapshot => snapshot.hasExternalPasswordForm !== undefined)) {
24
+ throw new TypeError('Object response and password-form attribution fields require Case schema 18; historical evidence was not reinterpreted.');
25
+ }
26
+ }
27
+ if (record.evidenceLinks !== undefined && declared && sourceVersion < EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION) {
28
+ throw new TypeError('Evidence relationships require the current Case schema.');
29
+ }
30
+ return {
31
+ record: declared && sourceVersion < INCIDENT_CASE_SCHEMA_VERSION ? {
32
+ ...record,
33
+ ...(sourceVersion < PUBLISHED_V2_CASE_SCHEMA_VERSION ? { observedEffects: undefined, closures: undefined } : {}),
34
+ title: '',
35
+ } : record,
36
+ timestampOptions: {
37
+ legacyTimestamps: declared && sourceVersion < PUBLISHED_V2_3_CASE_SCHEMA_VERSION,
38
+ ...(sourceVersion === undefined ? {} : { sourceVersion }),
39
+ },
40
+ };
41
+ }