@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,22 +1,15 @@
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.js";
5
- export const WATCHLIST_SCHEMA_VERSION = 2;
6
- export const WATCHLIST_SCHEMA = 'whoisleuth.watchlists';
7
- export const MAX_WATCHLISTS = 100;
8
- export const MAX_WATCHLIST_INPUTS = MAX_WATCHLISTS * 4;
9
- export const MAX_WATCHLIST_NAME_LENGTH = 100;
10
- // Cases and other analyst stores share the same origin quota. A two-megabyte
11
- // ceiling accommodates practical Bulk results while failing before the browser
12
- // turns storage pressure into an opaque quota exception.
13
- export const MAX_WATCHLIST_STORE_BYTES = 2 * 1024 * 1024;
4
+ import { MAX_WATCHLIST_DOMAINS, normalizeWatchlistEntry } from "./watchlist-history.mjs";
5
+ import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
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";
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";
14
9
  const BLOCKED_NAMES = new Set(['__proto__', 'prototype', 'constructor']);
15
10
  const CONTROL_RE = /[\x00-\x1f\x7f]/;
16
11
  function plainRecord(value) {
17
- return value && typeof value === 'object' && !Array.isArray(value)
18
- ? value
19
- : null;
12
+ return ordinaryWorkspaceRecord(value, 'Watchlist input');
20
13
  }
21
14
  function isEnvelope(value) {
22
15
  return Boolean(value && value.schema === WATCHLIST_SCHEMA && plainRecord(value.watchlists));
@@ -37,14 +30,23 @@ function watchlistMap(raw) {
37
30
  }
38
31
  export function watchlistStoreVersion(raw) {
39
32
  const value = plainRecord(raw);
40
- if (!value || !isEnvelope(value))
41
- return value ? 1 : null;
33
+ if (!value)
34
+ return null;
35
+ if (!isEnvelope(value))
36
+ return WATCHLIST_SCHEMA_VERSION;
42
37
  return typeof value.version === 'number' && Number.isFinite(value.version) && value.version > 0 ? value.version : null;
43
38
  }
44
39
  function defineEntry(target, name, entry) {
45
40
  Object.defineProperty(target, name, { value: entry, writable: true, enumerable: true, configurable: true });
46
41
  }
47
42
  export function normalizeWatchlistStore(raw) {
43
+ assertWorkspaceInputGraph(raw, 'Watchlist store');
44
+ const root = plainRecord(raw);
45
+ if (root?.schema === WATCHLIST_SCHEMA)
46
+ assertWorkspaceDeclaredVersion(raw, 'Watchlist store');
47
+ if (root?.schema === WATCHLIST_SCHEMA && watchlistStoreVersion(root) !== WATCHLIST_SCHEMA_VERSION) {
48
+ throw new Error(`Watchlist schema ${String(root.version)} is unsupported; no data was changed.`);
49
+ }
48
50
  const source = watchlistMap(raw);
49
51
  const watchlists = {};
50
52
  if (!source)
@@ -83,6 +85,9 @@ function validateImportShape(raw) {
83
85
  }
84
86
  }
85
87
  export function mergeWatchlistStores(localRaw, importedRaw) {
88
+ assertWorkspaceInputGraph(localRaw, 'Local watchlist store');
89
+ assertWorkspaceInputGraph(importedRaw, 'Imported watchlist document');
90
+ assertWorkspacePortableVersion(importedRaw, WATCHLIST_SCHEMA_VERSION, 'Imported watchlist document');
86
91
  validateImportShape(importedRaw);
87
92
  const importedVersion = watchlistStoreVersion(importedRaw);
88
93
  if (importedVersion !== null && importedVersion > WATCHLIST_SCHEMA_VERSION) {
@@ -117,11 +122,13 @@ export function mergeWatchlistStores(localRaw, importedRaw) {
117
122
  return { watchlists: local, added, updated, skipped };
118
123
  }
119
124
  export function buildWatchlistExport(watchlists, nowIso = new Date().toISOString()) {
120
- const parsed = typeof nowIso === 'string' ? Date.parse(nowIso) : Number.NaN;
125
+ const exportedAt = normalizeExplicitIsoTimestamp(nowIso);
126
+ if (!exportedAt)
127
+ throw new Error('Watchlist export time must use an explicit timezone.');
121
128
  return {
122
129
  schema: WATCHLIST_SCHEMA,
123
130
  version: WATCHLIST_SCHEMA_VERSION,
124
- exportedAt: Number.isFinite(parsed) ? new Date(parsed).toISOString() : new Date().toISOString(),
131
+ exportedAt,
125
132
  watchlists: normalizeWatchlistStore(watchlists).watchlists,
126
133
  };
127
134
  }
@@ -1,16 +1,14 @@
1
- import { normalizeDomain } from "./case-model.js";
2
- export const WEBSITE_SNAPSHOT_SCHEMA = 'whoisleuth.website-profile-snapshots';
3
- export const WEBSITE_SNAPSHOT_SCHEMA_VERSION = 4;
4
- export const MAX_WEBSITE_SNAPSHOTS = 60;
5
- export const MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN = 12;
6
- export const MAX_WEBSITE_SNAPSHOT_STORE_BYTES = 512 * 1024;
7
- export const MAX_WEBSITE_SNAPSHOT_IMPORT_BYTES = 768 * 1024;
1
+ import { normalizeDomain } from "../cases/case-model.mjs";
2
+ import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
3
+ 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
+ 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";
8
6
  const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
9
7
  const DIGEST_RE = /^[a-f0-9]{16,128}$/iu;
10
8
  const SHA256_RE = /^[a-f0-9]{64}$/iu;
11
9
  const SERIAL_RE = /^[a-f0-9]{1,128}$/iu;
12
10
  function record(value) {
13
- return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
11
+ return ordinaryWorkspaceRecord(value, 'Website-snapshot input');
14
12
  }
15
13
  function text(value, maximum) {
16
14
  return typeof value === 'string' && value.length <= maximum && !CONTROL_RE.test(value)
@@ -18,9 +16,7 @@ function text(value, maximum) {
18
16
  : '';
19
17
  }
20
18
  function timestamp(value) {
21
- const candidate = text(value, 64);
22
- const parsed = Date.parse(candidate);
23
- return Number.isFinite(parsed) ? new Date(parsed).toISOString() : '';
19
+ return normalizeExplicitIsoTimestamp(value) ?? '';
24
20
  }
25
21
  function digest(value) {
26
22
  const candidate = text(value, 128).toLowerCase();
@@ -183,13 +179,20 @@ export function normalizeWebsiteProfileSnapshot(raw) {
183
179
  };
184
180
  }
185
181
  export function normalizeWebsiteSnapshotStore(raw) {
182
+ assertWorkspaceInputGraph(raw, 'Website-snapshot store');
183
+ assertWorkspaceDeclaredVersion(raw, 'Website-snapshot store');
186
184
  const value = record(raw);
187
185
  if (value?.schema === WEBSITE_SNAPSHOT_SCHEMA
188
186
  && Number(value?.version) > WEBSITE_SNAPSHOT_SCHEMA_VERSION) {
189
187
  throw new Error('This website-snapshot collection uses a newer schema and cannot be read safely.');
190
188
  }
189
+ if (value?.schema === WEBSITE_SNAPSHOT_SCHEMA
190
+ && Number.isSafeInteger(value.version)
191
+ && !SUPPORTED_WEBSITE_SNAPSHOT_SCHEMA_VERSIONS.includes(Number(value.version))) {
192
+ throw new Error('This website-snapshot collection uses an unsupported schema and cannot be read safely.');
193
+ }
191
194
  const sourceValues = Array.isArray(raw) ? raw : Array.isArray(value?.snapshots) ? value.snapshots : [];
192
- const snapshots = values(sourceValues, MAX_WEBSITE_SNAPSHOTS * 2, normalizeWebsiteProfileSnapshot);
195
+ const snapshots = values(sourceValues, MAX_WEBSITE_SNAPSHOTS * 2, (candidate) => normalizeWebsiteProfileSnapshot(candidate));
193
196
  const perDomain = new Map();
194
197
  const retained = [];
195
198
  for (const snapshot of snapshots.sort((left, right) => right.savedAt.localeCompare(left.savedAt))) {
@@ -230,6 +233,9 @@ export function buildWebsiteSnapshotExport(raw, generatedAt = new Date().toISOSt
230
233
  return { ...store, generatedAt: timestamp(generatedAt) || new Date().toISOString() };
231
234
  }
232
235
  export function mergeWebsiteSnapshots(localRaw, incomingRaw) {
236
+ assertWorkspaceInputGraph(localRaw, 'Local website-snapshot store');
237
+ assertWorkspaceInputGraph(incomingRaw, 'Imported website-snapshot document');
238
+ assertWorkspacePortableVersion(incomingRaw, WEBSITE_SNAPSHOT_SCHEMA_VERSION, 'Imported website-snapshot document');
233
239
  const local = normalizeWebsiteSnapshotStore(localRaw).snapshots;
234
240
  const incoming = normalizeWebsiteSnapshotStore(incomingRaw).snapshots;
235
241
  const byId = new Map(local.map((item) => [item.id, item]));
@@ -1,19 +1,18 @@
1
1
  // Browser-local authenticated encryption for portable workspace archives.
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
- import { MAX_WORKSPACE_ARCHIVE_BYTES, WORKSPACE_ARCHIVE_SCHEMA, isSupportedWorkspaceArchiveVersion, readWorkspaceArchive, } from "./workspace-archive.js";
5
- export const ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA = 'whoisleuth.encrypted-workspace-archive';
6
- export const ENCRYPTED_WORKSPACE_ARCHIVE_VERSION = 1;
7
- export const WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS = 600_000;
8
- export const MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS = 12;
9
- export const MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES = 1024;
10
- export const MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES = Math.ceil(MAX_WORKSPACE_ARCHIVE_BYTES * 4 / 3) + 4096;
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";
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
+ import { assertWorkspaceInputGraph } from "./hostile-input.mjs";
8
+ 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, };
11
9
  const SALT_BYTES = 16;
12
10
  const IV_BYTES = 12;
13
11
  const AES_KEY_BITS = 256;
14
12
  const AES_GCM_TAG_BITS = 128;
15
13
  const MAX_CIPHERTEXT_BASE64URL_CHARACTERS = Math.ceil((MAX_WORKSPACE_ARCHIVE_BYTES + AES_GCM_TAG_BITS / 8) * 4 / 3);
16
14
  const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
15
+ const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
17
16
  const ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
18
17
  const encoder = new TextEncoder();
19
18
  const decoder = new TextDecoder('utf-8', { fatal: true });
@@ -46,11 +45,20 @@ function assertPassphrase(passphrase) {
46
45
  return bytes;
47
46
  }
48
47
  function base64url(bytes) {
49
- let binary = '';
50
- for (let index = 0; index < bytes.length; index += 0x8000) {
51
- binary += String.fromCharCode(...bytes.subarray(index, Math.min(index + 0x8000, bytes.length)));
48
+ let encoded = '';
49
+ for (let index = 0; index < bytes.length; index += 3) {
50
+ const first = bytes[index] ?? 0;
51
+ const second = bytes[index + 1] ?? 0;
52
+ const third = bytes[index + 2] ?? 0;
53
+ const aggregate = (first << 16) | (second << 8) | third;
54
+ encoded += BASE64URL_ALPHABET[(aggregate >>> 18) & 63];
55
+ encoded += BASE64URL_ALPHABET[(aggregate >>> 12) & 63];
56
+ if (index + 1 < bytes.length)
57
+ encoded += BASE64URL_ALPHABET[(aggregate >>> 6) & 63];
58
+ if (index + 2 < bytes.length)
59
+ encoded += BASE64URL_ALPHABET[aggregate & 63];
52
60
  }
53
- return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');
61
+ return encoded;
54
62
  }
55
63
  function decodeBase64url(value, expectedBytes, label) {
56
64
  const expectedCharacters = expectedBytes === null ? null : Math.ceil(expectedBytes * 4 / 3);
@@ -60,15 +68,25 @@ function decodeBase64url(value, expectedBytes, label) {
60
68
  || !BASE64URL_RE.test(value)) {
61
69
  throw new Error(`The encrypted workspace archive has an invalid ${label}.`);
62
70
  }
63
- const padding = (4 - value.length % 4) % 4;
64
- let binary = '';
65
- try {
66
- binary = atob(value.replaceAll('-', '+').replaceAll('_', '/') + '='.repeat(padding));
67
- }
68
- catch {
71
+ if (value.length % 4 === 1) {
69
72
  throw new Error(`The encrypted workspace archive has an invalid ${label}.`);
70
73
  }
71
- const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
74
+ const bytes = new Uint8Array(Math.floor(value.length * 6 / 8));
75
+ let outputIndex = 0;
76
+ let accumulator = 0;
77
+ let bits = 0;
78
+ for (const character of value) {
79
+ const decoded = BASE64URL_ALPHABET.indexOf(character);
80
+ if (decoded < 0)
81
+ throw new Error(`The encrypted workspace archive has an invalid ${label}.`);
82
+ accumulator = (accumulator << 6) | decoded;
83
+ bits += 6;
84
+ if (bits >= 8) {
85
+ bits -= 8;
86
+ bytes[outputIndex] = (accumulator >>> bits) & 0xff;
87
+ outputIndex += 1;
88
+ }
89
+ }
72
90
  if (expectedBytes !== null && bytes.byteLength !== expectedBytes) {
73
91
  throw new Error(`The encrypted workspace archive has an invalid ${label}.`);
74
92
  }
@@ -94,6 +112,7 @@ function validTimestamp(value) {
94
112
  return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
95
113
  }
96
114
  function validateEnvelope(raw) {
115
+ assertWorkspaceInputGraph(raw, 'Encrypted workspace archive');
97
116
  const value = record(raw);
98
117
  if (!value || value.schema !== ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA) {
99
118
  throw new Error('This file is not an encrypted WHOISleuth workspace archive.');
@@ -111,6 +130,14 @@ function validateEnvelope(raw) {
111
130
  throw new Error('The encrypted workspace archive has an invalid creation time.');
112
131
  }
113
132
  const content = record(value.content);
133
+ if (content?.schema === WORKSPACE_ARCHIVE_SCHEMA && Number.isSafeInteger(content.version)) {
134
+ if (content.version < Math.min(...SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS)) {
135
+ throw new Error(`Encrypted workspace content schema ${String(content.version)} is retired. Decrypt and re-export it as schema ${WORKSPACE_ARCHIVE_VERSION} with the last broad-reader release; no data was changed.`);
136
+ }
137
+ if (content.version > WORKSPACE_ARCHIVE_VERSION) {
138
+ throw new Error(`Encrypted workspace content schema ${String(content.version)} is newer than the supported schema ${WORKSPACE_ARCHIVE_VERSION}; no data was changed.`);
139
+ }
140
+ }
114
141
  if (!content
115
142
  || !hasExactKeys(content, ['schema', 'version'])
116
143
  || content.schema !== WORKSPACE_ARCHIVE_SCHEMA
@@ -266,7 +293,10 @@ export async function decryptWorkspaceArchive(raw, passphrase, provider) {
266
293
  if (plaintext.byteLength > MAX_WORKSPACE_ARCHIVE_BYTES) {
267
294
  throw new Error('The decrypted workspace archive exceeds its byte limit.');
268
295
  }
269
- return JSON.parse(decoder.decode(plaintext));
296
+ return parseBoundedJson(decoder.decode(plaintext), {
297
+ label: 'Decrypted workspace archive',
298
+ maximumBytes: MAX_WORKSPACE_ARCHIVE_BYTES,
299
+ });
270
300
  }
271
301
  catch (cause) {
272
302
  if (cause instanceof Error && cause.message === 'The decrypted workspace archive exceeds its byte limit.')