@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1

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 (418) hide show
  1. package/NOTICE +8 -2
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/argument-values.mjs +55 -0
  7. package/cli/arguments.mjs +24 -408
  8. package/cli/artifact-validation/case-response.mjs +65 -44
  9. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  10. package/cli/artifact-validation/lookup-evidence.mjs +15 -9
  11. package/cli/artifact-validation/signed-review.mjs +136 -68
  12. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  13. package/cli/artifact-verify.mjs +92 -24
  14. package/cli/assurance-arguments.mjs +56 -0
  15. package/cli/assurance-command-definitions.mjs +179 -0
  16. package/cli/assurance-command-runner.mjs +40 -5
  17. package/cli/bagit-folder.mjs +57 -0
  18. package/cli/bagit-review.mjs +23 -0
  19. package/cli/bounded-json.mjs +1 -1
  20. package/cli/bulk-checkpoint.mjs +1 -1
  21. package/cli/bulk-command-runner.mjs +4 -2
  22. package/cli/bulk-output.mjs +29 -3
  23. package/cli/bulk.mjs +2 -3
  24. package/cli/case-command.mjs +212 -0
  25. package/cli/case-pack.mjs +50 -32
  26. package/cli/ci-report.mjs +6 -3
  27. package/cli/collection-arguments.mjs +123 -0
  28. package/cli/collection-command-definitions.mjs +330 -0
  29. package/cli/collection-preflight.mjs +8 -1
  30. package/cli/command-argument-grammar.mjs +8 -206
  31. package/cli/command-definition.mjs +329 -0
  32. package/cli/command-reference.mjs +94 -1357
  33. package/cli/command-seeds.mjs +18 -0
  34. package/cli/compare.mjs +1 -1
  35. package/cli/completion.mjs +298 -79
  36. package/cli/config-profile.mjs +8 -6
  37. package/cli/ct-event-intake.mjs +16 -16
  38. package/cli/discover.mjs +5 -4
  39. package/cli/discovery-observation-snapshot.mjs +46 -18
  40. package/cli/discovery-scan-command-runner.mjs +5 -3
  41. package/cli/discovery-scan.mjs +7 -4
  42. package/cli/discovery-snapshot.mjs +2 -2
  43. package/cli/discovery-workflow.mjs +5 -5
  44. package/cli/domain-control-monitor.mjs +50 -20
  45. package/cli/domain-control-observations.mjs +151 -103
  46. package/cli/errors.mjs +37 -6
  47. package/cli/evidence-arguments.mjs +46 -0
  48. package/cli/evidence-command-definitions.mjs +69 -0
  49. package/cli/evidence-command-runner.mjs +18 -8
  50. package/cli/evidence-signing.mjs +5 -4
  51. package/cli/external-observation-mapping.mjs +9 -10
  52. package/cli/formatters/terminal-command-formats.mjs +6 -0
  53. package/cli/formatters/terminal-lookup.mjs +24 -46
  54. package/cli/formatters/terminal-shared.mjs +5 -3
  55. package/cli/formatters/terminal-technology.mjs +114 -0
  56. package/cli/history-arguments.mjs +39 -0
  57. package/cli/history-command-definitions.mjs +88 -0
  58. package/cli/indicator-set-command.mjs +33 -0
  59. package/cli/inline-command-families.mjs +6 -46
  60. package/cli/inline-command-runner.mjs +4 -31
  61. package/cli/input.mjs +2 -2
  62. package/cli/intake-command.mjs +107 -0
  63. package/cli/interactive-launcher.mjs +2 -160
  64. package/cli/interchange-report.mjs +8 -10
  65. package/cli/investigation-artifacts.mjs +176 -0
  66. package/cli/investigation-checkpoint.mjs +10 -0
  67. package/cli/investigation-folder.mjs +114 -0
  68. package/cli/investigation-manifest.mjs +1 -112
  69. package/cli/investigation-package-review.mjs +103 -0
  70. package/cli/investigation-plan.mjs +38 -147
  71. package/cli/investigation-recipes.mjs +177 -0
  72. package/cli/investigation-run.mjs +256 -35
  73. package/cli/local-document-checkpoint.mjs +170 -0
  74. package/cli/local-mmdb-review.mjs +197 -17
  75. package/cli/lookalike-calibration.mjs +1 -1
  76. package/cli/lookup-brief.mjs +2 -2
  77. package/cli/lookup-browser.mjs +5 -12
  78. package/cli/lookup-command-runner.mjs +33 -39
  79. package/cli/lookup-diff.mjs +47 -12
  80. package/cli/lookup-plan.mjs +16 -14
  81. package/cli/lookup-reconcile.mjs +15 -12
  82. package/cli/lookup-timeline.mjs +7 -6
  83. package/cli/mail-header-review.mjs +24 -247
  84. package/cli/mail-review.mjs +75 -21
  85. package/cli/manual.mjs +1 -0
  86. package/cli/network-arguments.mjs +51 -0
  87. package/cli/network-command-definitions.mjs +130 -0
  88. package/cli/network-command-runner.mjs +45 -31
  89. package/cli/offline-evidence-review.mjs +39 -6
  90. package/cli/output-file.mjs +31 -5
  91. package/cli/posture.mjs +1 -1
  92. package/cli/registry-doctor.mjs +3 -1
  93. package/cli/registry-fixture-scaffold.mjs +1 -1
  94. package/cli/registry-support.mjs +1 -1
  95. package/cli/retained-artifact-diff.mjs +10 -9
  96. package/cli/review-arguments.mjs +131 -0
  97. package/cli/review-command-definitions.mjs +273 -0
  98. package/cli/review-command-runner.mjs +104 -149
  99. package/cli/risk-calibration.mjs +8 -8
  100. package/cli/runner.mjs +74 -37
  101. package/cli/saved-lookup.mjs +2 -2
  102. package/cli/selected-input-worker.mjs +63 -0
  103. package/cli/sharing-review.mjs +8 -80
  104. package/cli/signer-trust.mjs +104 -0
  105. package/cli/source-reliability.mjs +75 -84
  106. package/cli/support-arguments.mjs +49 -0
  107. package/cli/support-command-definitions.mjs +209 -0
  108. package/cli/support-command-runner.mjs +2 -2
  109. package/cli/terminal-input.mjs +168 -0
  110. package/cli/workflow-arguments.mjs +86 -0
  111. package/cli/workflow-command-definitions.mjs +78 -0
  112. package/cli/workflow-command-runner.mjs +75 -13
  113. package/docs/cli-reference.md +103 -30
  114. package/docs/cli.md +510 -25
  115. package/docs/contextual-reviews.md +155 -0
  116. package/lib/abort.mjs +4 -1
  117. package/lib/analyst-taxonomy.mjs +2 -40
  118. package/lib/application-version.mjs +1 -1
  119. package/lib/availability.mjs +27 -754
  120. package/lib/bounded-contract-normalizers.mjs +4 -0
  121. package/lib/bounded-file.mjs +3 -1
  122. package/lib/bounded-html-document.mjs +125 -0
  123. package/lib/bounded-json.mjs +2 -290
  124. package/lib/browser-library-profile.mjs +99 -130
  125. package/lib/browser-library-worker.mjs +112 -0
  126. package/lib/candidate-provenance-bounds.mjs +2 -0
  127. package/lib/classify.mjs +1 -1
  128. package/lib/client-behavior-profile.mjs +3 -3
  129. package/lib/ct-query.mjs +2 -27
  130. package/lib/ct-response-bounds.mjs +2 -9
  131. package/lib/ct-search.mjs +89 -46
  132. package/lib/dmarc-discovery.mjs +62 -0
  133. package/lib/dns-convergence-review.mjs +21 -8
  134. package/lib/dns-delegation-health.mjs +36 -22
  135. package/lib/dns-inheritance-review.mjs +207 -0
  136. package/lib/dns-intelligence.mjs +44 -29
  137. package/lib/dns-mx.mjs +1 -1
  138. package/lib/dns-query-coverage.mjs +40 -0
  139. package/lib/dnssec-evidence-validation.mjs +11 -3
  140. package/lib/domain-assurance.mjs +1 -1
  141. package/lib/domain-change-packet.mjs +18 -14
  142. package/lib/domain-change-review.mjs +40 -16
  143. package/lib/domain-control-flight-recorder.mjs +301 -124
  144. package/lib/domain-control-manifest.mjs +126 -58
  145. package/lib/domain-posture-analysis.mjs +41 -11
  146. package/lib/domain-posture-parsers.mjs +88 -6
  147. package/lib/domain-posture.mjs +120 -32
  148. package/lib/domain-registration.mjs +392 -0
  149. package/lib/evidence-export-privacy.mjs +568 -0
  150. package/lib/evidence-export.mjs +12 -593
  151. package/lib/evidence-report.mjs +7 -2
  152. package/lib/external-intelligence-risk.mjs +2 -124
  153. package/lib/favicon.mjs +117 -41
  154. package/lib/generated/cisa-kev-catalog.mjs +50 -4
  155. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  156. package/lib/generated/retire-browser-catalog.mjs +2742 -1034
  157. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  158. package/lib/html-signals.mjs +87 -85
  159. package/lib/http-evidence-bounds.mjs +2 -7
  160. package/lib/http-intelligence.mjs +52 -27
  161. package/lib/idn-confusable-policy.mjs +6 -4
  162. package/lib/idn-confusables.mjs +1 -1
  163. package/lib/interchange-fidelity-registry.mjs +10 -0
  164. package/lib/lookup-cache.mjs +34 -11
  165. package/lib/lookup-child-profile-contract.mjs +24 -15
  166. package/lib/lookup-contract-primitives.mjs +1 -1
  167. package/lib/lookup-diagnostics.mjs +2 -1
  168. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  169. package/lib/lookup-progress.mjs +79 -13
  170. package/lib/lookup-response-contract.mjs +1077 -0
  171. package/lib/lookup-response.mjs +11 -24
  172. package/lib/lookup-source-progress.mjs +24 -35
  173. package/lib/lookup.mjs +49 -28
  174. package/lib/observed-network-context.mjs +43 -27
  175. package/lib/opportunity-scoring.mjs +2 -107
  176. package/lib/outbound-identity.mjs +1 -1
  177. package/lib/outbound-request-bounds.mjs +4 -1
  178. package/lib/page-fingerprints.mjs +87 -134
  179. package/lib/page-role-profile.mjs +12 -8
  180. package/lib/perceptual-hash-comparison.mjs +2 -34
  181. package/lib/perceptual-hash.mjs +79 -24
  182. package/lib/portable-generator.mjs +2 -29
  183. package/lib/project-metadata.mjs +2 -8
  184. package/lib/rdap-bootstrap.mjs +72 -22
  185. package/lib/rdap-client.mjs +26 -14
  186. package/lib/rdap-normalization.mjs +33 -18
  187. package/lib/rdap-search-workbench.mjs +1 -1
  188. package/lib/rdap-transport.mjs +47 -20
  189. package/lib/rdap-types.mjs +2 -1
  190. package/lib/rdap.mjs +18 -3
  191. package/lib/registrable-domain.mjs +2 -36
  192. package/lib/registrar-standing-contract.mjs +2 -1
  193. package/lib/registrar-standing.mjs +9 -13
  194. package/lib/registry-capabilities.mjs +7 -3
  195. package/lib/registry-capability-catalogue.mjs +29 -20
  196. package/lib/registry-dates.mjs +2 -190
  197. package/lib/registry-insights.mjs +56 -18
  198. package/lib/response-policy.mjs +86 -48
  199. package/lib/risk-calibration-summary.mjs +2 -338
  200. package/lib/risk-scoring.mjs +2 -345
  201. package/lib/rpki-evidence.mjs +8 -5
  202. package/lib/safe-fetch.mjs +54 -47
  203. package/lib/scoring-evidence-quality.mjs +2 -107
  204. package/lib/security-txt.mjs +10 -7
  205. package/lib/semantic-version.mjs +2 -54
  206. package/lib/service-binding-dns.mjs +35 -0
  207. package/lib/smtp-transport-review.mjs +75 -34
  208. package/lib/sslbl-certificates.generated.mjs +141 -138
  209. package/lib/static-html-analysis.mjs +117 -146
  210. package/lib/structured-data-identity.mjs +2 -1
  211. package/lib/technology-markup.mjs +100 -0
  212. package/lib/threat-intelligence-manifests.mjs +11 -139
  213. package/lib/threat-intelligence-runtime.mjs +24 -52
  214. package/lib/threat-intelligence-targets.mjs +6 -34
  215. package/lib/threat-intelligence-types.mjs +2 -36
  216. package/lib/typosquat-generator.mjs +29 -16
  217. package/lib/urlhaus-intelligence.mjs +8 -3
  218. package/lib/urlscan-intelligence.mjs +1 -1
  219. package/lib/website-enrichment.mjs +388 -0
  220. package/lib/website-security-posture.mjs +30 -6
  221. package/lib/website-technology.mjs +111 -71
  222. package/lib/whois-authority.mjs +55 -5
  223. package/lib/whois-chain.mjs +9 -2
  224. package/lib/whois-common-formats.mjs +5 -5
  225. package/lib/whois-contracts.mjs +2 -1
  226. package/lib/whois-dialects.mjs +2 -2
  227. package/lib/whois-normalization.mjs +3 -1
  228. package/lib/whois-parser-support.mjs +8 -1
  229. package/lib/whois-parser.mjs +5 -3
  230. package/lib/whois-transport.mjs +23 -7
  231. package/package.json +14 -14
  232. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  233. package/packages/analysis/bounded-json.mjs +330 -0
  234. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  235. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  236. package/packages/analysis/ct-query.mjs +27 -0
  237. package/packages/analysis/ct-response-bounds.mjs +9 -0
  238. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  239. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  240. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  241. package/packages/analysis/opportunity-scoring.mjs +107 -0
  242. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  243. package/packages/analysis/portable-generator.mjs +29 -0
  244. package/packages/analysis/project-metadata.mjs +8 -0
  245. package/packages/analysis/registrable-domain.mjs +56 -0
  246. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  247. package/packages/analysis/risk-scoring.mjs +345 -0
  248. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  249. package/packages/analysis/semantic-version.mjs +54 -0
  250. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  251. package/packages/cases/case-attachment-model.mjs +83 -0
  252. package/packages/cases/case-evidence-links.mjs +135 -0
  253. package/packages/cases/case-evidence-model.mjs +229 -69
  254. package/packages/cases/case-evidence-quality.mjs +5 -0
  255. package/packages/cases/case-export-input.mjs +55 -0
  256. package/packages/cases/case-incident-context.mjs +52 -0
  257. package/packages/cases/case-markdown.mjs +11 -0
  258. package/packages/cases/case-migration-model.mjs +98 -48
  259. package/packages/cases/case-model.mjs +1 -0
  260. package/packages/cases/case-recheck-model.mjs +149 -0
  261. package/packages/cases/case-record-contracts.mjs +1 -1
  262. package/packages/cases/case-record-core.mjs +4 -3
  263. package/packages/cases/case-record-operations.mjs +123 -133
  264. package/packages/cases/case-record-projection.mjs +30 -20
  265. package/packages/cases/case-record-version-input.mjs +19 -0
  266. package/packages/cases/case-report.mjs +114 -67
  267. package/packages/cases/case-requested-evidence.mjs +146 -0
  268. package/packages/cases/case-response-actions.mjs +756 -0
  269. package/packages/cases/case-response-model.mjs +49 -1129
  270. package/packages/cases/case-response-outcomes.mjs +338 -0
  271. package/packages/cases/case-response-packet-render.mjs +157 -0
  272. package/packages/cases/case-response-packet-types.mjs +1 -0
  273. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  274. package/packages/cases/case-response-packet.mjs +58 -593
  275. package/packages/cases/case-response-records.mjs +93 -0
  276. package/packages/cases/case-response-review-inputs.mjs +382 -0
  277. package/packages/cases/case-response-values.mjs +85 -0
  278. package/packages/cases/case-review-package.mjs +45 -0
  279. package/packages/cases/case-selection.mjs +46 -0
  280. package/packages/cases/case-storage-model.mjs +26 -1
  281. package/packages/cases/case-workflow-metadata.mjs +225 -0
  282. package/packages/cases/case-workflow-migration.mjs +43 -0
  283. package/packages/cases/http-summary.mjs +6 -1
  284. package/packages/cases/response-route-freshness.mjs +16 -0
  285. package/packages/comparison/capture-context.mjs +61 -0
  286. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  287. package/packages/comparison/favicon-similarity.mjs +1 -1
  288. package/packages/comparison/page-similarity.mjs +3 -2
  289. package/packages/comparison/relationship-evidence.mjs +103 -14
  290. package/packages/comparison/relationship-provenance.mjs +99 -0
  291. package/packages/contracts/analyst-interchange.mjs +234 -0
  292. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  293. package/packages/contracts/analyst-review-state.mjs +5 -4
  294. package/packages/contracts/case-portability.mjs +370 -114
  295. package/packages/contracts/case-views-contract.mjs +12 -0
  296. package/packages/contracts/case-views.mjs +26 -0
  297. package/packages/contracts/cli-command-semantics.mjs +3 -0
  298. package/packages/contracts/cli-grammar.mjs +6 -0
  299. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  300. package/packages/contracts/cli-lookup.mjs +0 -1
  301. package/packages/contracts/context-review.mjs +24 -0
  302. package/packages/contracts/document-intake.mjs +11 -0
  303. package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
  304. package/packages/contracts/domain-control-manifest.mjs +128 -74
  305. package/packages/contracts/domain-control-monitor.mjs +59 -45
  306. package/packages/contracts/domain-control-review.mjs +200 -121
  307. package/packages/contracts/domain-name.mjs +21 -0
  308. package/packages/contracts/external-observation-interchange.mjs +2 -1
  309. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  310. package/packages/contracts/har-review.mjs +2 -0
  311. package/packages/contracts/http-url.mjs +2 -0
  312. package/packages/contracts/identity-events.mjs +4 -0
  313. package/packages/contracts/investigation-package-limits.mjs +24 -0
  314. package/packages/contracts/investigation-portability.mjs +56 -27
  315. package/packages/contracts/investigation-run.mjs +18 -0
  316. package/packages/contracts/ip-address.mjs +44 -0
  317. package/packages/contracts/lookup-evidence.mjs +35 -0
  318. package/packages/contracts/mail-authentication.mjs +5 -0
  319. package/packages/contracts/message-intake.mjs +31 -0
  320. package/packages/contracts/offline-comparison.mjs +4 -2
  321. package/packages/contracts/page-fingerprints.mjs +15 -0
  322. package/packages/contracts/passive-mail.mjs +10 -0
  323. package/packages/contracts/risk-calibration.mjs +0 -1
  324. package/packages/contracts/schema-compatibility.mjs +1 -1
  325. package/packages/contracts/schema-lifecycle.mjs +83 -153
  326. package/packages/contracts/selected-file-limits.mjs +6 -0
  327. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  328. package/packages/contracts/web-capture.mjs +10 -0
  329. package/packages/contracts/workspace-portability.mjs +219 -238
  330. package/packages/evidence/artifact-integrity.mjs +8 -2
  331. package/packages/evidence/artifact-structure.mjs +105 -0
  332. package/packages/evidence/collection-quality.mjs +67 -0
  333. package/packages/evidence/decision-fact.mjs +4 -1
  334. package/packages/evidence/dns-query-outcome.mjs +72 -0
  335. package/packages/evidence/domain-control-runtime.mjs +155 -27
  336. package/packages/evidence/domain-posture-context.mjs +79 -0
  337. package/packages/evidence/email-recipient.mjs +18 -0
  338. package/packages/evidence/image-regions.mjs +56 -0
  339. package/packages/evidence/latest-observations.mjs +28 -0
  340. package/packages/evidence/lookup-target.mjs +108 -0
  341. package/packages/evidence/observation.mjs +18 -1
  342. package/packages/evidence/passphrase-encryption.mjs +44 -0
  343. package/packages/evidence/png-pixels.mjs +117 -0
  344. package/packages/evidence/public-address-policy.mjs +83 -0
  345. package/packages/evidence/record-identity.mjs +72 -0
  346. package/packages/evidence/registry-dates.mjs +190 -0
  347. package/packages/evidence/retained-file.mjs +69 -0
  348. package/packages/evidence/review-age.mjs +14 -0
  349. package/packages/interchange/bagit.mjs +342 -0
  350. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  351. package/packages/interchange/crc32.mjs +17 -0
  352. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  353. package/packages/interchange/external-findings-import.mjs +93 -61
  354. package/packages/interchange/managed-indicator-export.mjs +35 -0
  355. package/packages/interchange/managed-indicator-set.mjs +210 -0
  356. package/packages/interchange/misp-indicator-export.mjs +93 -0
  357. package/packages/interchange/sharing-policy.mjs +56 -0
  358. package/packages/interchange/stix-indicator-export.mjs +129 -0
  359. package/packages/interchange/web-capture-import.mjs +389 -0
  360. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  361. package/packages/investigation/bulk-mail-exposure.mjs +9 -14
  362. package/packages/investigation/capture-coverage.mjs +57 -0
  363. package/packages/investigation/connector-provenance-review.mjs +100 -0
  364. package/packages/investigation/context-review.mjs +35 -0
  365. package/packages/investigation/document-intake.mjs +81 -0
  366. package/packages/investigation/docx-intake.mjs +133 -0
  367. package/packages/investigation/domain-history-review.mjs +100 -0
  368. package/packages/investigation/domain-transition-review.mjs +47 -0
  369. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  370. package/packages/investigation/har-intake.mjs +68 -0
  371. package/packages/investigation/identity-event-intake.mjs +131 -0
  372. package/packages/investigation/identity-incident-review.mjs +39 -0
  373. package/packages/investigation/incident-sequence-review.mjs +58 -0
  374. package/packages/investigation/intake-report.mjs +19 -0
  375. package/packages/investigation/investigation-bagit.mjs +14 -0
  376. package/packages/investigation/investigation-capsule.mjs +33 -5
  377. package/packages/investigation/investigation-manifest.mjs +200 -0
  378. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  379. package/packages/investigation/investigation-package.mjs +200 -0
  380. package/packages/investigation/link-intake.mjs +179 -0
  381. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  382. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  383. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  384. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  385. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  386. package/packages/investigation/mail-authentication-review.mjs +222 -0
  387. package/packages/investigation/mail-header-identity.mjs +209 -0
  388. package/packages/investigation/message-intake.mjs +195 -0
  389. package/packages/investigation/page-behaviour.mjs +113 -0
  390. package/packages/investigation/pdf-intake.mjs +155 -0
  391. package/packages/investigation/platform-continuity-review.mjs +60 -0
  392. package/packages/investigation/qr-intake.mjs +56 -0
  393. package/packages/investigation/requested-action-hints.mjs +11 -0
  394. package/packages/investigation/selected-input-review.mjs +17 -0
  395. package/packages/investigation/storefront-review.mjs +70 -0
  396. package/packages/monitoring/analyst-review-state.mjs +38 -90
  397. package/packages/workspace/brand-profile-model.mjs +160 -22
  398. package/packages/workspace/bulk-columns.mjs +17 -0
  399. package/packages/workspace/bulk-review-model.mjs +39 -31
  400. package/packages/workspace/bulk-session-model.mjs +112 -27
  401. package/packages/workspace/bulk-sort.mjs +1 -0
  402. package/packages/workspace/campaign-model.mjs +1 -1
  403. package/packages/workspace/case-views.mjs +102 -0
  404. package/packages/workspace/detection-rule-model.mjs +1 -1
  405. package/packages/workspace/hostile-input.mjs +29 -8
  406. package/packages/workspace/investigation-guide.mjs +2 -2
  407. package/packages/workspace/investigation-template-model.mjs +45 -6
  408. package/packages/workspace/page-baseline.mjs +20 -17
  409. package/packages/workspace/relationship-observation-model.mjs +54 -35
  410. package/packages/workspace/shortlist-model.mjs +14 -5
  411. package/packages/workspace/watchlist-history.mjs +30 -14
  412. package/packages/workspace/watchlist-store.mjs +24 -6
  413. package/packages/workspace/website-snapshot-model.mjs +98 -33
  414. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  415. package/packages/workspace/workspace-archive.mjs +90 -25
  416. package/third-party-notices.txt +577 -17
  417. package/lib/hostname.mjs +0 -38
  418. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -2,10 +2,12 @@
2
2
  // the watchlist entry for rescanning, while history stores compact changes
3
3
  // against a last-known baseline so browser-local storage does not grow by one complete
4
4
  // result set on every check.
5
- import { explainRiskScore, normalizeRiskModelVersion } from "../../lib/risk-scoring.mjs";
5
+ import { explainRiskScore, normalizeRiskModelVersion } from "../analysis/risk-scoring.mjs";
6
6
  import { HTTP_SECURITY_HEADER_TOKENS, normalizeHttpSummary } from "../cases/http-summary.mjs";
7
- import { normalizeDomain } from "../cases/case-model.mjs";
7
+ import { normalizeDomain } from "../evidence/domain-name.mjs";
8
8
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
9
+ import { registryDateIso } from "../evidence/registry-dates.mjs";
10
+ import { mergeWebCollectionQuality, normalizeWebCollectionQuality, webCollectionAllowsComparison } from "../evidence/collection-quality.mjs";
9
11
  import { MAX_WATCHLIST_CHANGES_PER_EVENT, MAX_WATCHLIST_DOMAINS, MAX_WATCHLIST_HISTORY_DOMAIN_OPTIONS, MAX_WATCHLIST_HISTORY_EVENTS, MAX_WATCHLIST_INPUT_RECORDS, MAX_WATCHLIST_MUTATION_TYPES, MAX_WATCHLIST_NAMESERVERS, } from "../contracts/workspace-portability.mjs";
10
12
  export { MAX_WATCHLIST_CHANGES_PER_EVENT, MAX_WATCHLIST_DOMAINS, MAX_WATCHLIST_HISTORY_DOMAIN_OPTIONS, MAX_WATCHLIST_HISTORY_EVENTS, MAX_WATCHLIST_INPUT_RECORDS, MAX_WATCHLIST_MUTATION_TYPES, MAX_WATCHLIST_NAMESERVERS, } from "../contracts/workspace-portability.mjs";
11
13
  const MAX_TEXT_LENGTH = 300;
@@ -144,9 +146,6 @@ function boundedText(value, maximum = MAX_TEXT_LENGTH, allowNull = true) {
144
146
  const normalized = value.slice(0, maximum * 4).replace(/\s+/g, ' ').trim().slice(0, maximum).trim();
145
147
  return normalized || (allowNull ? null : '');
146
148
  }
147
- function isoTimestamp(value, fallback = new Date(0).toISOString()) {
148
- return normalizeExplicitIsoTimestamp(value) ?? fallback;
149
- }
150
149
  function boundedInteger(value, maximum, fallback = 0) {
151
150
  return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= maximum
152
151
  ? Number(value)
@@ -155,8 +154,7 @@ function boundedInteger(value, maximum, fallback = 0) {
155
154
  function normalizeDateValue(value) {
156
155
  if (typeof value !== 'string' || value.length > MAX_TIMESTAMP_LENGTH || CONTROL_RE.test(value))
157
156
  return null;
158
- const parsed = Date.parse(value);
159
- return Number.isFinite(parsed) ? new Date(parsed).toISOString().slice(0, 10) : null;
157
+ return registryDateIso(value)?.slice(0, 10) ?? null;
160
158
  }
161
159
  function inferredScanDepth(record) {
162
160
  if (record.scanDepth === 'deep' || record.scanDepth === 'fast')
@@ -205,11 +203,14 @@ function compactRecord(value) {
205
203
  if (!domain)
206
204
  return null;
207
205
  const scanDepth = inferredScanDepth(record);
206
+ const webCollectionQuality = normalizeWebCollectionQuality(record.webCollectionQuality, scanDepth);
208
207
  const httpSummary = normalizeHttpSummary(record) || {};
209
208
  const providedRiskScore = typeof record.riskScore === 'number' && Number.isFinite(record.riskScore)
210
209
  ? Math.max(0, Math.min(100, Math.round(record.riskScore)))
211
210
  : null;
212
- const computedRisk = scanDepth === 'deep' && providedRiskScore === null ? explainRiskScore(record) : null;
211
+ const computedRisk = scanDepth === 'deep' && providedRiskScore === null
212
+ && (!webCollectionQuality || (record.riskScore === undefined
213
+ && webCollectionAllowsComparison('riskScore', webCollectionQuality, scanDepth))) ? explainRiskScore(record) : null;
213
214
  const riskScore = scanDepth === 'deep' ? (providedRiskScore ?? computedRisk?.score ?? null) : null;
214
215
  const riskModelVersion = riskScore === null
215
216
  ? null
@@ -219,6 +220,7 @@ function compactRecord(value) {
219
220
  return {
220
221
  domain,
221
222
  scanDepth,
223
+ ...(webCollectionQuality ? { webCollectionQuality } : {}),
222
224
  availability: hasString(AVAILABILITY_VALUES, record.availability) ? record.availability : null,
223
225
  registrarName: boundedText(record.registrarName),
224
226
  nameservers: normalizeNameservers(record.nameservers),
@@ -260,6 +262,8 @@ export function compactWatchlistResults(results) {
260
262
  function isComparable(field, value, record) {
261
263
  if (DEEP_FIELDS.has(field) && record.scanDepth !== 'deep')
262
264
  return false;
265
+ if (!webCollectionAllowsComparison(field, record.webCollectionQuality, record.scanDepth))
266
+ return false;
263
267
  if (field === 'availability')
264
268
  return hasString(CONCLUSIVE_AVAILABILITY, value);
265
269
  if (field === 'nameservers')
@@ -358,7 +362,7 @@ export function mergeWatchlistBaseline(baseline, current) {
358
362
  return current.map((next) => {
359
363
  const previous = previousByDomain.get(next.domain);
360
364
  const updated = {
361
- ...(previous || {}),
365
+ ...(previous || next),
362
366
  domain: next.domain,
363
367
  scanDepth: previous?.scanDepth === 'deep' || next.scanDepth === 'deep' ? 'deep' : 'fast',
364
368
  };
@@ -370,9 +374,12 @@ export function mergeWatchlistBaseline(baseline, current) {
370
374
  // mean "unversioned" to the comparator, but retaining the key makes the
371
375
  // stored/exported contract unambiguous and prevents consumers from having
372
376
  // to infer why a score was deliberately excluded from comparison.
373
- if (next.scanDepth === 'deep' && typeof next.riskScore === 'number') {
377
+ if (next.scanDepth === 'deep' && typeof next.riskScore === 'number' && isComparable('riskScore', next.riskScore, next)) {
374
378
  updated.riskModelVersion = normalizeRiskModelVersion(next.riskModelVersion);
375
379
  }
380
+ const quality = mergeWebCollectionQuality(previous?.webCollectionQuality, next.webCollectionQuality);
381
+ if (quality)
382
+ updated.webCollectionQuality = quality;
376
383
  return updated;
377
384
  });
378
385
  }
@@ -452,12 +459,13 @@ function normalizeStoredFieldValue(field, value) {
452
459
  }
453
460
  function initialHistoryEvent(entry, baseline) {
454
461
  return {
455
- checkedAt: entry.updatedAt || new Date(0).toISOString(),
462
+ checkedAt: entry.updatedAt,
456
463
  mode: 'saved',
457
464
  resultCount: Array.isArray(entry.results) ? entry.results.length : baseline.length,
458
465
  conclusiveCount: baseline.filter((record) => hasString(CONCLUSIVE_AVAILABILITY, record.availability)).length,
459
466
  changeCount: 0,
460
467
  omittedChanges: 0,
468
+ webComparisonLimitedCount: limitedWebComparisonCount(baseline),
461
469
  changes: [],
462
470
  };
463
471
  }
@@ -480,7 +488,7 @@ export function normalizeWatchlistEntry(entry) {
480
488
  : [];
481
489
  const rawMode = event.mode;
482
490
  return {
483
- checkedAt: isoTimestamp(event.checkedAt, new Date(0).toISOString()),
491
+ checkedAt: normalizeExplicitIsoTimestamp(event.checkedAt),
484
492
  mode: typeof rawMode === 'string' && ['fast', 'deep', 'saved'].includes(rawMode)
485
493
  ? rawMode
486
494
  : 'saved',
@@ -488,12 +496,14 @@ export function normalizeWatchlistEntry(entry) {
488
496
  conclusiveCount: boundedInteger(event.conclusiveCount, MAX_WATCHLIST_DOMAINS),
489
497
  changeCount: boundedInteger(event.changeCount, MAX_CHANGE_COUNT, changes.length),
490
498
  omittedChanges: boundedInteger(event.omittedChanges, MAX_CHANGE_COUNT),
499
+ ...(event.webComparisonLimitedCount !== undefined
500
+ ? { webComparisonLimitedCount: boundedInteger(event.webComparisonLimitedCount, MAX_WATCHLIST_DOMAINS) } : {}),
491
501
  changes,
492
502
  };
493
503
  }).slice(-MAX_WATCHLIST_HISTORY_EVENTS)
494
504
  : [];
495
505
  const normalized = {
496
- updatedAt: isoTimestamp(input.updatedAt, new Date(0).toISOString()),
506
+ updatedAt: normalizeExplicitIsoTimestamp(input.updatedAt),
497
507
  results,
498
508
  baseline,
499
509
  history,
@@ -502,13 +512,18 @@ export function normalizeWatchlistEntry(entry) {
502
512
  normalized.history.push(initialHistoryEvent(normalized, baseline));
503
513
  return normalized;
504
514
  }
515
+ function limitedWebComparisonCount(current, baseline = []) {
516
+ const previous = new Map(baseline.map(record => [record.domain, record]));
517
+ return current.filter(record => record.scanDepth === 'deep' && (!webCollectionAllowsComparison('riskScore', record.webCollectionQuality, record.scanDepth)
518
+ || (previous.has(record.domain) && !webCollectionAllowsComparison('riskScore', previous.get(record.domain).webCollectionQuality, previous.get(record.domain).scanDepth)))).length;
519
+ }
505
520
  /**
506
521
  * @param {object | null} existingEntry
507
522
  * @param {object[]} results
508
523
  * @param {{ checkedAt?: string, mode?: string, ignoredDomains?: Set<string> }} [options]
509
524
  */
510
525
  export function appendWatchlistScan(existingEntry, results, options = {}) {
511
- const checkedAt = isoTimestamp(options.checkedAt, new Date().toISOString());
526
+ const checkedAt = options.checkedAt === undefined ? new Date().toISOString() : normalizeExplicitIsoTimestamp(options.checkedAt);
512
527
  const mode = typeof options.mode === 'string' && ['fast', 'deep', 'saved'].includes(options.mode)
513
528
  ? options.mode
514
529
  : 'saved';
@@ -525,6 +540,7 @@ export function appendWatchlistScan(existingEntry, results, options = {}) {
525
540
  conclusiveCount: current.filter((record) => hasString(CONCLUSIVE_AVAILABILITY, record.availability)).length,
526
541
  changeCount: changes.length,
527
542
  omittedChanges: Math.max(0, changes.length - storedChanges.length),
543
+ webComparisonLimitedCount: limitedWebComparisonCount(current, previous?.baseline),
528
544
  changes: storedChanges,
529
545
  };
530
546
  const history = [...(previous?.history || []), event].slice(-MAX_WATCHLIST_HISTORY_EVENTS);
@@ -4,7 +4,7 @@
4
4
  import { MAX_WATCHLIST_DOMAINS, normalizeWatchlistEntry } from "./watchlist-history.mjs";
5
5
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
6
6
  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";
7
+ import { MAX_WATCHLIST_INPUTS, MAX_WATCHLIST_NAME_LENGTH, MAX_WATCHLISTS, MAX_WATCHLIST_STORE_BYTES, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, WATCHLIST_BROWSER_SUPPORTED_VERSIONS, WATCHLIST_EXPORT_SUPPORTED_VERSIONS, } from "../contracts/workspace-portability.mjs";
8
8
  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
9
  const BLOCKED_NAMES = new Set(['__proto__', 'prototype', 'constructor']);
10
10
  const CONTROL_RE = /[\x00-\x1f\x7f]/;
@@ -39,12 +39,21 @@ export function watchlistStoreVersion(raw) {
39
39
  function defineEntry(target, name, entry) {
40
40
  Object.defineProperty(target, name, { value: entry, writable: true, enumerable: true, configurable: true });
41
41
  }
42
+ function assertHistoricalQuality(entry, version) {
43
+ if (version !== 2)
44
+ return;
45
+ for (const values of [entry.results, entry.baseline]) {
46
+ if (Array.isArray(values) && values.some(value => plainRecord(value)?.webCollectionQuality !== undefined)) {
47
+ throw new TypeError('Web collection quality requires Watchlist schema 3 or later; historical evidence was not reinterpreted.');
48
+ }
49
+ }
50
+ }
42
51
  export function normalizeWatchlistStore(raw) {
43
52
  assertWorkspaceInputGraph(raw, 'Watchlist store');
44
53
  const root = plainRecord(raw);
45
54
  if (root?.schema === WATCHLIST_SCHEMA)
46
55
  assertWorkspaceDeclaredVersion(raw, 'Watchlist store');
47
- if (root?.schema === WATCHLIST_SCHEMA && watchlistStoreVersion(root) !== WATCHLIST_SCHEMA_VERSION) {
56
+ if (root?.schema === WATCHLIST_SCHEMA && !WATCHLIST_BROWSER_SUPPORTED_VERSIONS.includes(Number(watchlistStoreVersion(root)))) {
48
57
  throw new Error(`Watchlist schema ${String(root.version)} is unsupported; no data was changed.`);
49
58
  }
50
59
  const source = watchlistMap(raw);
@@ -56,6 +65,7 @@ export function normalizeWatchlistStore(raw) {
56
65
  const entry = plainRecord(rawEntry);
57
66
  if (!name || !entry || !Array.isArray(entry.results) || entry.results.length > MAX_WATCHLIST_DOMAINS)
58
67
  continue;
68
+ assertHistoricalQuality(entry, root?.schema === WATCHLIST_SCHEMA ? watchlistStoreVersion(root) : null);
59
69
  defineEntry(watchlists, name, normalizeWatchlistEntry(entry));
60
70
  if (Object.keys(watchlists).length >= MAX_WATCHLISTS)
61
71
  break;
@@ -93,8 +103,8 @@ export function mergeWatchlistStores(localRaw, importedRaw) {
93
103
  if (importedVersion !== null && importedVersion > WATCHLIST_SCHEMA_VERSION) {
94
104
  throw new Error(`This watchlist file uses newer schema ${importedVersion}. Update the app before importing it.`);
95
105
  }
96
- if (importedVersion !== WATCHLIST_SCHEMA_VERSION) {
97
- throw new Error(`Expected a WHOISleuth watchlist export using schema ${WATCHLIST_SCHEMA_VERSION}.`);
106
+ if (!WATCHLIST_EXPORT_SUPPORTED_VERSIONS.includes(Number(importedVersion))) {
107
+ throw new Error('Expected a supported WHOISleuth watchlist export.');
98
108
  }
99
109
  const local = normalizeWatchlistStore(localRaw).watchlists;
100
110
  const source = watchlistMap(importedRaw) || {};
@@ -109,15 +119,23 @@ export function mergeWatchlistStores(localRaw, importedRaw) {
109
119
  skipped++;
110
120
  continue;
111
121
  }
112
- if (Object.prototype.hasOwnProperty.call(local, name))
122
+ assertHistoricalQuality(entry, importedVersion);
123
+ const normalized = normalizeWatchlistEntry(entry);
124
+ if (Object.prototype.hasOwnProperty.call(local, name)) {
125
+ const localTime = local[name].updatedAt;
126
+ if (!normalized.updatedAt || !localTime || normalized.updatedAt <= localTime) {
127
+ skipped++;
128
+ continue;
129
+ }
113
130
  updated++;
131
+ }
114
132
  else if (Object.keys(local).length >= MAX_WATCHLISTS) {
115
133
  skipped++;
116
134
  continue;
117
135
  }
118
136
  else
119
137
  added++;
120
- defineEntry(local, name, normalizeWatchlistEntry(entry));
138
+ defineEntry(local, name, normalized);
121
139
  }
122
140
  return { watchlists: local, added, updated, skipped };
123
141
  }
@@ -1,7 +1,8 @@
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
+ import { validWebObservationMode } from "../evidence/lookup-target.mjs";
3
4
  import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
4
- import { MAX_WEBSITE_SNAPSHOTS, MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN, MAX_WEBSITE_SNAPSHOT_STORE_BYTES, SUPPORTED_WEBSITE_SNAPSHOT_SCHEMA_VERSIONS, WEBSITE_SNAPSHOT_SCHEMA, WEBSITE_SNAPSHOT_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
5
+ import { MAX_WEBSITE_SNAPSHOTS, MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN, MAX_WEBSITE_SNAPSHOT_STORE_BYTES, PAGE_FINGERPRINT_PARSERS, SUPPORTED_WEBSITE_SNAPSHOT_SCHEMA_VERSIONS, WEBSITE_SNAPSHOT_SCHEMA, WEBSITE_SNAPSHOT_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
5
6
  export { MAX_WEBSITE_SNAPSHOTS, MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN, MAX_WEBSITE_SNAPSHOT_IMPORT_BYTES, MAX_WEBSITE_SNAPSHOT_STORE_BYTES, SUPPORTED_WEBSITE_SNAPSHOT_SCHEMA_VERSIONS, WEBSITE_SNAPSHOT_SCHEMA, WEBSITE_SNAPSHOT_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
6
7
  const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
7
8
  const DIGEST_RE = /^[a-f0-9]{16,128}$/iu;
@@ -10,6 +11,10 @@ const SERIAL_RE = /^[a-f0-9]{1,128}$/iu;
10
11
  const SNAPSHOT_TECHNOLOGY_ROLES = new Set([
11
12
  'observed_edge', 'application_platform', 'framework_runtime', 'embedded_dependency',
12
13
  ]);
14
+ export const WEBSITE_SNAPSHOT_COLLECTION_LIMITS = Object.freeze({
15
+ technologies: 40, posture: 40, sources: 16, dependencies: 20,
16
+ resourceHosts: 30, trackingIdentifiers: 30, formActionOrigins: 20,
17
+ });
13
18
  function record(value) {
14
19
  return ordinaryWorkspaceRecord(value, 'Website-snapshot input');
15
20
  }
@@ -67,18 +72,26 @@ function profileVersion(value) {
67
72
  ? Number(value)
68
73
  : null;
69
74
  }
70
- function profileProvenance(value, legacy = false) {
75
+ function profileProvenance(value, sourceVersion) {
76
+ const legacy = sourceVersion === 4;
71
77
  if (legacy) {
72
78
  return {
73
79
  technology: { version: null, state: 'legacy_unknown' },
74
80
  securityPosture: { version: null, state: 'legacy_unknown' },
81
+ pageFingerprint: { version: 1, state: 'known' },
75
82
  };
76
83
  }
77
84
  const item = record(value);
78
85
  const technologyItem = record(item?.technology);
79
86
  const securityPostureItem = record(item?.securityPosture);
87
+ const pageFingerprintItem = record(item?.pageFingerprint);
80
88
  const technologyVersion = profileVersion(technologyItem?.version);
81
89
  const securityPostureVersion = profileVersion(securityPostureItem?.version);
90
+ // Exact published snapshot writers 4 and 5 used fingerprint algorithm 1.
91
+ // Unversioned records do not receive invented historical provenance.
92
+ const candidateFingerprintVersion = sourceVersion === 5 ? 1 : profileVersion(pageFingerprintItem?.version);
93
+ const pageFingerprintVersion = candidateFingerprintVersion !== null && Object.hasOwn(PAGE_FINGERPRINT_PARSERS, candidateFingerprintVersion)
94
+ ? candidateFingerprintVersion : null;
82
95
  return {
83
96
  technology: {
84
97
  version: technologyVersion,
@@ -88,6 +101,7 @@ function profileProvenance(value, legacy = false) {
88
101
  version: securityPostureVersion,
89
102
  state: securityPostureVersion === null ? 'legacy_unknown' : 'known',
90
103
  },
104
+ pageFingerprint: { version: pageFingerprintVersion, state: pageFingerprintVersion === null ? 'legacy_unknown' : 'known' },
91
105
  };
92
106
  }
93
107
  function posture(value) {
@@ -183,8 +197,8 @@ function origin(value) {
183
197
  }
184
198
  function identityValues(value) {
185
199
  const item = record(value);
186
- const resourceHosts = values(item?.resourceHosts, 30, (candidate) => normalizeDomain(candidate));
187
- const trackingIdentifiers = values(item?.trackingIdentifiers, 30, (candidate) => {
200
+ const resourceHosts = values(item?.resourceHosts, WEBSITE_SNAPSHOT_COLLECTION_LIMITS.resourceHosts, (candidate) => normalizeDomain(candidate));
201
+ const trackingIdentifiers = values(item?.trackingIdentifiers, WEBSITE_SNAPSHOT_COLLECTION_LIMITS.trackingIdentifiers, (candidate) => {
188
202
  const tracker = record(candidate);
189
203
  const type = text(tracker?.type, 40).toLowerCase();
190
204
  const trackerValue = text(tracker?.value, 64).toUpperCase();
@@ -192,7 +206,7 @@ function identityValues(value) {
192
206
  ? { type, value: trackerValue }
193
207
  : null;
194
208
  });
195
- const formActionOrigins = values(item?.formActionOrigins, 20, origin);
209
+ const formActionOrigins = values(item?.formActionOrigins, WEBSITE_SNAPSHOT_COLLECTION_LIMITS.formActionOrigins, origin);
196
210
  return { resourceHosts, trackingIdentifiers, formActionOrigins };
197
211
  }
198
212
  export function normalizeWebsiteProfileSnapshot(raw, sourceVersion) {
@@ -203,20 +217,24 @@ export function normalizeWebsiteProfileSnapshot(raw, sourceVersion) {
203
217
  const id = text(value?.id, 128);
204
218
  if (!domain || !observedAt || !savedAt || !id)
205
219
  return null;
220
+ const acceptsWebScope = sourceVersion === undefined || sourceVersion >= 6;
221
+ if (acceptsWebScope && !validWebObservationMode(value?.webObservationMode))
222
+ return null;
206
223
  return {
207
224
  id,
208
225
  domain,
226
+ ...(acceptsWebScope && value?.webObservationMode === 'selected_url' ? { webObservationMode: value.webObservationMode } : {}),
209
227
  observedAt,
210
228
  savedAt,
211
229
  complete: value?.complete === true,
212
230
  truncated: value?.truncated === true,
213
- profileProvenance: profileProvenance(value?.profileProvenance, sourceVersion === 4),
214
- technologies: values(value?.technologies, 40, technology),
215
- posture: values(value?.posture, 40, posture),
231
+ profileProvenance: profileProvenance(value?.profileProvenance, sourceVersion),
232
+ technologies: values(value?.technologies, WEBSITE_SNAPSHOT_COLLECTION_LIMITS.technologies, technology),
233
+ posture: values(value?.posture, WEBSITE_SNAPSHOT_COLLECTION_LIMITS.posture, posture),
216
234
  identity: identity(value?.identity),
217
235
  identityValues: identityValues(value?.identityValues),
218
- sources: values(value?.sources, 16, source),
219
- dependencies: values(value?.dependencies, 20, dependency),
236
+ sources: values(value?.sources, WEBSITE_SNAPSHOT_COLLECTION_LIMITS.sources, source),
237
+ dependencies: values(value?.dependencies, WEBSITE_SNAPSHOT_COLLECTION_LIMITS.dependencies, dependency),
220
238
  certificate: certificate(value?.certificate),
221
239
  };
222
240
  }
@@ -284,17 +302,42 @@ export function mergeWebsiteSnapshots(localRaw, incomingRaw) {
284
302
  const local = normalizeWebsiteSnapshotStore(localRaw).snapshots;
285
303
  const incoming = normalizeWebsiteSnapshotStore(incomingRaw).snapshots;
286
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.');
287
312
  let added = 0;
288
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;
289
317
  for (const item of incoming) {
290
- 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) {
291
329
  updated += 1;
330
+ perDomain.set(existing.domain, (perDomain.get(existing.domain) ?? 1) - 1);
331
+ }
292
332
  else
293
333
  added += 1;
294
334
  byId.set(item.id, item);
335
+ perDomain.set(item.domain, (perDomain.get(item.domain) ?? 0) + 1);
336
+ bytes += delta;
295
337
  }
296
338
  const snapshots = normalizeWebsiteSnapshotStore([...byId.values()]).snapshots;
297
- 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.` : '' };
298
341
  }
299
342
  const MAX_RECONCILED_WEBSITE_SOURCES = 16;
300
343
  const UNAVAILABLE_WEBSITE_SOURCE_STATES = new Set([
@@ -366,6 +409,13 @@ export function websiteSnapshotComparisonEvidenceComplete(snapshot) {
366
409
  && websiteSnapshotFieldComplete(snapshot, 'dependency.CNAME')
367
410
  && (!snapshot.certificate || websiteSnapshotFieldComplete(snapshot, 'certificate.observation'));
368
411
  }
412
+ export function websiteSnapshotProfileComparability(before, after, profile) {
413
+ const left = before.profileProvenance[profile].version;
414
+ const right = after.profileProvenance[profile].version;
415
+ if (left === null || right === null)
416
+ return 'legacy_unknown';
417
+ return left === right ? 'comparable' : 'detector_changed';
418
+ }
369
419
  function compareMap(field, before, after, options = {}) {
370
420
  const changes = [];
371
421
  for (const key of [...new Set([...before.keys(), ...after.keys()])].sort()) {
@@ -394,15 +444,13 @@ export function compareWebsiteSnapshots(beforeRaw, afterRaw) {
394
444
  };
395
445
  }
396
446
  const technologyVersionBefore = before.profileProvenance.technology.version;
447
+ const selectedPage = before.webObservationMode === 'selected_url' || after.webObservationMode === 'selected_url';
397
448
  const technologyVersionAfter = after.profileProvenance.technology.version;
398
449
  const postureVersionBefore = before.profileProvenance.securityPosture.version;
399
450
  const postureVersionAfter = after.profileProvenance.securityPosture.version;
400
- const technologyComparability = technologyVersionBefore === null || technologyVersionAfter === null
401
- ? 'legacy_unknown'
402
- : technologyVersionBefore === technologyVersionAfter ? 'comparable' : 'detector_changed';
403
- const postureComparability = postureVersionBefore === null || postureVersionAfter === null
404
- ? 'legacy_unknown'
405
- : postureVersionBefore === postureVersionAfter ? 'comparable' : 'detector_changed';
451
+ const technologyComparability = websiteSnapshotProfileComparability(before, after, 'technology');
452
+ const postureComparability = websiteSnapshotProfileComparability(before, after, 'securityPosture');
453
+ const fingerprintComparability = websiteSnapshotProfileComparability(before, after, 'pageFingerprint');
406
454
  const changes = [
407
455
  ...(technologyComparability === 'comparable' ? compareMap('technology', new Map(before.technologies.map((item) => [item.id, `${item.name}|${item.category}|${item.confidence}|${item.roles.join(',')}`])), new Map(after.technologies.map((item) => [item.id, `${item.name}|${item.category}|${item.confidence}|${item.roles.join(',')}`])), {
408
456
  beforeComplete: () => websiteSnapshotFieldComplete(before, 'technology'),
@@ -462,20 +510,30 @@ export function compareWebsiteSnapshots(beforeRaw, afterRaw) {
462
510
  for (const key of Object.keys(before.identity)) {
463
511
  const left = before.identity[key];
464
512
  const right = after.identity[key];
513
+ if (key !== 'faviconHash' && fingerprintComparability !== 'comparable')
514
+ continue;
465
515
  if (left === right)
466
516
  continue;
467
517
  changes.push({ field: `identity.${key}`, state: left === null || right === null ? 'unavailable' : 'changed', before: left, after: right });
468
518
  }
469
- changes.push(...compareMap('identityValues.resourceHosts', new Map(before.identityValues.resourceHosts.map((item) => [item, item])), new Map(after.identityValues.resourceHosts.map((item) => [item, item])), {
470
- beforeComplete: () => websiteSnapshotFieldComplete(before, 'identityValues.resourceHosts'),
471
- afterComplete: () => websiteSnapshotFieldComplete(after, 'identityValues.resourceHosts'),
472
- }), ...compareMap('identityValues.trackingIdentifiers', new Map(before.identityValues.trackingIdentifiers.map((item) => [`${item.type}:${item.value}`, item.value])), new Map(after.identityValues.trackingIdentifiers.map((item) => [`${item.type}:${item.value}`, item.value])), {
473
- beforeComplete: () => websiteSnapshotFieldComplete(before, 'identityValues.trackingIdentifiers'),
474
- afterComplete: () => websiteSnapshotFieldComplete(after, 'identityValues.trackingIdentifiers'),
475
- }), ...compareMap('identityValues.formActionOrigins', new Map(before.identityValues.formActionOrigins.map((item) => [item, item])), new Map(after.identityValues.formActionOrigins.map((item) => [item, item])), {
476
- beforeComplete: () => websiteSnapshotFieldComplete(before, 'identityValues.formActionOrigins'),
477
- afterComplete: () => websiteSnapshotFieldComplete(after, 'identityValues.formActionOrigins'),
478
- }));
519
+ if (fingerprintComparability !== 'comparable') {
520
+ changes.push({
521
+ field: 'identity.profileVersion', state: 'incomparable',
522
+ before: before.profileProvenance.pageFingerprint.version?.toString() ?? null,
523
+ after: after.profileProvenance.pageFingerprint.version?.toString() ?? null,
524
+ });
525
+ }
526
+ else
527
+ changes.push(...compareMap('identityValues.resourceHosts', new Map(before.identityValues.resourceHosts.map((item) => [item, item])), new Map(after.identityValues.resourceHosts.map((item) => [item, item])), {
528
+ beforeComplete: () => websiteSnapshotFieldComplete(before, 'identityValues.resourceHosts'),
529
+ afterComplete: () => websiteSnapshotFieldComplete(after, 'identityValues.resourceHosts'),
530
+ }), ...compareMap('identityValues.trackingIdentifiers', new Map(before.identityValues.trackingIdentifiers.map((item) => [`${item.type}:${item.value}`, item.value])), new Map(after.identityValues.trackingIdentifiers.map((item) => [`${item.type}:${item.value}`, item.value])), {
531
+ beforeComplete: () => websiteSnapshotFieldComplete(before, 'identityValues.trackingIdentifiers'),
532
+ afterComplete: () => websiteSnapshotFieldComplete(after, 'identityValues.trackingIdentifiers'),
533
+ }), ...compareMap('identityValues.formActionOrigins', new Map(before.identityValues.formActionOrigins.map((item) => [item, item])), new Map(after.identityValues.formActionOrigins.map((item) => [item, item])), {
534
+ beforeComplete: () => websiteSnapshotFieldComplete(before, 'identityValues.formActionOrigins'),
535
+ afterComplete: () => websiteSnapshotFieldComplete(after, 'identityValues.formActionOrigins'),
536
+ }));
479
537
  if (before.certificate || after.certificate) {
480
538
  if (!before.certificate || !after.certificate) {
481
539
  changes.push({
@@ -519,15 +577,22 @@ export function compareWebsiteSnapshots(beforeRaw, afterRaw) {
519
577
  }
520
578
  return {
521
579
  compatible: true,
522
- complete: websiteSnapshotComparisonEvidenceComplete(before)
580
+ complete: !selectedPage && websiteSnapshotComparisonEvidenceComplete(before)
523
581
  && websiteSnapshotComparisonEvidenceComplete(after)
524
582
  && technologyComparability === 'comparable'
525
- && postureComparability === 'comparable',
583
+ && postureComparability === 'comparable'
584
+ && fingerprintComparability === 'comparable',
526
585
  profileComparability: {
527
586
  technology: technologyComparability,
528
587
  securityPosture: postureComparability,
588
+ pageFingerprint: fingerprintComparability,
529
589
  },
530
- changes,
531
- dependencyTransitions,
590
+ changes: selectedPage ? [
591
+ { field: 'web.collectionTarget', state: 'incomparable', before: before.webObservationMode ?? 'homepage', after: after.webObservationMode ?? 'homepage' },
592
+ ...changes.map((change) => change.field.startsWith('certificate.') || change.field.startsWith('source.') || change.field === 'completeness'
593
+ || /^dependency\.(?:CNAME|HTTPS|NS|MX)(?::|$)/u.test(change.field)
594
+ ? change : { ...change, state: 'incomparable' }),
595
+ ] : changes,
596
+ dependencyTransitions: selectedPage ? dependencyTransitions.filter((transition) => transition.recordType !== 'HTTP') : dependencyTransitions,
532
597
  };
533
598
  }
@@ -2,13 +2,13 @@
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 { 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
+ import { AES_KEY_BITS, arrayBuffer, assertPassphrase, cryptoProvider, deriveArchiveKey } from "../evidence/passphrase-encryption.mjs";
8
9
  export { ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA, ENCRYPTED_WORKSPACE_ARCHIVE_VERSION, MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES, MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES, MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS, WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS, };
9
10
  const SALT_BYTES = 16;
10
11
  const IV_BYTES = 12;
11
- const AES_KEY_BITS = 256;
12
12
  const AES_GCM_TAG_BITS = 128;
13
13
  const MAX_CIPHERTEXT_BASE64URL_CHARACTERS = Math.ceil((MAX_WORKSPACE_ARCHIVE_BYTES + AES_GCM_TAG_BITS / 8) * 4 / 3);
14
14
  const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
@@ -25,25 +25,6 @@ function hasExactKeys(value, expected) {
25
25
  const keys = Object.keys(value);
26
26
  return keys.length === expected.length && keys.every((key) => expected.includes(key));
27
27
  }
28
- function arrayBuffer(bytes) {
29
- return bytes.slice().buffer;
30
- }
31
- function assertPassphrase(passphrase) {
32
- if (typeof passphrase !== 'string') {
33
- throw new Error(`Use a backup passphrase with at least ${MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS} characters.`);
34
- }
35
- if (passphrase.length > MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES) {
36
- throw new Error('Backup passphrases are limited to 1024 UTF-8 bytes.');
37
- }
38
- if (Array.from(passphrase).length < MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS) {
39
- throw new Error(`Use a backup passphrase with at least ${MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS} characters.`);
40
- }
41
- const bytes = encoder.encode(passphrase);
42
- if (bytes.byteLength > MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES) {
43
- throw new Error('Backup passphrases are limited to 1024 UTF-8 bytes.');
44
- }
45
- return bytes;
46
- }
47
28
  function base64url(bytes) {
48
29
  let encoded = '';
49
30
  for (let index = 0; index < bytes.length; index += 3) {
@@ -112,7 +93,7 @@ function validTimestamp(value) {
112
93
  return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
113
94
  }
114
95
  function validateEnvelope(raw) {
115
- assertWorkspaceInputGraph(raw, 'Encrypted workspace archive');
96
+ assertWorkspaceInputGraph(raw, 'Encrypted workspace archive', { maximumBytes: MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES });
116
97
  const value = record(raw);
117
98
  if (!value || value.schema !== ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA) {
118
99
  throw new Error('This file is not an encrypted WHOISleuth workspace archive.');
@@ -197,26 +178,6 @@ function validateEnvelope(raw) {
197
178
  ciphertext,
198
179
  };
199
180
  }
200
- function cryptoProvider(provider = globalThis.crypto) {
201
- if (!provider
202
- || typeof provider.getRandomValues !== 'function'
203
- || typeof provider.subtle?.importKey !== 'function'
204
- || typeof provider.subtle?.deriveKey !== 'function'
205
- || typeof provider.subtle?.encrypt !== 'function'
206
- || typeof provider.subtle?.decrypt !== 'function') {
207
- throw new Error('Encrypted workspace archives are unavailable in this browser.');
208
- }
209
- return provider;
210
- }
211
- async function deriveArchiveKey(provider, passphraseBytes, salt, usages) {
212
- const material = await provider.subtle.importKey('raw', arrayBuffer(passphraseBytes), 'PBKDF2', false, ['deriveKey']);
213
- return provider.subtle.deriveKey({
214
- name: 'PBKDF2',
215
- hash: 'SHA-256',
216
- salt: arrayBuffer(salt),
217
- iterations: WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS,
218
- }, material, { name: 'AES-GCM', length: AES_KEY_BITS }, false, usages);
219
- }
220
181
  export function isEncryptedWorkspaceArchive(raw) {
221
182
  return record(raw)?.schema === ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA;
222
183
  }
@@ -227,15 +188,19 @@ export function inspectEncryptedWorkspaceArchive(raw) {
227
188
  }
228
189
  /** Encrypt one already-built workspace archive using browser-native Web Crypto. */
229
190
  export async function encryptWorkspaceArchive(archive, passphrase, provider) {
230
- await readWorkspaceArchive(archive);
231
- const archiveVersion = record(archive)?.version;
232
- if (!isSupportedWorkspaceArchiveVersion(archiveVersion)) {
233
- throw new Error('The workspace archive declares an unsupported content contract.');
234
- }
191
+ assertWorkspaceInputGraph(archive, 'Workspace archive', { maximumBytes: MAX_WORKSPACE_ARCHIVE_BYTES });
235
192
  const plaintext = JSON.stringify(archive);
236
193
  const plaintextBytes = encoder.encode(plaintext);
237
194
  if (plaintextBytes.byteLength > MAX_WORKSPACE_ARCHIVE_BYTES) {
238
- throw new Error('Workspace archives are limited to 10 MiB. Export smaller collections separately before trying again.');
195
+ throw new Error(`Workspace archives are limited to ${MAX_WORKSPACE_ARCHIVE_BYTES / 1024 / 1024} MiB. Export smaller collections separately before trying again.`);
196
+ }
197
+ // Validation and encryption consume the same snapshot, including when the
198
+ // caller changes the original object while checksum verification is pending.
199
+ const snapshot = JSON.parse(plaintext);
200
+ await readWorkspaceArchive(snapshot);
201
+ const archiveVersion = record(snapshot)?.version;
202
+ if (!isSupportedWorkspaceArchiveVersion(archiveVersion)) {
203
+ throw new Error('The workspace archive declares an unsupported content contract.');
239
204
  }
240
205
  const crypto = cryptoProvider(provider);
241
206
  const passphraseBytes = assertPassphrase(passphrase);
@@ -278,30 +243,45 @@ export async function encryptWorkspaceArchive(archive, passphrase, provider) {
278
243
  }
279
244
  /** Decrypt and parse one encrypted archive. The ordinary archive reader remains authoritative. */
280
245
  export async function decryptWorkspaceArchive(raw, passphrase, provider) {
246
+ return (await decryptWorkspaceArchiveWithMetadata(raw, passphrase, provider)).archive;
247
+ }
248
+ /** Authenticate one envelope and return its inner JSON with verified byte metadata. */
249
+ export async function decryptWorkspaceArchiveWithMetadata(raw, passphrase, provider) {
281
250
  const { envelope, salt, iv, ciphertext } = validateEnvelope(raw);
282
251
  const crypto = cryptoProvider(provider);
283
252
  const passphraseBytes = assertPassphrase(passphrase);
284
253
  const { ciphertext: _ciphertext, ...metadata } = envelope;
285
254
  try {
286
- const key = await deriveArchiveKey(crypto, passphraseBytes, salt, ['decrypt']);
287
- const plaintext = await crypto.subtle.decrypt({
288
- name: 'AES-GCM',
289
- iv: arrayBuffer(iv),
290
- additionalData: arrayBuffer(encoder.encode(canonicalMetadata(metadata))),
291
- tagLength: AES_GCM_TAG_BITS,
292
- }, key, arrayBuffer(ciphertext));
255
+ let plaintext;
256
+ try {
257
+ const key = await deriveArchiveKey(crypto, passphraseBytes, salt, ['decrypt']);
258
+ plaintext = await crypto.subtle.decrypt({
259
+ name: 'AES-GCM',
260
+ iv: arrayBuffer(iv),
261
+ additionalData: arrayBuffer(encoder.encode(canonicalMetadata(metadata))),
262
+ tagLength: AES_GCM_TAG_BITS,
263
+ }, key, arrayBuffer(ciphertext));
264
+ }
265
+ catch {
266
+ // Authentication failure cannot distinguish a wrong key from tampering.
267
+ throw new Error('The backup passphrase is incorrect or the encrypted file is corrupted.');
268
+ }
293
269
  if (plaintext.byteLength > MAX_WORKSPACE_ARCHIVE_BYTES) {
294
270
  throw new Error('The decrypted workspace archive exceeds its byte limit.');
295
271
  }
296
- return parseBoundedJson(decoder.decode(plaintext), {
272
+ let json;
273
+ try {
274
+ json = decoder.decode(plaintext);
275
+ }
276
+ catch {
277
+ throw new Error('The backup decrypted, but its contents are not valid UTF-8.');
278
+ }
279
+ const archive = parseBoundedJson(json, {
297
280
  label: 'Decrypted workspace archive',
298
281
  maximumBytes: MAX_WORKSPACE_ARCHIVE_BYTES,
282
+ limits: boundedJsonLimitsForBytes(MAX_WORKSPACE_ARCHIVE_BYTES),
299
283
  });
300
- }
301
- catch (cause) {
302
- if (cause instanceof Error && cause.message === 'The decrypted workspace archive exceeds its byte limit.')
303
- throw cause;
304
- throw new Error('The backup passphrase is incorrect or the encrypted file is corrupted.');
284
+ return { archive, ciphertextBytes: ciphertext.byteLength };
305
285
  }
306
286
  finally {
307
287
  passphraseBytes.fill(0);