@slicedearth/whoisleuth-cli 1.47.4 → 2.0.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 (235) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/provider-policy-admission.mjs +3 -1
  100. package/lib/rdap-bootstrap.mjs +76 -16
  101. package/lib/rdap-client.mjs +20 -12
  102. package/lib/rdap-normalization.mjs +306 -75
  103. package/lib/rdap-transport.mjs +2 -2
  104. package/lib/rdap-validation.mjs +65 -14
  105. package/lib/rdap.mjs +2 -2
  106. package/lib/registrable-domain.mjs +36 -0
  107. package/lib/registry-insights.mjs +7 -0
  108. package/lib/response-policy.mjs +27 -4
  109. package/lib/risk-calibration-summary.mjs +35 -33
  110. package/lib/risk-scoring.mjs +4 -3
  111. package/lib/safe-fetch.mjs +62 -40
  112. package/lib/security-txt.mjs +1 -1
  113. package/lib/smtp-transport-review.mjs +8 -5
  114. package/lib/sslbl-intelligence.mjs +6 -8
  115. package/lib/static-html-analysis.mjs +301 -4
  116. package/lib/structured-data-identity.mjs +51 -19
  117. package/lib/threat-intelligence-fixtures.mjs +2 -4
  118. package/lib/threat-intelligence-manifests.mjs +2 -7
  119. package/lib/threat-intelligence-runtime.mjs +3 -5
  120. package/lib/threat-intelligence-types.mjs +3 -0
  121. package/lib/threatfox-intelligence.mjs +7 -3
  122. package/lib/tls-intelligence.mjs +4 -4
  123. package/lib/urlhaus-intelligence.mjs +4 -2
  124. package/lib/urlscan-intelligence.mjs +6 -5
  125. package/lib/website-security-posture.mjs +1 -1
  126. package/lib/website-technology.mjs +1 -1
  127. package/lib/whois-authority.mjs +43 -8
  128. package/lib/whois-chain.mjs +25 -3
  129. package/lib/whois-normalization.mjs +3 -1
  130. package/lib/whois-transport.mjs +12 -3
  131. package/lib/zone-intent-review.mjs +1 -1
  132. package/package.json +12 -4
  133. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  134. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  135. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  136. package/packages/cases/case-migration-model.mjs +476 -0
  137. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  138. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  139. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  140. package/packages/cases/case-record-model.mjs +9 -0
  141. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  142. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  143. package/packages/cases/case-response-model.mjs +1604 -0
  144. package/packages/cases/case-response-packet.mjs +1312 -0
  145. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  146. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  147. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  148. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  149. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  150. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  151. package/packages/comparison/favicon-similarity.mjs +78 -0
  152. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  153. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  154. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  155. package/packages/contracts/analyst-review-state.mjs +50 -0
  156. package/packages/contracts/case-portability.mjs +1310 -0
  157. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  158. package/packages/contracts/cli-lookup.mjs +394 -0
  159. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  160. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  161. package/packages/contracts/domain-control-monitor.mjs +310 -0
  162. package/packages/contracts/domain-control-review.mjs +828 -0
  163. package/packages/contracts/domain-name.mjs +4 -0
  164. package/packages/contracts/external-observation-interchange.mjs +224 -0
  165. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  166. package/packages/contracts/investigation-portability.mjs +331 -0
  167. package/packages/contracts/offline-comparison.mjs +160 -0
  168. package/packages/contracts/risk-calibration.mjs +1288 -0
  169. package/packages/contracts/schema-compatibility.mjs +71 -0
  170. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  171. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  172. package/packages/contracts/workspace-portability.mjs +1347 -0
  173. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  174. package/packages/evidence/decision-fact.mjs +735 -0
  175. package/packages/evidence/domain-control-runtime.mjs +547 -0
  176. package/packages/evidence/domain-name.mjs +41 -0
  177. package/packages/evidence/observation.mjs +384 -0
  178. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  179. package/packages/interchange/retained-text.mjs +8 -0
  180. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  181. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  182. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  183. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  184. package/packages/investigation/bulk-review-policy.mjs +1 -0
  185. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  186. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  187. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  188. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  189. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  190. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  191. package/packages/monitoring/analyst-review-state.mjs +680 -0
  192. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
  193. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  194. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  195. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  196. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  197. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  198. package/packages/workspace/hostile-input.mjs +172 -0
  199. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  200. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  201. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  202. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  203. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  204. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  205. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  206. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  207. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  208. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  209. package/third-party-notices.txt +14 -14
  210. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  211. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  212. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  213. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  214. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  215. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  216. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  217. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  218. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  219. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  220. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  221. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  222. package/frontend/src/lib/analysis/scoring.js +0 -55
  223. package/frontend/src/lib/analysis/utils.js +0 -255
  224. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  225. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  226. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  227. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  228. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  229. package/lib/bounded-json-response.mjs +0 -170
  230. package/lib/lookup-response-contract.mjs +0 -1034
  231. package/lib/observation.mjs +0 -115
  232. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  233. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -0,0 +1,476 @@
1
+ import { CASE_DISPOSITIONS, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, CASE_SOURCES, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_SOURCE, DEFAULT_STATUS, MAX_CASES, MAX_NOTES_PER_CASE, deterministicId, caseTimestampOrNull, normalizeCase, normalizeDomain, normalizeEvidenceHistory, normalizeNotes, normalizeReviewReasonCode, normalizeTags, objectRecord, safeId, } from "./case-record-model.mjs";
2
+ import { CASE_REPORT_SCHEMA, caseReportVersionMatchesCase, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, } from "../contracts/case-portability.mjs";
3
+ import { canonicalArtifactJsonV2 } from "../evidence/artifact-integrity.mjs";
4
+ import { assertBoundedJsonStructure } from "../../lib/bounded-json.mjs";
5
+ import { inspectCaseBrandProfileIds, unionCaseBrandProfileIds, } from "./case-brand-profile-references.mjs";
6
+ import { caseInvestigationBranchReferences, mergeCaseInvestigationBranches, normalizeCaseInvestigationBranches, } from "./case-investigation-branch-model.mjs";
7
+ import { buildCaseClosureLinkContext, mergeCaseActions, mergeCaseAssertions, mergeCaseClosureHistories, mergeCaseDecisions, mergeCaseEvidencePins, mergeCaseManualTrail, mergeCaseObservedEffectHistories, mergeCaseSightings, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseClosureHistory, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseObservedEffectHistory, normalizeCaseSightings, } from "./case-response-model.mjs";
8
+ const STATUS_VALUES = new Set(CASE_STATUSES.map((item) => item.value));
9
+ const DISPOSITION_VALUES = new Set(CASE_DISPOSITIONS.map((item) => item.value));
10
+ const SOURCE_VALUES = new Set(CASE_SOURCES.map((item) => item.value));
11
+ export const MAX_CASE_INPUT_RECORDS = 2_000;
12
+ function compareCodeUnits(left, right) {
13
+ return left < right ? -1 : left > right ? 1 : 0;
14
+ }
15
+ const CASE_INPUT_JSON_LIMITS = Object.freeze({
16
+ maximumDepth: 48,
17
+ maximumKeys: 500_000,
18
+ maximumValues: 1_000_000,
19
+ maximumContainerItems: 10_000,
20
+ });
21
+ function assignUniqueIds(cases) {
22
+ const used = new Set();
23
+ for (const record of [...cases].sort((a, b) => compareCodeUnits(a.domain, b.domain))) {
24
+ let id = safeId(record.id) || deterministicId(record.domain);
25
+ if (used.has(id)) {
26
+ const base = deterministicId(record.domain);
27
+ id = base;
28
+ let suffix = 2;
29
+ while (used.has(id))
30
+ id = `${base}-${suffix++}`;
31
+ }
32
+ used.add(id);
33
+ record.id = id;
34
+ }
35
+ }
36
+ /**
37
+ * Recovers a clean, bounded store from the current versioned envelope or an
38
+ * internal bare array, drops malformed records, keeps a single
39
+ * case per domain (most recently updated wins), caps to MAX_CASES by recency,
40
+ * and guarantees globally unique safe ids. Retired, future, accessor-bearing,
41
+ * sparse, or otherwise non-JSON envelopes fail before normalisation.
42
+ * @param {unknown} raw
43
+ * @returns {CaseStore}
44
+ */
45
+ export function normalizeCaseStore(raw) {
46
+ assertBoundedJsonStructure(raw, 'Case store', CASE_INPUT_JSON_LIMITS);
47
+ const fallback = new Date(0).toISOString();
48
+ const sourceVersion = parseStoreVersion(raw);
49
+ if (!Array.isArray(raw)) {
50
+ if (sourceVersion === null) {
51
+ throw new TypeError(`Unversioned Case stores are retired. Export or reset them explicitly before using Case schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
52
+ }
53
+ if (!CASE_IMPORT_VERSIONS.includes(sourceVersion) && sourceVersion < CASE_SCHEMA_VERSION) {
54
+ throw new TypeError(`Case schema ${sourceVersion} is not part of the public compatibility boundary. Only public schema 12 migrates to schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
55
+ }
56
+ if (sourceVersion > CASE_SCHEMA_VERSION) {
57
+ throw new TypeError(`Case schema ${sourceVersion} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
58
+ }
59
+ if (sourceVersion === CASE_SCHEMA_VERSION) {
60
+ assertCurrentEvidenceHostnameShape(raw);
61
+ }
62
+ }
63
+ const byDomain = new Map();
64
+ for (const item of boundedCaseList(raw).items) {
65
+ const normalized = normalizeCase(item, undefined, fallback, sourceVersion ?? CASE_SCHEMA_VERSION);
66
+ if (!normalized)
67
+ continue;
68
+ const existing = byDomain.get(normalized.domain);
69
+ if (!existing || Date.parse(normalized.updatedAt) >= Date.parse(existing.updatedAt)) {
70
+ byDomain.set(normalized.domain, normalized);
71
+ }
72
+ }
73
+ const cases = [...byDomain.values()]
74
+ .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
75
+ .slice(0, MAX_CASES);
76
+ assignUniqueIds(cases);
77
+ return { version: CASE_SCHEMA_VERSION, cases };
78
+ }
79
+ function assertCurrentEvidenceHostnameShape(raw) {
80
+ for (const item of boundedCaseList(raw).items) {
81
+ const evidenceHistory = objectRecord(item).evidenceHistory;
82
+ if (!Array.isArray(evidenceHistory))
83
+ continue;
84
+ for (const snapshot of evidenceHistory) {
85
+ const record = objectRecord(snapshot);
86
+ if (Object.keys(record).length > 0 && !Object.hasOwn(record, 'inputHostname')) {
87
+ throw new TypeError(`Case schema ${CASE_SCHEMA_VERSION} evidence snapshots must declare their exact submitted hostname, including null; unreleased local Case checkpoints are not interpreted as the v2 format and no data was changed.`);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ /**
93
+ * The schema version declared by a stored/parsed value, or null. The storage
94
+ * wrapper uses this to refuse overwriting data written by a newer, unsupported
95
+ * version instead of silently downgrading it.
96
+ * @param {unknown} raw
97
+ * @returns {number | null}
98
+ */
99
+ export function parseStoreVersion(raw) {
100
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
101
+ return null;
102
+ let descriptor;
103
+ try {
104
+ descriptor = Object.getOwnPropertyDescriptor(raw, 'version');
105
+ }
106
+ catch {
107
+ throw new TypeError('The Case store version could not be inspected safely.');
108
+ }
109
+ if (!descriptor)
110
+ return null;
111
+ if (!Object.hasOwn(descriptor, 'value')) {
112
+ throw new TypeError('The Case store version must not be an accessor.');
113
+ }
114
+ return typeof descriptor.value === 'number' ? descriptor.value : null;
115
+ }
116
+ /** @param {unknown} raw @returns {unknown[]} */
117
+ function asCaseList(raw) {
118
+ if (Array.isArray(raw))
119
+ return raw;
120
+ const cases = objectRecord(raw).cases;
121
+ if (Array.isArray(cases))
122
+ return cases;
123
+ return [];
124
+ }
125
+ function boundedCaseList(raw) {
126
+ const source = asCaseList(raw);
127
+ return {
128
+ items: source.slice(0, MAX_CASE_INPUT_RECORDS),
129
+ omitted: Math.max(0, source.length - MAX_CASE_INPUT_RECORDS),
130
+ };
131
+ }
132
+ /**
133
+ * @param {{ domain: unknown, status?: unknown, disposition?: unknown, source?: unknown, tags?: unknown, evidence?: unknown, note?: unknown }} input
134
+ * @param {string} [nowIso]
135
+ * @returns {CaseRecord}
136
+ */
137
+ function importScalar(value, valid) {
138
+ return typeof value === 'string' && valid.has(value) ? value : undefined;
139
+ }
140
+ /**
141
+ * Validates one imported record into a patch. Unlike normalizeCase, absent or
142
+ * invalid scalar fields stay `undefined` (never defaulted) and a missing/invalid
143
+ * updatedAt stays `null` (treated as older than any real local timestamp), so an
144
+ * incomplete import can never win a merge over valid local data. Imported
145
+ * evidence is normalized additively; a snapshot with no captured time falls back
146
+ * only to the imported record's own (older) timestamps, never to "now".
147
+ * @param {unknown} raw
148
+ * @returns {ImportPatch | null}
149
+ */
150
+ function extractImportPatch(raw, importedVersion) {
151
+ const record = objectRecord(raw);
152
+ const domain = normalizeDomain(record.domain);
153
+ if (!domain)
154
+ return null;
155
+ const importFallback = caseTimestampOrNull(record.updatedAt, importedVersion)
156
+ || caseTimestampOrNull(record.createdAt, importedVersion)
157
+ || null;
158
+ const normalizedFallback = importFallback || '1970-01-01T00:00:00.000Z';
159
+ const timestampOptions = {
160
+ legacyTimestamps: importedVersion < CASE_SCHEMA_VERSION,
161
+ sourceVersion: importedVersion,
162
+ };
163
+ const rawEvidence = Array.isArray(record.evidenceHistory) ? record.evidenceHistory : [];
164
+ const evidencePins = normalizeCaseEvidencePins(record.evidencePins, normalizedFallback, {
165
+ allowCertificateObservation: importedVersion >= 11,
166
+ ...timestampOptions,
167
+ });
168
+ const pinIds = new Set(evidencePins.map((item) => item.id));
169
+ const actions = normalizeCaseActions(record.actions, normalizedFallback, { ...timestampOptions, validEvidencePinIds: pinIds });
170
+ const assertions = normalizeCaseAssertions(record.assertions, normalizedFallback, pinIds, timestampOptions);
171
+ const sightings = normalizeCaseSightings(record.sightings, normalizedFallback, pinIds, timestampOptions);
172
+ const observedEffects = normalizeCaseObservedEffectHistory(importedVersion >= 13 ? record.observedEffects : undefined, normalizedFallback, pinIds, new Set(sightings.map((item) => item.id)), timestampOptions);
173
+ const closures = normalizeCaseClosureHistory(importedVersion >= 13 ? record.closures : undefined, normalizedFallback, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), timestampOptions, buildCaseClosureLinkContext(observedEffects, actions));
174
+ const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
175
+ const brandProfileReferences = importedVersion >= 12
176
+ ? inspectCaseBrandProfileIds(record.brandProfileIds)
177
+ : { ids: [], omitted: 0 };
178
+ return {
179
+ domain,
180
+ rawId: typeof record.id === 'string' ? record.id : null,
181
+ status: importScalar(record.status, STATUS_VALUES),
182
+ disposition: importScalar(record.disposition, DISPOSITION_VALUES),
183
+ reviewReasonCode: Object.hasOwn(record, 'reviewReasonCode')
184
+ ? normalizeReviewReasonCode(record.reviewReasonCode)
185
+ : undefined,
186
+ brandProfileIds: brandProfileReferences.ids,
187
+ brandProfileReferencesOmitted: brandProfileReferences.omitted,
188
+ source: importScalar(record.source, SOURCE_VALUES),
189
+ evidenceHistory: normalizeEvidenceHistory(rawEvidence, {
190
+ source: 'import',
191
+ fallback: importFallback,
192
+ sourceVersion: importedVersion,
193
+ caseDomain: domain,
194
+ }),
195
+ evidencePins,
196
+ decisions: normalizeCaseDecisions(record.decisions, normalizedFallback, pinIds, timestampOptions),
197
+ actions,
198
+ assertions,
199
+ manualTrail: normalizeCaseManualTrail(record.manualTrail, normalizedFallback, timestampOptions),
200
+ sightings,
201
+ observedEffects,
202
+ closures,
203
+ branches: importedVersion >= 11
204
+ ? normalizeCaseInvestigationBranches(record.branches, normalizedFallback, branchReferences, timestampOptions)
205
+ : [],
206
+ tags: normalizeTags(record.tags),
207
+ // Imported notes fall back only to the imported record's own timestamps
208
+ // (never "now"), so a timestamp-less note gets a stable, deterministic time
209
+ // and id, and re-importing the same file cannot manufacture a duplicate or a
210
+ // spuriously-newer note.
211
+ notes: normalizeNotes(record.notes, importFallback, importedVersion),
212
+ createdAt: caseTimestampOrNull(record.createdAt, importedVersion),
213
+ updatedAt: caseTimestampOrNull(record.updatedAt, importedVersion),
214
+ };
215
+ }
216
+ /** @param {CaseNote[]} a @param {CaseNote[]} b @returns {CaseNote[]} */
217
+ function unionNotes(a, b) {
218
+ const byId = new Map();
219
+ for (const note of [...a, ...b]) {
220
+ if (!byId.has(note.id))
221
+ byId.set(note.id, note);
222
+ }
223
+ const notes = [...byId.values()].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
224
+ return notes.slice(Math.max(0, notes.length - MAX_NOTES_PER_CASE));
225
+ }
226
+ // Additive, deduplicated union of two evidence histories. Identical material
227
+ // collapses (earliest firstCapturedAt, latest capturedAt), distinct snapshots
228
+ // are retained subject to the per-case bound, and an older import can never
229
+ // move an existing observation backwards.
230
+ function mergeEvidenceHistories(local, imported, caseDomain) {
231
+ return normalizeEvidenceHistory([...local, ...imported], { source: 'import', fallback: null, caseDomain });
232
+ }
233
+ /** @param {ImportPatch} patch @param {string} now @returns {CaseRecord} */
234
+ function caseFromPatch(patch, now) {
235
+ return {
236
+ id: '', // assigned by mergeCases so it can guarantee uniqueness against locals
237
+ domain: patch.domain,
238
+ status: patch.status ?? DEFAULT_STATUS,
239
+ disposition: patch.disposition ?? DEFAULT_DISPOSITION,
240
+ reviewReasonCode: patch.reviewReasonCode ?? null,
241
+ brandProfileIds: patch.brandProfileIds,
242
+ tags: patch.tags,
243
+ notes: patch.notes,
244
+ source: patch.source ?? DEFAULT_SOURCE,
245
+ evidenceHistory: patch.evidenceHistory,
246
+ evidencePins: patch.evidencePins,
247
+ decisions: patch.decisions,
248
+ actions: patch.actions,
249
+ assertions: patch.assertions,
250
+ manualTrail: patch.manualTrail,
251
+ sightings: patch.sightings,
252
+ observedEffects: patch.observedEffects,
253
+ closures: patch.closures,
254
+ branches: patch.branches,
255
+ createdAt: patch.createdAt || patch.updatedAt || now,
256
+ updatedAt: patch.updatedAt || patch.createdAt || now,
257
+ };
258
+ }
259
+ /**
260
+ * Merges an imported patch into an existing local case. Notes, tags, and
261
+ * evidence history are merged additively (never destructive); a scalar field is
262
+ * only overwritten when the import provided a valid value AND is strictly newer
263
+ * than the local record. A patch with no/invalid updatedAt is never newer.
264
+ * @param {CaseRecord} local
265
+ * @param {ImportPatch} patch
266
+ * @returns {CaseRecord}
267
+ */
268
+ function applyImportPatch(local, patch) {
269
+ const importNewer = patch.updatedAt !== null && Date.parse(patch.updatedAt) > Date.parse(local.updatedAt);
270
+ const fallback = patch.updatedAt || local.updatedAt;
271
+ const evidencePins = mergeCaseEvidencePins(local.evidencePins, patch.evidencePins, fallback);
272
+ const pinIds = new Set(evidencePins.map((item) => item.id));
273
+ const actions = mergeCaseActions(local.actions, patch.actions, fallback, pinIds);
274
+ const assertions = mergeCaseAssertions(local.assertions, patch.assertions, fallback, pinIds);
275
+ const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
276
+ const sightings = mergeCaseSightings(local.sightings, patch.sightings, fallback, pinIds);
277
+ const sightingIds = new Set(sightings.map((item) => item.id));
278
+ const observedEffects = mergeCaseObservedEffectHistories(local.observedEffects, patch.observedEffects, fallback, pinIds, sightingIds);
279
+ const closures = mergeCaseClosureHistories(local.closures, patch.closures, fallback, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), buildCaseClosureLinkContext(observedEffects, actions));
280
+ const brandProfileReferences = unionCaseBrandProfileIds(local.brandProfileIds, patch.brandProfileIds);
281
+ return { record: {
282
+ ...local,
283
+ status: patch.status !== undefined && importNewer ? patch.status : local.status,
284
+ disposition: patch.disposition !== undefined && importNewer ? patch.disposition : local.disposition,
285
+ reviewReasonCode: patch.reviewReasonCode !== undefined && importNewer ? patch.reviewReasonCode : local.reviewReasonCode ?? null,
286
+ brandProfileIds: brandProfileReferences.ids,
287
+ source: patch.source !== undefined && importNewer ? patch.source : local.source,
288
+ evidenceHistory: mergeEvidenceHistories(local.evidenceHistory, patch.evidenceHistory, local.domain),
289
+ evidencePins,
290
+ decisions: mergeCaseDecisions(local.decisions, patch.decisions, fallback, pinIds),
291
+ actions,
292
+ assertions,
293
+ manualTrail: mergeCaseManualTrail(local.manualTrail, patch.manualTrail, fallback),
294
+ sightings,
295
+ observedEffects,
296
+ closures,
297
+ branches: mergeCaseInvestigationBranches(local.branches ?? [], patch.branches, fallback, branchReferences),
298
+ tags: normalizeTags([...local.tags, ...patch.tags]),
299
+ notes: unionNotes(local.notes, patch.notes),
300
+ createdAt: patch.createdAt && Date.parse(patch.createdAt) < Date.parse(local.createdAt) ? patch.createdAt : local.createdAt,
301
+ updatedAt: importNewer ? (patch.updatedAt ?? local.updatedAt) : local.updatedAt,
302
+ }, brandProfileReferencesOmitted: brandProfileReferences.omitted };
303
+ }
304
+ function pickFreeId(preferred, domain, used) {
305
+ const wanted = safeId(preferred);
306
+ let id = wanted && !used.has(wanted) ? wanted : deterministicId(domain);
307
+ const base = deterministicId(domain);
308
+ let suffix = 2;
309
+ while (used.has(id))
310
+ id = `${base}-${suffix++}`;
311
+ return id;
312
+ }
313
+ const CASE_EXPORT_ROOT_KEYS = Object.freeze(['version', 'exportedAt', 'cases']);
314
+ function assertOnlyEnvelopeKeys(value, allowed, label) {
315
+ const allowedKeys = new Set(allowed);
316
+ if (Object.keys(value).some((key) => !allowedKeys.has(key))) {
317
+ throw new Error(`The WHOISleuth ${label} contains undeclared envelope fields.`);
318
+ }
319
+ }
320
+ function exactStringList(value, expected) {
321
+ return Array.isArray(value)
322
+ && value.length === expected.length
323
+ && value.every((item, index) => item === expected[index]);
324
+ }
325
+ /**
326
+ * A CLI Case-pack remains browser-importable through its exact top-level Case
327
+ * collection. This projection closes the known pack envelopes before dropping
328
+ * pack-only review and integrity metadata; it does not make an integrity claim.
329
+ * The statically bound CLI verifier remains the exact structure-and-digest
330
+ * verifier for the durable Case-pack version.
331
+ */
332
+ function caseCollectionImportEnvelope(importedRaw) {
333
+ const root = objectRecord(importedRaw);
334
+ const hasPackField = Object.hasOwn(root, 'packet') || Object.hasOwn(root, 'integrity');
335
+ if (!hasPackField) {
336
+ assertOnlyEnvelopeKeys(root, CASE_EXPORT_ROOT_KEYS, 'case export');
337
+ return root;
338
+ }
339
+ assertOnlyEnvelopeKeys(root, CLI_CASE_PACK_ROOT_KEYS, 'CLI Case-pack');
340
+ if (CLI_CASE_PACK_ROOT_KEYS.some((key) => !Object.hasOwn(root, key))) {
341
+ throw new Error('The WHOISleuth CLI Case-pack is missing a required envelope field.');
342
+ }
343
+ const packet = objectRecord(root.packet);
344
+ const integrity = objectRecord(root.integrity);
345
+ const redactionManifest = objectRecord(packet.redactionManifest);
346
+ assertOnlyEnvelopeKeys(packet, CLI_CASE_PACK_PACKET_KEYS, 'CLI Case-pack packet');
347
+ assertOnlyEnvelopeKeys(integrity, CLI_CASE_PACK_INTEGRITY_KEYS, 'CLI Case-pack integrity');
348
+ const rootVersion = root.version;
349
+ const packVersion = packet.version;
350
+ if (Number.isSafeInteger(packVersion) && packVersion < CLI_CASE_PACK_VERSION) {
351
+ throw new Error(`WHOISleuth CLI Case-pack version ${String(packVersion)} is retired. Re-export it as version ${CLI_CASE_PACK_VERSION}; no data was changed.`);
352
+ }
353
+ if (Number.isSafeInteger(packVersion) && packVersion > CLI_CASE_PACK_VERSION) {
354
+ throw new Error(`WHOISleuth CLI Case-pack version ${String(packVersion)} is newer than the supported version ${CLI_CASE_PACK_VERSION}; no data was changed.`);
355
+ }
356
+ if (Number.isSafeInteger(rootVersion) && rootVersion < CASE_SCHEMA_VERSION
357
+ && !CASE_IMPORT_VERSIONS.includes(rootVersion)) {
358
+ throw new Error(`Case schema ${String(rootVersion)} in this CLI Case-pack is not part of the public compatibility boundary; no data was changed.`);
359
+ }
360
+ if (Number.isSafeInteger(rootVersion) && rootVersion > CASE_SCHEMA_VERSION) {
361
+ throw new Error(`Case schema ${String(rootVersion)} in this CLI Case-pack is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
362
+ }
363
+ assertOnlyEnvelopeKeys(redactionManifest, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, 'CLI Case-pack redaction manifest');
364
+ const reports = packet.reports;
365
+ const cases = root.cases;
366
+ const audience = packet.audience;
367
+ if (packet.schema !== CLI_CASE_PACK_SCHEMA
368
+ || packVersion !== CLI_CASE_PACK_VERSION
369
+ || typeof rootVersion !== 'number'
370
+ || !CASE_IMPORT_VERSIONS.includes(rootVersion)
371
+ || packet.reviewed !== true
372
+ || (audience !== 'internal' && audience !== 'public' && audience !== 'trusted')
373
+ || !Array.isArray(cases)
374
+ || !Array.isArray(reports)
375
+ || reports.length !== cases.length
376
+ || !exactStringList(packet.limitations, CLI_CASE_PACK_LIMITATIONS)
377
+ || !Number.isSafeInteger(redactionManifest.sourceCaseCount)
378
+ || redactionManifest.sourceCaseCount !== cases.length
379
+ || !Number.isSafeInteger(redactionManifest.brandProfileReferencesOmitted)
380
+ || redactionManifest.brandProfileReferencesOmitted < 0
381
+ || integrity.algorithm !== 'SHA-256'
382
+ || integrity.canonicalization !== 'sorted-json-v2'
383
+ || typeof integrity.digestSha256 !== 'string'
384
+ || !/^sha256:[a-f0-9]{64}$/u.test(integrity.digestSha256)
385
+ || typeof root.exportedAt !== 'string'
386
+ || caseTimestampOrNull(root.exportedAt) !== root.exportedAt) {
387
+ throw new Error('The WHOISleuth CLI Case-pack envelope is invalid.');
388
+ }
389
+ for (const reportValue of reports) {
390
+ const report = objectRecord(reportValue);
391
+ assertOnlyEnvelopeKeys(report, rootVersion === CASE_SCHEMA_VERSION ? CLI_CASE_PACK_REPORT_KEYS : CLI_CASE_PACK_PUBLIC_REPORT_KEYS, 'CLI Case-pack report');
392
+ if (report.schema !== CASE_REPORT_SCHEMA
393
+ || !caseReportVersionMatchesCase(rootVersion, report.schemaVersion)
394
+ || report.generatedAt !== root.exportedAt) {
395
+ throw new Error('The WHOISleuth CLI Case-pack report epoch is invalid.');
396
+ }
397
+ }
398
+ const normalised = normalizeCaseStore(root).cases;
399
+ if (normalised.length !== cases.length
400
+ || (rootVersion === CASE_SCHEMA_VERSION
401
+ && canonicalArtifactJsonV2(cases) !== canonicalArtifactJsonV2(normalised))) {
402
+ throw new Error(`The WHOISleuth CLI Case-pack contains a non-canonical schema ${rootVersion} Case collection.`);
403
+ }
404
+ return { version: rootVersion, exportedAt: root.exportedAt, cases };
405
+ }
406
+ /**
407
+ * Merges an imported (already parsed) value into the local cases. Predictable
408
+ * and idempotent: unknown records are skipped, existing domains merge without
409
+ * losing newer local decisions, and new ones are added until the store bound is
410
+ * reached. An imported envelope that declares a schema version newer than we
411
+ * support is rejected up front (before any local data is touched) rather than
412
+ * reinterpreted.
413
+ * @param {CaseRecord[]} localCases
414
+ * @param {unknown} importedRaw
415
+ * @returns {{ cases: CaseRecord[], added: number, updated: number, skipped: number, brandProfileReferencesOmitted: number }}
416
+ */
417
+ export function mergeCases(localCases, importedRaw) {
418
+ assertBoundedJsonStructure(importedRaw, 'Case import', CASE_INPUT_JSON_LIMITS);
419
+ const importedEnvelope = caseCollectionImportEnvelope(importedRaw);
420
+ const importedVersion = parseStoreVersion(importedEnvelope);
421
+ if (importedVersion !== null && Number.isSafeInteger(importedVersion) && importedVersion < CASE_SCHEMA_VERSION
422
+ && !CASE_IMPORT_VERSIONS.includes(importedVersion)) {
423
+ throw new Error(`Case schema ${importedVersion} is not part of the public compatibility boundary. Only public schema 12 migrates to schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
424
+ }
425
+ if (importedVersion !== null && Number.isSafeInteger(importedVersion) && importedVersion > CASE_SCHEMA_VERSION) {
426
+ throw new Error(`Case schema ${importedVersion} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
427
+ }
428
+ if (!CASE_IMPORT_VERSIONS.includes(importedVersion)
429
+ || !Array.isArray(importedEnvelope.cases)) {
430
+ throw new Error(`Expected a well-formed WHOISleuth Case export using schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
431
+ }
432
+ if (importedVersion === CASE_SCHEMA_VERSION)
433
+ assertCurrentEvidenceHostnameShape(importedEnvelope);
434
+ const local = normalizeCaseStore(localCases).cases;
435
+ const supportedImportedVersion = importedVersion ?? 0;
436
+ const byDomain = new Map(local.map((item) => [item.domain, item]));
437
+ const usedIds = new Set(local.map((item) => item.id));
438
+ let added = 0;
439
+ let updated = 0;
440
+ const imported = boundedCaseList(importedEnvelope);
441
+ let skipped = imported.omitted;
442
+ let brandProfileReferencesOmitted = 0;
443
+ const fallback = new Date(0).toISOString();
444
+ for (const item of imported.items) {
445
+ const patch = extractImportPatch(item, supportedImportedVersion);
446
+ if (!patch) {
447
+ skipped += 1;
448
+ continue;
449
+ }
450
+ const existing = byDomain.get(patch.domain);
451
+ if (existing) {
452
+ const merged = applyImportPatch(existing, patch);
453
+ byDomain.set(patch.domain, merged.record);
454
+ brandProfileReferencesOmitted += patch.brandProfileReferencesOmitted + merged.brandProfileReferencesOmitted;
455
+ updated += 1;
456
+ }
457
+ else if (byDomain.size < MAX_CASES) {
458
+ const record = caseFromPatch(patch, fallback);
459
+ record.id = pickFreeId(patch.rawId, patch.domain, usedIds);
460
+ usedIds.add(record.id);
461
+ byDomain.set(patch.domain, record);
462
+ brandProfileReferencesOmitted += patch.brandProfileReferencesOmitted;
463
+ added += 1;
464
+ }
465
+ else {
466
+ skipped += 1;
467
+ }
468
+ }
469
+ return {
470
+ cases: normalizeCaseStore([...byDomain.values()]).cases,
471
+ added,
472
+ updated,
473
+ skipped,
474
+ brandProfileReferencesOmitted,
475
+ };
476
+ }
@@ -1,5 +1,5 @@
1
1
  // Stable case-domain facade. Records, migrations, and storage concerns remain
2
2
  // separate internally while callers retain one public contract.
3
- export * from "./case-record-model.js";
4
- export * from "./case-migration-model.js";
5
- export * from "./case-storage-model.js";
3
+ export * from "./case-record-model.mjs";
4
+ export * from "./case-migration-model.mjs";
5
+ export * from "./case-storage-model.mjs";
@@ -1,24 +1,6 @@
1
- import { ANALYST_REVIEW_REASONS } from "../../../../lib/analyst-taxonomy.mjs";
2
- export const CASE_SCHEMA_VERSION = 12;
3
- export const CASE_IMPORT_VERSIONS = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, CASE_SCHEMA_VERSION];
4
- export const MAX_CASES = 500;
5
- export const MAX_NOTES_PER_CASE = 50;
6
- export const MAX_NOTE_LENGTH = 2000;
7
- export const MAX_TAGS_PER_CASE = 20;
8
- export const MAX_TAG_LENGTH = 40;
9
- export const MAX_DOMAIN_LENGTH = 253;
10
- export const MAX_CASE_IMPORT_BYTES = 2 * 1024 * 1024;
11
- export const MAX_EVIDENCE_SNAPSHOTS_PER_CASE = 25;
12
- export const MAX_EVIDENCE_FACTORS = 20;
13
- export const MAX_EVIDENCE_NAMESERVERS = 12;
14
- export const MAX_EVIDENCE_MUTATIONS = 20;
15
- export const MAX_EVIDENCE_STRING_LENGTH = 200;
16
- export const MAX_EVIDENCE_TITLE_LENGTH = 200;
17
- export const MAX_EVIDENCE_DETAIL_LENGTH = 200;
18
- export const MAX_EVIDENCE_CHANGES = 40;
19
- export const MAX_CASE_STORE_BYTES = 4 * 1024 * 1024;
20
- export const MAX_CASE_BRAND_PROFILE_IDS = 8;
21
- export const MAX_CASE_BRAND_PROFILE_ID_CANDIDATES = 32;
1
+ import { ANALYST_REVIEW_REASONS } from "../../lib/analyst-taxonomy.mjs";
2
+ import { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, PUBLIC_CASE_SCHEMA_VERSION, MAX_CASE_BRAND_PROFILE_ID_CANDIDATES, MAX_CASE_BRAND_PROFILE_IDS, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_CASES, MAX_DOMAIN_LENGTH, MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, MAX_NOTE_LENGTH, MAX_NOTES_PER_CASE, MAX_TAG_LENGTH, MAX_TAGS_PER_CASE, } from "../contracts/case-portability.mjs";
3
+ export { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, PUBLIC_CASE_SCHEMA_VERSION, MAX_CASE_BRAND_PROFILE_ID_CANDIDATES, MAX_CASE_BRAND_PROFILE_IDS, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_CASES, MAX_DOMAIN_LENGTH, MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, MAX_NOTE_LENGTH, MAX_NOTES_PER_CASE, MAX_TAG_LENGTH, MAX_TAGS_PER_CASE, };
22
4
  export const CASE_STATUSES = [
23
5
  { value: 'new', label: 'New' },
24
6
  { value: 'reviewing', label: 'Reviewing' },