@slicedearth/whoisleuth-cli 2.2.0 → 2.3.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 (77) hide show
  1. package/README.md +2 -1
  2. package/cli/arguments.mjs +389 -1745
  3. package/cli/artifact-validation/case-response.mjs +55 -30
  4. package/cli/artifact-validation/signed-review.mjs +30 -12
  5. package/cli/bulk-checkpoint.mjs +34 -3
  6. package/cli/case-pack.mjs +39 -92
  7. package/cli/command-argument-grammar.mjs +211 -0
  8. package/cli/command-reference.mjs +200 -213
  9. package/cli/completion.mjs +5 -1
  10. package/cli/discovery-observation-snapshot.mjs +16 -3
  11. package/cli/discovery-scan-command-runner.mjs +19 -0
  12. package/cli/discovery-scan.mjs +6 -0
  13. package/cli/domain-control-observations.mjs +1 -1
  14. package/cli/inline-command-families.mjs +1 -0
  15. package/cli/input.mjs +116 -0
  16. package/cli/interchange-report.mjs +14 -0
  17. package/cli/lookalike-calibration.mjs +7 -2
  18. package/cli/lookup-brief.mjs +4 -2
  19. package/cli/lookup-browser-view.mjs +1 -1
  20. package/cli/mail-header-review.mjs +467 -0
  21. package/cli/review-command-runner.mjs +25 -0
  22. package/cli/risk-calibration.mjs +19 -17
  23. package/cli/runner.mjs +7 -1
  24. package/docs/cli-reference.md +8 -1
  25. package/docs/cli.md +12 -1
  26. package/lib/bounded-json.mjs +9 -6
  27. package/lib/dns-convergence-review.mjs +1 -0
  28. package/lib/dns-intelligence.mjs +37 -3
  29. package/lib/dnssec-chain-validation.mjs +6 -0
  30. package/lib/domain-change-packet.mjs +42 -14
  31. package/lib/domain-control-flight-recorder.mjs +4 -1
  32. package/lib/domain-control-manifest.mjs +16 -16
  33. package/lib/domain-posture-parsers.mjs +114 -3
  34. package/lib/domain-posture.mjs +34 -3
  35. package/lib/evidence-export.mjs +60 -11
  36. package/lib/evidence-report.mjs +2 -2
  37. package/lib/html-signals.mjs +39 -33
  38. package/lib/local-geoip-evidence.mjs +19 -3
  39. package/lib/lookup-response.mjs +5 -4
  40. package/lib/lookup-source-progress.mjs +27 -4
  41. package/lib/nameserver-preflight-review.mjs +1 -0
  42. package/lib/rdap-capabilities.mjs +1 -1
  43. package/lib/registry-insights.mjs +35 -22
  44. package/lib/risk-calibration-summary.mjs +1 -3
  45. package/lib/security-txt.mjs +33 -1
  46. package/lib/static-html-analysis.mjs +69 -13
  47. package/lib/website-technology.mjs +79 -11
  48. package/lib/zone-intent-review.mjs +16 -6
  49. package/package.json +1 -1
  50. package/packages/cases/case-migration-model.mjs +71 -22
  51. package/packages/cases/case-record-contracts.mjs +5 -20
  52. package/packages/cases/case-record-core.mjs +10 -21
  53. package/packages/cases/case-record-decisions.mjs +72 -0
  54. package/packages/cases/case-record-model.mjs +1 -0
  55. package/packages/cases/case-record-operations.mjs +6 -4
  56. package/packages/cases/case-record-projection.mjs +218 -0
  57. package/packages/cases/case-report.mjs +11 -6
  58. package/packages/cases/case-response-model.mjs +43 -6
  59. package/packages/cases/case-response-packet.mjs +78 -27
  60. package/packages/cases/case-storage-model.mjs +6 -2
  61. package/packages/comparison/comparison-ledger-bulk.mjs +9 -10
  62. package/packages/comparison/page-similarity.mjs +3 -1
  63. package/packages/contracts/case-portability.mjs +171 -51
  64. package/packages/contracts/cli-command-semantics.mjs +93 -0
  65. package/packages/contracts/risk-calibration.mjs +15 -10
  66. package/packages/contracts/workspace-portability.mjs +44 -7
  67. package/packages/evidence/domain-control-runtime.mjs +46 -6
  68. package/packages/interchange/external-findings-import.mjs +3 -2
  69. package/packages/investigation/lookup-asset-graph.mjs +35 -27
  70. package/packages/workspace/brand-profile-model.mjs +118 -2
  71. package/packages/workspace/bulk-review-model.mjs +13 -7
  72. package/packages/workspace/bulk-session-model.mjs +5 -4
  73. package/packages/workspace/detection-rule-model.mjs +70 -22
  74. package/packages/workspace/relationship-observation-model.mjs +4 -1
  75. package/packages/workspace/website-snapshot-model.mjs +106 -8
  76. package/packages/workspace/workspace-archive.mjs +2 -0
  77. package/cli/evidence-command-arguments.mjs +0 -156
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canonicalArtifactJsonV2 } from "../../packages/evidence/artifact-integrity.mjs";
3
- import { CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
3
+ import { CASE_RESPONSE_PACKET_VERSION, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
4
4
  import { CASE_ACTION_EVENT_SOURCE_CLASSES, CASE_ACTION_STATES, CASE_ACTION_TYPES, CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, CASE_PROVIDER_OUTCOMES, isLegalCaseActionTransition, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_CASE_ACTIONS, MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_RECIPIENT_LENGTH, } from "../../packages/cases/case-response-model.mjs";
5
5
  import { HEX_DIGEST_RE, array, boolean, domain, enumeration, exact, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
6
6
  const CASE_RESPONSE_PREFLIGHT_IDS_V6 = [
@@ -21,6 +21,9 @@ const CASE_RESPONSE_PREFLIGHT_IDS_V8 = [
21
21
  'action_tracking',
22
22
  ];
23
23
  const PUBLIC_CASE_ACTION_STATES = ['planned', 'ready_for_review', 'submitted', 'acknowledged', 'resolved', 'closed'];
24
+ const PRE_PLATFORM_PROFILE_IDS = RESPONSE_PACKET_PROFILE_IDS.filter((id) => id !== 'application_platform');
25
+ const PRE_PLATFORM_CONTACT_KINDS = RESPONSE_CONTACT_KINDS.filter((kind) => kind !== 'application_platform');
26
+ const PRE_PLATFORM_ACTION_TYPES = CASE_ACTION_TYPES.filter((type) => type !== 'platform_report');
24
27
  function legacyProfileRedactions(profileId) {
25
28
  if (profileId === 'registrar' || profileId === 'registry') {
26
29
  return ['Remove unnecessary personal registration data', 'Remove credentials, tokens, and URL fragments'];
@@ -68,7 +71,7 @@ function validateCaseResponsePacketV6(value) {
68
71
  if (root.reviewRequired !== true || root.submissionPerformed !== false)
69
72
  fail('Case-response packet review state');
70
73
  const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
71
- const profileId = enumeration(profile.id, RESPONSE_PACKET_PROFILE_IDS, 'Case-response profile id');
74
+ const profileId = enumeration(profile.id, PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
72
75
  text(profile.label, 'Case-response profile label', 200);
73
76
  text(profile.audience, 'Case-response profile audience', 300);
74
77
  text(profile.subject, 'Case-response profile subject', 500);
@@ -124,7 +127,7 @@ function validateCaseResponsePacketV6(value) {
124
127
  const contactKeys = new Set();
125
128
  for (const candidate of contacts) {
126
129
  const contact = exact(candidate, ['kind', 'contact', 'source', 'limitations'], 'Case-response contact');
127
- const kind = enumeration(contact.kind, RESPONSE_CONTACT_KINDS, 'Case-response contact kind');
130
+ const kind = enumeration(contact.kind, PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
128
131
  const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
129
132
  const contactKey = `${kind}\u0000${contactValue.toLowerCase()}`;
130
133
  if (contactKeys.has(contactKey))
@@ -160,7 +163,7 @@ function validateCaseResponsePacketV6(value) {
160
163
  const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
161
164
  for (const candidate of history) {
162
165
  const action = exact(candidate, ['type', 'recipient', 'contactSource', 'state', 'reference', 'outcome', 'createdAt', 'updatedAt'], 'Case-response escalation action');
163
- enumeration(action.type, CASE_ACTION_TYPES, 'Case-response action type');
166
+ enumeration(action.type, PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
164
167
  text(action.recipient, 'Case-response action recipient', 320, true);
165
168
  text(action.contactSource, 'Case-response action source', 120, true);
166
169
  enumeration(action.state, PUBLIC_CASE_ACTION_STATES, 'Case-response action state');
@@ -221,14 +224,15 @@ function validateCurrentActionSummary(value, label) {
221
224
  }
222
225
  return total;
223
226
  }
224
- function validateCaseResponsePacketV7OrV8(value, version) {
227
+ function validateCaseResponsePacketV7ToV9(value, version) {
225
228
  const current = version === CASE_RESPONSE_PACKET_VERSION;
229
+ const modern = version === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION || current;
226
230
  const root = exact(value, [
227
231
  'schema', 'schemaVersion', 'generatedAt', 'reviewRequired', 'submissionPerformed', 'profile', 'case',
228
232
  'incident', 'contacts', 'selectedEvidence', 'contradictions', 'readiness', 'artefactReferences',
229
233
  'authorisation', 'preflight', 'escalationHistory', 'escalationHistoryOmitted',
230
234
  'escalationHistoryLimitations', 'responseLifecycle', 'provenance', 'integrity',
231
- ...(current ? ['recipientRoute', 'actionBinding'] : []),
235
+ ...(modern ? ['recipientRoute', 'actionBinding'] : []),
232
236
  ], `Case-response packet v${version}`);
233
237
  if (root.schemaVersion !== version)
234
238
  fail(`Case-response packet v${version}`);
@@ -236,14 +240,14 @@ function validateCaseResponsePacketV7OrV8(value, version) {
236
240
  if (root.reviewRequired !== true || root.submissionPerformed !== false)
237
241
  fail('Case-response packet review state');
238
242
  const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
239
- const profileId = enumeration(profile.id, RESPONSE_PACKET_PROFILE_IDS, 'Case-response profile id');
243
+ const profileId = enumeration(profile.id, current ? RESPONSE_PACKET_PROFILE_IDS : PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
240
244
  text(profile.label, 'Case-response profile label', 200);
241
245
  text(profile.audience, 'Case-response profile audience', 300);
242
246
  text(profile.subject, 'Case-response profile subject', 500);
243
247
  for (const key of ['checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'])
244
248
  strings(profile[key], `Case-response profile ${key}`, 24, 500);
245
249
  const selectedProfile = RESPONSE_PACKET_PROFILES.find((candidate) => candidate.id === profileId);
246
- const expectedRedactions = current ? selectedProfile?.redactions : legacyProfileRedactions(profileId);
250
+ const expectedRedactions = modern ? selectedProfile?.redactions : legacyProfileRedactions(profileId);
247
251
  if (!selectedProfile
248
252
  || profile.label !== selectedProfile.label
249
253
  || profile.audience !== selectedProfile.audience
@@ -282,8 +286,10 @@ function validateCaseResponsePacketV7OrV8(value, version) {
282
286
  const contacts = array(root.contacts, 'Case-response contacts', MAX_RESPONSE_CONTACTS);
283
287
  const contactKeys = new Set();
284
288
  for (const candidate of contacts) {
285
- const contact = exact(candidate, ['kind', 'contact', 'source', 'observedAt', 'freshness', 'limitations'], 'Case-response contact');
286
- const kind = enumeration(contact.kind, RESPONSE_CONTACT_KINDS, 'Case-response contact kind');
289
+ const contact = exact(candidate, [
290
+ 'kind', 'contact', 'source', 'observedAt', ...(current ? ['reviewAfter'] : []), 'freshness', 'limitations',
291
+ ], 'Case-response contact');
292
+ const kind = enumeration(contact.kind, current ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
287
293
  const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
288
294
  const key = `${kind}\u0000${contactValue.toLowerCase()}`;
289
295
  if (contactKeys.has(key))
@@ -291,13 +297,18 @@ function validateCaseResponsePacketV7OrV8(value, version) {
291
297
  contactKeys.add(key);
292
298
  text(contact.source, 'Case-response contact source', 120);
293
299
  iso(contact.observedAt, 'Case-response contact observedAt', true);
300
+ if (current)
301
+ iso(contact.reviewAfter, 'Case-response contact reviewAfter', true);
294
302
  const freshness = enumeration(contact.freshness, ['current', 'stale', 'unknown'], 'Case-response contact freshness');
295
303
  const routeAge = contact.observedAt === null
296
304
  ? null
297
305
  : Date.parse(root.generatedAt) - Date.parse(contact.observedAt);
306
+ const reviewExpired = current && contact.reviewAfter !== null
307
+ && Date.parse(root.generatedAt) >= Date.parse(contact.reviewAfter);
298
308
  const expectedFreshness = routeAge === null
299
309
  ? 'unknown'
300
- : routeAge < -300_000 || routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000
310
+ : routeAge < -300_000 || reviewExpired
311
+ || (contact.reviewAfter == null && routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000)
301
312
  ? 'stale'
302
313
  : 'current';
303
314
  if (freshness !== expectedFreshness)
@@ -308,24 +319,29 @@ function validateCaseResponsePacketV7OrV8(value, version) {
308
319
  let selectedActionId = null;
309
320
  let lineageActionIds = [];
310
321
  let lineageComplete = true;
311
- if (current) {
322
+ if (modern) {
312
323
  if (root.recipientRoute !== null) {
313
324
  recipientRoute = exact(root.recipientRoute, [
314
- 'actionId', 'kind', 'contact', 'source', 'observedAt', 'freshness', 'limitations',
325
+ 'actionId', 'kind', 'contact', 'source', 'observedAt', ...(current ? ['reviewAfter'] : []), 'freshness', 'limitations',
315
326
  ], 'Case-response recipient route');
316
327
  text(recipientRoute.actionId, 'Case-response recipient action id', 64);
317
- enumeration(recipientRoute.kind, [...RESPONSE_CONTACT_KINDS, 'manual'], 'Case-response recipient kind');
328
+ enumeration(recipientRoute.kind, [...(current ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS), 'manual'], 'Case-response recipient kind');
318
329
  text(recipientRoute.contact, 'Case-response recipient value', MAX_RESPONSE_RECIPIENT_LENGTH);
319
330
  text(recipientRoute.source, 'Case-response recipient source', 120);
320
331
  iso(recipientRoute.observedAt, 'Case-response recipient observedAt', true);
332
+ if (current)
333
+ iso(recipientRoute.reviewAfter, 'Case-response recipient reviewAfter', true);
321
334
  const freshness = enumeration(recipientRoute.freshness, ['current', 'stale', 'unknown'], 'Case-response recipient freshness');
322
335
  const routeAge = recipientRoute.observedAt === null
323
336
  ? null
324
337
  : Date.parse(root.generatedAt) - Date.parse(recipientRoute.observedAt);
338
+ const reviewExpired = current && recipientRoute.reviewAfter !== null
339
+ && Date.parse(root.generatedAt) >= Date.parse(recipientRoute.reviewAfter);
325
340
  const expectedFreshness = routeAge === null
326
341
  ? 'unknown'
327
342
  : routeAge < -MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS
328
- || routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000 ? 'stale' : 'current';
343
+ || reviewExpired
344
+ || (recipientRoute.reviewAfter == null && routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000) ? 'stale' : 'current';
329
345
  if (freshness !== expectedFreshness)
330
346
  fail('Case-response recipient freshness');
331
347
  strings(recipientRoute.limitations, 'Case-response recipient limitations', MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH);
@@ -354,6 +370,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
354
370
  || selectedContact.contact !== recipientRoute.contact
355
371
  || selectedContact.source !== recipientRoute.source
356
372
  || selectedContact.observedAt !== recipientRoute.observedAt
373
+ || (current && selectedContact.reviewAfter !== recipientRoute.reviewAfter)
357
374
  || selectedContact.freshness !== recipientRoute.freshness)
358
375
  fail('Case-response selected contact projection');
359
376
  }
@@ -395,7 +412,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
395
412
  ...(profileId === 'internal_soc' ? [] : ['recipient_route']),
396
413
  ...(['registrar', 'registry', 'network_hosting'].includes(profileId) ? ['infrastructure_responsibility'] : []),
397
414
  ]);
398
- const requiredContact = current
415
+ const requiredContact = modern
399
416
  ? recipientRoute ?? undefined
400
417
  : selectedProfile?.requiredContactKind
401
418
  ? contacts.find((candidate) => candidate.kind === selectedProfile.requiredContactKind)
@@ -490,8 +507,8 @@ function validateCaseResponsePacketV7OrV8(value, version) {
490
507
  || (authorisationStatus === 'draft' && authorisation.confirmedAt !== null))
491
508
  fail('Case-response authorisation');
492
509
  const preflight = exact(root.preflight, ['version', 'status', 'canExport', 'counts', 'checks', 'actionSummary'], 'Case-response preflight');
493
- const preflightIds = current ? CASE_RESPONSE_PREFLIGHT_IDS_V8 : CASE_RESPONSE_PREFLIGHT_IDS_V7;
494
- if (preflight.version !== (current ? 3 : 2))
510
+ const preflightIds = modern ? CASE_RESPONSE_PREFLIGHT_IDS_V8 : CASE_RESPONSE_PREFLIGHT_IDS_V7;
511
+ if (preflight.version !== (modern ? 3 : 2))
495
512
  fail('Case-response preflight');
496
513
  const checks = array(preflight.checks, 'Case-response preflight checks', preflightIds.length, preflightIds.length);
497
514
  const actualCounts = { block: 0, caution: 0, pass: 0 };
@@ -515,8 +532,8 @@ function validateCaseResponsePacketV7OrV8(value, version) {
515
532
  const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
516
533
  const escalationHistoryOmitted = integer(root.escalationHistoryOmitted, 'Case-response omitted actions', 0, MAX_CASE_ACTIONS);
517
534
  const escalationHistoryLimitations = strings(root.escalationHistoryLimitations, 'Case-response history limitations', 4, 600);
518
- if ((!current && history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
519
- || (current && history.length > Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
535
+ if ((!modern && history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
536
+ || (modern && history.length > Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
520
537
  || escalationHistoryOmitted !== actionTotal - history.length
521
538
  || (escalationHistoryOmitted > 0) !== (escalationHistoryLimitations.length > 0))
522
539
  fail('Case-response action-history bounds');
@@ -528,18 +545,21 @@ function validateCaseResponsePacketV7OrV8(value, version) {
528
545
  'actionId', 'type', 'recipient', 'contactSource', 'state', 'reference',
529
546
  'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted',
530
547
  'historyLimitations', 'transitions', 'createdAt', 'updatedAt',
531
- ...(current ? ['routeObservedAt'] : []),
548
+ ...(modern ? ['routeObservedAt'] : []),
549
+ ...(current ? ['routeReviewAfter'] : []),
532
550
  ], 'Case-response escalation action');
533
551
  const actionId = text(action.actionId, 'Case-response action id', 64);
534
552
  if (actionIds.has(actionId))
535
553
  fail('Case-response action identity');
536
554
  actionIds.add(actionId);
537
555
  actionIdOrder.push(actionId);
538
- enumeration(action.type, CASE_ACTION_TYPES, 'Case-response action type');
556
+ enumeration(action.type, current ? CASE_ACTION_TYPES : PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
539
557
  text(action.recipient, 'Case-response action recipient', 320);
540
558
  text(action.contactSource, 'Case-response action source', 120);
541
- if (current)
559
+ if (modern)
542
560
  iso(action.routeObservedAt, 'Case-response action route observedAt', true);
561
+ if (current)
562
+ iso(action.routeReviewAfter, 'Case-response action route reviewAfter', true);
543
563
  enumeration(action.state, CASE_ACTION_STATES, 'Case-response action state');
544
564
  optionalText(action.reference, 'Case-response action reference', 500);
545
565
  if (action.providerOutcome !== null)
@@ -638,7 +658,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
638
658
  iso(action.createdAt, 'Case-response action createdAt');
639
659
  iso(action.updatedAt, 'Case-response action updatedAt');
640
660
  }
641
- if (current) {
661
+ if (modern) {
642
662
  if (actionIds.size !== lineageActionIds.length
643
663
  || actionIdOrder.some((id, index) => lineageActionIds[index] !== id))
644
664
  fail('Case-response action lineage');
@@ -670,11 +690,11 @@ function validateCaseResponsePacketV7OrV8(value, version) {
670
690
  const latestProviderEvents = latestProviderTime === null
671
691
  ? []
672
692
  : providerEvents.filter((event) => event.occurredAt === latestProviderTime);
673
- const expectedLatestProvider = (current ? lineageComplete : escalationHistoryOmitted === 0)
693
+ const expectedLatestProvider = (modern ? lineageComplete : escalationHistoryOmitted === 0)
674
694
  && latestProviderEvents.length === 1 && latestProviderEvents[0].applied
675
695
  ? latestProviderEvents[0]
676
696
  : null;
677
- const expectedProviderState = (current ? !lineageComplete : escalationHistoryOmitted > 0)
697
+ const expectedProviderState = (modern ? !lineageComplete : escalationHistoryOmitted > 0)
678
698
  ? 'ambiguous'
679
699
  : providerEvents.length === 0 ? 'missing' : expectedLatestProvider ? 'available' : 'ambiguous';
680
700
  if (providerOutcomeState !== expectedProviderState
@@ -707,7 +727,9 @@ function validateCaseResponsePacketV7OrV8(value, version) {
707
727
  strings(lifecycle.limitations, 'Case-response lifecycle limitations', 8, 600);
708
728
  const reviewedInputs = {
709
729
  contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
710
- version: current ? CASE_RESPONSE_REVIEW_INPUTS_VERSION : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION,
730
+ version: current
731
+ ? CASE_RESPONSE_REVIEW_INPUTS_VERSION
732
+ : modern ? LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION,
711
733
  profile: {
712
734
  id: profile.id,
713
735
  label: profile.label,
@@ -721,7 +743,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
721
743
  case: root.case,
722
744
  incident: root.incident,
723
745
  contacts: root.contacts,
724
- ...(current ? { recipientRoute: root.recipientRoute, actionBinding: root.actionBinding } : {}),
746
+ ...(modern ? { recipientRoute: root.recipientRoute, actionBinding: root.actionBinding } : {}),
725
747
  selectedEvidence: root.selectedEvidence,
726
748
  contradictions: root.contradictions,
727
749
  readiness: root.readiness,
@@ -759,7 +781,10 @@ export function validateCaseResponsePacket(value) {
759
781
  if (value.schemaVersion === PUBLIC_CASE_RESPONSE_PACKET_VERSION)
760
782
  return validateCaseResponsePacketV6(value);
761
783
  if (value.schemaVersion === PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION) {
762
- return validateCaseResponsePacketV7OrV8(value, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION);
784
+ return validateCaseResponsePacketV7ToV9(value, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION);
785
+ }
786
+ if (value.schemaVersion === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION) {
787
+ return validateCaseResponsePacketV7ToV9(value, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION);
763
788
  }
764
789
  if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion < CASE_RESPONSE_PACKET_VERSION) {
765
790
  throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is not part of the public compatibility boundary; no data was changed.`);
@@ -767,5 +792,5 @@ export function validateCaseResponsePacket(value) {
767
792
  if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion > CASE_RESPONSE_PACKET_VERSION) {
768
793
  throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is newer than the supported version ${CASE_RESPONSE_PACKET_VERSION}; no data was changed.`);
769
794
  }
770
- validateCaseResponsePacketV7OrV8(value, CASE_RESPONSE_PACKET_VERSION);
795
+ validateCaseResponsePacketV7ToV9(value, CASE_RESPONSE_PACKET_VERSION);
771
796
  }
@@ -5,7 +5,7 @@ import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, } from "../.
5
5
  import { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_TARGET_TYPES, LOOKUP_CLAIM_PASSPORT_VERSION, MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS, } from "../../packages/investigation/lookup-claim-passport.mjs";
6
6
  import { LOOKUP_CLAIM_IDS, LOOKUP_CLAIM_READINESS_VERSION, LOOKUP_CLAIM_REQUIREMENT_IDS, } from "../../packages/investigation/lookup-claim-readiness.mjs";
7
7
  import { normalizeDomainControlManifestDocument } from "../../packages/evidence/domain-control-runtime.mjs";
8
- import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_VERSION } from "../../lib/domain-change-packet.mjs";
8
+ import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_VERSION, } from "../../lib/domain-change-packet.mjs";
9
9
  import { DOMAIN_CONTROL_MANIFEST_SCHEMA } from "../../packages/contracts/domain-control-manifest.mjs";
10
10
  import { INVESTIGATION_MANIFEST_SCHEMA, INVESTIGATION_MANIFEST_VERSION, MAX_INVESTIGATION_MANIFEST_ARTIFACTS, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, } from "../investigation-manifest.mjs";
11
11
  import { array, boolean, domain, digest, enumeration, exact, fail, integer, iso, optionalText, sameValues, strings, text, validateIntegrity, } from "./structure-primitives.mjs";
@@ -501,18 +501,36 @@ function expectedDomainChangeSummary(before, after) {
501
501
  const beforeRows = rows(before);
502
502
  const afterRows = rows(after);
503
503
  const keys = [...new Set([...beforeRows.keys(), ...afterRows.keys()])].sort();
504
- return keys.slice(0, 500).flatMap((key) => {
505
- const left = beforeRows.get(key);
506
- const right = afterRows.get(key);
507
- const values = (row) => [...new Set((row?.observations ?? [])
508
- .flatMap((observation) => observation.values))].sort();
509
- const beforeValues = values(left);
510
- const afterValues = values(right);
511
- if (sameValues(beforeValues, afterValues))
512
- return [];
504
+ const collectionComplete = (review) => {
505
+ const row = (review.authoritativeRecordMatrix[0]);
506
+ const observations = row?.observations ?? [];
507
+ return observations.length >= 2 && observations.every((item) => item.state === 'observed');
508
+ };
509
+ const rowEvidence = (review, row) => {
510
+ if (!row)
511
+ return { state: collectionComplete(review) ? 'complete' : 'unavailable', values: [] };
512
+ const observations = row.observations;
513
+ if (observations.some((item) => item.state !== 'observed'))
514
+ return { state: 'partial', values: [] };
515
+ if (row.state === 'different')
516
+ return { state: 'inconsistent', values: [] };
517
+ if (row.state !== 'aligned')
518
+ return { state: 'insufficient', values: [] };
519
+ return { state: 'complete', values: [...new Set(observations.flatMap((item) => item.values))].sort() };
520
+ };
521
+ const changed = [];
522
+ for (const key of keys.slice(0, 500)) {
523
+ const left = rowEvidence(before, beforeRows.get(key));
524
+ const right = rowEvidence(after, afterRows.get(key));
513
525
  const [owner, type] = key.split('\u0000');
514
- return [{ owner: owner ?? '', type: type ?? '', beforeValues, afterValues }];
515
- });
526
+ if (left.state !== 'complete' || right.state !== 'complete') {
527
+ continue;
528
+ }
529
+ else if (!sameValues(left.values, right.values)) {
530
+ changed.push({ owner: owner ?? '', type: type ?? '', beforeValues: left.values, afterValues: right.values });
531
+ }
532
+ }
533
+ return changed;
516
534
  }
517
535
  function validateDomainChangePacket(value) {
518
536
  const root = exact(value, ['schema', 'version', 'generatedAt', 'domain', 'reference', 'state', 'gate', 'summary', 'evidence', 'limitations', 'integrity'], 'Domain change packet');
@@ -54,6 +54,24 @@ function normalizeCompactResult(value) {
54
54
  return null;
55
55
  return result;
56
56
  }
57
+ function normalizeCollectionContext(value) {
58
+ if (value === undefined)
59
+ return undefined;
60
+ const context = recordOrNull(value);
61
+ if (!context || Object.keys(context).some((key) => !['dnsResolver', 'resolverServers'].includes(key)))
62
+ return null;
63
+ const resolverServers = context.resolverServers;
64
+ if (!Array.isArray(resolverServers) || resolverServers.length > 3
65
+ || resolverServers.some((server) => typeof server !== 'string' || !server || server.length > 45))
66
+ return null;
67
+ if (context.dnsResolver === 'system_default' && resolverServers.length === 0) {
68
+ return { dnsResolver: 'system_default', resolverServers: [] };
69
+ }
70
+ if (context.dnsResolver === 'analyst_selected' && resolverServers.length > 0) {
71
+ return { dnsResolver: 'analyst_selected', resolverServers: [...resolverServers] };
72
+ }
73
+ return null;
74
+ }
57
75
  function normalizeCheckpointResult(value, queries, classifyQuery) {
58
76
  const item = recordOrNull(value);
59
77
  const index = item?.index;
@@ -67,10 +85,16 @@ function normalizeCheckpointResult(value, queries, classifyQuery) {
67
85
  : normalizedTimestamp(item.observedAt) ?? undefined;
68
86
  if (observedAt === undefined)
69
87
  return null;
88
+ const collectionContext = normalizeCollectionContext(item.collectionContext);
89
+ if (collectionContext === null)
90
+ return null;
70
91
  if (item.ok === false) {
71
92
  if (typeof item.error !== 'string' || !item.error || item.error.length > 300)
72
93
  return null;
73
- return { index: Number(index), query, ok: false, error: item.error, observedAt };
94
+ return {
95
+ index: Number(index), query, ok: false, error: item.error, observedAt,
96
+ ...(collectionContext ? { collectionContext } : {}),
97
+ };
74
98
  }
75
99
  const result = normalizeCompactResult(item.result);
76
100
  if (item.ok !== true || !result)
@@ -79,7 +103,10 @@ function normalizeCheckpointResult(value, queries, classifyQuery) {
79
103
  const storedClassified = recordOrNull(item.classified);
80
104
  if (storedClassified?.type !== classified.type || storedClassified.value !== classified.value)
81
105
  return null;
82
- return { index: Number(index), query, ok: true, classified, result, observedAt };
106
+ return {
107
+ index: Number(index), query, ok: true, classified, result, observedAt,
108
+ ...(collectionContext ? { collectionContext } : {}),
109
+ };
83
110
  }
84
111
  function parseBulkCheckpoint(text, options) {
85
112
  if (typeof text !== 'string' || Buffer.byteLength(text, 'utf8') > MAX_BULK_CHECKPOINT_BYTES) {
@@ -214,8 +241,10 @@ async function createBulkCheckpointWriter(options) {
214
241
  }
215
242
  }
216
243
  function scheduleWrite() {
244
+ if (writeFailure)
245
+ return;
217
246
  dirty = true;
218
- if (activeWrite || writeFailure)
247
+ if (activeWrite)
219
248
  return;
220
249
  const pending = drainWrites();
221
250
  activeWrite = pending;
@@ -250,6 +279,8 @@ async function createBulkCheckpointWriter(options) {
250
279
  },
251
280
  async flush() {
252
281
  while (activeWrite || dirty) {
282
+ if (writeFailure && !activeWrite)
283
+ break;
253
284
  if (!activeWrite)
254
285
  scheduleWrite();
255
286
  if (activeWrite)
package/cli/case-pack.mjs CHANGED
@@ -4,42 +4,16 @@ import { canonicalArtifactJson, canonicalArtifactJsonV2, SORTED_JSON_V2, } from
4
4
  import { buildCaseReport } from "../packages/cases/case-report.mjs";
5
5
  import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
6
6
  import { assertBoundedJsonStructure, scanBoundedJson } from "../lib/bounded-json.mjs";
7
- import { assertCaseBrandProfileIds, normalizeDomain, normalizeCaseStore, safeId, } from "../packages/cases/case-model.mjs";
7
+ import { assertCaseBrandProfileIds, CASE_AUDIENCE_SENSITIVE_FIELD_NAMES, CASE_RECORD_FIELD_NAMES, caseAudienceExclusions, normalizeDomain, normalizeCaseStore, projectCaseForAudience, safeId, } from "../packages/cases/case-model.mjs";
8
8
  import { CASE_REPORT_SCHEMA, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS as INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS as PACKET_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS as PACKET_KEYS, CLI_CASE_PACK_REPORT_KEYS as REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS as ROOT_KEYS, MAX_CASE_IMPORT_BYTES, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, PUBLIC_CASE_SCHEMA_VERSION, caseReportVersionMatchesCase, } from "../packages/contracts/case-portability.mjs";
9
9
  import { CliUsageError } from "./errors.mjs";
10
10
  export { CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, };
11
11
  const MAX_CASE_PACK_REFERENCE_SCAN_DEPTH = 64;
12
- const INTERNAL_EXCLUSIONS = Object.freeze(['Raw upstream payloads and credentials are outside the case schema.']);
13
- const TRUSTED_EXCLUSIONS = Object.freeze(['Case notes', 'Recipient values', 'Manual trail targets', 'Raw upstream payloads and credentials']);
14
- const PUBLIC_EXCLUSIONS = Object.freeze([
15
- 'Case notes',
16
- 'Brand Profile references',
17
- 'Actions and recipient values',
18
- 'Analyst assertions',
19
- 'Investigation branches',
20
- 'Manual trail targets',
21
- 'Raw upstream payloads and credentials',
22
- 'Independent observed-effect reviews and closure history',
23
- ]);
24
- const PUBLIC_V12_EXCLUSIONS = Object.freeze(PUBLIC_EXCLUSIONS.slice(0, -1));
25
- const SENSITIVE_CASE_PACK_FIELDS = Object.freeze([
26
- 'brandProfileIds', 'notes', 'actions', 'assertions', 'manualTrail', 'observedEffects',
27
- 'closures', 'branches', 'recipient', 'target',
28
- ]);
29
- const PUBLIC_OBSERVED_EFFECT_LIMITATION = 'Independent observed-effect review records were excluded from this public Case pack.';
30
- const PUBLIC_CLOSURE_LIMITATION = 'Deliberate closure records were excluded from this public Case pack.';
31
12
  function record(value) {
32
13
  return value && typeof value === 'object' && !Array.isArray(value)
33
14
  ? value
34
15
  : null;
35
16
  }
36
- function expectedExclusions(audience, caseVersion = CASE_SCHEMA_VERSION) {
37
- if (audience === 'internal')
38
- return INTERNAL_EXCLUSIONS;
39
- if (audience === 'trusted')
40
- return TRUSTED_EXCLUSIONS;
41
- return caseVersion === PUBLIC_CASE_SCHEMA_VERSION ? PUBLIC_V12_EXCLUSIONS : PUBLIC_EXCLUSIONS;
42
- }
43
17
  function stringListsMatch(value, expected) {
44
18
  return Array.isArray(value)
45
19
  && value.length === expected.length
@@ -105,7 +79,7 @@ function assertSensitiveFieldPlacement(root, serializedLength) {
105
79
  const entry = stack.pop();
106
80
  if (!entry || !entry.value || typeof entry.value !== 'object')
107
81
  continue;
108
- for (const key of SENSITIVE_CASE_PACK_FIELDS) {
82
+ for (const key of CASE_AUDIENCE_SENSITIVE_FIELD_NAMES) {
109
83
  if (Object.hasOwn(entry.value, key) && !allowedSensitiveFieldPath(key, entry.path)) {
110
84
  throw new TypeError('The CLI case pack contains audience-sensitive data outside its versioned case or report fields.');
111
85
  }
@@ -170,42 +144,22 @@ function assertCanonicalCaseIdentities(cases, label) {
170
144
  }
171
145
  }
172
146
  function assertCurrentCaseProjection(rawCases, normalised, label) {
173
- if (rawCases.length !== normalised.length
174
- || rawCases.some((item, index) => !canonicalValuesMatch(item, normalised[index]))) {
147
+ const requiredFields = new Set(CASE_RECORD_FIELD_NAMES);
148
+ if (rawCases.length !== normalised.length || rawCases.some((item, index) => {
149
+ const raw = record(item);
150
+ return !raw
151
+ || Object.keys(raw).length !== requiredFields.size
152
+ || Object.keys(raw).some((key) => !requiredFields.has(key))
153
+ || !canonicalValuesMatch(raw, normalised[index]);
154
+ })) {
175
155
  throw new TypeError(`${label} contains a schema ${CASE_SCHEMA_VERSION} Case that would be repaired, truncated, or otherwise changed during normalisation.`);
176
156
  }
177
157
  }
178
- function publicObservedEffectHistory(preV13HistoryUnavailable) {
179
- return {
180
- reviews: [],
181
- omitted: 0,
182
- preV13HistoryUnavailable,
183
- limitations: [
184
- ...(preV13HistoryUnavailable
185
- ? ['Migrated from a pre-v13 Case; earlier independent observed-effect review history is unavailable.']
186
- : []),
187
- 'Observed-effect reviews are independent point-in-time records; provider workflow events do not create or replace them.',
188
- PUBLIC_OBSERVED_EFFECT_LIMITATION,
189
- ].sort(),
190
- };
191
- }
192
- function publicClosureHistory(preV13HistoryUnavailable) {
193
- return {
194
- records: [],
195
- omitted: 0,
196
- preV13HistoryUnavailable,
197
- limitations: [
198
- ...(preV13HistoryUnavailable
199
- ? ['Migrated from a pre-v13 Case; earlier deliberate closure history is unavailable.']
200
- : []),
201
- 'Closure records are deliberate analyst actions and do not establish absence, safety, provider performance, or legal sufficiency.',
202
- PUBLIC_CLOSURE_LIMITATION,
203
- ].sort(),
204
- };
205
- }
206
- function assertAudienceFields(value, audience) {
158
+ /** Historical packs keep their immutable wire shape, so only their retained audience guarantees are checked here. */
159
+ function assertLegacyAudienceFields(value, normalised, audience) {
207
160
  if (audience === 'internal')
208
161
  return;
162
+ const expected = projectCaseForAudience(normalised, audience);
209
163
  if (Object.hasOwn(value, 'notes') && (!Array.isArray(value.notes) || value.notes.length !== 0)) {
210
164
  throw new TypeError('The CLI case pack contains Case notes excluded by its audience.');
211
165
  }
@@ -218,11 +172,8 @@ function assertAudienceFields(value, audience) {
218
172
  for (const [field, list] of [['observedEffects', 'reviews'], ['closures', 'records']]) {
219
173
  if (Object.hasOwn(value, field)) {
220
174
  const history = record(value[field]);
221
- const expected = field === 'observedEffects'
222
- ? publicObservedEffectHistory(history?.preV13HistoryUnavailable === true)
223
- : publicClosureHistory(history?.preV13HistoryUnavailable === true);
224
175
  if (!history || !Array.isArray(history[list]) || history[list].length !== 0
225
- || !canonicalValuesMatch(history, expected)) {
176
+ || !canonicalValuesMatch(history, expected[field])) {
226
177
  throw new TypeError(`The public CLI case pack contains ${field} excluded by its audience.`);
227
178
  }
228
179
  }
@@ -245,6 +196,20 @@ function assertAudienceFields(value, audience) {
245
196
  }
246
197
  }
247
198
  }
199
+ function assertCurrentAudienceProjection(value, normalised, audience) {
200
+ const expected = projectCaseForAudience(normalised, audience);
201
+ for (const field of CASE_RECORD_FIELD_NAMES) {
202
+ if (!canonicalValuesMatch(value[field], expected[field])) {
203
+ throw new TypeError(`The CLI case pack contains ${field} excluded by its audience or transformed incorrectly.`);
204
+ }
205
+ }
206
+ }
207
+ function assertAudienceProjection(value, normalised, audience, currentSchema) {
208
+ if (currentSchema)
209
+ assertCurrentAudienceProjection(value, normalised, audience);
210
+ else
211
+ assertLegacyAudienceFields(value, normalised, audience);
212
+ }
248
213
  function reportApplicationVersion(report) {
249
214
  return record(report.application)?.version;
250
215
  }
@@ -261,28 +226,6 @@ function assertCurrentReportProjection(report, rawCase) {
261
226
  throw new TypeError('The CLI case pack contains an invalid or mismatched Case report projection.');
262
227
  }
263
228
  }
264
- function redactedCase(record, audience) {
265
- if (audience === 'internal')
266
- return structuredClone(record);
267
- return {
268
- ...structuredClone(record),
269
- status: audience === 'public' && record.status === 'resolved' && record.closures.records.length
270
- ? 'reviewing'
271
- : record.status,
272
- brandProfileIds: audience === 'public' ? [] : [...record.brandProfileIds],
273
- notes: [],
274
- actions: audience === 'public' ? [] : record.actions.map((item) => ({ ...item, recipient: '[redacted]' })),
275
- manualTrail: record.manualTrail.map((item) => ({ ...item, target: null })),
276
- assertions: audience === 'public' ? [] : structuredClone(record.assertions),
277
- observedEffects: audience === 'public'
278
- ? publicObservedEffectHistory(record.observedEffects.preV13HistoryUnavailable)
279
- : structuredClone(record.observedEffects),
280
- closures: audience === 'public'
281
- ? publicClosureHistory(record.closures.preV13HistoryUnavailable)
282
- : structuredClone(record.closures),
283
- branches: audience === 'public' ? [] : structuredClone(record.branches ?? []),
284
- };
285
- }
286
229
  export function buildCliCasePack(input, options, generatedAt = new Date().toISOString()) {
287
230
  if (!options.reviewed)
288
231
  throw new CliUsageError('case-pack requires --reviewed after the audience-specific output has been checked.');
@@ -337,7 +280,7 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
337
280
  catch (cause) {
338
281
  throw new CliUsageError(cause instanceof Error ? cause.message : `Case-pack schema ${CASE_SCHEMA_VERSION} input is not exact.`);
339
282
  }
340
- const cases = normalised.map((item) => redactedCase(item, options.audience));
283
+ const cases = normalised.map((item) => projectCaseForAudience(item, options.audience));
341
284
  const brandProfileReferencesOmitted = options.audience === 'public'
342
285
  ? normalised.reduce((count, item) => count + item.brandProfileIds.length, 0)
343
286
  : 0;
@@ -346,7 +289,7 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
346
289
  includeNotes: false,
347
290
  generatedAt,
348
291
  }).json);
349
- const exclusions = expectedExclusions(options.audience);
292
+ const exclusions = caseAudienceExclusions(options.audience, CASE_SCHEMA_VERSION);
350
293
  const packet = Object.freeze({
351
294
  schema: CLI_CASE_PACK_SCHEMA,
352
295
  version: CLI_CASE_PACK_VERSION,
@@ -436,7 +379,7 @@ export function verifyCliCasePack(input) {
436
379
  const audience = packet.audience;
437
380
  if (!Number.isSafeInteger(redactionManifest.sourceCaseCount)
438
381
  || redactionManifest.sourceCaseCount !== root.cases.length
439
- || !stringListsMatch(redactionManifest.excluded, expectedExclusions(audience, root.version))) {
382
+ || !stringListsMatch(redactionManifest.excluded, caseAudienceExclusions(audience, root.version))) {
440
383
  throw new TypeError('The CLI case pack has an invalid audience redaction manifest.');
441
384
  }
442
385
  assertCanonicalCaseIdentities(root.cases, 'The CLI case pack');
@@ -444,7 +387,8 @@ export function verifyCliCasePack(input) {
444
387
  if (normalised.length !== root.cases.length) {
445
388
  throw new TypeError('The CLI case pack contains an invalid case collection.');
446
389
  }
447
- if (root.version === CASE_SCHEMA_VERSION)
390
+ const currentCaseSchema = root.version === CASE_SCHEMA_VERSION;
391
+ if (currentCaseSchema)
448
392
  assertCurrentCaseProjection(root.cases, normalised, 'The CLI case pack');
449
393
  const normalisedByDomain = new Map(normalised.map((item) => [item.domain, item]));
450
394
  const caseReferenceLists = [];
@@ -464,7 +408,10 @@ export function verifyCliCasePack(input) {
464
408
  || !normalisedByDomain.has(rawCase.domain)) {
465
409
  throw new TypeError('The CLI case pack contains an invalid or mismatched Case report.');
466
410
  }
467
- assertAudienceFields(rawCase, audience);
411
+ const normalisedCase = normalisedByDomain.get(rawCase.domain);
412
+ if (!normalisedCase)
413
+ throw new TypeError('The CLI case pack contains an invalid Case projection.');
414
+ assertAudienceProjection(rawCase, normalisedCase, audience, currentCaseSchema);
468
415
  try {
469
416
  if (!Object.hasOwn(rawCase, 'brandProfileIds') || !Object.hasOwn(reportCase, 'brandProfileIds'))
470
417
  throw new Error('missing');
@@ -474,8 +421,8 @@ export function verifyCliCasePack(input) {
474
421
  catch {
475
422
  throw new TypeError('The CLI case pack contains invalid Brand Profile references.');
476
423
  }
477
- if (root.version === CASE_SCHEMA_VERSION)
478
- assertCurrentReportProjection(report, rawCase);
424
+ if (currentCaseSchema)
425
+ assertCurrentReportProjection(report, normalisedCase);
479
426
  }
480
427
  const omitted = redactionManifest.brandProfileReferencesOmitted;
481
428
  if (!Object.hasOwn(redactionManifest, 'brandProfileReferencesOmitted')