@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,6 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { domainToASCII } from 'node:url';
4
+ import { normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
4
5
  const DNSSEC_EVIDENCE_SCHEMA = 'whoisleuth.dnssec-evidence-validation';
5
6
  const DNSSEC_EVIDENCE_VERSION = 1;
6
7
  const MAX_DNSSEC_RECORDS = 50;
@@ -99,20 +100,25 @@ function dsDigest(owner, dnskey, digestType) {
99
100
  }
100
101
  function normalizeRrsig(value) {
101
102
  const source = record(value);
102
- if (!source || typeof source.inception !== 'string' || typeof source.expiration !== 'string')
103
+ if (!source)
104
+ return null;
105
+ const normalizedInception = normalizeLegacyIsoTimestamp(source.inception);
106
+ const normalizedExpiration = normalizeLegacyIsoTimestamp(source.expiration);
107
+ if (!normalizedInception || !normalizedExpiration)
103
108
  return null;
104
- const inception = Date.parse(source.inception);
105
- const expiration = Date.parse(source.expiration);
106
- if (!Number.isFinite(inception) || !Number.isFinite(expiration) || inception > expiration)
109
+ const inception = Date.parse(normalizedInception);
110
+ const expiration = Date.parse(normalizedExpiration);
111
+ if (inception > expiration)
107
112
  return null;
108
113
  return Object.freeze({ inception, expiration });
109
114
  }
110
115
  function signatureTimeState(value, observedAt) {
111
- if (!value.length || typeof observedAt !== 'string')
116
+ if (!value.length)
112
117
  return 'unavailable';
113
- const observed = Date.parse(observedAt);
114
- if (!Number.isFinite(observed))
118
+ const normalizedObservedAt = normalizeLegacyIsoTimestamp(observedAt);
119
+ if (!normalizedObservedAt)
115
120
  return 'unavailable';
121
+ const observed = Date.parse(normalizedObservedAt);
116
122
  let withinWindow = 0;
117
123
  for (const item of value) {
118
124
  if (observed >= item.inception && observed <= item.expiration)
@@ -160,7 +166,8 @@ function validateDnssecEvidence(input) {
160
166
  limitations: Object.freeze(limitations),
161
167
  });
162
168
  }
163
- if (input.delegationSigned === false && dsRecords.length === 0) {
169
+ const incompleteInput = rejectedCount > 0 || truncated;
170
+ if (input.delegationSigned === false && dsRecords.length === 0 && !incompleteInput) {
164
171
  findings.push('The supplied delegation state was unsigned and no DS records were supplied.');
165
172
  return Object.freeze({
166
173
  schema: DNSSEC_EVIDENCE_SCHEMA,
@@ -191,18 +198,24 @@ function validateDnssecEvidence(input) {
191
198
  if (matched)
192
199
  matchedDsCount += 1;
193
200
  }
194
- const definiteConflict = input.delegationSigned === false && dsRecords.length > 0
201
+ const observedConflict = input.delegationSigned === false && dsRecords.length > 0
195
202
  || input.delegationSigned === true && dsRecords.length === 0 && !truncated
196
203
  || signatureState === 'outside_window'
197
204
  || (dsRecords.length > 0 && dnskeys.length > 0 && matchedDsCount === 0 && unsupportedDigestCount < dsRecords.length);
205
+ const definiteConflict = observedConflict && !incompleteInput;
198
206
  if (definiteConflict)
199
207
  findings.push('The supplied delegation, signature-time, DS, or DNSKEY evidence conflicts.');
208
+ else if (observedConflict)
209
+ findings.push('The retained subset contains conflicting evidence, but omitted or rejected records prevent a definitive conflict conclusion.');
200
210
  if (matchedDsCount > 0)
201
211
  findings.push(`${matchedDsCount} supplied DS record(s) matched a supplied DNSKEY.`);
202
212
  if (unsupportedDigestCount > 0)
203
213
  findings.push(`${unsupportedDigestCount} DS digest type(s) were retained as unsupported.`);
204
214
  if (signatureState === 'mixed')
205
215
  findings.push('The supplied RRSIG validity windows were mixed at the observation time.');
216
+ if (input.delegationSigned === false && dsRecords.length === 0 && incompleteInput) {
217
+ findings.push('The retained subset contains no usable DS record, but omitted or rejected records prevent a definitive unsigned conclusion.');
218
+ }
206
219
  if (rejectedCount > 0)
207
220
  findings.push(`${rejectedCount} malformed record(s) were rejected.`);
208
221
  if (truncated)
@@ -210,7 +223,7 @@ function validateDnssecEvidence(input) {
210
223
  const completeLocalRelationship = dsRecords.length > 0
211
224
  && dnskeys.length > 0
212
225
  && matchedDsCount > 0
213
- && !definiteConflict
226
+ && !observedConflict
214
227
  && rejectedCount === 0
215
228
  && !truncated
216
229
  && unsupportedDigestCount === 0
@@ -1,9 +1,10 @@
1
1
  import { domainToASCII } from 'node:url';
2
2
  import { exactKeys } from "./bounded-contract-normalizers.mjs";
3
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
3
4
  const DOMAIN_ASSURANCE_INPUT_SCHEMA = 'whoisleuth.domain-assurance.input';
4
5
  const DOMAIN_ASSURANCE_SCHEMA = 'whoisleuth.domain-assurance';
5
6
  const DOMAIN_ASSURANCE_VERSION = 2;
6
- const DOMAIN_ASSURANCE_SUPPORTED_INPUT_VERSIONS = Object.freeze([1, 2]);
7
+ const DOMAIN_ASSURANCE_SUPPORTED_INPUT_VERSIONS = Object.freeze([DOMAIN_ASSURANCE_VERSION]);
7
8
  const MAX_ASSURANCE_INPUT_BYTES = 2 * 1024 * 1024;
8
9
  const ROOT_KEYS = Object.freeze({
9
10
  'planned-change': new Set(['schema', 'version', 'kind', 'domain', 'change']),
@@ -45,9 +46,10 @@ function optionalText(value, maximum = 160) {
45
46
  }
46
47
  function timestamp(value, field) {
47
48
  const normalized = text(value, field, 64);
48
- if (!Number.isFinite(Date.parse(normalized)))
49
- throw new Error(`${field} must be an ISO-compatible timestamp.`);
50
- return new Date(normalized).toISOString();
49
+ const canonical = normalizeExplicitIsoTimestamp(normalized);
50
+ if (canonical)
51
+ return canonical;
52
+ throw new Error(`${field} must be an ISO timestamp with an explicit timezone.`);
51
53
  }
52
54
  function optionalTimestamp(value, field) {
53
55
  if (value === null || value === undefined || value === '')
@@ -252,7 +254,7 @@ const RETIREMENT_CHECKS = Object.freeze([
252
254
  ['delegatedDnsMonitored', 'Delegated DNS remains monitored during retirement', true],
253
255
  ['reRegistrationPrevented', 'Registration continuity or defensive renewal is planned', true],
254
256
  ]);
255
- function buildRetirement(input, inputVersion) {
257
+ function buildRetirement(input) {
256
258
  const checksInput = record(input.checks);
257
259
  exactKeys(checksInput, new Set(RETIREMENT_CHECKS.map(([id]) => id)), 'checks');
258
260
  const fixedChecks = RETIREMENT_CHECKS.map(([id, checkLabel, expected]) => {
@@ -266,7 +268,7 @@ function buildRetirement(input, inputVersion) {
266
268
  });
267
269
  const fixedIds = new Set(RETIREMENT_CHECKS.map(([id]) => id));
268
270
  const customIds = new Set();
269
- const customChecks = inputVersion === 2 && input.customChecks !== undefined
271
+ const customChecks = input.customChecks !== undefined
270
272
  ? boundedArray(input.customChecks, 'customChecks', 0, 20).map((raw, index) => {
271
273
  const item = record(raw);
272
274
  exactKeys(item, CUSTOM_RETIREMENT_CHECK_KEYS, `customChecks[${index}]`);
@@ -304,18 +306,16 @@ function buildRetirement(input, inputVersion) {
304
306
  }
305
307
  function buildDomainAssurance(inputRaw, generatedAt = new Date().toISOString()) {
306
308
  const input = record(inputRaw);
307
- if (input.schema !== DOMAIN_ASSURANCE_INPUT_SCHEMA
308
- || !DOMAIN_ASSURANCE_SUPPORTED_INPUT_VERSIONS.includes(input.version)) {
309
- throw new Error(`Domain assurance input must use ${DOMAIN_ASSURANCE_INPUT_SCHEMA} version 1 or ${DOMAIN_ASSURANCE_VERSION}.`);
309
+ if (input.schema !== DOMAIN_ASSURANCE_INPUT_SCHEMA || input.version !== DOMAIN_ASSURANCE_VERSION) {
310
+ throw new Error(`Domain assurance input must use ${DOMAIN_ASSURANCE_INPUT_SCHEMA} version ${DOMAIN_ASSURANCE_VERSION}.`);
310
311
  }
311
- const inputVersion = input.version;
312
312
  const kind = enumValue(input.kind, 'kind', ['planned-change', 'recovery-dependencies', 'retirement']);
313
- exactKeys(input, kind === 'retirement' && inputVersion === 2 ? RETIREMENT_ROOT_KEYS_V2 : ROOT_KEYS[kind], 'Domain assurance input');
313
+ exactKeys(input, kind === 'retirement' ? RETIREMENT_ROOT_KEYS_V2 : ROOT_KEYS[kind], 'Domain assurance input');
314
314
  const result = kind === 'planned-change'
315
315
  ? buildPlannedChange(input)
316
316
  : kind === 'recovery-dependencies'
317
317
  ? buildRecoveryDependencies(input)
318
- : buildRetirement(input, inputVersion);
318
+ : buildRetirement(input);
319
319
  return {
320
320
  schema: DOMAIN_ASSURANCE_SCHEMA,
321
321
  version: DOMAIN_ASSURANCE_VERSION,
@@ -1,10 +1,11 @@
1
1
  import { exactKeys, requireBoundedString, requireDomainName, requireIsoTimestamp, requireRecord, } from "./bounded-contract-normalizers.mjs";
2
2
  import { DOMAIN_ASSURANCE_INPUT_SCHEMA, buildDomainAssurance, } from "./domain-assurance.mjs";
3
3
  import { DOMAIN_CHANGE_INPUT_SCHEMA, reviewDomainChange, } from "./domain-change-review.mjs";
4
- import { sha256ArtifactDigest } from "../frontend/src/lib/analysis/artifact-integrity.js";
4
+ import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../packages/evidence/artifact-integrity.mjs";
5
5
  export const DOMAIN_CHANGE_PACKET_INPUT_SCHEMA = 'whoisleuth.domain-change-packet.input';
6
6
  export const DOMAIN_CHANGE_PACKET_SCHEMA = 'whoisleuth.domain-change-packet';
7
- export const DOMAIN_CHANGE_PACKET_VERSION = 1;
7
+ export const DOMAIN_CHANGE_PACKET_INPUT_VERSION = 1;
8
+ export const DOMAIN_CHANGE_PACKET_VERSION = 2;
8
9
  export const MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES = 6 * 1024 * 1024;
9
10
  const ROOT_KEYS = new Set([
10
11
  'schema',
@@ -57,8 +58,8 @@ function changeSummary(before, after) {
57
58
  }
58
59
  export async function buildDomainChangePacket(inputRaw, generatedAtValue = new Date().toISOString()) {
59
60
  const input = record(inputRaw, 'Domain change packet input');
60
- if (input.schema !== DOMAIN_CHANGE_PACKET_INPUT_SCHEMA || input.version !== 1) {
61
- throw new TypeError(`Domain change packet input must use ${DOMAIN_CHANGE_PACKET_INPUT_SCHEMA} version 1.`);
61
+ if (input.schema !== DOMAIN_CHANGE_PACKET_INPUT_SCHEMA || input.version !== DOMAIN_CHANGE_PACKET_INPUT_VERSION) {
62
+ throw new TypeError(`Domain change packet input must use ${DOMAIN_CHANGE_PACKET_INPUT_SCHEMA} version ${DOMAIN_CHANGE_PACKET_INPUT_VERSION}.`);
62
63
  }
63
64
  exactKeys(input, ROOT_KEYS, 'Domain change packet input');
64
65
  const domain = requireDomainName(input.domain, 'domain');
@@ -105,7 +106,8 @@ export async function buildDomainChangePacket(inputRaw, generatedAtValue = new D
105
106
  ...unsigned,
106
107
  integrity: Object.freeze({
107
108
  algorithm: 'SHA-256',
108
- digestSha256: await sha256ArtifactDigest(unsigned),
109
+ canonicalization: SORTED_JSON_V2,
110
+ digestSha256: await sha256ArtifactDigestV2(unsigned),
109
111
  }),
110
112
  });
111
113
  }