@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -1,13 +1,103 @@
1
1
  // Pure browser-local watchlist collection model. Watchlist history owns the
2
2
  // evidence shape and diff semantics; this module owns collection names, schema
3
3
  // migration, import merging, and exact serialized-byte accounting.
4
- import { MAX_WATCHLIST_DOMAINS, normalizeWatchlistEntry } from "./watchlist-history.mjs";
4
+ import { MAX_WATCHLIST_DOMAINS, normalizeWatchlistEntry, compactWatchlistResults, appendWatchlistScan, mergeWatchlistBaseline, watchlistActiveDomains, assertWatchlistEditable } from "./watchlist-history.mjs";
5
+ import { mergeWatchDomainMetadata, normalizeWatchDomainMetadata } from "./brand-candidate-workflow.mjs";
5
6
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
6
7
  import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
7
- import { MAX_WATCHLIST_INPUTS, MAX_WATCHLIST_NAME_LENGTH, MAX_WATCHLISTS, MAX_WATCHLIST_STORE_BYTES, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
8
+ import { boundedJsonLimitsForBytes, parseBoundedJson } from "../analysis/bounded-json.mjs";
9
+ import { MAX_WATCHLIST_INPUTS, MAX_WATCHLIST_NAME_LENGTH, MAX_WATCHLISTS, MAX_WATCHLIST_STORE_BYTES, MAX_WATCHLIST_PORTABLE_BYTES, WATCHLIST_RECOVERY_METADATA_BYTES, serialiseWorkspacePortableJson, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, WATCHLIST_BROWSER_SUPPORTED_VERSIONS, WATCHLIST_EXPORT_SUPPORTED_VERSIONS, } from "../contracts/workspace-portability.mjs";
8
10
  export { MAX_WATCHLIST_INPUTS, MAX_WATCHLIST_NAME_LENGTH, MAX_WATCHLISTS, MAX_WATCHLIST_STORE_BYTES, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
9
11
  const BLOCKED_NAMES = new Set(['__proto__', 'prototype', 'constructor']);
10
12
  const CONTROL_RE = /[\x00-\x1f\x7f]/;
13
+ /** Restore evidence while retaining every local active member and its context. */
14
+ export function planHostedWatchlistRestore(current, name, input) {
15
+ const normalizedName = normalizeWatchlistName(name);
16
+ if (!normalizedName)
17
+ throw new Error('Hosted watchlist name is invalid.');
18
+ const target = resolveWatchlistMutationTarget(normalizeWatchlistStore(current).watchlists, normalizedName);
19
+ const snapshot = normalizeWatchlistEntry(input);
20
+ assertWatchlistEditable(snapshot);
21
+ const before = new Set(target.previous ? watchlistActiveDomains(target.previous) : []);
22
+ const metadata = normalizeWatchDomainMetadata(target.previous?.domainMetadata, snapshot.results.map(record => record.domain));
23
+ const after = new Set(metadata.map(record => record.domain));
24
+ // Check bytes as well as membership before requesting consent.
25
+ assertWatchlistStoreBudget({ ...current, [target.name]: { ...snapshot, domainMetadata: metadata } });
26
+ return freezePreview(structuredClone({ name: target.name, previous: target.previous, snapshot,
27
+ retained: [...before].sort(), added: [...after].filter(domain => !before.has(domain)).sort(),
28
+ removed: [], capacity: MAX_WATCHLIST_DOMAINS }));
29
+ }
30
+ export function applyReviewedHostedWatchlistRestore(current, reviewed) {
31
+ const fresh = planHostedWatchlistRestore(current, reviewed.name, reviewed.snapshot);
32
+ if (JSON.stringify(fresh) !== JSON.stringify(reviewed))
33
+ throw new Error('The watchlist changed after review. Preview its membership again; nothing was overwritten.');
34
+ return assertWatchlistStoreBudget({ ...current, [fresh.name]: { ...fresh.snapshot,
35
+ domainMetadata: normalizeWatchDomainMetadata(fresh.previous?.domainMetadata, fresh.snapshot.results.map(record => record.domain)) } }).watchlists;
36
+ }
37
+ export function resolveWatchlistMutationTarget(current, requestedName) {
38
+ const existingName = Object.keys(current).find(candidate => candidate.toLowerCase() === requestedName.toLowerCase());
39
+ const previous = existingName ? current[existingName] ?? null : null;
40
+ assertWatchlistEditable(previous);
41
+ if (!previous && Object.keys(current).length >= MAX_WATCHLISTS)
42
+ throw new Error('Watchlist storage is full. Export and remove a watchlist before saving more.');
43
+ return { name: existingName || requestedName, previous };
44
+ }
45
+ function freezePreview(value) {
46
+ if (value && typeof value === 'object') {
47
+ for (const child of Object.values(value))
48
+ freezePreview(child);
49
+ Object.freeze(value);
50
+ }
51
+ return value;
52
+ }
53
+ /** Review current membership and the exact compact submitted evidence together. */
54
+ export function planWatchlistUpdate(current, name, input, mode, operation) {
55
+ const normalizedName = normalizeWatchlistName(name);
56
+ if (!normalizedName)
57
+ throw new Error('Watchlist names must be 1–100 characters and use a safe name.');
58
+ if (!Array.isArray(input) || input.length > MAX_WATCHLIST_DOMAINS)
59
+ throw new Error(`Watchlists are limited to ${MAX_WATCHLIST_DOMAINS} domains.`);
60
+ if (!['fast', 'deep'].includes(mode) || !['merge', 'replace'].includes(operation))
61
+ throw new Error('Choose a supported Monitor update operation.');
62
+ const target = resolveWatchlistMutationTarget(normalizeWatchlistStore(current).watchlists, normalizedName);
63
+ const submitted = compactWatchlistResults(input);
64
+ if (!submitted.length || submitted.length !== input.length)
65
+ throw new Error('The selected Monitor results contain invalid or repeated domains. Nothing was saved.');
66
+ const before = new Set(target.previous ? watchlistActiveDomains(target.previous) : []);
67
+ const after = new Set([...(operation === 'merge' ? before : []), ...submitted.map(record => record.domain)]);
68
+ if (after.size > MAX_WATCHLIST_DOMAINS)
69
+ throw new Error(`The merged watchlist exceeds its ${MAX_WATCHLIST_DOMAINS}-domain limit. Existing members were preserved.`);
70
+ return freezePreview(structuredClone({ name: target.name, operation, mode, previous: target.previous, input: submitted,
71
+ retained: [...before].filter(domain => after.has(domain)).sort(),
72
+ added: [...after].filter(domain => !before.has(domain)).sort(),
73
+ removed: [...before].filter(domain => !after.has(domain)).sort() }));
74
+ }
75
+ /** Reconcile consent inside the transaction, then retain only bounded history. */
76
+ export function applyReviewedWatchlistUpdate(current, reviewed, checkedAt = new Date().toISOString()) {
77
+ const fresh = planWatchlistUpdate(current, reviewed.name, reviewed.input, reviewed.mode, reviewed.operation);
78
+ if (JSON.stringify(fresh) !== JSON.stringify(reviewed))
79
+ throw new Error('The watchlist changed after review. Preview its membership again; nothing was overwritten.');
80
+ // An explicit replacement removes active membership, not retained history.
81
+ const selected = new Set(fresh.input.map(record => record.domain));
82
+ const previous = fresh.operation === 'replace' && fresh.previous
83
+ ? { ...fresh.previous, results: fresh.previous.results.filter(record => selected.has(record.domain)),
84
+ domainMetadata: fresh.previous.domainMetadata.filter(record => selected.has(record.domain)) }
85
+ : fresh.previous;
86
+ const appended = appendWatchlistScan(previous, fresh.input, { mode: fresh.mode, checkedAt });
87
+ let entry = appended.entry;
88
+ if (fresh.operation === 'merge' && fresh.previous) {
89
+ const records = new Map(fresh.previous.results.map(record => [record.domain, record]));
90
+ for (const record of fresh.input)
91
+ records.set(record.domain, record);
92
+ const results = [...records.values()];
93
+ entry = normalizeWatchlistEntry({ ...entry, results,
94
+ baseline: mergeWatchlistBaseline(fresh.previous.baseline, results),
95
+ domainMetadata: fresh.previous.domainMetadata });
96
+ }
97
+ const all = { ...current };
98
+ Object.defineProperty(all, fresh.name, { value: entry, enumerable: true, writable: true, configurable: true });
99
+ return { watchlists: assertWatchlistStoreBudget(all).watchlists, changes: appended.changes };
100
+ }
11
101
  function plainRecord(value) {
12
102
  return ordinaryWorkspaceRecord(value, 'Watchlist input');
13
103
  }
@@ -39,12 +129,34 @@ export function watchlistStoreVersion(raw) {
39
129
  function defineEntry(target, name, entry) {
40
130
  Object.defineProperty(target, name, { value: entry, writable: true, enumerable: true, configurable: true });
41
131
  }
132
+ function assertHistoricalQuality(entry, version) {
133
+ if (version !== null && version < 6 && Object.hasOwn(entry, 'membershipRecovery'))
134
+ throw new TypeError('Watchlist membership recovery requires schema 6.');
135
+ if (version !== null && version < 5 && Object.hasOwn(entry, 'domainMetadata')) {
136
+ throw new TypeError('Domain watch metadata requires Watchlist schema 5; historical evidence was not reinterpreted.');
137
+ }
138
+ if (version !== null && version < 5) {
139
+ const values = [entry.results, entry.baseline, ...((Array.isArray(entry.history) ? entry.history : []).map(event => plainRecord(event)?.changes))];
140
+ if (values.some(rows => Array.isArray(rows) && rows.some(value => {
141
+ const row = plainRecord(value);
142
+ return row && (Object.hasOwn(row, 'hasExternalPasswordForm') || row.field === 'hasExternalPasswordForm');
143
+ })))
144
+ throw new TypeError('Password-form attribution requires Watchlist schema 5; historical evidence was not reinterpreted.');
145
+ }
146
+ if (version !== 2)
147
+ return;
148
+ for (const values of [entry.results, entry.baseline]) {
149
+ if (Array.isArray(values) && values.some(value => plainRecord(value)?.webCollectionQuality !== undefined)) {
150
+ throw new TypeError('Web collection quality requires Watchlist schema 3 or later; historical evidence was not reinterpreted.');
151
+ }
152
+ }
153
+ }
42
154
  export function normalizeWatchlistStore(raw) {
43
155
  assertWorkspaceInputGraph(raw, 'Watchlist store');
44
156
  const root = plainRecord(raw);
45
157
  if (root?.schema === WATCHLIST_SCHEMA)
46
158
  assertWorkspaceDeclaredVersion(raw, 'Watchlist store');
47
- if (root?.schema === WATCHLIST_SCHEMA && watchlistStoreVersion(root) !== WATCHLIST_SCHEMA_VERSION) {
159
+ if (root?.schema === WATCHLIST_SCHEMA && !WATCHLIST_BROWSER_SUPPORTED_VERSIONS.includes(Number(watchlistStoreVersion(root)))) {
48
160
  throw new Error(`Watchlist schema ${String(root.version)} is unsupported; no data was changed.`);
49
161
  }
50
162
  const source = watchlistMap(raw);
@@ -56,7 +168,8 @@ export function normalizeWatchlistStore(raw) {
56
168
  const entry = plainRecord(rawEntry);
57
169
  if (!name || !entry || !Array.isArray(entry.results) || entry.results.length > MAX_WATCHLIST_DOMAINS)
58
170
  continue;
59
- defineEntry(watchlists, name, normalizeWatchlistEntry(entry));
171
+ assertHistoricalQuality(entry, root?.schema === WATCHLIST_SCHEMA ? watchlistStoreVersion(root) : null);
172
+ defineEntry(watchlists, name, normalizeWatchlistEntry(entry, { recoverLegacyMembership: root?.schema === WATCHLIST_SCHEMA && root.version === 5 }));
60
173
  if (Object.keys(watchlists).length >= MAX_WATCHLISTS)
61
174
  break;
62
175
  }
@@ -67,7 +180,8 @@ function byteLength(value) {
67
180
  }
68
181
  export function assertWatchlistStoreBudget(watchlists) {
69
182
  const store = normalizeWatchlistStore(watchlists);
70
- if (byteLength(JSON.stringify(store)) > MAX_WATCHLIST_STORE_BYTES) {
183
+ const recoveryBytes = Object.values(store.watchlists).filter(entry => entry.membershipRecovery).length * WATCHLIST_RECOVERY_METADATA_BYTES;
184
+ if (byteLength(JSON.stringify(store)) - recoveryBytes > MAX_WATCHLIST_STORE_BYTES) {
71
185
  throw new Error('Watchlist storage is full. Export and remove a watchlist before saving more.');
72
186
  }
73
187
  return store;
@@ -93,8 +207,8 @@ export function mergeWatchlistStores(localRaw, importedRaw) {
93
207
  if (importedVersion !== null && importedVersion > WATCHLIST_SCHEMA_VERSION) {
94
208
  throw new Error(`This watchlist file uses newer schema ${importedVersion}. Update the app before importing it.`);
95
209
  }
96
- if (importedVersion !== WATCHLIST_SCHEMA_VERSION) {
97
- throw new Error(`Expected a WHOISleuth watchlist export using schema ${WATCHLIST_SCHEMA_VERSION}.`);
210
+ if (!WATCHLIST_EXPORT_SUPPORTED_VERSIONS.includes(Number(importedVersion))) {
211
+ throw new Error('Expected a supported WHOISleuth watchlist export.');
98
212
  }
99
213
  const local = normalizeWatchlistStore(localRaw).watchlists;
100
214
  const source = watchlistMap(importedRaw) || {};
@@ -109,12 +223,29 @@ export function mergeWatchlistStores(localRaw, importedRaw) {
109
223
  skipped++;
110
224
  continue;
111
225
  }
112
- const normalized = normalizeWatchlistEntry(entry);
226
+ assertHistoricalQuality(entry, importedVersion);
227
+ const normalized = normalizeWatchlistEntry(entry, { recoverLegacyMembership: importedVersion === 5 });
113
228
  if (Object.prototype.hasOwnProperty.call(local, name)) {
114
- if (normalized.updatedAt <= local[name].updatedAt) {
115
- skipped++;
229
+ if (normalized.membershipRecovery || local[name].membershipRecovery) {
230
+ if (JSON.stringify(normalized) === JSON.stringify(local[name])) {
231
+ skipped++;
232
+ continue;
233
+ }
234
+ throw new Error('A watchlist awaiting membership recovery cannot be merged or replaced. Import it under a separate name to preserve both records.');
235
+ }
236
+ const localTime = local[name].updatedAt;
237
+ const previous = local[name];
238
+ const domainMetadata = mergeWatchDomainMetadata(previous.domainMetadata, Object.hasOwn(entry, 'domainMetadata') ? normalized.domainMetadata : []);
239
+ if (!normalized.updatedAt || !localTime || normalized.updatedAt <= localTime) {
240
+ if (JSON.stringify(domainMetadata) !== JSON.stringify(previous.domainMetadata)) {
241
+ defineEntry(local, name, { ...previous, domainMetadata });
242
+ updated++;
243
+ }
244
+ else
245
+ skipped++;
116
246
  continue;
117
247
  }
248
+ normalized.domainMetadata = domainMetadata;
118
249
  updated++;
119
250
  }
120
251
  else if (Object.keys(local).length >= MAX_WATCHLISTS) {
@@ -135,6 +266,16 @@ export function buildWatchlistExport(watchlists, nowIso = new Date().toISOString
135
266
  schema: WATCHLIST_SCHEMA,
136
267
  version: WATCHLIST_SCHEMA_VERSION,
137
268
  exportedAt,
138
- watchlists: normalizeWatchlistStore(watchlists).watchlists,
269
+ watchlists: assertWatchlistStoreBudget(watchlists).watchlists,
139
270
  };
140
271
  }
272
+ /** Preserve readable formatting when it fits; maximum stores need compact JSON. */
273
+ export function serializeWatchlistExport(watchlists, nowIso) {
274
+ const value = buildWatchlistExport(watchlists, nowIso);
275
+ const formatted = serialiseWorkspacePortableJson(value);
276
+ return byteLength(formatted) <= MAX_WATCHLIST_PORTABLE_BYTES ? formatted : JSON.stringify(value);
277
+ }
278
+ export function parseWatchlistExport(json) {
279
+ return parseBoundedJson(json, { label: 'Watchlist import', maximumBytes: MAX_WATCHLIST_PORTABLE_BYTES,
280
+ limits: boundedJsonLimitsForBytes(MAX_WATCHLIST_PORTABLE_BYTES) });
281
+ }
@@ -1,4 +1,4 @@
1
- import { normalizeDomain } from "../cases/case-model.mjs";
1
+ import { normalizeDomain } from "../evidence/domain-name.mjs";
2
2
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
3
3
  import { validWebObservationMode } from "../evidence/lookup-target.mjs";
4
4
  import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
@@ -302,17 +302,42 @@ export function mergeWebsiteSnapshots(localRaw, incomingRaw) {
302
302
  const local = normalizeWebsiteSnapshotStore(localRaw).snapshots;
303
303
  const incoming = normalizeWebsiteSnapshotStore(incomingRaw).snapshots;
304
304
  const byId = new Map(local.map((item) => [item.id, item]));
305
+ const perDomain = new Map();
306
+ for (const item of local)
307
+ perDomain.set(item.domain, (perDomain.get(item.domain) ?? 0) + 1);
308
+ const byteLength = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
309
+ let bytes = byteLength({ schema: WEBSITE_SNAPSHOT_SCHEMA, version: WEBSITE_SNAPSHOT_SCHEMA_VERSION, snapshots: local });
310
+ if (bytes > MAX_WEBSITE_SNAPSHOT_STORE_BYTES)
311
+ throw new Error('Existing website snapshots exceed the storage limit; no imported changes were applied.');
305
312
  let added = 0;
306
313
  let updated = 0;
314
+ const supplied = record(incomingRaw)?.snapshots;
315
+ let skipped = Array.isArray(supplied) ? supplied.length - incoming.length : 0;
316
+ let capacitySkipped = 0;
307
317
  for (const item of incoming) {
308
- if (byId.has(item.id))
318
+ const existing = byId.get(item.id);
319
+ const targetCount = (perDomain.get(item.domain) ?? 0) - Number(existing?.domain === item.domain);
320
+ const delta = byteLength(item) - (existing ? byteLength(existing) : 0) + (!existing && byId.size ? 1 : 0);
321
+ if ((!existing && byId.size >= MAX_WEBSITE_SNAPSHOTS)
322
+ || targetCount >= MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN
323
+ || bytes + delta > MAX_WEBSITE_SNAPSHOT_STORE_BYTES) {
324
+ skipped += 1;
325
+ capacitySkipped += 1;
326
+ continue;
327
+ }
328
+ if (existing) {
309
329
  updated += 1;
330
+ perDomain.set(existing.domain, (perDomain.get(existing.domain) ?? 1) - 1);
331
+ }
310
332
  else
311
333
  added += 1;
312
334
  byId.set(item.id, item);
335
+ perDomain.set(item.domain, (perDomain.get(item.domain) ?? 0) + 1);
336
+ bytes += delta;
313
337
  }
314
338
  const snapshots = normalizeWebsiteSnapshotStore([...byId.values()]).snapshots;
315
- return { snapshots, added, updated, skipped: Math.max(0, incoming.length - added - updated) };
339
+ return { snapshots, added, updated, skipped,
340
+ reason: capacitySkipped ? `${capacitySkipped} imported website snapshots did not fit the remaining per-domain or total storage capacity. Existing local snapshots are preserved.` : '' };
316
341
  }
317
342
  const MAX_RECONCILED_WEBSITE_SOURCES = 16;
318
343
  const UNAVAILABLE_WEBSITE_SOURCE_STATES = new Set([
@@ -2,7 +2,7 @@
2
2
  // This protects a downloaded file while it is locked. It does not encrypt the
3
3
  // active IndexedDB workspace or defend an unlocked page from same-origin code.
4
4
  import { WORKSPACE_ARCHIVE_SCHEMA, WORKSPACE_ARCHIVE_VERSION, SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS, isSupportedWorkspaceArchiveVersion, readWorkspaceArchive, } from "./workspace-archive.mjs";
5
- import { boundedJsonLimitsForBytes, parseBoundedJson } from "../../lib/bounded-json.mjs";
5
+ import { boundedJsonLimitsForBytes, parseBoundedJson } from "../analysis/bounded-json.mjs";
6
6
  import { ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA, ENCRYPTED_WORKSPACE_ARCHIVE_VERSION, MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES, MAX_WORKSPACE_ARCHIVE_BYTES, MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES, MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS, WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS, } from "../contracts/case-portability.mjs";
7
7
  import { assertWorkspaceInputGraph } from "./hostile-input.mjs";
8
8
  import { AES_KEY_BITS, arrayBuffer, assertPassphrase, cryptoProvider, deriveArchiveKey } from "../evidence/passphrase-encryption.mjs";
@@ -1,7 +1,8 @@
1
1
  // Pure, bounded workspace archive composition. Existing typed store models remain the
2
2
  // authority for normalization and merge semantics; this module only packages
3
3
  // their portable contracts, verifies integrity, and previews conflicts.
4
- import { buildCaseExport, enforceStoreBudget, mergeCases, } from "../cases/case-model.mjs";
4
+ import { buildCaseExport, enforceStoreBudget } from "../cases/case-storage-model.mjs";
5
+ import { mergeCases } from "../cases/case-migration-model.mjs";
5
6
  import { assertBrandProfileStoreBudget, buildBrandProfileExport, mergeBrandProfiles, normalizeBrandProfileId, } from "./brand-profile-model.mjs";
6
7
  import { assertCampaignStoreBudget, buildCampaignExport, mergeCampaigns, } from "./campaign-model.mjs";
7
8
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
@@ -584,15 +585,14 @@ function previewVerifiedWorkspaceArchive(archive, localInput, options) {
584
585
  results.push({
585
586
  ...section,
586
587
  status: 'ready',
587
- reason: definition.id === 'settings' && selected && (result.skipped ?? 0) > 0
588
- ? result.reason ?? 'The imported workspace preference was skipped. Existing browser-local context is preserved.'
589
- : section.reason,
588
+ reason: selected && result.reason ? result.reason : section.reason,
590
589
  added: result.added ?? 0,
591
590
  updated: result.updated ?? 0,
592
591
  skipped: result.skipped ?? 0,
593
592
  pruned: result.pruned ?? 0,
594
593
  brandProfileReferencesOmitted: result.brandProfileReferencesOmitted ?? 0,
595
594
  authoredHistoryOmitted: result.authoredHistoryOmitted ?? 0,
595
+ evidenceHistoryOmitted: result.evidenceHistoryOmitted ?? 0,
596
596
  selected,
597
597
  normalizedSettings: result.settings || null,
598
598
  });
@@ -633,6 +633,7 @@ function previewVerifiedWorkspaceArchive(archive, localInput, options) {
633
633
  pruned: 0,
634
634
  brandProfileReferencesOmitted: 0,
635
635
  authoredHistoryOmitted: 0,
636
+ evidenceHistoryOmitted: 0,
636
637
  selected: false,
637
638
  normalizedSettings: dependentId === 'settings' ? null : dependentSection.normalizedSettings ?? null,
638
639
  };