@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,72 +1,307 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ import { types as utilTypes } from 'node:util';
3
+ import { classifyQuery } from "../lib/classify.mjs";
4
+ import { isSafeJsonObjectKey, scanBoundedJson, } from "../lib/bounded-json.mjs";
5
+ import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
2
6
  import { CliUsageError } from "./arguments.mjs";
3
- const MAX_SAVED_LOOKUP_INPUT_BYTES = 8 * 1024 * 1024;
4
- const MAX_SAVED_LOOKUP_STRING_LENGTH = 1024;
5
- const SAVED_LOOKUP_SCHEMA = 'whoisleuth.cli.lookup';
6
- const SAVED_LOOKUP_SCHEMA_VERSION = 1;
7
- const RDAP_STATUSES = new Set(['success', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled']);
8
- 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']);
9
19
  function objectOrNull(value) {
10
20
  return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
11
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
+ }
12
241
  function requiredBoundedString(value, field) {
13
242
  if (typeof value !== 'string' || !value.trim())
14
- throw new CliUsageError(`${field} is missing.`);
243
+ throw new TypeError(`${field} is missing.`);
15
244
  if (value.length > MAX_SAVED_LOOKUP_STRING_LENGTH) {
16
- throw new CliUsageError(`${field} exceeds the saved-document value limit.`);
245
+ throw new TypeError(`${field} exceeds the saved-document value limit.`);
17
246
  }
18
247
  return value;
19
248
  }
20
249
  function requiredStatus(value, allowed, field) {
21
250
  if (typeof value !== 'string' || !allowed.has(value)) {
22
- throw new CliUsageError(`${field} is missing or unsupported.`);
251
+ throw new TypeError(`${field} is missing or unsupported.`);
23
252
  }
24
253
  return value;
25
254
  }
26
- async function readSavedLookupInputBounded(stream, options = {}) {
27
- const limit = options.limit || MAX_SAVED_LOOKUP_INPUT_BYTES;
28
- const label = options.label || 'Saved lookup input';
29
- if (!stream || stream.isTTY)
30
- return '';
31
- const chunks = [];
32
- let total = 0;
33
- for await (const chunk of stream) {
34
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
35
- total += buffer.length;
36
- if (total > limit)
37
- throw new CliUsageError(`${label} is limited to ${limit} bytes.`);
38
- chunks.push(buffer);
39
- }
40
- return Buffer.concat(chunks).toString('utf8');
41
- }
42
- function parseSavedLookupDocument(text, options = {}) {
43
- const label = options.label || 'Saved lookup input';
44
- if (typeof text !== 'string')
45
- throw new CliUsageError(`${label} must be a JSON document.`);
46
- if (Buffer.byteLength(text, 'utf8') > MAX_SAVED_LOOKUP_INPUT_BYTES) {
47
- throw new CliUsageError(`${label} is limited to ${MAX_SAVED_LOOKUP_INPUT_BYTES} bytes.`);
48
- }
49
- let parsed;
255
+ function assertLookupQueryIdentity(document, query, type) {
256
+ let classified;
50
257
  try {
51
- parsed = JSON.parse(text.replace(/^\uFEFF/, ''));
258
+ classified = classifyQuery(query);
52
259
  }
53
260
  catch {
54
- throw new CliUsageError(`${label} must be valid JSON.`);
261
+ throw new TypeError('query must identify the declared lookup type.');
55
262
  }
56
- const document = objectOrNull(parsed);
57
- if (!document)
58
- throw new CliUsageError(`${label} must be one JSON object.`);
59
- if (document.schema !== SAVED_LOOKUP_SCHEMA || document.version !== SAVED_LOOKUP_SCHEMA_VERSION) {
60
- throw new CliUsageError(`${label} must use ${SAVED_LOOKUP_SCHEMA} version ${SAVED_LOOKUP_SCHEMA_VERSION}.`);
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.`);
61
294
  }
62
- if (document.type !== 'domain')
63
- throw new CliUsageError(`${label} supports domain lookup documents only.`);
64
295
  if (document.mode !== 'fast' && document.mode !== 'deep') {
65
- throw new CliUsageError(`${label} has an unsupported lookup mode.`);
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.');
66
304
  }
67
- requiredBoundedString(document.query, 'query');
68
- requiredBoundedString(document.registrableDomain, 'registrableDomain');
69
- requiredBoundedString(document.generatedAt, 'generatedAt');
70
305
  const diagnostics = objectOrNull(document.diagnostics);
71
306
  const rdapDiagnostics = objectOrNull(diagnostics?.rdap);
72
307
  const whoisDiagnostics = objectOrNull(diagnostics?.whois);
@@ -74,14 +309,169 @@ function parseSavedLookupDocument(text, options = {}) {
74
309
  const whoisStatus = requiredStatus(whoisDiagnostics?.status, WHOIS_STATUSES, 'diagnostics.whois.status');
75
310
  const rdap = objectOrNull(document.rdap);
76
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
+ }
77
329
  const rdapParsed = objectOrNull(rdap?.parsed);
78
330
  const whoisParsed = objectOrNull(whois?.parsed);
79
331
  if (rdapStatus === 'success' && !rdapParsed) {
80
- throw new CliUsageError('Successful RDAP input is missing normalised parsed data.');
332
+ throw new TypeError('Successful RDAP input is missing normalised parsed data.');
81
333
  }
82
334
  if ((whoisStatus === 'complete' || whoisStatus === 'partial') && !whoisParsed) {
83
- throw new CliUsageError('Successful WHOIS input is missing normalised parsed data.');
335
+ throw new TypeError('Successful WHOIS input is missing normalised parsed data.');
336
+ }
337
+ return generatedAt;
338
+ }
339
+ function normalizeCliLookupDocument(value, options = {}) {
340
+ const label = options.label || 'Saved lookup input';
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.`);
351
+ }
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);
357
+ }
358
+ function parseCliLookupDocument(text, options = {}) {
359
+ const label = options.label || 'Saved lookup input';
360
+ if (typeof text !== 'string')
361
+ throw new CliUsageError(`${label} must be a JSON document.`);
362
+ if (Buffer.byteLength(text, 'utf8') > MAX_SAVED_LOOKUP_INPUT_BYTES) {
363
+ throw new CliUsageError(`${label} is limited to ${MAX_SAVED_LOOKUP_INPUT_BYTES} bytes.`);
364
+ }
365
+ const normalized = text.replace(/^\uFEFF/u, '');
366
+ try {
367
+ scanBoundedJson(normalized);
368
+ }
369
+ catch (cause) {
370
+ const detail = cause instanceof Error ? cause.message : '';
371
+ if (detail === 'Artefact input is not valid JSON.') {
372
+ throw new CliUsageError(`${label} must be valid JSON.`);
373
+ }
374
+ const boundedDetail = detail.replace(/^Artefact JSON /u, '');
375
+ throw new CliUsageError(boundedDetail ? `${label} ${boundedDetail}` : `${label} must be valid JSON.`);
376
+ }
377
+ let parsed;
378
+ try {
379
+ parsed = JSON.parse(normalized);
380
+ }
381
+ catch {
382
+ throw new CliUsageError(`${label} must be valid JSON.`);
383
+ }
384
+ try {
385
+ return normalizeCliLookupDocument(parsed, { label });
386
+ }
387
+ catch (cause) {
388
+ throw new CliUsageError(cause instanceof Error ? cause.message : `${label} is invalid.`);
84
389
  }
390
+ }
391
+ function parseSavedLookupDocument(text, options = {}) {
392
+ const label = options.label || 'Saved lookup input';
393
+ const document = parseCliLookupDocument(text, { label });
394
+ if (document.type !== 'domain')
395
+ throw new CliUsageError(`${label} supports domain lookup documents only.`);
85
396
  return document;
86
397
  }
87
- export { MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_SAVED_LOOKUP_STRING_LENGTH, SAVED_LOOKUP_SCHEMA, SAVED_LOOKUP_SCHEMA_VERSION, parseSavedLookupDocument, readSavedLookupInputBounded, };
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.`);
402
+ }
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.');
414
+ }
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.');
428
+ }
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;
453
+ }
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.`);
475
+ }
476
+ }
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,6 +1,7 @@
1
- import { UnsupportedOfflineArtifactError, verifyOfflineArtifact, } from "./artifact-verify.mjs";
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
- const SHARING_REVIEW_VERSION = 1;
4
+ const SHARING_REVIEW_VERSION = 2;
4
5
  const MAX_SHARING_REVIEW_BYTES = 15 * 1024 * 1024;
5
6
  const TLP_MARKINGS = ['clear', 'green', 'amber', 'amber-strict', 'red'];
6
7
  const RECIPIENT_SCOPES = ['public', 'community', 'organization', 'named-recipients'];
@@ -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)
@@ -103,7 +108,11 @@ async function buildSharingReview(raw, options, generatedAt = new Date().toISOSt
103
108
  let integrity = 'unsupported';
104
109
  try {
105
110
  const verification = await verifyOfflineArtifact(raw);
106
- integrity = verification.checks.contentIntegrity === 'verified' ? 'verified' : 'structure_only';
111
+ integrity = verification.state === 'verified' && hasVerifiedWholeArtifactIntegrity(verification)
112
+ ? 'verified'
113
+ : verification.state === 'integrity_valid'
114
+ ? 'projection_integrity'
115
+ : 'structure_only';
107
116
  }
108
117
  catch (error) {
109
118
  integrity = error instanceof UnsupportedOfflineArtifactError ? 'unsupported' : 'failed';
@@ -114,9 +123,11 @@ async function buildSharingReview(raw, options, generatedAt = new Date().toISOSt
114
123
  ? 'The artefact passed its supported local integrity contract.'
115
124
  : integrity === 'structure_only'
116
125
  ? 'The artefact passed its structural contract but has no verified content digest.'
117
- : integrity === 'unsupported'
118
- ? 'This artefact schema has no supported local integrity verifier; review its producer and digest contract manually.'
119
- : 'The artefact failed its supported structure or integrity contract.');
126
+ : integrity === 'projection_integrity'
127
+ ? 'The artefact passed its structural contract and projection digests, but those digests do not cover the whole file.'
128
+ : integrity === 'unsupported'
129
+ ? 'This artefact schema has no supported local integrity verifier; review its producer and digest contract manually.'
130
+ : 'The artefact failed its supported structure or integrity contract.');
120
131
  add('human-review', options.humanReviewed ? 'pass' : 'block', 'Human review', options.humanReviewed
121
132
  ? 'A deliberate human review was recorded for this sharing decision.'
122
133
  : 'Complete a deliberate human review before sharing.');