@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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 (282) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +20 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +454 -1748
  7. package/cli/artifact-validation/case-response.mjs +66 -35
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +112 -72
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-checkpoint.mjs +34 -3
  18. package/cli/bulk-command-runner.mjs +2 -0
  19. package/cli/bulk-output.mjs +29 -3
  20. package/cli/case-command.mjs +195 -0
  21. package/cli/case-pack.mjs +66 -111
  22. package/cli/command-argument-grammar.mjs +215 -0
  23. package/cli/command-reference.mjs +342 -285
  24. package/cli/completion.mjs +303 -80
  25. package/cli/config-profile.mjs +7 -5
  26. package/cli/ct-event-intake.mjs +12 -12
  27. package/cli/discover.mjs +4 -3
  28. package/cli/discovery-observation-snapshot.mjs +59 -18
  29. package/cli/discovery-scan-command-runner.mjs +21 -0
  30. package/cli/discovery-scan.mjs +13 -4
  31. package/cli/discovery-workflow.mjs +1 -1
  32. package/cli/domain-control-monitor.mjs +50 -20
  33. package/cli/domain-control-observations.mjs +149 -102
  34. package/cli/errors.mjs +33 -1
  35. package/cli/evidence-command-runner.mjs +16 -5
  36. package/cli/evidence-signing.mjs +2 -2
  37. package/cli/external-observation-mapping.mjs +7 -8
  38. package/cli/formatters/terminal-lookup.mjs +18 -46
  39. package/cli/formatters/terminal-shared.mjs +5 -3
  40. package/cli/formatters/terminal-technology.mjs +114 -0
  41. package/cli/inline-command-families.mjs +2 -0
  42. package/cli/input.mjs +116 -0
  43. package/cli/interactive-launcher.mjs +2 -160
  44. package/cli/interchange-report.mjs +18 -8
  45. package/cli/investigation-artifacts.mjs +176 -0
  46. package/cli/investigation-checkpoint.mjs +10 -0
  47. package/cli/investigation-folder.mjs +114 -0
  48. package/cli/investigation-manifest.mjs +1 -112
  49. package/cli/investigation-package-review.mjs +103 -0
  50. package/cli/investigation-plan.mjs +32 -19
  51. package/cli/investigation-run.mjs +188 -33
  52. package/cli/local-document-checkpoint.mjs +170 -0
  53. package/cli/lookalike-calibration.mjs +7 -2
  54. package/cli/lookup-brief.mjs +4 -2
  55. package/cli/lookup-browser-view.mjs +1 -1
  56. package/cli/lookup-browser.mjs +5 -12
  57. package/cli/lookup-command-runner.mjs +17 -6
  58. package/cli/lookup-diff.mjs +37 -3
  59. package/cli/lookup-plan.mjs +11 -5
  60. package/cli/lookup-reconcile.mjs +10 -8
  61. package/cli/mail-header-review.mjs +467 -0
  62. package/cli/mail-review.mjs +66 -14
  63. package/cli/manual.mjs +1 -0
  64. package/cli/network-command-runner.mjs +2 -0
  65. package/cli/output-file.mjs +31 -5
  66. package/cli/registry-doctor.mjs +3 -1
  67. package/cli/registry-support.mjs +1 -1
  68. package/cli/retained-artifact-diff.mjs +2 -2
  69. package/cli/review-command-runner.mjs +70 -0
  70. package/cli/risk-calibration.mjs +19 -17
  71. package/cli/runner.mjs +62 -7
  72. package/cli/sharing-review.mjs +21 -34
  73. package/cli/signer-trust.mjs +104 -0
  74. package/cli/source-reliability.mjs +74 -83
  75. package/cli/terminal-input.mjs +168 -0
  76. package/cli/workflow-command-runner.mjs +68 -13
  77. package/docs/cli-reference.md +77 -24
  78. package/docs/cli.md +300 -25
  79. package/lib/abort.mjs +4 -1
  80. package/lib/availability.mjs +161 -112
  81. package/lib/bounded-file.mjs +3 -1
  82. package/lib/bounded-html-document.mjs +125 -0
  83. package/lib/bounded-json.mjs +51 -8
  84. package/lib/browser-library-profile.mjs +42 -51
  85. package/lib/browser-library-worker.mjs +112 -0
  86. package/lib/client-behavior-profile.mjs +3 -3
  87. package/lib/ct-search.mjs +11 -4
  88. package/lib/dns-convergence-review.mjs +1 -0
  89. package/lib/dns-delegation-health.mjs +24 -14
  90. package/lib/dns-inheritance-review.mjs +217 -0
  91. package/lib/dns-intelligence.mjs +73 -26
  92. package/lib/dns-mx.mjs +1 -1
  93. package/lib/dnssec-chain-validation.mjs +6 -0
  94. package/lib/dnssec-evidence-validation.mjs +11 -3
  95. package/lib/domain-change-packet.mjs +49 -16
  96. package/lib/domain-change-review.mjs +14 -8
  97. package/lib/domain-control-flight-recorder.mjs +221 -117
  98. package/lib/domain-control-manifest.mjs +129 -69
  99. package/lib/domain-posture-analysis.mjs +20 -7
  100. package/lib/domain-posture-parsers.mjs +201 -8
  101. package/lib/domain-posture.mjs +147 -29
  102. package/lib/evidence-export-privacy.mjs +561 -0
  103. package/lib/evidence-export.mjs +11 -543
  104. package/lib/evidence-report.mjs +5 -3
  105. package/lib/favicon.mjs +117 -41
  106. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  107. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  108. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  109. package/lib/hostname.mjs +2 -38
  110. package/lib/html-signals.mjs +111 -108
  111. package/lib/http-evidence-bounds.mjs +2 -1
  112. package/lib/http-intelligence.mjs +39 -26
  113. package/lib/local-geoip-evidence.mjs +19 -3
  114. package/lib/lookup-cache.mjs +34 -11
  115. package/lib/lookup-child-profile-contract.mjs +24 -15
  116. package/lib/lookup-diagnostics.mjs +24 -1
  117. package/lib/lookup-progress.mjs +30 -13
  118. package/lib/lookup-response-contract.mjs +1048 -0
  119. package/lib/lookup-response.mjs +8 -10
  120. package/lib/lookup-source-progress.mjs +40 -14
  121. package/lib/lookup.mjs +48 -27
  122. package/lib/nameserver-preflight-review.mjs +1 -0
  123. package/lib/observed-network-context.mjs +43 -27
  124. package/lib/outbound-request-bounds.mjs +4 -1
  125. package/lib/page-fingerprints.mjs +69 -133
  126. package/lib/page-role-profile.mjs +12 -8
  127. package/lib/perceptual-hash.mjs +43 -14
  128. package/lib/rdap-bootstrap.mjs +72 -22
  129. package/lib/rdap-capabilities.mjs +1 -1
  130. package/lib/rdap-client.mjs +15 -7
  131. package/lib/rdap-normalization.mjs +33 -18
  132. package/lib/rdap-search-workbench.mjs +1 -1
  133. package/lib/rdap-transport.mjs +47 -20
  134. package/lib/rdap-types.mjs +2 -1
  135. package/lib/rdap.mjs +18 -3
  136. package/lib/registrable-domain.mjs +21 -1
  137. package/lib/registrar-standing-contract.mjs +1 -0
  138. package/lib/registry-capabilities.mjs +5 -1
  139. package/lib/registry-capability-catalogue.mjs +2 -1
  140. package/lib/registry-dates.mjs +2 -190
  141. package/lib/registry-insights.mjs +85 -34
  142. package/lib/response-policy.mjs +86 -48
  143. package/lib/risk-calibration-summary.mjs +18 -6
  144. package/lib/rpki-evidence.mjs +8 -5
  145. package/lib/safe-fetch.mjs +54 -47
  146. package/lib/security-txt.mjs +42 -7
  147. package/lib/service-binding-dns.mjs +8 -0
  148. package/lib/smtp-transport-review.mjs +27 -15
  149. package/lib/sslbl-certificates.generated.mjs +136 -135
  150. package/lib/static-html-analysis.mjs +178 -151
  151. package/lib/structured-data-identity.mjs +1 -0
  152. package/lib/technology-markup.mjs +100 -0
  153. package/lib/threat-intelligence-manifests.mjs +11 -139
  154. package/lib/threat-intelligence-runtime.mjs +24 -52
  155. package/lib/threat-intelligence-targets.mjs +6 -34
  156. package/lib/threat-intelligence-types.mjs +39 -0
  157. package/lib/typosquat-generator.mjs +19 -15
  158. package/lib/urlhaus-intelligence.mjs +8 -3
  159. package/lib/urlscan-intelligence.mjs +1 -1
  160. package/lib/website-security-posture.mjs +30 -6
  161. package/lib/website-technology.mjs +153 -45
  162. package/lib/whois-authority.mjs +14 -2
  163. package/lib/whois-chain.mjs +9 -2
  164. package/lib/whois-common-formats.mjs +5 -5
  165. package/lib/whois-contracts.mjs +2 -1
  166. package/lib/whois-dialects.mjs +2 -2
  167. package/lib/whois-parser-support.mjs +8 -1
  168. package/lib/whois-transport.mjs +23 -7
  169. package/lib/zone-intent-review.mjs +16 -6
  170. package/package.json +2 -1
  171. package/packages/cases/case-attachment-model.mjs +83 -0
  172. package/packages/cases/case-evidence-model.mjs +156 -62
  173. package/packages/cases/case-export-input.mjs +53 -0
  174. package/packages/cases/case-migration-model.mjs +127 -44
  175. package/packages/cases/case-model.mjs +1 -0
  176. package/packages/cases/case-recheck-model.mjs +91 -0
  177. package/packages/cases/case-record-contracts.mjs +5 -20
  178. package/packages/cases/case-record-core.mjs +12 -22
  179. package/packages/cases/case-record-decisions.mjs +72 -0
  180. package/packages/cases/case-record-model.mjs +1 -0
  181. package/packages/cases/case-record-operations.mjs +95 -31
  182. package/packages/cases/case-record-projection.mjs +221 -0
  183. package/packages/cases/case-report.mjs +70 -54
  184. package/packages/cases/case-response-actions.mjs +654 -0
  185. package/packages/cases/case-response-model.mjs +64 -1115
  186. package/packages/cases/case-response-outcomes.mjs +317 -0
  187. package/packages/cases/case-response-packet-render.mjs +153 -0
  188. package/packages/cases/case-response-packet-types.mjs +1 -0
  189. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  190. package/packages/cases/case-response-packet.mjs +74 -572
  191. package/packages/cases/case-response-records.mjs +94 -0
  192. package/packages/cases/case-response-review-inputs.mjs +371 -0
  193. package/packages/cases/case-response-values.mjs +85 -0
  194. package/packages/cases/case-review-package.mjs +45 -0
  195. package/packages/cases/case-selection.mjs +46 -0
  196. package/packages/cases/case-storage-model.mjs +31 -2
  197. package/packages/cases/response-route-freshness.mjs +16 -0
  198. package/packages/comparison/capture-context.mjs +61 -0
  199. package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
  200. package/packages/comparison/page-similarity.mjs +5 -2
  201. package/packages/comparison/relationship-evidence.mjs +85 -8
  202. package/packages/comparison/relationship-provenance.mjs +93 -0
  203. package/packages/contracts/analyst-interchange.mjs +216 -0
  204. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  205. package/packages/contracts/analyst-review-state.mjs +5 -4
  206. package/packages/contracts/case-portability.mjs +366 -83
  207. package/packages/contracts/case-views-contract.mjs +12 -0
  208. package/packages/contracts/case-views.mjs +26 -0
  209. package/packages/contracts/cli-command-semantics.mjs +94 -0
  210. package/packages/contracts/cli-lookup.mjs +0 -1
  211. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  212. package/packages/contracts/domain-control-manifest.mjs +127 -73
  213. package/packages/contracts/domain-control-monitor.mjs +48 -45
  214. package/packages/contracts/domain-control-review.mjs +179 -121
  215. package/packages/contracts/domain-name.mjs +21 -0
  216. package/packages/contracts/external-observation-interchange.mjs +2 -1
  217. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  218. package/packages/contracts/http-url.mjs +2 -0
  219. package/packages/contracts/investigation-package-limits.mjs +24 -0
  220. package/packages/contracts/investigation-portability.mjs +56 -27
  221. package/packages/contracts/investigation-run.mjs +16 -0
  222. package/packages/contracts/ip-address.mjs +34 -0
  223. package/packages/contracts/lookup-evidence.mjs +35 -0
  224. package/packages/contracts/offline-comparison.mjs +1 -2
  225. package/packages/contracts/page-fingerprints.mjs +15 -0
  226. package/packages/contracts/risk-calibration.mjs +15 -11
  227. package/packages/contracts/schema-compatibility.mjs +1 -1
  228. package/packages/contracts/schema-lifecycle.mjs +83 -153
  229. package/packages/contracts/selected-file-limits.mjs +6 -0
  230. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  231. package/packages/contracts/web-capture.mjs +10 -0
  232. package/packages/contracts/workspace-portability.mjs +183 -211
  233. package/packages/evidence/artifact-integrity.mjs +8 -2
  234. package/packages/evidence/artifact-structure.mjs +105 -0
  235. package/packages/evidence/decision-fact.mjs +4 -1
  236. package/packages/evidence/domain-control-runtime.mjs +201 -33
  237. package/packages/evidence/domain-posture-context.mjs +79 -0
  238. package/packages/evidence/image-regions.mjs +56 -0
  239. package/packages/evidence/latest-observations.mjs +28 -0
  240. package/packages/evidence/lookup-target.mjs +104 -0
  241. package/packages/evidence/observation.mjs +16 -1
  242. package/packages/evidence/passphrase-encryption.mjs +44 -0
  243. package/packages/evidence/record-identity.mjs +72 -0
  244. package/packages/evidence/registry-dates.mjs +190 -0
  245. package/packages/evidence/retained-file.mjs +69 -0
  246. package/packages/evidence/review-age.mjs +14 -0
  247. package/packages/interchange/bagit.mjs +342 -0
  248. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  249. package/packages/interchange/crc32.mjs +17 -0
  250. package/packages/interchange/external-findings-import.mjs +92 -51
  251. package/packages/interchange/web-capture-import.mjs +382 -0
  252. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  253. package/packages/investigation/investigation-bagit.mjs +14 -0
  254. package/packages/investigation/investigation-capsule.mjs +33 -5
  255. package/packages/investigation/investigation-manifest.mjs +200 -0
  256. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  257. package/packages/investigation/investigation-package.mjs +200 -0
  258. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  259. package/packages/investigation/lookup-asset-graph.mjs +193 -165
  260. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  261. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  262. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  263. package/packages/monitoring/analyst-review-state.mjs +38 -90
  264. package/packages/workspace/brand-profile-model.mjs +276 -22
  265. package/packages/workspace/bulk-columns.mjs +17 -0
  266. package/packages/workspace/bulk-review-model.mjs +23 -11
  267. package/packages/workspace/bulk-session-model.mjs +100 -24
  268. package/packages/workspace/bulk-sort.mjs +1 -0
  269. package/packages/workspace/case-views.mjs +102 -0
  270. package/packages/workspace/detection-rule-model.mjs +70 -22
  271. package/packages/workspace/hostile-input.mjs +29 -8
  272. package/packages/workspace/investigation-template-model.mjs +19 -4
  273. package/packages/workspace/page-baseline.mjs +18 -15
  274. package/packages/workspace/relationship-observation-model.mjs +41 -23
  275. package/packages/workspace/watchlist-history.mjs +2 -2
  276. package/packages/workspace/watchlist-store.mjs +8 -2
  277. package/packages/workspace/website-snapshot-model.mjs +164 -26
  278. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  279. package/packages/workspace/workspace-archive.mjs +87 -21
  280. package/third-party-notices.txt +29 -2
  281. package/cli/evidence-command-arguments.mjs +0 -156
  282. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -39,7 +39,13 @@ function canonicalArtifactJsonV2(value) {
39
39
  }
40
40
  async function sha256ArtifactDigestV2(value) {
41
41
  const bytes = new TextEncoder().encode(canonicalArtifactJsonV2(value));
42
- const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
42
+ return sha256ArtifactBytes(bytes);
43
+ }
44
+ async function sha256ArtifactBytes(bytes) {
45
+ const buffer = bytes.buffer;
46
+ if (!(buffer instanceof ArrayBuffer))
47
+ throw new TypeError('Artefact digests require non-shared bytes.');
48
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', new Uint8Array(buffer, bytes.byteOffset, bytes.byteLength));
43
49
  return `sha256:${[...new Uint8Array(digest)]
44
50
  .map((byte) => byte.toString(16).padStart(2, '0'))
45
51
  .join('')}`;
@@ -62,4 +68,4 @@ function sha256ArtifactDigestFor(value, canonicalization) {
62
68
  ? sha256ArtifactDigest(value)
63
69
  : sha256ArtifactDigestV2(value);
64
70
  }
65
- export { SORTED_JSON_V1, SORTED_JSON_V2, canonicalArtifactJson, canonicalArtifactJsonFor, canonicalArtifactJsonV2, resolveArtifactCanonicalization, sha256ArtifactDigest, sha256ArtifactDigestFor, sha256ArtifactDigestV2, };
71
+ export { SORTED_JSON_V1, SORTED_JSON_V2, canonicalArtifactJson, canonicalArtifactJsonFor, canonicalArtifactJsonV2, resolveArtifactCanonicalization, sha256ArtifactDigest, sha256ArtifactDigestFor, sha256ArtifactDigestV2, sha256ArtifactBytes, };
@@ -0,0 +1,105 @@
1
+ const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
2
+ const DIGEST_RE = /^sha256:[a-f0-9]{64}$/u;
3
+ const DOMAIN_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u;
4
+ export const HEX_DIGEST_RE = /^[a-f0-9]{64}$/u;
5
+ export const SEMANTIC_VERSION_RE = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
6
+ export function fail(label) {
7
+ throw new TypeError(`${label} has an unsupported or malformed structure.`);
8
+ }
9
+ export function record(value, label) {
10
+ if (!value || typeof value !== 'object' || Array.isArray(value))
11
+ fail(label);
12
+ return value;
13
+ }
14
+ export function exact(value, keys, label) {
15
+ const source = record(value, label);
16
+ const actual = Object.keys(source);
17
+ if (actual.length !== keys.length || actual.some((key) => !keys.includes(key)))
18
+ fail(label);
19
+ return source;
20
+ }
21
+ export function exactOptional(value, required, optional, label) {
22
+ const source = record(value, label);
23
+ const actual = Object.keys(source);
24
+ if (required.some((key) => !actual.includes(key))
25
+ || actual.some((key) => !required.includes(key) && !optional.includes(key)))
26
+ fail(label);
27
+ return source;
28
+ }
29
+ export function text(value, label, maximum = 2_000, allowEmpty = false) {
30
+ if (typeof value !== 'string' || value.length > maximum || CONTROL_RE.test(value) || (!allowEmpty && !value))
31
+ fail(label);
32
+ return value;
33
+ }
34
+ export function optionalText(value, label, maximum = 2_000) {
35
+ if (value !== null)
36
+ text(value, label, maximum);
37
+ }
38
+ export function iso(value, label, nullable = false) {
39
+ if (nullable && value === null)
40
+ return;
41
+ const candidate = text(value, label, 64);
42
+ const parsed = Date.parse(candidate);
43
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== candidate)
44
+ fail(label);
45
+ }
46
+ export function integer(value, label, minimum = 0, maximum = Number.MAX_SAFE_INTEGER) {
47
+ if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum)
48
+ fail(label);
49
+ return Number(value);
50
+ }
51
+ export function boolean(value, label) {
52
+ if (typeof value !== 'boolean')
53
+ fail(label);
54
+ }
55
+ export function enumeration(value, allowed, label) {
56
+ if (typeof value !== 'string' || !allowed.includes(value))
57
+ fail(label);
58
+ return value;
59
+ }
60
+ export function array(value, label, maximum, minimum = 0) {
61
+ if (!Array.isArray(value) || value.length < minimum || value.length > maximum)
62
+ fail(label);
63
+ return value;
64
+ }
65
+ export function strings(value, label, maximum, textMaximum = 2_000) {
66
+ const values = array(value, label, maximum);
67
+ for (const item of values)
68
+ text(item, label, textMaximum);
69
+ return values;
70
+ }
71
+ export function digest(value, label) {
72
+ if (typeof value !== 'string' || !DIGEST_RE.test(value))
73
+ fail(label);
74
+ }
75
+ export function sameValues(left, right) {
76
+ return left.length === right.length && left.every((value, index) => value === right[index]);
77
+ }
78
+ export function domain(value, label) {
79
+ if (typeof value !== 'string' || !DOMAIN_RE.test(value))
80
+ fail(label);
81
+ }
82
+ export function absoluteUrl(value, label, protocols) {
83
+ const candidate = text(value, label, 2_048);
84
+ try {
85
+ const parsed = new URL(candidate);
86
+ if (!protocols.includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash)
87
+ fail(label);
88
+ return parsed;
89
+ }
90
+ catch {
91
+ return fail(label);
92
+ }
93
+ }
94
+ export function nullableRecord(value, label) {
95
+ if (value !== null)
96
+ record(value, label);
97
+ }
98
+ export function validateIntegrity(value, label, version, currentVersion) {
99
+ if (version !== currentVersion)
100
+ fail(label);
101
+ const integrity = exact(value, ['algorithm', 'canonicalization', 'digestSha256'], label);
102
+ if (integrity.algorithm !== 'SHA-256' || integrity.canonicalization !== 'sorted-json-v2')
103
+ fail(label);
104
+ digest(integrity.digestSha256, label);
105
+ }
@@ -352,7 +352,10 @@ function boundedDataArray(value, maximum, label) {
352
352
  || Number(length) < 0) {
353
353
  throw new TypeError(`${label} must have a stable non-negative length.`);
354
354
  }
355
- const retainedLength = Math.min(Number(length), maximum);
355
+ if (Number(length) > maximum) {
356
+ throw new TypeError(`${label} exceeds its ${maximum}-item admission bound; no partial fact was retained.`);
357
+ }
358
+ const retainedLength = Number(length);
356
359
  const output = [];
357
360
  for (let index = 0; index < retainedLength; index += 1) {
358
361
  const descriptor = stableOwnDescriptor(value, String(index), label);
@@ -1,7 +1,9 @@
1
+ import { analystReviewAgeAt, ANALYST_REVIEW_STALE_AFTER_DAYS } from "./review-age.mjs";
1
2
  import { canonicalArtifactJsonFor, resolveArtifactCanonicalization, } from "./artifact-integrity.mjs";
2
3
  import { normalizeDomain } from "./domain-name.mjs";
3
4
  import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "./observation.mjs";
4
- import { DOMAIN_CONTROL_CAA_RECORD_KEYS, DOMAIN_CONTROL_DIGEST_SHA256_HEX_LENGTH, DOMAIN_CONTROL_DS_RECORD_KEYS, DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS, DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA as DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA, DOMAIN_CONTROL_MANIFEST_INPUT_KEYS, DOMAIN_CONTROL_MANIFEST_INPUT_VERSION, DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, DOMAIN_CONTROL_MANIFEST_INTEGRITY_KEYS, DOMAIN_CONTROL_MANIFEST_LIMITATIONS, DOMAIN_CONTROL_MANIFEST_ROOT_KEYS, DOMAIN_CONTROL_MANIFEST_SCHEMA as DOMAIN_CONTROL_PASSPORT_SCHEMA, DOMAIN_CONTROL_MANIFEST_VERSION, DOMAIN_CONTROL_MX_RECORD_KEYS, DOMAIN_CONTROL_RECORD_LIST_FIELDS, DOMAIN_CONTROL_SPKI_SHA256_HEX_LENGTH, DOMAIN_CONTROL_TEXT_INPUT_BOUND_FACTOR, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_CAA_FLAGS, MAX_DOMAIN_CONTROL_CAA_PRESENTATION_LENGTH, MAX_DOMAIN_CONTROL_CAA_TAG_LENGTH, MAX_DOMAIN_CONTROL_CAA_VALUE_LENGTH, MAX_DOMAIN_CONTROL_DOMAIN_LENGTH, MAX_DOMAIN_CONTROL_DS_ALGORITHM, MAX_DOMAIN_CONTROL_DS_DIGEST_LENGTH, MAX_DOMAIN_CONTROL_DS_DIGEST_TYPE, MAX_DOMAIN_CONTROL_DS_KEY_TAG, MAX_DOMAIN_CONTROL_DS_PRESENTATION_LENGTH, MAX_DOMAIN_CONTROL_INPUT_RECORDS, MAX_DOMAIN_CONTROL_JSON_DEPTH, MAX_DOMAIN_CONTROL_JSON_VALUES, MAX_DOMAIN_CONTROL_MANIFEST_BYTES, MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES, MAX_DOMAIN_CONTROL_MX_PRIORITY, MAX_DOMAIN_CONTROL_MX_TEXT_LENGTH, MAX_DOMAIN_CONTROL_NAME_INPUT_LENGTH, MAX_DOMAIN_CONTROL_NOTE_LENGTH, MAX_DOMAIN_CONTROL_RECORD_INTEGER_TEXT_LENGTH, MAX_DOMAIN_CONTROL_TEXT_LENGTH, MAX_DOMAIN_CONTROL_TIMESTAMP_LENGTH, MIN_DOMAIN_CONTROL_DS_DIGEST_LENGTH, MIN_DOMAIN_CONTROL_MANIFEST_ENTRIES, MIN_DOMAIN_CONTROL_RECORDS, } from "../contracts/domain-control-manifest.mjs";
5
+ import { MAX_POSTURE_CHECK_RECORDS } from "./domain-posture-context.mjs";
6
+ import { DOMAIN_CONTROL_CAA_RECORD_KEYS, DOMAIN_CONTROL_DIGEST_SHA256_HEX_LENGTH, DOMAIN_CONTROL_DS_RECORD_KEYS, DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS, DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA as DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA, DOMAIN_CONTROL_MANIFEST_INPUT_KEYS, DOMAIN_CONTROL_MANIFEST_INPUT_VERSION, DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, DOMAIN_CONTROL_MANIFEST_INTEGRITY_KEYS, DOMAIN_CONTROL_MANIFEST_LIMITATIONS, DOMAIN_CONTROL_MANIFEST_ROOT_KEYS, DOMAIN_CONTROL_MANIFEST_SCHEMA as DOMAIN_CONTROL_PASSPORT_SCHEMA, DOMAIN_CONTROL_MANIFEST_VERSION, DOMAIN_CONTROL_MX_RECORD_KEYS, DOMAIN_CONTROL_RECORD_LIST_FIELDS, DOMAIN_CONTROL_RECORD_MODE_OPTIONS, DOMAIN_CONTROL_SPKI_SHA256_HEX_LENGTH, DOMAIN_CONTROL_TEXT_INPUT_BOUND_FACTOR, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS, MAX_CURRENT_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_CAA_FLAGS, MAX_DOMAIN_CONTROL_CAA_PRESENTATION_LENGTH, MAX_DOMAIN_CONTROL_CAA_TAG_LENGTH, MAX_DOMAIN_CONTROL_CAA_VALUE_LENGTH, MAX_DOMAIN_CONTROL_DOMAIN_LENGTH, MAX_DOMAIN_CONTROL_DS_ALGORITHM, MAX_DOMAIN_CONTROL_DS_DIGEST_LENGTH, MAX_DOMAIN_CONTROL_DS_DIGEST_TYPE, MAX_DOMAIN_CONTROL_DS_KEY_TAG, MAX_DOMAIN_CONTROL_DS_PRESENTATION_LENGTH, MAX_DOMAIN_CONTROL_INPUT_RECORDS, MAX_DOMAIN_CONTROL_JSON_DEPTH, MAX_DOMAIN_CONTROL_JSON_VALUES, MAX_DOMAIN_CONTROL_MANIFEST_BYTES, MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES, MAX_DOMAIN_CONTROL_MX_PRIORITY, MAX_DOMAIN_CONTROL_MX_TEXT_LENGTH, MAX_DOMAIN_CONTROL_NAME_INPUT_LENGTH, MAX_DOMAIN_CONTROL_NOTE_LENGTH, MAX_DOMAIN_CONTROL_RECORD_INTEGER_TEXT_LENGTH, MAX_DOMAIN_CONTROL_TEXT_LENGTH, MAX_DOMAIN_CONTROL_TIMESTAMP_LENGTH, MIN_DOMAIN_CONTROL_DS_DIGEST_LENGTH, MIN_DOMAIN_CONTROL_MANIFEST_ENTRIES, MIN_DOMAIN_CONTROL_RECORDS, PUBLIC_DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS, PUBLIC_DOMAIN_CONTROL_MANIFEST_INPUT_VERSION, PUBLIC_DOMAIN_CONTROL_MANIFEST_LIMITATIONS, PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION, SUPPORTED_DOMAIN_CONTROL_MANIFEST_INPUT_VERSIONS, } from "../contracts/domain-control-manifest.mjs";
5
7
  const DOMAIN_CONTROL_PASSPORT_VERSION = DOMAIN_CONTROL_MANIFEST_INPUT_VERSION;
6
8
  export { DOMAIN_CONTROL_MANIFEST_VERSION, DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA, DOMAIN_CONTROL_PASSPORT_SCHEMA, DOMAIN_CONTROL_PASSPORT_VERSION, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_MANIFEST_BYTES, };
7
9
  export const MAX_DOMAIN_CONTROL_PASSPORT_BYTES = MAX_DOMAIN_CONTROL_MANIFEST_BYTES;
@@ -10,6 +12,8 @@ export const DOMAIN_CONTROL_PASSPORT_LIMITATIONS = DOMAIN_CONTROL_MANIFEST_LIMIT
10
12
  const INPUT_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_INPUT_KEYS);
11
13
  const MANIFEST_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_ROOT_KEYS);
12
14
  const ENTRY_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS);
15
+ const PUBLIC_ENTRY_KEYS = new Set(PUBLIC_DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS);
16
+ const RECORD_MODE_KEYS = new Set(DOMAIN_CONTROL_RECORD_LIST_FIELDS);
13
17
  const INTEGRITY_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_INTEGRITY_KEYS);
14
18
  const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
15
19
  const RECORD_LIST_FIELDS = DOMAIN_CONTROL_RECORD_LIST_FIELDS;
@@ -82,6 +86,7 @@ function boundedDataArray(value, label, minimum, maximum, maximumMessage) {
82
86
  return result;
83
87
  }
84
88
  const MX_RECORD_KEYS = new Set(DOMAIN_CONTROL_MX_RECORD_KEYS);
89
+ const RESOLVER_MX_RECORD_KEYS = new Set([...MX_RECORD_KEYS, 'type']);
85
90
  const CAA_RECORD_KEYS = new Set(DOMAIN_CONTROL_CAA_RECORD_KEYS);
86
91
  const DS_RECORD_KEYS = new Set(DOMAIN_CONTROL_DS_RECORD_KEYS);
87
92
  const DS_DIGEST_PATTERN = new RegExp(`^[a-f0-9]{${MIN_DOMAIN_CONTROL_DS_DIGEST_LENGTH},${MAX_DOMAIN_CONTROL_DS_DIGEST_LENGTH}}$`, 'u');
@@ -155,31 +160,90 @@ function canonicalIntegerAlias(source, keys, maximum, label, fallback = null) {
155
160
  }
156
161
  return normalised[0] ?? null;
157
162
  }
158
- export function canonicalMxRecord(value) {
159
- const item = structuredRecord(value, MX_RECORD_KEYS, 'Domain control MX record');
163
+ function mxRecordParts(value, resolverRecord = false) {
164
+ const item = structuredRecord(value, resolverRecord ? RESOLVER_MX_RECORD_KEYS : MX_RECORD_KEYS, 'Domain control MX record');
160
165
  if (item) {
161
- const host = canonicalStringAlias(item, ['exchange', 'host', 'value'], exchange, 'Domain control MX host');
166
+ if (resolverRecord && Object.hasOwn(item, 'type') && item.type !== 'MX')
167
+ return null;
168
+ const host = canonicalStringAlias(item, ['exchange', 'host', 'value'], (value) => value === '' ? '.' : exchange(value), 'Domain control MX host');
162
169
  const priority = canonicalIntegerAlias(item, ['priority', 'preference'], MAX_DOMAIN_CONTROL_MX_PRIORITY, 'Domain control MX priority');
163
- return host && priority !== null ? `${priority} ${host}` : '';
170
+ return host && priority !== null ? { priority, exchange: host } : null;
164
171
  }
165
172
  const candidate = recordText(value, MAX_DOMAIN_CONTROL_MX_TEXT_LENGTH).toLowerCase();
166
173
  const match = /^(\d{1,5})\s+(.+)$/u.exec(candidate);
167
174
  if (match) {
168
175
  const priority = recordInteger(match[1], MAX_DOMAIN_CONTROL_MX_PRIORITY);
169
176
  const host = exchange(match[2]);
170
- return priority !== null && host ? `${priority} ${host}` : '';
177
+ return priority !== null && host ? { priority, exchange: host } : null;
178
+ }
179
+ const host = exchange(candidate);
180
+ return host ? { priority: null, exchange: host } : null;
181
+ }
182
+ export function canonicalMxRecord(value) {
183
+ const parts = mxRecordParts(value);
184
+ return !parts ? '' : parts.priority === null ? parts.exchange : `${parts.priority} ${parts.exchange}`;
185
+ }
186
+ function completeMxRecord(value, resolverRecord) {
187
+ try {
188
+ const parts = mxRecordParts(value, resolverRecord);
189
+ return parts && parts.priority !== null
190
+ ? { priority: parts.priority, exchange: parts.exchange } : null;
191
+ }
192
+ catch (error) {
193
+ if (!(error instanceof TypeError))
194
+ throw error;
195
+ return null;
171
196
  }
172
- return exchange(candidate);
173
197
  }
174
- function caaValue(value) {
175
- return recordText(value, MAX_DOMAIN_CONTROL_CAA_VALUE_LENGTH).replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u, '$1$2').trim().toLowerCase();
198
+ /** Complete canonical records require a preference and retain the root exchange as '.'. */
199
+ export function normalizeMxRecord(value) {
200
+ return completeMxRecord(value, false);
201
+ }
202
+ /** Accept the native resolver's optional record type without broadening import shapes. */
203
+ export function normalizeResolverMxRecord(value) {
204
+ return completeMxRecord(value, true);
205
+ }
206
+ function caaValue(value, tag) {
207
+ const candidate = recordText(value, MAX_DOMAIN_CONTROL_CAA_VALUE_LENGTH)
208
+ .replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u, '$1$2').trim();
209
+ if (!candidate)
210
+ return '';
211
+ if (tag === 'issue' || tag === 'issuewild') {
212
+ const [issuer = '', ...parameters] = candidate.split(';');
213
+ const normalizedIssuer = issuer.trim().toLowerCase().replace(/\.$/u, '');
214
+ return [normalizedIssuer, ...parameters.map((parameter) => parameter.trim())].join('; ');
215
+ }
216
+ const scheme = /^([a-z][a-z0-9+.-]*):(.*)$/isu.exec(candidate);
217
+ if (!scheme)
218
+ return candidate;
219
+ const normalizedScheme = scheme[1].toLowerCase();
220
+ if (normalizedScheme === 'http' || normalizedScheme === 'https') {
221
+ try {
222
+ const url = new URL(candidate);
223
+ url.protocol = normalizedScheme;
224
+ url.hostname = url.hostname.toLowerCase();
225
+ return url.toString();
226
+ }
227
+ catch {
228
+ return candidate;
229
+ }
230
+ }
231
+ if (normalizedScheme === 'mailto') {
232
+ const [address = '', ...query] = scheme[2].split('?');
233
+ const at = address.lastIndexOf('@');
234
+ const normalizedAddress = at > 0
235
+ ? `${address.slice(0, at)}@${address.slice(at + 1).toLowerCase()}`
236
+ : address;
237
+ return `${normalizedScheme}:${normalizedAddress}${query.length ? `?${query.join('?')}` : ''}`;
238
+ }
239
+ return `${normalizedScheme}:${scheme[2]}`;
176
240
  }
177
241
  export function canonicalCaaRecord(value) {
178
242
  const item = structuredRecord(value, CAA_RECORD_KEYS, 'Domain control CAA record');
179
243
  if (item) {
180
244
  const flags = canonicalIntegerAlias(item, ['critical', 'flags'], MAX_DOMAIN_CONTROL_CAA_FLAGS, 'Domain control CAA flags', 0);
181
245
  const tag = recordText(item.tag, MAX_DOMAIN_CONTROL_CAA_TAG_LENGTH).toLowerCase();
182
- const payload = caaValue(item.value);
246
+ const payload = caaValue(item.value, tag);
183
247
  return flags !== null && /^(?:issue|issuewild|iodef)$/u.test(tag) && payload
184
248
  ? `${flags} ${tag} ${payload}`
185
249
  : '';
@@ -190,7 +254,7 @@ export function canonicalCaaRecord(value) {
190
254
  return '';
191
255
  const flags = recordInteger(match[1], MAX_DOMAIN_CONTROL_CAA_FLAGS);
192
256
  const tag = match[2]?.toLowerCase() ?? '';
193
- const payload = caaValue(match[3]);
257
+ const payload = caaValue(match[3], tag);
194
258
  return flags !== null && payload ? `${flags} ${tag} ${payload}` : '';
195
259
  }
196
260
  export function canonicalDsRecord(value) {
@@ -233,6 +297,88 @@ export function canonicalDomainControlRecordList(value, kind) {
233
297
  .sort(compareCodeUnits)
234
298
  .slice(0, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS);
235
299
  }
300
+ /** Complete field values for posture review; an invalid value never becomes an absent record. */
301
+ export function canonicalPostureRecords(checkId, values) {
302
+ if (values.length > MAX_POSTURE_CHECK_RECORDS)
303
+ return null;
304
+ const normalise = checkId === 'nameservers' ? domainControlName
305
+ : checkId === 'mx' ? canonicalMxRecord : checkId === 'caa' ? canonicalCaaRecord
306
+ : checkId === 'registration_lock'
307
+ ? (value) => typeof value === 'string' && value.length <= 120 && !/[\u0000-\u001f\u007f]/u.test(value)
308
+ ? value.trim().toLowerCase().replace(/[\s_-]+/gu, '') : ''
309
+ : null;
310
+ if (!normalise)
311
+ return null;
312
+ try {
313
+ const records = values.map(normalise);
314
+ if (records.some((value) => !value))
315
+ return null;
316
+ return [...new Set(records)].sort(compareCodeUnits);
317
+ }
318
+ catch {
319
+ return null;
320
+ }
321
+ }
322
+ /** Complete current record inputs are rejected, not shortened, when invalid or over capacity. */
323
+ export function canonicalDomainControlRecords(value, field) {
324
+ if (value === undefined)
325
+ return [];
326
+ const rows = boundedDataArray(value, `Domain control ${field}`, 0, MAX_DOMAIN_CONTROL_INPUT_RECORDS);
327
+ const normalise = field === 'nameservers' ? domainControlName : field === 'ds' ? canonicalDsRecord
328
+ : field === 'mx' ? canonicalMxRecord : canonicalCaaRecord;
329
+ const values = rows.map(normalise);
330
+ if (values.some((item) => !item))
331
+ throw new TypeError(`Domain control ${field} contains an invalid record.`);
332
+ const records = [...new Set(values)].sort(compareCodeUnits);
333
+ if (records.length > MAX_CURRENT_DOMAIN_CONTROL_RECORDS)
334
+ throw new TypeError(`Domain control ${field} exceeds ${MAX_CURRENT_DOMAIN_CONTROL_RECORDS} records.`);
335
+ return records;
336
+ }
337
+ export function domainControlRecordMode(entry, field) {
338
+ return entry.recordModes?.[field] ?? (entry[field].length ? 'expect_records' : 'unconfigured');
339
+ }
340
+ export function normalizeDomainControlRecordModes(value, records) {
341
+ const source = normalizeDeclaredDomainControlRecordModes(value);
342
+ return Object.freeze(Object.fromEntries(DOMAIN_CONTROL_RECORD_LIST_FIELDS.map((field) => {
343
+ const mode = Object.hasOwn(source, field) ? source[field] : (records[field].length ? 'expect_records' : 'unconfigured');
344
+ if ((mode === 'expect_records') !== (records[field].length > 0)) {
345
+ throw new TypeError(`Domain control ${field} requires records only in the specified-records mode.`);
346
+ }
347
+ return [field, mode];
348
+ })));
349
+ }
350
+ export function normalizeDeclaredDomainControlRecordModes(value) {
351
+ const source = value === undefined ? {} : boundedDataRecord(value, RECORD_MODE_KEYS, 'Domain control record modes');
352
+ if (!source)
353
+ throw new TypeError('Domain control record modes must be an ordinary object.');
354
+ const modes = {};
355
+ for (const field of DOMAIN_CONTROL_RECORD_LIST_FIELDS) {
356
+ if (!Object.hasOwn(source, field))
357
+ continue;
358
+ const mode = source[field];
359
+ if (!DOMAIN_CONTROL_RECORD_MODE_OPTIONS.some((option) => option.value === mode))
360
+ throw new TypeError(`Domain control ${field} has an unsupported record mode.`);
361
+ modes[field] = mode;
362
+ }
363
+ return Object.freeze(modes);
364
+ }
365
+ export function normalizeDomainControlRecordSettings(value) {
366
+ const records = {
367
+ nameservers: canonicalDomainControlRecords(value.nameservers, 'nameservers'),
368
+ ds: canonicalDomainControlRecords(value.ds, 'ds'),
369
+ mx: canonicalDomainControlRecords(value.mx, 'mx'),
370
+ caa: canonicalDomainControlRecords(value.caa, 'caa'),
371
+ };
372
+ return { ...records, recordModes: normalizeDomainControlRecordModes(value.recordModes, records) };
373
+ }
374
+ export function domainControlEvidenceAdmission(observedAt, now) {
375
+ const age = analystReviewAgeAt(observedAt, now);
376
+ if (age === 'unknown')
377
+ return 'A valid source observation time at or before the review is required.';
378
+ if (age === 'stale')
379
+ return `The source observation is more than ${ANALYST_REVIEW_STALE_AFTER_DAYS} days old; collect a current observation before comparing expected settings.`;
380
+ return null;
381
+ }
236
382
  class DomainControlJsonBudgetError extends Error {
237
383
  }
238
384
  class DomainControlJsonStructureError extends Error {
@@ -365,10 +511,10 @@ export function domainControlPassportSerialisedBytes(value, label = 'Domain cont
365
511
  export function assertDomainControlPassportByteBudget(value, label = 'Domain control manifest') {
366
512
  domainControlPassportSerialisedBytes(value, label);
367
513
  }
368
- function boundedDomainControlPassportEntries(value, label) {
514
+ function boundedDomainControlPassportEntries(value, label, legacy = false) {
369
515
  const entries = boundedDataArray(value, label, MIN_DOMAIN_CONTROL_MANIFEST_ENTRIES, MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES);
370
516
  return entries.map((entry) => {
371
- const source = boundedDataRecord(entry, ENTRY_KEYS, `${label} entry`);
517
+ const source = boundedDataRecord(entry, legacy ? PUBLIC_ENTRY_KEYS : ENTRY_KEYS, `${label} entry`);
372
518
  if (!source)
373
519
  return entry;
374
520
  for (const field of RECORD_LIST_FIELDS) {
@@ -376,6 +522,11 @@ function boundedDomainControlPassportEntries(value, label) {
376
522
  continue;
377
523
  source[field] = boundedDataArray(source[field], `${label} entry ${field}`, MIN_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_INPUT_RECORDS, `${label} entry ${field} exceeds ${MAX_DOMAIN_CONTROL_INPUT_RECORDS} input records.`);
378
524
  }
525
+ if (Object.hasOwn(source, 'recordModes')) {
526
+ source.recordModes = boundedDataRecord(source.recordModes, RECORD_MODE_KEYS, `${label} entry record modes`);
527
+ if (!source.recordModes)
528
+ throw new TypeError(`${label} entry record modes must be an ordinary object.`);
529
+ }
379
530
  return source;
380
531
  });
381
532
  }
@@ -405,48 +556,62 @@ function hostnames(value) {
405
556
  return [...new Set(names)].sort(compareCodeUnits).slice(0, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS);
406
557
  }
407
558
  function digest(value) {
408
- const candidate = text(value, DOMAIN_CONTROL_SPKI_SHA256_HEX_LENGTH)?.toLowerCase() ?? '';
559
+ if (typeof value !== 'string' || CONTROL_RE.test(value))
560
+ return null;
561
+ const candidate = value.trim().toLowerCase();
409
562
  return SPKI_DIGEST_PATTERN.test(candidate) ? candidate : null;
410
563
  }
411
- function normalizeEntry(value) {
564
+ function normalizeEntry(value, legacyRecords, includeModes) {
412
565
  if (!value || typeof value !== 'object' || Array.isArray(value))
413
566
  return null;
414
567
  const source = value;
415
568
  const domain = domainControlName(source.domain);
416
569
  if (!domain)
417
570
  return null;
571
+ const tlsSpkiSha256 = digest(source.tlsSpkiSha256);
572
+ if (source.tlsSpkiSha256 !== undefined && source.tlsSpkiSha256 !== null
573
+ && (typeof source.tlsSpkiSha256 !== 'string' || source.tlsSpkiSha256.trim())
574
+ && !tlsSpkiSha256) {
575
+ throw new TypeError('Domain control TLS public-key fingerprint must be exactly 64 hexadecimal characters.');
576
+ }
577
+ const records = {
578
+ nameservers: Object.freeze(legacyRecords ? hostnames(source.nameservers) : canonicalDomainControlRecords(source.nameservers, 'nameservers')),
579
+ ds: Object.freeze(legacyRecords ? canonicalDomainControlRecordList(source.ds, 'ds') : canonicalDomainControlRecords(source.ds, 'ds')),
580
+ mx: Object.freeze(legacyRecords ? canonicalDomainControlRecordList(source.mx, 'mx') : canonicalDomainControlRecords(source.mx, 'mx')),
581
+ caa: Object.freeze(legacyRecords ? canonicalDomainControlRecordList(source.caa, 'caa') : canonicalDomainControlRecords(source.caa, 'caa')),
582
+ };
418
583
  return Object.freeze({
419
584
  domain,
420
- nameservers: Object.freeze(hostnames(source.nameservers)),
421
- ds: Object.freeze(canonicalDomainControlRecordList(source.ds, 'ds')),
422
- mx: Object.freeze(canonicalDomainControlRecordList(source.mx, 'mx')),
423
- caa: Object.freeze(canonicalDomainControlRecordList(source.caa, 'caa')),
585
+ ...records,
586
+ ...(includeModes ? { recordModes: normalizeDomainControlRecordModes(source.recordModes, records) } : {}),
424
587
  tlsIssuer: text(source.tlsIssuer, MAX_DOMAIN_CONTROL_TEXT_LENGTH)?.toLowerCase() ?? null,
425
- tlsSpkiSha256: digest(source.tlsSpkiSha256),
588
+ tlsSpkiSha256,
426
589
  registrarLock: source.registrarLock === 'required' || source.registrarLock === 'not_required'
427
590
  ? source.registrarLock
428
591
  : null,
429
- renewalReviewAt: timestamp(source.renewalReviewAt, true),
592
+ renewalReviewAt: timestamp(source.renewalReviewAt, legacyRecords),
430
593
  note: text(source.note, MAX_DOMAIN_CONTROL_NOTE_LENGTH),
431
594
  });
432
595
  }
433
596
  function buildUnsignedDomainControlPassportInternal(input, generatedAtValue, options = {}) {
434
597
  const source = boundedDataRecord(input, INPUT_KEYS, 'Domain control manifest input');
435
- if (!source || source.schema !== DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA || source.version !== DOMAIN_CONTROL_MANIFEST_INPUT_VERSION) {
436
- throw new TypeError(`Domain control manifest input must use ${DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA} version ${DOMAIN_CONTROL_MANIFEST_INPUT_VERSION}.`);
598
+ if (!source || source.schema !== DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA || !SUPPORTED_DOMAIN_CONTROL_MANIFEST_INPUT_VERSIONS.some((version) => version === source.version)) {
599
+ throw new TypeError(`Domain control manifest input must use ${DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA} version ${SUPPORTED_DOMAIN_CONTROL_MANIFEST_INPUT_VERSIONS.join(' or ')}.`);
437
600
  }
438
- const inputEntries = boundedDomainControlPassportEntries(source.entries, 'Domain control manifest input');
601
+ const legacy = source.version === PUBLIC_DOMAIN_CONTROL_MANIFEST_INPUT_VERSION;
602
+ const version = options.manifestVersion ?? DOMAIN_CONTROL_MANIFEST_VERSION;
603
+ const inputEntries = boundedDomainControlPassportEntries(source.entries, 'Domain control manifest input', legacy);
439
604
  source.entries = inputEntries;
440
605
  assertDomainControlPassportByteBudget(source, 'Domain control manifest input');
441
606
  const generatedAt = timestamp(generatedAtValue, options.legacyGeneratedAt === true);
442
- const expiresAt = timestamp(source.expiresAt, true);
607
+ const expiresAt = timestamp(source.expiresAt, legacy);
443
608
  if (!generatedAt) {
444
609
  throw new TypeError('Domain control manifest generation time must be valid and include an explicit timezone.');
445
610
  }
446
611
  if (!expiresAt || Date.parse(expiresAt) <= Date.parse(generatedAt)) {
447
612
  throw new TypeError('Domain control manifest expiry must be a valid time after generation.');
448
613
  }
449
- const entries = inputEntries.map(normalizeEntry);
614
+ const entries = inputEntries.map((entry) => normalizeEntry(entry, legacy, version === DOMAIN_CONTROL_MANIFEST_VERSION));
450
615
  if (entries.some((entry) => entry === null))
451
616
  throw new TypeError('Domain control manifest contains an invalid entry.');
452
617
  const normalizedEntries = entries;
@@ -458,11 +623,11 @@ function buildUnsignedDomainControlPassportInternal(input, generatedAtValue, opt
458
623
  : [...normalizedEntries].sort((left, right) => compareCodeUnits(left.domain, right.domain));
459
624
  return Object.freeze({
460
625
  schema: DOMAIN_CONTROL_PASSPORT_SCHEMA,
461
- version: DOMAIN_CONTROL_MANIFEST_VERSION,
626
+ version,
462
627
  generatedAt,
463
628
  expiresAt,
464
629
  entries: Object.freeze(orderedEntries),
465
- limitations: DOMAIN_CONTROL_PASSPORT_LIMITATIONS,
630
+ limitations: version === PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION ? PUBLIC_DOMAIN_CONTROL_MANIFEST_LIMITATIONS : DOMAIN_CONTROL_PASSPORT_LIMITATIONS,
466
631
  });
467
632
  }
468
633
  export function buildUnsignedDomainControlPassport(input, generatedAtValue, options = {}) {
@@ -479,29 +644,32 @@ function normalizeDomainControlPassportDocumentInternal(value) {
479
644
  || !MANIFEST_DIGEST_PATTERN.test(integrity.digestSha256)) {
480
645
  throw new TypeError('Domain control manifest has an unsupported or malformed structure.');
481
646
  }
647
+ const canonicalization = resolveArtifactCanonicalization(source.version, integrity.canonicalization, DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, 'Domain control manifest');
648
+ const legacy = source.version === PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION;
649
+ const expectedLimitations = legacy ? PUBLIC_DOMAIN_CONTROL_MANIFEST_LIMITATIONS : DOMAIN_CONTROL_PASSPORT_LIMITATIONS;
482
650
  let limitations;
483
651
  try {
484
- limitations = boundedDataArray(source.limitations, 'Domain control manifest limitations', DOMAIN_CONTROL_PASSPORT_LIMITATIONS.length, DOMAIN_CONTROL_PASSPORT_LIMITATIONS.length);
652
+ limitations = boundedDataArray(source.limitations, 'Domain control manifest limitations', expectedLimitations.length, expectedLimitations.length);
485
653
  }
486
654
  catch {
487
655
  throw new TypeError('Domain control manifest has an unsupported or malformed structure.');
488
656
  }
489
- if (limitations.some((item, index) => item !== DOMAIN_CONTROL_PASSPORT_LIMITATIONS[index])) {
657
+ if (limitations.some((item, index) => item !== expectedLimitations[index])) {
490
658
  throw new TypeError('Domain control manifest has an unsupported or malformed structure.');
491
659
  }
492
660
  source.limitations = limitations;
493
- const manifestEntries = boundedDomainControlPassportEntries(source.entries, 'Domain control manifest');
661
+ const manifestEntries = boundedDomainControlPassportEntries(source.entries, 'Domain control manifest', legacy);
494
662
  source.entries = manifestEntries;
495
663
  source.integrity = integrity;
496
664
  assertDomainControlPassportByteBudget(source);
497
- const canonicalization = resolveArtifactCanonicalization(source.version, integrity.canonicalization, DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, 'Domain control manifest');
498
665
  const baseUnsigned = buildUnsignedDomainControlPassportInternal({
499
666
  schema: DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA,
500
- version: DOMAIN_CONTROL_MANIFEST_INPUT_VERSION,
667
+ version: legacy ? PUBLIC_DOMAIN_CONTROL_MANIFEST_INPUT_VERSION : DOMAIN_CONTROL_MANIFEST_INPUT_VERSION,
501
668
  expiresAt: source.expiresAt,
502
669
  entries: manifestEntries,
503
670
  }, source.generatedAt, {
504
671
  preserveEntryOrder: true,
672
+ manifestVersion: legacy ? PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION : DOMAIN_CONTROL_MANIFEST_VERSION,
505
673
  });
506
674
  const unsigned = baseUnsigned;
507
675
  const { integrity: _integrity, ...suppliedUnsigned } = source;
@@ -0,0 +1,79 @@
1
+ import { normalizeExplicitIsoTimestamp } from "./observation.mjs";
2
+ import { normalizeDomain } from "./domain-name.mjs";
3
+ export const DOMAIN_POSTURE_COMPARISON_VERSION = 1;
4
+ export const MAX_POSTURE_CHECKS = 32;
5
+ export const MAX_POSTURE_CHECK_RECORDS = 64;
6
+ export const MAX_POSTURE_RECORD_LENGTH = 4096;
7
+ function contextRecord(value, keys, label) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value)
9
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))
10
+ || Reflect.ownKeys(value).length !== keys.length)
11
+ throw new TypeError(`${label} has an invalid structure.`);
12
+ const result = {};
13
+ for (const key of keys) {
14
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
15
+ if (!descriptor?.enumerable || !('value' in descriptor))
16
+ throw new TypeError(`${label} requires ordinary data fields.`);
17
+ result[key] = descriptor.value;
18
+ }
19
+ if (result.version !== DOMAIN_POSTURE_COMPARISON_VERSION)
20
+ throw new TypeError(`${label} has an unsupported comparison version.`);
21
+ return result;
22
+ }
23
+ /** Missing legacy provenance stays missing; declared malformed or future context is rejected. */
24
+ export function normalizeDomainPostureSourceContext(value) {
25
+ if (value === undefined)
26
+ return undefined;
27
+ const source = contextRecord(value, ['version', 'source', 'observedAt', 'state', 'omittedRecords'], 'Posture source context');
28
+ const observedAt = normalizeExplicitIsoTimestamp(source.observedAt);
29
+ if (typeof source.source !== 'string' || !['dns_ns', 'dns_mx', 'dns_caa', 'registry_rdap'].includes(source.source)
30
+ || typeof source.state !== 'string' || !['complete', 'partial', 'unavailable'].includes(source.state)
31
+ || (source.observedAt !== null && observedAt === null)
32
+ || (source.omittedRecords !== null && (!Number.isSafeInteger(source.omittedRecords) || Number(source.omittedRecords) < 0))
33
+ || (source.state === 'complete' && source.omittedRecords !== 0)) {
34
+ throw new TypeError('Posture source context has invalid provenance or omission counts.');
35
+ }
36
+ return Object.freeze({ ...source, observedAt });
37
+ }
38
+ export function normalizeDomainPostureProfileContext(value) {
39
+ if (value === undefined)
40
+ return undefined;
41
+ const source = contextRecord(value, ['version', 'domain', 'profileId', 'profileFingerprint'], 'Posture profile context');
42
+ const domain = typeof source.domain === 'string' ? normalizeDomain(source.domain) : '';
43
+ if (!domain || domain !== source.domain || typeof source.profileId !== 'string'
44
+ || !source.profileId.trim() || source.profileId.length > 200 || /[\u0000-\u001f\u007f]/u.test(source.profileId)
45
+ || typeof source.profileFingerprint !== 'string' || !/^[a-f0-9]{64}$/u.test(source.profileFingerprint)) {
46
+ throw new TypeError('Posture profile context has an invalid target or collection identity.');
47
+ }
48
+ return Object.freeze(source);
49
+ }
50
+ export const POSTURE_CHECK_SOURCES = Object.freeze({
51
+ nameservers: 'dns_ns', mx: 'dns_mx', caa: 'dns_caa', registration_lock: 'registry_rdap',
52
+ });
53
+ export const POSTURE_SOURCE_LABELS = Object.freeze({
54
+ dns_ns: 'DNS NS', dns_mx: 'DNS MX', dns_caa: 'DNS CAA', registry_rdap: 'Registry RDAP',
55
+ });
56
+ export function postureTransferRestriction(records) {
57
+ if (!records.length)
58
+ return null;
59
+ return records.some((value) => value === 'clienttransferprohibited' || value === 'servertransferprohibited')
60
+ ? 'required' : 'not_required';
61
+ }
62
+ export function postureSourceAdmission(check, capturedAt, now) {
63
+ if (!check?.sourceContext)
64
+ return 'Source comparison context is unavailable for this retained check.';
65
+ const context = check.sourceContext;
66
+ const expected = POSTURE_CHECK_SOURCES[check.id];
67
+ if (context.version !== DOMAIN_POSTURE_COMPARISON_VERSION || !expected || expected !== context.source)
68
+ return 'The source or comparison version does not match this field.';
69
+ const captured = normalizeExplicitIsoTimestamp(capturedAt);
70
+ const observed = normalizeExplicitIsoTimestamp(context.observedAt);
71
+ const reviewed = normalizeExplicitIsoTimestamp(now);
72
+ if (!captured || !observed || !reviewed)
73
+ return 'A source, capture or review time is unavailable.';
74
+ if (Date.parse(observed) > Date.parse(captured) || Date.parse(captured) > Date.parse(reviewed))
75
+ return 'The source or capture time is later than its review.';
76
+ if (context.state !== 'complete' || context.omittedRecords !== 0)
77
+ return 'The source observation is incomplete; retained values cannot establish alignment or drift.';
78
+ return null;
79
+ }