@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,384 @@
1
+ // Compact provenance envelope for network-derived evidence. Type-specific
2
+ // payloads remain beside this object; the envelope only standardises source
3
+ // health, timing, completeness, truncation, and bounded limitations.
4
+ const OBSERVATION_VERSION = 1;
5
+ const STATUSES = new Set([
6
+ 'success',
7
+ 'partial',
8
+ 'not_found',
9
+ 'skipped',
10
+ 'error',
11
+ 'unsupported',
12
+ 'not_applicable',
13
+ ]);
14
+ const SCAN_MODES = new Set(['fast', 'deep']);
15
+ const MAX_OBSERVATION_LIMITATIONS = 10;
16
+ const MAX_OBSERVATION_LIMITATION_LENGTH = 300;
17
+ const MAX_OBSERVATION_DIAGNOSTICS = 20;
18
+ const MAX_DIAGNOSTICS = MAX_OBSERVATION_DIAGNOSTICS;
19
+ const MAX_DIAGNOSTIC_KEY = 40;
20
+ const MAX_DIAGNOSTIC_STRING = 240;
21
+ const MAX_DURATION_MS = 120_000;
22
+ const MAX_OBSERVATION_INPUT_LIMITATIONS = MAX_OBSERVATION_LIMITATIONS * 4;
23
+ const MAX_DIAGNOSTIC_DEPTH = 4;
24
+ const MAX_DIAGNOSTIC_VALUES_PER_ENTRY = 32;
25
+ const OBSERVATION_INPUT_FIELDS = [
26
+ 'version',
27
+ 'status',
28
+ 'observedAt',
29
+ 'scanMode',
30
+ 'source',
31
+ 'durationMs',
32
+ 'complete',
33
+ 'truncated',
34
+ 'limitations',
35
+ 'diagnostics',
36
+ ];
37
+ // Observation v1 diagnostics use a closed, source-owned vocabulary. Reading
38
+ // only these descriptors keeps normalisation independent of an untrusted
39
+ // object's total key count and preserves the deployed producer fields.
40
+ const REGISTERED_DIAGNOSTIC_KEYS = [
41
+ 'a',
42
+ 'aaaa',
43
+ 'addressSource',
44
+ 'advisoryMatches',
45
+ 'arrayItemsExamined',
46
+ 'attemptCount',
47
+ 'authorityCount',
48
+ 'caa',
49
+ 'caa_policy',
50
+ 'catalogComponents',
51
+ 'certificateGroups',
52
+ 'certificateGroupsTruncated',
53
+ 'certificateRows',
54
+ 'charactersExamined',
55
+ 'cidrCount',
56
+ 'classifiedInputs',
57
+ 'cname',
58
+ 'connectionAttempts',
59
+ 'delegation',
60
+ 'discarded',
61
+ 'discardedAuthorityRecordValueCount',
62
+ 'discardedFields',
63
+ 'discardedProperties',
64
+ 'discardedUrls',
65
+ 'discarded_attributes',
66
+ 'discarded_entities',
67
+ 'discarded_metadata',
68
+ 'discarded_relationships',
69
+ 'dmarc',
70
+ 'documentsParsed',
71
+ 'entities',
72
+ 'error',
73
+ 'externalScriptsSkipped',
74
+ 'findings',
75
+ 'formsObserved',
76
+ 'generatorEvaluated',
77
+ 'htmlEvaluated',
78
+ 'httpStatus',
79
+ 'https',
80
+ 'ignoredCount',
81
+ 'indicatorsObserved',
82
+ 'inlineCharactersExamined',
83
+ 'inlineScriptsExamined',
84
+ 'inlineSignatureCharactersExamined',
85
+ 'inlineSignatureTimedOut',
86
+ 'inlineSignatureUnavailable',
87
+ 'inputsObserved',
88
+ 'knownExploitedMatches',
89
+ 'lameAuthorityCount',
90
+ 'malformedCount',
91
+ 'malformedScripts',
92
+ 'matches',
93
+ 'mx',
94
+ 'namesExamined',
95
+ 'ns',
96
+ 'objectsExamined',
97
+ 'observed',
98
+ 'observedAbsence',
99
+ 'partialAuthorityCount',
100
+ 'passiveHeadersEvaluated',
101
+ 'potentialExposure',
102
+ 'ptr',
103
+ 'queriedAddressCount',
104
+ 'redirectCount',
105
+ 'referencesExamined',
106
+ 'rejectedRows',
107
+ 'relationshipTagsExamined',
108
+ 'relationshipUrlsDiscarded',
109
+ 'relationships',
110
+ 'requestCount',
111
+ 'resolvedAddressCount',
112
+ 'resourceOriginsEvaluated',
113
+ 'rolesObserved',
114
+ 'scriptElementsExamined',
115
+ 'scriptsExamined',
116
+ 'scriptsObserved',
117
+ 'serverEvaluated',
118
+ 'soa',
119
+ 'spf',
120
+ 'successfulAuthorityCount',
121
+ 'tagLimitReached',
122
+ 'tagsExamined',
123
+ 'trackingIdentifiersTruncated',
124
+ 'tree',
125
+ 'truncatedAuthorityRecordSetCount',
126
+ 'truncatedNames',
127
+ 'truncatedSameAsLists',
128
+ 'truncatedTypeLists',
129
+ 'unavailable',
130
+ 'unclassifiedActions',
131
+ 'unreachableAuthorityCount',
132
+ 'workTruncated',
133
+ ];
134
+ const ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,43})?(Z|[+-]\d{2}:\d{2})$/iu;
135
+ const LEGACY_ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,43})?(Z|[+-]\d{2}:\d{2})?$/iu;
136
+ const CT_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(\.\d{1,43})?(Z|[+-]\d{2}:\d{2})?$/iu;
137
+ function isObservationStatus(value) {
138
+ return typeof value === 'string' && STATUSES.has(value);
139
+ }
140
+ function isScanMode(value) {
141
+ return typeof value === 'string' && SCAN_MODES.has(value);
142
+ }
143
+ function safeString(value, maxLength) {
144
+ if (typeof value !== 'string' || value.length > maxLength * 4)
145
+ return null;
146
+ const normalised = value.trim().replace(/[\u0000-\u001f\u007f]/g, ' ');
147
+ return normalised ? normalised.slice(0, maxLength) : null;
148
+ }
149
+ function ownDataValue(value, key) {
150
+ try {
151
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
152
+ return descriptor && 'value' in descriptor ? descriptor.value : undefined;
153
+ }
154
+ catch {
155
+ return undefined;
156
+ }
157
+ }
158
+ function snapshotObservationInput(value) {
159
+ const snapshot = {};
160
+ for (const field of OBSERVATION_INPUT_FIELDS) {
161
+ const fieldValue = ownDataValue(value, field);
162
+ if (fieldValue === undefined)
163
+ continue;
164
+ Object.defineProperty(snapshot, field, {
165
+ configurable: true,
166
+ enumerable: true,
167
+ value: fieldValue,
168
+ writable: true,
169
+ });
170
+ }
171
+ return snapshot;
172
+ }
173
+ function safelyIsArray(value) {
174
+ try {
175
+ return Array.isArray(value);
176
+ }
177
+ catch {
178
+ return true;
179
+ }
180
+ }
181
+ function boundedArrayDataValues(value, maximum) {
182
+ if (!safelyIsArray(value))
183
+ return [];
184
+ const output = [];
185
+ const rawLength = ownDataValue(value, 'length');
186
+ const length = typeof rawLength === 'number' && Number.isSafeInteger(rawLength) && rawLength >= 0
187
+ ? Math.min(rawLength, maximum)
188
+ : 0;
189
+ for (let index = 0; index < length; index += 1) {
190
+ const item = ownDataValue(value, String(index));
191
+ if (item !== undefined)
192
+ output.push(item);
193
+ }
194
+ return output;
195
+ }
196
+ function validCalendarParts(parts) {
197
+ const year = Number(parts[1]);
198
+ const month = Number(parts[2]);
199
+ const day = Number(parts[3]);
200
+ const hour = Number(parts[4]);
201
+ const minute = Number(parts[5]);
202
+ const second = Number(parts[6]);
203
+ if (year < 1 || month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59)
204
+ return false;
205
+ const date = new Date(0);
206
+ date.setUTCFullYear(year, month - 1, day);
207
+ date.setUTCHours(hour, minute, second, 0);
208
+ return date.getUTCFullYear() === year
209
+ && date.getUTCMonth() === month - 1
210
+ && date.getUTCDate() === day
211
+ && date.getUTCHours() === hour
212
+ && date.getUTCMinutes() === minute
213
+ && date.getUTCSeconds() === second;
214
+ }
215
+ function canonicalTimestamp(value, expression, assignUtcWhenMissing) {
216
+ if (typeof value !== 'string' || value.length === 0 || value.length > 64 || /[\u0000-\u001f\u007f]/u.test(value))
217
+ return null;
218
+ const match = expression.exec(value);
219
+ if (!match || !validCalendarParts(match))
220
+ return null;
221
+ const zone = match[8] || (assignUtcWhenMissing ? 'Z' : '');
222
+ if (!zone)
223
+ return null;
224
+ if (zone !== 'Z' && zone.toUpperCase() !== 'Z') {
225
+ const hours = Number(zone.slice(1, 3));
226
+ const minutes = Number(zone.slice(4, 6));
227
+ if (hours > 23 || minutes > 59)
228
+ return null;
229
+ }
230
+ const normalised = `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}${match[7] || ''}${zone.toUpperCase() === 'Z' ? 'Z' : zone}`;
231
+ const timestamp = Date.parse(normalised);
232
+ if (!Number.isFinite(timestamp))
233
+ return null;
234
+ const date = new Date(timestamp);
235
+ const utcYear = date.getUTCFullYear();
236
+ return utcYear >= 1 && utcYear <= 9_999 ? date.toISOString() : null;
237
+ }
238
+ function normalizeExplicitIsoTimestamp(value) {
239
+ return canonicalTimestamp(value, ISO_DATE_TIME_RE, false);
240
+ }
241
+ // Frozen legacy schemas accepted zone-less ISO date-times. Preserve that
242
+ // migration path deterministically by assigning UTC instead of consulting the
243
+ // host timezone. Current schemas must use normalizeExplicitIsoTimestamp.
244
+ function normalizeLegacyIsoTimestamp(value) {
245
+ return canonicalTimestamp(value, LEGACY_ISO_DATE_TIME_RE, true);
246
+ }
247
+ function normalizeCtTimestamp(value) {
248
+ return canonicalTimestamp(value, CT_DATE_TIME_RE, true);
249
+ }
250
+ function isoTimestamp(value) {
251
+ return normalizeExplicitIsoTimestamp(value);
252
+ }
253
+ function normalizeDiagnosticValue(value, budget, depth = 0) {
254
+ budget.remaining -= 1;
255
+ if (budget.remaining < 0) {
256
+ budget.exhausted = true;
257
+ return null;
258
+ }
259
+ if (depth > MAX_DIAGNOSTIC_DEPTH)
260
+ return null;
261
+ if (typeof value === 'boolean')
262
+ return value;
263
+ if (typeof value === 'number')
264
+ return Number.isFinite(value) ? value : null;
265
+ if (typeof value === 'string')
266
+ return safeString(value, MAX_DIAGNOSTIC_STRING);
267
+ if (!value || typeof value !== 'object' || safelyIsArray(value))
268
+ return null;
269
+ const objectValue = value;
270
+ if (budget.active.has(objectValue))
271
+ return null;
272
+ const record = value;
273
+ const output = {};
274
+ budget.active.add(objectValue);
275
+ try {
276
+ for (const field of ['status', 'error', 'detail', 'truncated', 'discarded', 'count']) {
277
+ const fieldValue = ownDataValue(record, field);
278
+ if (fieldValue === undefined)
279
+ continue;
280
+ const normalised = normalizeDiagnosticValue(fieldValue, budget, depth + 1);
281
+ if (normalised !== null) {
282
+ Object.defineProperty(output, field, {
283
+ configurable: true,
284
+ enumerable: true,
285
+ value: normalised,
286
+ writable: true,
287
+ });
288
+ }
289
+ }
290
+ }
291
+ finally {
292
+ budget.active.delete(objectValue);
293
+ }
294
+ return Object.keys(output).length ? output : null;
295
+ }
296
+ function normalizeDiagnostics(value) {
297
+ if (!value || typeof value !== 'object' || safelyIsArray(value))
298
+ return {};
299
+ const record = value;
300
+ const output = {};
301
+ let retained = 0;
302
+ for (const key of REGISTERED_DIAGNOSTIC_KEYS) {
303
+ if (retained >= MAX_DIAGNOSTICS)
304
+ break;
305
+ if (key.length > MAX_DIAGNOSTIC_KEY)
306
+ continue;
307
+ const entryValue = ownDataValue(record, key);
308
+ if (entryValue === undefined)
309
+ continue;
310
+ const budget = {
311
+ remaining: MAX_DIAGNOSTIC_VALUES_PER_ENTRY,
312
+ active: new Set(),
313
+ exhausted: false,
314
+ };
315
+ const normalised = normalizeDiagnosticValue(entryValue, budget);
316
+ if (normalised !== null && !budget.exhausted) {
317
+ Object.defineProperty(output, key, {
318
+ configurable: true,
319
+ enumerable: true,
320
+ value: normalised,
321
+ writable: true,
322
+ });
323
+ retained += 1;
324
+ }
325
+ }
326
+ return output;
327
+ }
328
+ function createObservationFromSnapshot(record) {
329
+ const rawStatus = record.status;
330
+ const rawSource = record.source;
331
+ const rawObservedAt = record.observedAt;
332
+ const rawDuration = record.durationMs;
333
+ const rawScanMode = record.scanMode;
334
+ const status = isObservationStatus(rawStatus) ? rawStatus : 'error';
335
+ const source = safeString(rawSource, 40) || 'unknown';
336
+ const observedAt = isoTimestamp(rawObservedAt) || new Date().toISOString();
337
+ const duration = typeof rawDuration === 'number' ? rawDuration : Number.NaN;
338
+ const limitations = [...new Set(boundedArrayDataValues(record.limitations, MAX_OBSERVATION_INPUT_LIMITATIONS)
339
+ .map((item) => safeString(item, MAX_OBSERVATION_LIMITATION_LENGTH))
340
+ .filter((item) => item !== null))]
341
+ .slice(0, MAX_OBSERVATION_LIMITATIONS);
342
+ return {
343
+ version: OBSERVATION_VERSION,
344
+ status,
345
+ observedAt,
346
+ scanMode: isScanMode(rawScanMode) ? rawScanMode : null,
347
+ source,
348
+ durationMs: Number.isFinite(duration) ? Math.max(0, Math.min(MAX_DURATION_MS, Math.round(duration))) : null,
349
+ complete: record.complete === true,
350
+ truncated: record.truncated === true,
351
+ limitations,
352
+ diagnostics: normalizeDiagnostics(record.diagnostics),
353
+ };
354
+ }
355
+ function createObservation(input = {}) {
356
+ const record = input && typeof input === 'object' && !safelyIsArray(input)
357
+ ? snapshotObservationInput(input)
358
+ : {};
359
+ return createObservationFromSnapshot(record);
360
+ }
361
+ function readObservationEnvelope(value) {
362
+ if (value === undefined || value === null)
363
+ return { state: 'absent', observation: null };
364
+ if (!value || typeof value !== 'object' || safelyIsArray(value))
365
+ return { state: 'invalid', observation: null };
366
+ const record = snapshotObservationInput(value);
367
+ const version = record.version;
368
+ const status = record.status;
369
+ const source = record.source;
370
+ const observedAt = record.observedAt;
371
+ if (typeof version === 'number'
372
+ && Number.isInteger(version)
373
+ && version > OBSERVATION_VERSION) {
374
+ return { state: 'unsupported', observation: null };
375
+ }
376
+ if (version !== OBSERVATION_VERSION
377
+ || !isObservationStatus(status)
378
+ || !safeString(source, 40)
379
+ || !isoTimestamp(observedAt)) {
380
+ return { state: 'invalid', observation: null };
381
+ }
382
+ return { state: 'supported', observation: createObservationFromSnapshot(record) };
383
+ }
384
+ export { MAX_OBSERVATION_DIAGNOSTICS, MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, OBSERVATION_VERSION, createObservation, normalizeCtTimestamp, normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp, readObservationEnvelope, };
@@ -1,10 +1,8 @@
1
- import { normalizeDomain, openOrCreateCase, updateCase, } from "./case-model.js";
2
- export const EXTERNAL_FINDINGS_SCHEMA = 'whoisleuth.external-findings';
3
- export const EXTERNAL_FINDINGS_VERSION = 4;
4
- export const MAX_EXTERNAL_FINDINGS_IMPORT_BYTES = 384 * 1024;
5
- export const MAX_EXTERNAL_FINDINGS = 100;
6
- export const MAX_EXTERNAL_FINDINGS_PER_DOMAIN = 20;
7
- export const MAX_EXTERNAL_FINDING_DOMAINS = 25;
1
+ import { normalizeDomain, openOrCreateCase, updateCase, } from "../cases/case-model.mjs";
2
+ import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
3
+ import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, } from "../contracts/external-observation-interchange.mjs";
4
+ import { hasUnsafeRetainedText } from "./retained-text.mjs";
5
+ export { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, };
8
6
  export const EXTERNAL_FINDING_CATEGORIES = [
9
7
  'certificate',
10
8
  'dns',
@@ -17,7 +15,6 @@ export const EXTERNAL_FINDING_CATEGORIES = [
17
15
  ];
18
16
  const CATEGORIES = new Set(EXTERNAL_FINDING_CATEGORIES);
19
17
  const COMPLETENESS = new Set(['complete', 'inconclusive', 'partial', 'unknown']);
20
- const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
21
18
  const ROOT_KEYS = new Set(['schema', 'schemaVersion', 'source', 'findings']);
22
19
  const SOURCE_KEYS = new Set(['name', 'reference', 'collectedAt']);
23
20
  const FINDING_KEYS = new Set([
@@ -62,8 +59,8 @@ function requiredText(value, maximum, label) {
62
59
  if (typeof value !== 'string'
63
60
  || !value.trim()
64
61
  || value.length > maximum
65
- || CONTROL_RE.test(value)) {
66
- throw new Error(`${label} must be non-empty, bounded text without control characters.`);
62
+ || hasUnsafeRetainedText(value)) {
63
+ throw new Error(`${label} must be non-empty, bounded text without unsafe control characters or formatting controls.`);
67
64
  }
68
65
  return value.trim();
69
66
  }
@@ -76,10 +73,10 @@ function iso(value, label, optional = false) {
76
73
  if (optional && (value === undefined || value === null || value === ''))
77
74
  return null;
78
75
  const text = requiredText(value, 64, label);
79
- const parsed = Date.parse(text);
80
- if (!Number.isFinite(parsed))
81
- throw new Error(`${label} must be a valid date and time.`);
82
- return new Date(parsed).toISOString();
76
+ const normalized = normalizeExplicitIsoTimestamp(text);
77
+ if (!normalized)
78
+ throw new Error(`${label} must be a valid date and time with an explicit timezone.`);
79
+ return normalized;
83
80
  }
84
81
  function limitations(value, index) {
85
82
  if (value === undefined)
@@ -92,7 +89,7 @@ function limitations(value, index) {
92
89
  unique.add(requiredText(item, 240, `Finding ${index + 1} limitation`));
93
90
  return [...unique];
94
91
  }
95
- function structuredObservation(value, index, documentVersion) {
92
+ function structuredObservation(value, index) {
96
93
  if (value === undefined || value === null)
97
94
  return null;
98
95
  const item = record(value);
@@ -124,7 +121,7 @@ function structuredObservation(value, index, documentVersion) {
124
121
  let dnsNameCount = null;
125
122
  let namesComplete = null;
126
123
  if (declaresEventMetadata) {
127
- if (!certificateSchema || documentVersion < 4) {
124
+ if (!certificateSchema) {
128
125
  throw new Error(`Finding ${index + 1} certificate-event metadata requires external-findings schema version 4.`);
129
126
  }
130
127
  eventId = requiredText(item.eventId, 64, `Finding ${index + 1} certificate event id`);
@@ -180,8 +177,8 @@ export function parseExternalFindingsDocument(value) {
180
177
  throw new Error('External findings must use the documented object shape without additional top-level fields.');
181
178
  }
182
179
  if (root.schema !== EXTERNAL_FINDINGS_SCHEMA
183
- || ![1, 2, 3, EXTERNAL_FINDINGS_VERSION].includes(Number(root.schemaVersion))) {
184
- throw new Error(`External findings must use ${EXTERNAL_FINDINGS_SCHEMA} schema version 1, 2, 3, or ${EXTERNAL_FINDINGS_VERSION}.`);
180
+ || root.schemaVersion !== EXTERNAL_FINDINGS_VERSION) {
181
+ throw new Error(`External findings must use ${EXTERNAL_FINDINGS_SCHEMA} schema version ${EXTERNAL_FINDINGS_VERSION}.`);
185
182
  }
186
183
  const sourceValue = record(root.source);
187
184
  if (!sourceValue || !hasOnlyKeys(sourceValue, SOURCE_KEYS)) {
@@ -213,9 +210,7 @@ export function parseExternalFindingsDocument(value) {
213
210
  if (typeof item.completeness !== 'string' || !COMPLETENESS.has(item.completeness)) {
214
211
  throw new Error(`Finding ${index + 1} completeness is unsupported.`);
215
212
  }
216
- const evidenceClass = item.evidenceClass === undefined && root.schemaVersion === 1
217
- ? 'provider_report'
218
- : item.evidenceClass;
213
+ const evidenceClass = item.evidenceClass;
219
214
  if (evidenceClass !== 'deployment_observation' && evidenceClass !== 'provider_report') {
220
215
  throw new Error(`Finding ${index + 1} evidence class is unsupported.`);
221
216
  }
@@ -228,7 +223,7 @@ export function parseExternalFindingsDocument(value) {
228
223
  completeness: item.completeness,
229
224
  limitations: limitations(item.limitations, index),
230
225
  reference: optionalText(item.reference, 500, `Finding ${index + 1} reference`),
231
- structuredObservation: structuredObservation(item.structuredObservation, index, Number(root.schemaVersion)),
226
+ structuredObservation: structuredObservation(item.structuredObservation, index),
232
227
  };
233
228
  const key = findingKey(finding, source.name);
234
229
  if (seen.has(key))
@@ -253,6 +248,13 @@ export function parseExternalFindingsDocument(value) {
253
248
  findings: normalized,
254
249
  };
255
250
  }
251
+ export function serializeExternalFindingsDocument(value) {
252
+ const serialised = JSON.stringify(value, null, 2);
253
+ if (typeof serialised !== 'string') {
254
+ throw new TypeError('External findings must be JSON-serialisable.');
255
+ }
256
+ return `${serialised}\n`;
257
+ }
256
258
  function pinValue(finding) {
257
259
  return finding.reference
258
260
  ? `${finding.summary} Reference: ${finding.reference}`
@@ -0,0 +1,8 @@
1
+ const UNSAFE_RETAINED_TEXT_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
2
+ const UNSAFE_RETAINED_TEXT_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
3
+ export function hasUnsafeRetainedText(value) {
4
+ return UNSAFE_RETAINED_TEXT_RE.test(value);
5
+ }
6
+ export function neutralizeUnsafeRetainedText(value) {
7
+ return value.replace(UNSAFE_RETAINED_TEXT_GLOBAL_RE, ' ');
8
+ }
@@ -1,6 +1,7 @@
1
- import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "./artifact-integrity.js";
2
- export const ACQUISITION_DECISION_PACKET_SCHEMA = 'whoisleuth.acquisition-decision';
3
- export const ACQUISITION_DECISION_PACKET_VERSION = 2;
1
+ import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
2
+ import { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION, } from "../contracts/investigation-portability.mjs";
3
+ import { isValidAsciiDomainName } from "../../lib/hostname.mjs";
4
+ export { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION };
4
5
  export const ACQUISITION_DECISIONS = [
5
6
  'unresolved',
6
7
  'continue_manual_review',
@@ -28,11 +29,13 @@ function timestamp(value) {
28
29
  return new Date(value).toISOString();
29
30
  }
30
31
  function target(value) {
31
- const normalized = text(value, 253).toLowerCase().replace(/\.$/u, '');
32
- if (!normalized || !normalized.includes('.') || /[/\\\s]/u.test(normalized)) {
32
+ const submitted = typeof value === 'string'
33
+ ? value.trim().replace(/\.$/u, '')
34
+ : '';
35
+ if (!isValidAsciiDomainName(submitted, { requireDot: true })) {
33
36
  throw new Error('A canonical domain is required for an acquisition decision packet.');
34
37
  }
35
- return normalized;
38
+ return submitted.toLowerCase();
36
39
  }
37
40
  function decision(value) {
38
41
  return typeof value === 'string' && DECISION_SET.has(value)
@@ -1,9 +1,8 @@
1
- import { normalizeBulkSessionResult, } from "./bulk-session-model.js";
2
- import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "./artifact-integrity.js";
3
- import { BULK_REVIEW_STALE_AFTER_DAYS } from "./bulk-retry-plan.js";
4
- export const BULK_DOMAIN_COMPARISON_SCHEMA = 'whoisleuth.domain-comparison';
5
- export const BULK_DOMAIN_COMPARISON_VERSION = 3;
6
- export const BULK_DOMAIN_COMPARISON_EXPORT_VERSION = 4;
1
+ import { normalizeBulkSessionResult, } from "../workspace/bulk-session-model.mjs";
2
+ import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
3
+ import { BULK_REVIEW_STALE_AFTER_DAYS } from "./bulk-review-policy.mjs";
4
+ import { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_DOMAIN_COMPARISON_VERSION, } from "../contracts/investigation-portability.mjs";
5
+ export { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_DOMAIN_COMPARISON_VERSION };
7
6
  function text(value, maximum = 500) {
8
7
  return typeof value === 'string'
9
8
  ? value.replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
@@ -1,9 +1,7 @@
1
- import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "./artifact-integrity.js";
2
- import { BULK_PROFILE_CONTEXT_MISMATCH_LIMITATION, normalizeBulkProfileContext, normalizeBulkSessionResult, } from "./bulk-session-model.js";
3
- export const BULK_MAIL_EXPOSURE_SCHEMA = 'whoisleuth.bulk-mail-exposure';
4
- export const BULK_MAIL_EXPOSURE_VERSION = 1;
5
- export const BULK_MAIL_EXPOSURE_EXPORT_VERSION = 2;
6
- export const MAX_BULK_MAIL_EXPOSURE_ROWS = 2_000;
1
+ import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
2
+ import { BULK_PROFILE_CONTEXT_MISMATCH_LIMITATION, normalizeBulkProfileContext, normalizeBulkSessionResult, } from "../workspace/bulk-session-model.mjs";
3
+ import { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_VERSION, MAX_BULK_MAIL_EXPOSURE_ROWS, } from "../contracts/investigation-portability.mjs";
4
+ export { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_VERSION, MAX_BULK_MAIL_EXPOSURE_ROWS };
7
5
  const STATE_LABELS = {
8
6
  authenticated_mail: 'Receiving mail with SPF and DMARC',
9
7
  evidence_incomplete: 'Mail evidence incomplete',
@@ -1,8 +1,8 @@
1
- import { normalizeBulkSessionResult, } from "./bulk-session-model.js";
2
- import { BULK_REVIEW_STATES, } from "./bulk-review-model.js";
3
- import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "./artifact-integrity.js";
4
- export const BULK_REVIEW_MANIFEST_SCHEMA = 'whoisleuth.bulk-review-manifest';
5
- export const BULK_REVIEW_MANIFEST_VERSION = 2;
1
+ import { normalizeBulkSessionResult, } from "../workspace/bulk-session-model.mjs";
2
+ import { BULK_REVIEW_STATES, } from "../workspace/bulk-review-model.mjs";
3
+ import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
4
+ import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, } from "../contracts/investigation-portability.mjs";
5
+ export { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION };
6
6
  const MAX_MANIFEST_ROWS = 2_000;
7
7
  const REVIEW_STATE_SET = new Set(BULK_REVIEW_STATES);
8
8
  function timestamp(value, fallback) {
@@ -0,0 +1 @@
1
+ export const BULK_REVIEW_STALE_AFTER_DAYS = 7;