@slicedearth/whoisleuth-cli 1.47.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/provider-policy-admission.mjs +3 -1
  100. package/lib/rdap-bootstrap.mjs +76 -16
  101. package/lib/rdap-client.mjs +20 -12
  102. package/lib/rdap-normalization.mjs +306 -75
  103. package/lib/rdap-transport.mjs +2 -2
  104. package/lib/rdap-validation.mjs +65 -14
  105. package/lib/rdap.mjs +2 -2
  106. package/lib/registrable-domain.mjs +36 -0
  107. package/lib/registry-insights.mjs +7 -0
  108. package/lib/response-policy.mjs +27 -4
  109. package/lib/risk-calibration-summary.mjs +35 -33
  110. package/lib/risk-scoring.mjs +4 -3
  111. package/lib/safe-fetch.mjs +62 -40
  112. package/lib/security-txt.mjs +1 -1
  113. package/lib/smtp-transport-review.mjs +8 -5
  114. package/lib/sslbl-intelligence.mjs +6 -8
  115. package/lib/static-html-analysis.mjs +301 -4
  116. package/lib/structured-data-identity.mjs +51 -19
  117. package/lib/threat-intelligence-fixtures.mjs +2 -4
  118. package/lib/threat-intelligence-manifests.mjs +2 -7
  119. package/lib/threat-intelligence-runtime.mjs +3 -5
  120. package/lib/threat-intelligence-types.mjs +3 -0
  121. package/lib/threatfox-intelligence.mjs +7 -3
  122. package/lib/tls-intelligence.mjs +4 -4
  123. package/lib/urlhaus-intelligence.mjs +4 -2
  124. package/lib/urlscan-intelligence.mjs +6 -5
  125. package/lib/website-security-posture.mjs +1 -1
  126. package/lib/website-technology.mjs +1 -1
  127. package/lib/whois-authority.mjs +43 -8
  128. package/lib/whois-chain.mjs +25 -3
  129. package/lib/whois-normalization.mjs +3 -1
  130. package/lib/whois-transport.mjs +12 -3
  131. package/lib/zone-intent-review.mjs +1 -1
  132. package/package.json +12 -4
  133. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  134. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  135. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  136. package/packages/cases/case-migration-model.mjs +476 -0
  137. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  138. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  139. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  140. package/packages/cases/case-record-model.mjs +9 -0
  141. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  142. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  143. package/packages/cases/case-response-model.mjs +1604 -0
  144. package/packages/cases/case-response-packet.mjs +1312 -0
  145. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  146. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  147. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  148. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  149. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  150. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  151. package/packages/comparison/favicon-similarity.mjs +78 -0
  152. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  153. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  154. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  155. package/packages/contracts/analyst-review-state.mjs +50 -0
  156. package/packages/contracts/case-portability.mjs +1310 -0
  157. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  158. package/packages/contracts/cli-lookup.mjs +394 -0
  159. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  160. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  161. package/packages/contracts/domain-control-monitor.mjs +310 -0
  162. package/packages/contracts/domain-control-review.mjs +828 -0
  163. package/packages/contracts/domain-name.mjs +4 -0
  164. package/packages/contracts/external-observation-interchange.mjs +224 -0
  165. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  166. package/packages/contracts/investigation-portability.mjs +331 -0
  167. package/packages/contracts/offline-comparison.mjs +160 -0
  168. package/packages/contracts/risk-calibration.mjs +1288 -0
  169. package/packages/contracts/schema-compatibility.mjs +71 -0
  170. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  171. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  172. package/packages/contracts/workspace-portability.mjs +1347 -0
  173. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  174. package/packages/evidence/decision-fact.mjs +735 -0
  175. package/packages/evidence/domain-control-runtime.mjs +547 -0
  176. package/packages/evidence/domain-name.mjs +41 -0
  177. package/packages/evidence/observation.mjs +384 -0
  178. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  179. package/packages/interchange/retained-text.mjs +8 -0
  180. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  181. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  182. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  183. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  184. package/packages/investigation/bulk-review-policy.mjs +1 -0
  185. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  186. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  187. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  188. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  189. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  190. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  191. package/packages/monitoring/analyst-review-state.mjs +680 -0
  192. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
  193. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  194. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  195. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  196. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  197. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  198. package/packages/workspace/hostile-input.mjs +172 -0
  199. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  200. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  201. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  202. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  203. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  204. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  205. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  206. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  207. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  208. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  209. package/third-party-notices.txt +14 -14
  210. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  211. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  212. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  213. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  214. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  215. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  216. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  217. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  218. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  219. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  220. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  221. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  222. package/frontend/src/lib/analysis/scoring.js +0 -55
  223. package/frontend/src/lib/analysis/utils.js +0 -255
  224. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  225. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  226. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  227. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  228. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  229. package/lib/bounded-json-response.mjs +0 -170
  230. package/lib/lookup-response-contract.mjs +0 -1034
  231. package/lib/observation.mjs +0 -115
  232. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  233. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,54 +1,368 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ import { types as utilTypes } from 'node:util';
2
3
  import { classifyQuery } from "../lib/classify.mjs";
3
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
4
+ import { isSafeJsonObjectKey, scanBoundedJson, } from "../lib/bounded-json.mjs";
5
+ import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
4
6
  import { CliUsageError } from "./arguments.mjs";
5
- const MAX_SAVED_LOOKUP_INPUT_BYTES = 8 * 1024 * 1024;
6
- const MAX_SAVED_LOOKUP_STRING_LENGTH = 1024;
7
- const SAVED_LOOKUP_SCHEMA = 'whoisleuth.cli.lookup';
8
- const SAVED_LOOKUP_SCHEMA_VERSION = 1;
9
- const RDAP_STATUSES = new Set(['success', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled']);
10
- const WHOIS_STATUSES = new Set(['complete', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled']);
7
+ import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "../lib/homepage-metadata-contract.mjs";
8
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
9
+ import { CLI_LOOKUP_SCHEMA as SAVED_LOOKUP_SCHEMA, CLI_LOOKUP_VERSION as SAVED_LOOKUP_SCHEMA_VERSION, PUBLIC_CLI_LOOKUP_VERSION as PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION, MAX_CLI_LOOKUP_BYTES as MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_CLI_LOOKUP_JSON_CONTAINER_ITEMS as MAX_BOUNDED_JSON_CONTAINER_ITEMS, MAX_CLI_LOOKUP_JSON_DEPTH as MAX_BOUNDED_JSON_DEPTH, MAX_CLI_LOOKUP_JSON_KEYS as MAX_BOUNDED_JSON_KEYS, MAX_CLI_LOOKUP_JSON_VALUES as MAX_BOUNDED_JSON_VALUES, MAX_CLI_LOOKUP_STRING_LENGTH as MAX_SAVED_LOOKUP_STRING_LENGTH, SUPPORTED_CLI_LOOKUP_VERSIONS as SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS, } from "../packages/contracts/cli-lookup.mjs";
10
+ const RDAP_STATUSES = new Set([
11
+ 'success', 'complete', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled',
12
+ 'unavailable', 'rate_limited', 'not_applicable', 'stale',
13
+ ]);
14
+ const WHOIS_STATUSES = new Set([
15
+ 'success', 'complete', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled',
16
+ 'unavailable', 'rate_limited', 'not_applicable', 'stale',
17
+ ]);
18
+ const QUERY_TYPES = new Set(['domain', 'ipv4', 'ipv6', 'asn']);
11
19
  function objectOrNull(value) {
12
20
  return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
13
21
  }
22
+ function snapshotLookupResultRoot(value) {
23
+ if (utilTypes.isProxy(value)
24
+ || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
25
+ throw new TypeError('Lookup result must be one ordinary object.');
26
+ }
27
+ const keys = Reflect.ownKeys(value);
28
+ if (keys.length > MAX_BOUNDED_JSON_CONTAINER_ITEMS) {
29
+ throw new TypeError(`Lookup result contains more than ${MAX_BOUNDED_JSON_CONTAINER_ITEMS} fields.`);
30
+ }
31
+ const result = {};
32
+ for (const key of keys) {
33
+ if (typeof key !== 'string' || !isSafeJsonObjectKey(key)) {
34
+ throw new TypeError('Lookup result contains an unsafe object key.');
35
+ }
36
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
37
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
38
+ throw new TypeError('Lookup result contains a non-ordinary object field.');
39
+ }
40
+ result[key] = descriptor.value;
41
+ }
42
+ return result;
43
+ }
44
+ function snapshotKnownDataFields(value, label, allowedKeys) {
45
+ if (!value || typeof value !== 'object' || Array.isArray(value) || utilTypes.isProxy(value)) {
46
+ throw new TypeError(`${label} must be one ordinary object.`);
47
+ }
48
+ const prototype = Object.getPrototypeOf(value);
49
+ if (prototype !== Object.prototype && prototype !== null) {
50
+ throw new TypeError(`${label} must be one ordinary object.`);
51
+ }
52
+ const keys = Reflect.ownKeys(value);
53
+ if (keys.some((key) => typeof key !== 'string' || !allowedKeys.has(key))) {
54
+ throw new TypeError(`${label} contains an unsupported field.`);
55
+ }
56
+ const result = {};
57
+ for (const key of keys) {
58
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
59
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
60
+ throw new TypeError(`${label} contains a non-ordinary field.`);
61
+ }
62
+ result[key] = descriptor.value;
63
+ }
64
+ return result;
65
+ }
66
+ function addJsonBytes(budget, compactBytes, prettyBytes, label) {
67
+ budget.compactBytes += compactBytes;
68
+ budget.prettyBytes += prettyBytes;
69
+ if (budget.compactBytes > MAX_SAVED_LOOKUP_INPUT_BYTES
70
+ || budget.prettyBytes > MAX_SAVED_LOOKUP_INPUT_BYTES) {
71
+ throw new TypeError(`${label} is limited to ${MAX_SAVED_LOOKUP_INPUT_BYTES} bytes.`);
72
+ }
73
+ }
74
+ function jsonStringCodePointBytes(value, index) {
75
+ const code = value.charCodeAt(index);
76
+ if (code === 0x22 || code === 0x5c
77
+ || code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
78
+ return { bytes: 2, advance: 1 };
79
+ }
80
+ if (code <= 0x1f)
81
+ return { bytes: 6, advance: 1 };
82
+ if (code <= 0x7f)
83
+ return { bytes: 1, advance: 1 };
84
+ if (code <= 0x7ff)
85
+ return { bytes: 2, advance: 1 };
86
+ if (code >= 0xd800 && code <= 0xdbff) {
87
+ const next = value.charCodeAt(index + 1);
88
+ return next >= 0xdc00 && next <= 0xdfff
89
+ ? { bytes: 4, advance: 2 }
90
+ : { bytes: 6, advance: 1 };
91
+ }
92
+ if (code >= 0xdc00 && code <= 0xdfff)
93
+ return { bytes: 6, advance: 1 };
94
+ return { bytes: 3, advance: 1 };
95
+ }
96
+ function addJsonStringBytes(budget, value, label) {
97
+ addJsonBytes(budget, 2, 2, label);
98
+ for (let index = 0; index < value.length;) {
99
+ const item = jsonStringCodePointBytes(value, index);
100
+ addJsonBytes(budget, item.bytes, item.bytes, label);
101
+ index += item.advance;
102
+ }
103
+ }
104
+ function jsonStringBytes(value) {
105
+ let bytes = 2;
106
+ for (let index = 0; index < value.length;) {
107
+ const item = jsonStringCodePointBytes(value, index);
108
+ bytes += item.bytes;
109
+ index += item.advance;
110
+ }
111
+ return bytes;
112
+ }
113
+ function jsonNonStringScalarBytes(value) {
114
+ if (value === null)
115
+ return 4;
116
+ if (typeof value === 'boolean')
117
+ return value ? 4 : 5;
118
+ return Buffer.byteLength(String(value), 'utf8');
119
+ }
120
+ function snapshotBoundedJson(value, label) {
121
+ // Reserve the one terminal LF used by the portable pretty-JSON document.
122
+ const budget = { keys: 0, values: 0, compactBytes: 0, prettyBytes: 1 };
123
+ const ancestors = new Set();
124
+ const copy = (current, depth) => {
125
+ budget.values += 1;
126
+ if (budget.values > MAX_BOUNDED_JSON_VALUES) {
127
+ throw new TypeError(`${label} exceeds the ${MAX_BOUNDED_JSON_VALUES}-value limit.`);
128
+ }
129
+ if (depth > MAX_BOUNDED_JSON_DEPTH) {
130
+ throw new TypeError(`${label} exceeds the ${MAX_BOUNDED_JSON_DEPTH}-level nesting limit.`);
131
+ }
132
+ if (typeof current === 'string') {
133
+ addJsonStringBytes(budget, current, label);
134
+ return current;
135
+ }
136
+ if (current === null || typeof current === 'boolean') {
137
+ const bytes = jsonNonStringScalarBytes(current);
138
+ addJsonBytes(budget, bytes, bytes, label);
139
+ return current;
140
+ }
141
+ if (typeof current === 'number') {
142
+ if (!Number.isFinite(current))
143
+ throw new TypeError(`${label} contains a non-finite number.`);
144
+ const bytes = jsonNonStringScalarBytes(current);
145
+ addJsonBytes(budget, bytes, bytes, label);
146
+ return current;
147
+ }
148
+ if (!current || typeof current !== 'object') {
149
+ throw new TypeError(`${label} contains a non-JSON value.`);
150
+ }
151
+ if (utilTypes.isProxy(current)) {
152
+ throw new TypeError(`${label} contains a non-ordinary object.`);
153
+ }
154
+ if (ancestors.has(current))
155
+ throw new TypeError(`${label} contains a cyclic object reference.`);
156
+ ancestors.add(current);
157
+ try {
158
+ if (Array.isArray(current)) {
159
+ if (Object.getPrototypeOf(current) !== Array.prototype
160
+ || Object.prototype.hasOwnProperty.call(current, Symbol.iterator)) {
161
+ throw new TypeError(`${label} contains a non-ordinary array.`);
162
+ }
163
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(current, 'length');
164
+ const length = lengthDescriptor && 'value' in lengthDescriptor ? lengthDescriptor.value : null;
165
+ if (!Number.isSafeInteger(length)
166
+ || Number(length) < 0
167
+ || Number(length) > MAX_BOUNDED_JSON_CONTAINER_ITEMS) {
168
+ throw new TypeError(`${label} contains a container with more than ${MAX_BOUNDED_JSON_CONTAINER_ITEMS} items.`);
169
+ }
170
+ const keys = Reflect.ownKeys(current);
171
+ if (keys.length !== Number(length) + 1
172
+ || keys.at(-1) !== 'length'
173
+ || keys.slice(0, -1).some((key, index) => key !== String(index))) {
174
+ throw new TypeError(`${label} contains a sparse or extended array.`);
175
+ }
176
+ if (Number(length) === 0)
177
+ addJsonBytes(budget, 2, 2, label);
178
+ else
179
+ addJsonBytes(budget, 1, 2, label);
180
+ const result = [];
181
+ for (let index = 0; index < Number(length); index += 1) {
182
+ if (index > 0)
183
+ addJsonBytes(budget, 1, 2, label);
184
+ addJsonBytes(budget, 0, (depth + 1) * 2, label);
185
+ const descriptor = Object.getOwnPropertyDescriptor(current, String(index));
186
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
187
+ throw new TypeError(`${label} contains a non-ordinary array entry.`);
188
+ }
189
+ result.push(copy(descriptor.value, depth + 1));
190
+ }
191
+ if (Number(length) > 0)
192
+ addJsonBytes(budget, 1, 2 + (depth * 2), label);
193
+ return Object.freeze(result);
194
+ }
195
+ const prototype = Object.getPrototypeOf(current);
196
+ if (prototype !== Object.prototype && prototype !== null) {
197
+ throw new TypeError(`${label} contains a non-ordinary object.`);
198
+ }
199
+ const keys = Reflect.ownKeys(current);
200
+ if (keys.length > MAX_BOUNDED_JSON_CONTAINER_ITEMS) {
201
+ throw new TypeError(`${label} contains a container with more than ${MAX_BOUNDED_JSON_CONTAINER_ITEMS} items.`);
202
+ }
203
+ budget.keys += keys.length;
204
+ if (budget.keys > MAX_BOUNDED_JSON_KEYS) {
205
+ throw new TypeError(`${label} exceeds the ${MAX_BOUNDED_JSON_KEYS}-key limit.`);
206
+ }
207
+ if (keys.length === 0)
208
+ addJsonBytes(budget, 2, 2, label);
209
+ else
210
+ addJsonBytes(budget, 1, 2, label);
211
+ const result = {};
212
+ for (const [index, key] of keys.entries()) {
213
+ if (typeof key !== 'string' || !isSafeJsonObjectKey(key)) {
214
+ throw new TypeError(`${label} contains an unsafe object key.`);
215
+ }
216
+ if (index > 0)
217
+ addJsonBytes(budget, 1, 2, label);
218
+ addJsonBytes(budget, 1, ((depth + 1) * 2) + 2, label);
219
+ addJsonStringBytes(budget, key, label);
220
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
221
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
222
+ throw new TypeError(`${label} contains a non-ordinary object field.`);
223
+ }
224
+ result[key] = copy(descriptor.value, depth + 1);
225
+ }
226
+ if (keys.length > 0)
227
+ addJsonBytes(budget, 1, 2 + (depth * 2), label);
228
+ return Object.freeze(result);
229
+ }
230
+ finally {
231
+ ancestors.delete(current);
232
+ }
233
+ };
234
+ const copied = copy(value, 0);
235
+ return Object.freeze({
236
+ value: copied,
237
+ compactBytes: budget.compactBytes,
238
+ prettyBytes: budget.prettyBytes,
239
+ });
240
+ }
14
241
  function requiredBoundedString(value, field) {
15
242
  if (typeof value !== 'string' || !value.trim())
16
- throw new CliUsageError(`${field} is missing.`);
243
+ throw new TypeError(`${field} is missing.`);
17
244
  if (value.length > MAX_SAVED_LOOKUP_STRING_LENGTH) {
18
- throw new CliUsageError(`${field} exceeds the saved-document value limit.`);
245
+ throw new TypeError(`${field} exceeds the saved-document value limit.`);
19
246
  }
20
247
  return value;
21
248
  }
22
249
  function requiredStatus(value, allowed, field) {
23
250
  if (typeof value !== 'string' || !allowed.has(value)) {
24
- throw new CliUsageError(`${field} is missing or unsupported.`);
251
+ throw new TypeError(`${field} is missing or unsupported.`);
25
252
  }
26
253
  return value;
27
254
  }
28
- async function readSavedLookupInputBounded(stream, options = {}) {
29
- const limit = options.limit || MAX_SAVED_LOOKUP_INPUT_BYTES;
255
+ function assertLookupQueryIdentity(document, query, type) {
256
+ let classified;
257
+ try {
258
+ classified = classifyQuery(query);
259
+ }
260
+ catch {
261
+ throw new TypeError('query must identify the declared lookup type.');
262
+ }
263
+ if (classified.type !== type)
264
+ throw new TypeError('query must identify the declared lookup type.');
265
+ if (type !== 'domain' || classified.type !== 'domain') {
266
+ if (document.inputHostname !== undefined
267
+ || document.registrableDomain !== undefined
268
+ || document.isSubdomain !== undefined) {
269
+ throw new TypeError('Non-domain lookup documents cannot contain domain identity fields.');
270
+ }
271
+ return;
272
+ }
273
+ const registrableDomain = requiredBoundedString(document.registrableDomain, 'registrableDomain');
274
+ if (classified.registrableDomain !== registrableDomain.toLowerCase().replace(/\.$/u, '')) {
275
+ throw new TypeError('query must identify the declared registrable domain.');
276
+ }
277
+ if (document.inputHostname !== undefined
278
+ && requiredBoundedString(document.inputHostname, 'inputHostname') !== classified.inputHostname) {
279
+ throw new TypeError('inputHostname must match the classified query.');
280
+ }
281
+ if (document.isSubdomain !== undefined && document.isSubdomain !== classified.isSubdomain) {
282
+ throw new TypeError('isSubdomain must match the classified query.');
283
+ }
284
+ }
285
+ function assertLookupEnvelope(document, label) {
286
+ if (document.schema !== SAVED_LOOKUP_SCHEMA
287
+ || typeof document.version !== 'number'
288
+ || !Number.isSafeInteger(document.version)
289
+ || !SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS.some((version) => version === document.version)) {
290
+ throw new TypeError(`${label} must use ${SAVED_LOOKUP_SCHEMA} version ${SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS.join(' or ')}.`);
291
+ }
292
+ if (typeof document.type !== 'string' || !QUERY_TYPES.has(document.type)) {
293
+ throw new TypeError(`${label} has an unsupported lookup type.`);
294
+ }
295
+ if (document.mode !== 'fast' && document.mode !== 'deep') {
296
+ throw new TypeError(`${label} has an unsupported lookup mode.`);
297
+ }
298
+ const query = requiredBoundedString(document.query, 'query');
299
+ assertLookupQueryIdentity(document, query, document.type);
300
+ const rawGeneratedAt = requiredBoundedString(document.generatedAt, 'generatedAt');
301
+ const generatedAt = normalizeExplicitIsoTimestamp(rawGeneratedAt);
302
+ if (!generatedAt) {
303
+ throw new TypeError('generatedAt must be a valid timestamp with an explicit timezone.');
304
+ }
305
+ const diagnostics = objectOrNull(document.diagnostics);
306
+ const rdapDiagnostics = objectOrNull(diagnostics?.rdap);
307
+ const whoisDiagnostics = objectOrNull(diagnostics?.whois);
308
+ const rdapStatus = requiredStatus(rdapDiagnostics?.status, RDAP_STATUSES, 'diagnostics.rdap.status');
309
+ const whoisStatus = requiredStatus(whoisDiagnostics?.status, WHOIS_STATUSES, 'diagnostics.whois.status');
310
+ const rdap = objectOrNull(document.rdap);
311
+ const whois = objectOrNull(document.whois);
312
+ const availability = objectOrNull(document.availability);
313
+ const pageIdentity = objectOrNull(availability?.pageIdentity);
314
+ const http = objectOrNull(availability?.http);
315
+ const httpResponse = objectOrNull(http?.response);
316
+ const publicationMetadata = pageIdentity?.publicationMetadata;
317
+ const deliveryMetadata = httpResponse?.deliveryMetadata;
318
+ if (document.version === PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION
319
+ && (publicationMetadata !== undefined || deliveryMetadata !== undefined)) {
320
+ throw new TypeError(`${label} version ${PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION} cannot contain version ${SAVED_LOOKUP_SCHEMA_VERSION} homepage metadata.`);
321
+ }
322
+ if (document.version === SAVED_LOOKUP_SCHEMA_VERSION
323
+ && (publicationMetadata !== undefined && (!['success', 'partial'].includes(String(pageIdentity?.status))
324
+ || !validPagePublicationMetadata(publicationMetadata))
325
+ || deliveryMetadata !== undefined && (!['success', 'partial'].includes(String(http?.status))
326
+ || !validHttpDeliveryMetadata(deliveryMetadata)))) {
327
+ throw new TypeError(`${label} contains invalid homepage metadata.`);
328
+ }
329
+ const rdapParsed = objectOrNull(rdap?.parsed);
330
+ const whoisParsed = objectOrNull(whois?.parsed);
331
+ if (rdapStatus === 'success' && !rdapParsed) {
332
+ throw new TypeError('Successful RDAP input is missing normalised parsed data.');
333
+ }
334
+ if ((whoisStatus === 'complete' || whoisStatus === 'partial') && !whoisParsed) {
335
+ throw new TypeError('Successful WHOIS input is missing normalised parsed data.');
336
+ }
337
+ return generatedAt;
338
+ }
339
+ function normalizeCliLookupDocument(value, options = {}) {
30
340
  const label = options.label || 'Saved lookup input';
31
- if (!stream || stream.isTTY)
32
- return '';
33
- const chunks = [];
34
- let total = 0;
35
- for await (const chunk of stream) {
36
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
37
- total += buffer.length;
38
- if (total > limit)
39
- throw new CliUsageError(`${label} is limited to ${limit} bytes.`);
40
- chunks.push(buffer);
341
+ const snapshot = snapshotBoundedJson(value, label);
342
+ const document = objectOrNull(snapshot.value);
343
+ if (!document)
344
+ throw new TypeError(`${label} must be one JSON object.`);
345
+ const generatedAt = assertLookupEnvelope(document, label);
346
+ const generatedAtByteDelta = jsonStringBytes(generatedAt)
347
+ - jsonStringBytes(document.generatedAt);
348
+ if (snapshot.compactBytes + generatedAtByteDelta > MAX_SAVED_LOOKUP_INPUT_BYTES
349
+ || snapshot.prettyBytes + generatedAtByteDelta > MAX_SAVED_LOOKUP_INPUT_BYTES) {
350
+ throw new TypeError(`${label} is limited to ${MAX_SAVED_LOOKUP_INPUT_BYTES} bytes.`);
41
351
  }
42
- return Buffer.concat(chunks).toString('utf8');
352
+ const normalized = {};
353
+ for (const [key, item] of Object.entries(document)) {
354
+ normalized[key] = key === 'generatedAt' ? generatedAt : item;
355
+ }
356
+ return Object.freeze(normalized);
43
357
  }
44
- function parseSavedLookupDocument(text, options = {}) {
358
+ function parseCliLookupDocument(text, options = {}) {
45
359
  const label = options.label || 'Saved lookup input';
46
360
  if (typeof text !== 'string')
47
361
  throw new CliUsageError(`${label} must be a JSON document.`);
48
362
  if (Buffer.byteLength(text, 'utf8') > MAX_SAVED_LOOKUP_INPUT_BYTES) {
49
363
  throw new CliUsageError(`${label} is limited to ${MAX_SAVED_LOOKUP_INPUT_BYTES} bytes.`);
50
364
  }
51
- const normalized = text.replace(/^\uFEFF/, '');
365
+ const normalized = text.replace(/^\uFEFF/u, '');
52
366
  try {
53
367
  scanBoundedJson(normalized);
54
368
  }
@@ -67,44 +381,97 @@ function parseSavedLookupDocument(text, options = {}) {
67
381
  catch {
68
382
  throw new CliUsageError(`${label} must be valid JSON.`);
69
383
  }
70
- const document = objectOrNull(parsed);
71
- if (!document)
72
- throw new CliUsageError(`${label} must be one JSON object.`);
73
- if (document.schema !== SAVED_LOOKUP_SCHEMA || document.version !== SAVED_LOOKUP_SCHEMA_VERSION) {
74
- throw new CliUsageError(`${label} must use ${SAVED_LOOKUP_SCHEMA} version ${SAVED_LOOKUP_SCHEMA_VERSION}.`);
384
+ try {
385
+ return normalizeCliLookupDocument(parsed, { label });
386
+ }
387
+ catch (cause) {
388
+ throw new CliUsageError(cause instanceof Error ? cause.message : `${label} is invalid.`);
75
389
  }
390
+ }
391
+ function parseSavedLookupDocument(text, options = {}) {
392
+ const label = options.label || 'Saved lookup input';
393
+ const document = parseCliLookupDocument(text, { label });
76
394
  if (document.type !== 'domain')
77
395
  throw new CliUsageError(`${label} supports domain lookup documents only.`);
78
- if (document.mode !== 'fast' && document.mode !== 'deep') {
79
- throw new CliUsageError(`${label} has an unsupported lookup mode.`);
396
+ return document;
397
+ }
398
+ function serialiseNormalizedCliLookupDocument(document) {
399
+ const output = `${JSON.stringify(document, null, 2)}\n`;
400
+ if (Buffer.byteLength(output, 'utf8') > MAX_SAVED_LOOKUP_INPUT_BYTES) {
401
+ throw new TypeError(`Lookup document output is limited to ${MAX_SAVED_LOOKUP_INPUT_BYTES} bytes.`);
80
402
  }
81
- const query = requiredBoundedString(document.query, 'query');
82
- const registrableDomain = requiredBoundedString(document.registrableDomain, 'registrableDomain');
83
- try {
84
- const classified = classifyQuery(query);
85
- if (classified.type !== 'domain' || classified.registrableDomain !== registrableDomain.toLowerCase().replace(/\.$/u, '')) {
86
- throw new Error('Saved query does not match its declared registrable domain.');
87
- }
403
+ return output;
404
+ }
405
+ function serializeCliLookupDocument(value) {
406
+ return serialiseNormalizedCliLookupDocument(normalizeCliLookupDocument(value, {
407
+ label: 'Lookup document output',
408
+ }));
409
+ }
410
+ function buildCliLookupDocument(query, classified, result, generatedAt = new Date().toISOString(), mode = 'fast', collectionContext = {}) {
411
+ const normalizedGeneratedAt = normalizeExplicitIsoTimestamp(generatedAt);
412
+ if (!normalizedGeneratedAt) {
413
+ throw new TypeError('Lookup document generation time must be valid and include an explicit timezone.');
88
414
  }
89
- catch {
90
- throw new CliUsageError('query must identify the declared registrable domain.');
415
+ const classifiedSnapshot = snapshotKnownDataFields(classified, 'Lookup classification', new Set(['type', 'value', 'inputHostname', 'registrableDomain', 'isSubdomain']));
416
+ const contextSnapshot = snapshotKnownDataFields(collectionContext, 'Lookup collection context', new Set(['observerLabel', 'vantageLabel']));
417
+ const observerLabel = typeof contextSnapshot.observerLabel === 'string'
418
+ ? contextSnapshot.observerLabel
419
+ : null;
420
+ const vantageLabel = typeof contextSnapshot.vantageLabel === 'string'
421
+ ? contextSnapshot.vantageLabel
422
+ : null;
423
+ const resultSnapshot = snapshotLookupResultRoot(result);
424
+ delete resultSnapshot.collectionContext;
425
+ const classifiedType = classifiedSnapshot.type;
426
+ if (typeof classifiedType !== 'string' || !QUERY_TYPES.has(classifiedType)) {
427
+ throw new TypeError('Lookup classification has an unsupported query type.');
91
428
  }
92
- requiredBoundedString(document.generatedAt, 'generatedAt');
93
- const diagnostics = objectOrNull(document.diagnostics);
94
- const rdapDiagnostics = objectOrNull(diagnostics?.rdap);
95
- const whoisDiagnostics = objectOrNull(diagnostics?.whois);
96
- const rdapStatus = requiredStatus(rdapDiagnostics?.status, RDAP_STATUSES, 'diagnostics.rdap.status');
97
- const whoisStatus = requiredStatus(whoisDiagnostics?.status, WHOIS_STATUSES, 'diagnostics.whois.status');
98
- const rdap = objectOrNull(document.rdap);
99
- const whois = objectOrNull(document.whois);
100
- const rdapParsed = objectOrNull(rdap?.parsed);
101
- const whoisParsed = objectOrNull(whois?.parsed);
102
- if (rdapStatus === 'success' && !rdapParsed) {
103
- throw new CliUsageError('Successful RDAP input is missing normalised parsed data.');
429
+ const candidate = {
430
+ ...resultSnapshot,
431
+ schema: SAVED_LOOKUP_SCHEMA,
432
+ version: SAVED_LOOKUP_SCHEMA_VERSION,
433
+ generatedAt: normalizedGeneratedAt,
434
+ mode: mode === 'deep' ? 'deep' : 'fast',
435
+ query,
436
+ type: classifiedType,
437
+ ...(classifiedType === 'domain' ? {
438
+ inputHostname: classifiedSnapshot.inputHostname,
439
+ registrableDomain: classifiedSnapshot.registrableDomain,
440
+ isSubdomain: classifiedSnapshot.isSubdomain,
441
+ } : {}),
442
+ ...((observerLabel || vantageLabel) ? {
443
+ collectionContext: {
444
+ ...(observerLabel ? { observerLabel } : {}),
445
+ ...(vantageLabel ? { vantageLabel } : {}),
446
+ },
447
+ } : {}),
448
+ };
449
+ if (classifiedType !== 'domain') {
450
+ delete candidate.inputHostname;
451
+ delete candidate.registrableDomain;
452
+ delete candidate.isSubdomain;
104
453
  }
105
- if ((whoisStatus === 'complete' || whoisStatus === 'partial') && !whoisParsed) {
106
- throw new CliUsageError('Successful WHOIS input is missing normalised parsed data.');
454
+ return normalizeCliLookupDocument(candidate, { label: 'Lookup document output' });
455
+ }
456
+ async function readSavedLookupInputBounded(stream, options = {}) {
457
+ const limit = options.limit || MAX_SAVED_LOOKUP_INPUT_BYTES;
458
+ const label = options.label || 'Saved lookup input';
459
+ if (!stream || stream.isTTY)
460
+ return '';
461
+ const chunks = [];
462
+ let total = 0;
463
+ for await (const chunk of stream) {
464
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
465
+ total += buffer.length;
466
+ if (total > limit)
467
+ throw new CliUsageError(`${label} is limited to ${limit} bytes.`);
468
+ chunks.push(buffer);
469
+ }
470
+ try {
471
+ return decodeBoundedUtf8(Buffer.concat(chunks), label);
472
+ }
473
+ catch (cause) {
474
+ throw new CliUsageError(cause instanceof Error ? cause.message : `${label} must contain valid UTF-8 text.`);
107
475
  }
108
- return document;
109
476
  }
110
- export { MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_SAVED_LOOKUP_STRING_LENGTH, SAVED_LOOKUP_SCHEMA, SAVED_LOOKUP_SCHEMA_VERSION, parseSavedLookupDocument, readSavedLookupInputBounded, };
477
+ export { MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_SAVED_LOOKUP_STRING_LENGTH, SAVED_LOOKUP_SCHEMA, PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION, SAVED_LOOKUP_SCHEMA_VERSION, SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS, buildCliLookupDocument, normalizeCliLookupDocument, parseCliLookupDocument, parseSavedLookupDocument, readSavedLookupInputBounded, serializeCliLookupDocument, };
@@ -1,4 +1,5 @@
1
1
  import { hasVerifiedWholeArtifactIntegrity, UnsupportedOfflineArtifactError, verifyOfflineArtifact, } from "./artifact-verify.mjs";
2
+ import { scanBoundedJson } from "../lib/bounded-json.mjs";
2
3
  const SHARING_REVIEW_SCHEMA = 'whoisleuth.cli.sharing-review';
3
4
  const SHARING_REVIEW_VERSION = 2;
4
5
  const MAX_SHARING_REVIEW_BYTES = 15 * 1024 * 1024;
@@ -84,12 +85,16 @@ function tlpLabel(marking) {
84
85
  return `TLP:${marking === 'amber-strict' ? 'AMBER+STRICT' : marking.toUpperCase()}`;
85
86
  }
86
87
  async function buildSharingReview(raw, options, generatedAt = new Date().toISOString()) {
88
+ if (Buffer.byteLength(raw, 'utf8') > MAX_SHARING_REVIEW_BYTES) {
89
+ throw new TypeError(`Sharing review input is limited to ${MAX_SHARING_REVIEW_BYTES} bytes.`);
90
+ }
87
91
  let artifactValue;
88
92
  try {
93
+ scanBoundedJson(raw);
89
94
  artifactValue = JSON.parse(raw);
90
95
  }
91
96
  catch {
92
- throw new TypeError('Sharing review input must be valid JSON.');
97
+ throw new TypeError('Sharing review input must be valid bounded JSON without duplicate keys.');
93
98
  }
94
99
  const artifact = record(artifactValue);
95
100
  if (!artifact)