@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1

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 (236) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/project-metadata.mjs +1 -1
  100. package/lib/provider-policy-admission.mjs +3 -1
  101. package/lib/rdap-bootstrap.mjs +76 -16
  102. package/lib/rdap-client.mjs +20 -12
  103. package/lib/rdap-normalization.mjs +306 -75
  104. package/lib/rdap-transport.mjs +2 -2
  105. package/lib/rdap-validation.mjs +65 -14
  106. package/lib/rdap.mjs +2 -2
  107. package/lib/registrable-domain.mjs +36 -0
  108. package/lib/registry-insights.mjs +7 -0
  109. package/lib/response-policy.mjs +27 -4
  110. package/lib/risk-calibration-summary.mjs +35 -33
  111. package/lib/risk-scoring.mjs +4 -3
  112. package/lib/safe-fetch.mjs +62 -40
  113. package/lib/security-txt.mjs +1 -1
  114. package/lib/smtp-transport-review.mjs +8 -5
  115. package/lib/sslbl-intelligence.mjs +6 -8
  116. package/lib/static-html-analysis.mjs +301 -4
  117. package/lib/structured-data-identity.mjs +51 -19
  118. package/lib/threat-intelligence-fixtures.mjs +2 -4
  119. package/lib/threat-intelligence-manifests.mjs +2 -7
  120. package/lib/threat-intelligence-runtime.mjs +3 -5
  121. package/lib/threat-intelligence-types.mjs +3 -0
  122. package/lib/threatfox-intelligence.mjs +7 -3
  123. package/lib/tls-intelligence.mjs +4 -4
  124. package/lib/urlhaus-intelligence.mjs +4 -2
  125. package/lib/urlscan-intelligence.mjs +6 -5
  126. package/lib/website-security-posture.mjs +1 -1
  127. package/lib/website-technology.mjs +1 -1
  128. package/lib/whois-authority.mjs +43 -8
  129. package/lib/whois-chain.mjs +25 -3
  130. package/lib/whois-normalization.mjs +3 -1
  131. package/lib/whois-transport.mjs +12 -3
  132. package/lib/zone-intent-review.mjs +1 -1
  133. package/package.json +13 -5
  134. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  135. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  136. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  137. package/packages/cases/case-migration-model.mjs +476 -0
  138. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  139. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  140. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  141. package/packages/cases/case-record-model.mjs +9 -0
  142. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  143. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  144. package/packages/cases/case-response-model.mjs +1604 -0
  145. package/packages/cases/case-response-packet.mjs +1312 -0
  146. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  147. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  148. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  149. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  150. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  151. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  152. package/packages/comparison/favicon-similarity.mjs +78 -0
  153. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  154. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  155. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  156. package/packages/contracts/analyst-review-state.mjs +50 -0
  157. package/packages/contracts/case-portability.mjs +1310 -0
  158. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  159. package/packages/contracts/cli-lookup.mjs +394 -0
  160. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  161. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  162. package/packages/contracts/domain-control-monitor.mjs +310 -0
  163. package/packages/contracts/domain-control-review.mjs +828 -0
  164. package/packages/contracts/domain-name.mjs +4 -0
  165. package/packages/contracts/external-observation-interchange.mjs +224 -0
  166. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  167. package/packages/contracts/investigation-portability.mjs +331 -0
  168. package/packages/contracts/offline-comparison.mjs +160 -0
  169. package/packages/contracts/risk-calibration.mjs +1288 -0
  170. package/packages/contracts/schema-compatibility.mjs +71 -0
  171. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  172. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  173. package/packages/contracts/workspace-portability.mjs +1347 -0
  174. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  175. package/packages/evidence/decision-fact.mjs +735 -0
  176. package/packages/evidence/domain-control-runtime.mjs +547 -0
  177. package/packages/evidence/domain-name.mjs +41 -0
  178. package/packages/evidence/observation.mjs +384 -0
  179. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  180. package/packages/interchange/retained-text.mjs +8 -0
  181. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  182. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  183. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  184. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  185. package/packages/investigation/bulk-review-policy.mjs +1 -0
  186. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  187. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  188. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  189. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  190. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  191. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  192. package/packages/monitoring/analyst-review-state.mjs +680 -0
  193. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +71 -48
  194. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  195. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  196. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  197. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  198. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  199. package/packages/workspace/hostile-input.mjs +172 -0
  200. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  201. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  202. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  203. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  204. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  205. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  206. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  207. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  208. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  209. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  210. package/third-party-notices.txt +14 -14
  211. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  212. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  213. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  214. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  215. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  216. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  217. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  218. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  219. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  220. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  221. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  222. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  223. package/frontend/src/lib/analysis/scoring.js +0 -55
  224. package/frontend/src/lib/analysis/utils.js +0 -255
  225. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  226. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  227. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  228. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  229. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  230. package/lib/bounded-json-response.mjs +0 -170
  231. package/lib/lookup-response-contract.mjs +0 -1034
  232. package/lib/observation.mjs +0 -115
  233. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  236. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,33 +1,261 @@
1
- import { exactKeys, requireBoundedString, requireDomainName, requireIsoTimestamp, requireRecord, } from "./bounded-contract-normalizers.mjs";
2
- export const DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA = 'whoisleuth.domain-control-flight-recorder.input';
3
- export const DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA = 'whoisleuth.domain-control-flight-recorder';
4
- export const DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION = 1;
5
- export const MAX_FLIGHT_RECORDER_OBSERVATIONS = 200;
6
- export const MAX_FLIGHT_RECORDER_WINDOWS = 40;
7
- export const MAX_FLIGHT_RECORDER_FIELDS = 24;
8
- export const MAX_FLIGHT_RECORDER_VALUES = 32;
9
- export const DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS = [
10
- 'registrar',
11
- 'registrar_lock',
12
- 'registry_dnssec',
13
- 'registry_nameservers',
14
- 'whois_nameservers',
15
- 'delegated_nameservers',
16
- 'delegation_ds',
17
- 'mail_exchangers',
18
- 'caa_policy',
19
- 'tls_certificate',
20
- 'tls_public_key',
21
- 'http_origin',
22
- 'page_identity',
23
- ];
24
- const ROOT_KEYS = new Set(['schema', 'version', 'observations', 'approvedWindows']);
25
- const OBSERVATION_KEYS = new Set(['domain', 'observedAt', 'collectionDepth', 'fields']);
26
- const FIELD_KEYS = new Set(['id', 'source', 'state', 'values']);
27
- const WINDOW_KEYS = new Set(['id', 'domain', 'startsAt', 'endsAt', 'fields', 'reason']);
1
+ import { Buffer } from 'node:buffer';
2
+ import { requireBoundedString, requireDomainName, requireIsoTimestamp, } from "./bounded-contract-normalizers.mjs";
3
+ import { DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS, DOMAIN_CONTROL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_FIELD_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS, DOMAIN_CONTROL_FLIGHT_RECORDER_OBSERVATION_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_ROOT_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_SUMMARY_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, DOMAIN_CONTROL_FLIGHT_RECORDER_WINDOW_KEYS, MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_BYTES, MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_OUTPUT_BYTES, MAX_FLIGHT_RECORDER_FIELDS, MAX_FLIGHT_RECORDER_EVENTS, MAX_FLIGHT_RECORDER_INPUT_VALUES, MAX_FLIGHT_RECORDER_JSON_DEPTH, MAX_FLIGHT_RECORDER_JSON_VALUES, MAX_FLIGHT_RECORDER_OBSERVATIONS, MAX_FLIGHT_RECORDER_SOURCE_LENGTH, MAX_FLIGHT_RECORDER_VALUE_LENGTH, MAX_FLIGHT_RECORDER_VALUES, MAX_FLIGHT_RECORDER_WINDOWS, MAX_FLIGHT_RECORDER_WINDOW_ID_LENGTH, MAX_FLIGHT_RECORDER_WINDOW_REASON_LENGTH, MIN_FLIGHT_RECORDER_OBSERVATIONS, } from "../packages/contracts/domain-control-flight-recorder.mjs";
4
+ export { DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, MAX_FLIGHT_RECORDER_FIELDS, MAX_FLIGHT_RECORDER_OBSERVATIONS, MAX_FLIGHT_RECORDER_VALUES, MAX_FLIGHT_RECORDER_WINDOWS, };
5
+ const ROOT_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_KEYS);
6
+ const OBSERVATION_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_OBSERVATION_KEYS);
7
+ const FIELD_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_FIELD_KEYS);
8
+ const WINDOW_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_WINDOW_KEYS);
9
+ const DOCUMENT_ROOT_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_ROOT_KEYS);
10
+ const EVENT_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS);
11
+ const EVENT_APPROVED_WINDOW_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS);
12
+ const SUMMARY_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_SUMMARY_KEYS);
28
13
  const FIELD_IDS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS);
29
14
  const STATES = new Set(['observed', 'partial', 'unavailable', 'unsupported']);
30
15
  const CONTROL_REPLACE_RE = /[\u0000-\u001f\u007f]+/gu;
16
+ function ordinalCompare(left, right) {
17
+ return left < right ? -1 : left > right ? 1 : 0;
18
+ }
19
+ function exactRecord(value, keys, label) {
20
+ try {
21
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
22
+ throw new TypeError(`${label} must be an object.`);
23
+ }
24
+ const prototype = Object.getPrototypeOf(value);
25
+ if (prototype !== Object.prototype && prototype !== null) {
26
+ throw new TypeError(`${label} must be an ordinary object.`);
27
+ }
28
+ const ownKeys = Reflect.ownKeys(value);
29
+ const unknownKey = ownKeys.find((key) => typeof key !== 'string' || !keys.has(key));
30
+ if (unknownKey !== undefined) {
31
+ throw new TypeError(`${label} contains an unknown field: ${String(unknownKey)}`);
32
+ }
33
+ if (ownKeys.length !== keys.size) {
34
+ throw new TypeError(`${label} must use its exact registered fields.`);
35
+ }
36
+ const result = {};
37
+ for (const key of keys) {
38
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
39
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
40
+ throw new TypeError(`${label}.${key} must be an enumerable data field.`);
41
+ }
42
+ result[key] = descriptor.value;
43
+ }
44
+ return Object.freeze(result);
45
+ }
46
+ catch (cause) {
47
+ if (cause instanceof TypeError && cause.message.startsWith(label))
48
+ throw cause;
49
+ throw new TypeError(`${label} must be an ordinary object.`);
50
+ }
51
+ }
52
+ function boundedDataArray(value, label, minimum, maximum) {
53
+ try {
54
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
55
+ throw new TypeError(`${label} must be an ordinary array.`);
56
+ }
57
+ if (Object.getOwnPropertyDescriptor(value, Symbol.iterator)
58
+ || Object.getOwnPropertyDescriptor(value, 'toJSON')) {
59
+ throw new TypeError(`${label} must not override array behaviour.`);
60
+ }
61
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
62
+ const length = lengthDescriptor && !lengthDescriptor.enumerable && 'value' in lengthDescriptor
63
+ ? lengthDescriptor.value
64
+ : null;
65
+ if (!Number.isSafeInteger(length) || Number(length) < minimum || Number(length) > maximum) {
66
+ throw new TypeError(`${label} must contain from ${minimum} to ${maximum} entries.`);
67
+ }
68
+ const boundedLength = Number(length);
69
+ const ownKeys = Reflect.ownKeys(value);
70
+ if (ownKeys.length !== boundedLength + 1 || !ownKeys.includes('length')) {
71
+ throw new TypeError(`${label} must be a dense array without additional fields.`);
72
+ }
73
+ const result = [];
74
+ for (let index = 0; index < boundedLength; index += 1) {
75
+ const key = String(index);
76
+ if (!ownKeys.includes(key))
77
+ throw new TypeError(`${label} must be dense.`);
78
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
79
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
80
+ throw new TypeError(`${label}[${index}] must be an enumerable data field.`);
81
+ }
82
+ result.push(descriptor.value);
83
+ }
84
+ return Object.freeze(result);
85
+ }
86
+ catch (cause) {
87
+ if (cause instanceof TypeError && cause.message.startsWith(label))
88
+ throw cause;
89
+ throw new TypeError(`${label} must be an ordinary bounded array.`);
90
+ }
91
+ }
92
+ class FlightRecorderJsonBudgetError extends Error {
93
+ }
94
+ class FlightRecorderJsonStructureError extends Error {
95
+ }
96
+ function addJsonBytes(current, addition, maximum) {
97
+ const total = current + addition;
98
+ if (!Number.isSafeInteger(total) || total > maximum)
99
+ throw new FlightRecorderJsonBudgetError();
100
+ return total;
101
+ }
102
+ function jsonStringBytes(value, maximum) {
103
+ let bytes = 2;
104
+ for (let index = 0; index < value.length; index += 1) {
105
+ const code = value.charCodeAt(index);
106
+ let addition = 0;
107
+ if (code === 0x22 || code === 0x5c)
108
+ addition = 2;
109
+ else if (code <= 0x1f) {
110
+ addition = code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d ? 2 : 6;
111
+ }
112
+ else if (code >= 0xd800 && code <= 0xdbff) {
113
+ const next = value.charCodeAt(index + 1);
114
+ if (next >= 0xdc00 && next <= 0xdfff) {
115
+ addition = 4;
116
+ index += 1;
117
+ }
118
+ else
119
+ addition = 6;
120
+ }
121
+ else if (code >= 0xdc00 && code <= 0xdfff)
122
+ addition = 6;
123
+ else if (code <= 0x7f)
124
+ addition = 1;
125
+ else if (code <= 0x7ff)
126
+ addition = 2;
127
+ else
128
+ addition = 3;
129
+ bytes = addJsonBytes(bytes, addition, maximum);
130
+ }
131
+ return bytes;
132
+ }
133
+ function boundedPrettyJsonBytes(value, maximum) {
134
+ const active = new Set();
135
+ let valuesSeen = 0;
136
+ const measure = (input, depth, arrayElement) => {
137
+ valuesSeen += 1;
138
+ if (valuesSeen > MAX_FLIGHT_RECORDER_JSON_VALUES || depth > MAX_FLIGHT_RECORDER_JSON_DEPTH) {
139
+ throw new FlightRecorderJsonStructureError();
140
+ }
141
+ if (input === null)
142
+ return 4;
143
+ if (typeof input === 'string')
144
+ return jsonStringBytes(input, maximum);
145
+ if (typeof input === 'boolean')
146
+ return input ? 4 : 5;
147
+ if (typeof input === 'number')
148
+ return Number.isFinite(input) ? String(input).length : 4;
149
+ if (typeof input === 'undefined' || typeof input === 'function' || typeof input === 'symbol') {
150
+ return arrayElement ? 4 : null;
151
+ }
152
+ if (typeof input === 'bigint' || typeof input !== 'object' || active.has(input)) {
153
+ throw new FlightRecorderJsonStructureError();
154
+ }
155
+ active.add(input);
156
+ try {
157
+ const childIndent = (depth + 1) * 2;
158
+ const closingIndent = depth * 2;
159
+ if (Array.isArray(input)) {
160
+ const items = boundedDataArray(input, 'Domain control flight-recorder JSON array', 0, MAX_FLIGHT_RECORDER_JSON_VALUES);
161
+ if (!items.length)
162
+ return 2;
163
+ let bytes = 1;
164
+ for (let index = 0; index < items.length; index += 1) {
165
+ if (index > 0)
166
+ bytes = addJsonBytes(bytes, 1, maximum);
167
+ bytes = addJsonBytes(bytes, 1 + childIndent, maximum);
168
+ bytes = addJsonBytes(bytes, measure(items[index], depth + 1, true) ?? 4, maximum);
169
+ }
170
+ return addJsonBytes(bytes, 1 + closingIndent + 1, maximum);
171
+ }
172
+ const prototype = Object.getPrototypeOf(input);
173
+ if (prototype !== Object.prototype && prototype !== null)
174
+ throw new FlightRecorderJsonStructureError();
175
+ if (Object.getOwnPropertyDescriptor(input, 'toJSON'))
176
+ throw new FlightRecorderJsonStructureError();
177
+ const keys = Reflect.ownKeys(input);
178
+ let bytes = 1;
179
+ let properties = 0;
180
+ for (const key of keys) {
181
+ if (typeof key !== 'string')
182
+ throw new FlightRecorderJsonStructureError();
183
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
184
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
185
+ throw new FlightRecorderJsonStructureError();
186
+ }
187
+ const measured = measure(descriptor.value, depth + 1, false);
188
+ if (measured === null)
189
+ continue;
190
+ if (properties > 0)
191
+ bytes = addJsonBytes(bytes, 1, maximum);
192
+ bytes = addJsonBytes(bytes, 1 + childIndent, maximum);
193
+ bytes = addJsonBytes(bytes, jsonStringBytes(key, maximum), maximum);
194
+ bytes = addJsonBytes(bytes, 2, maximum);
195
+ bytes = addJsonBytes(bytes, measured, maximum);
196
+ properties += 1;
197
+ }
198
+ return properties ? addJsonBytes(bytes, 1 + closingIndent + 1, maximum) : 2;
199
+ }
200
+ finally {
201
+ active.delete(input);
202
+ }
203
+ };
204
+ const bytes = measure(value, 0, false);
205
+ if (bytes === null)
206
+ throw new FlightRecorderJsonStructureError();
207
+ return addJsonBytes(bytes, 1, maximum);
208
+ }
209
+ function assertPrettyJsonByteBudget(value, maximum, label) {
210
+ try {
211
+ boundedPrettyJsonBytes(value, maximum);
212
+ }
213
+ catch (cause) {
214
+ if (cause instanceof FlightRecorderJsonBudgetError) {
215
+ throw new TypeError(`${label} exceeds ${maximum} serialised bytes.`);
216
+ }
217
+ throw new TypeError(`${label} must be serialisable as bounded JSON.`);
218
+ }
219
+ }
220
+ function snapshotFlightRecorderInput(input) {
221
+ const observations = boundedDataArray(input.observations, 'Domain control flight-recorder input observations', MIN_FLIGHT_RECORDER_OBSERVATIONS, MAX_FLIGHT_RECORDER_OBSERVATIONS).map((raw, index) => {
222
+ const item = exactRecord(raw, OBSERVATION_KEYS, `observations[${index}]`);
223
+ const fields = boundedDataArray(item.fields, `observations[${index}].fields`, 0, MAX_FLIGHT_RECORDER_FIELDS)
224
+ .map((fieldRaw, fieldIndex) => {
225
+ const field = exactRecord(fieldRaw, FIELD_KEYS, `observations[${index}].fields[${fieldIndex}]`);
226
+ return Object.freeze({
227
+ id: field.id,
228
+ source: field.source,
229
+ state: field.state,
230
+ values: boundedDataArray(field.values, `observations[${index}].fields[${fieldIndex}].values`, 0, MAX_FLIGHT_RECORDER_INPUT_VALUES),
231
+ });
232
+ });
233
+ return Object.freeze({
234
+ domain: item.domain,
235
+ observedAt: item.observedAt,
236
+ collectionDepth: item.collectionDepth,
237
+ fields: Object.freeze(fields),
238
+ });
239
+ });
240
+ const approvedWindows = boundedDataArray(input.approvedWindows, 'approvedWindows', 0, MAX_FLIGHT_RECORDER_WINDOWS)
241
+ .map((raw, index) => {
242
+ const item = exactRecord(raw, WINDOW_KEYS, `approvedWindows[${index}]`);
243
+ return Object.freeze({
244
+ id: item.id,
245
+ domain: item.domain,
246
+ startsAt: item.startsAt,
247
+ endsAt: item.endsAt,
248
+ fields: boundedDataArray(item.fields, `approvedWindows[${index}].fields`, 1, MAX_FLIGHT_RECORDER_FIELDS),
249
+ reason: item.reason,
250
+ });
251
+ });
252
+ return Object.freeze({
253
+ schema: DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA,
254
+ version: DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
255
+ observations: Object.freeze(observations),
256
+ approvedWindows: Object.freeze(approvedWindows),
257
+ });
258
+ }
31
259
  function text(value, label, maximum = 240) {
32
260
  return requireBoundedString(value, label, maximum)
33
261
  .replace(CONTROL_REPLACE_RE, ' ')
@@ -35,29 +263,21 @@ function text(value, label, maximum = 240) {
35
263
  .trim();
36
264
  }
37
265
  function values(value, label) {
38
- if (!Array.isArray(value) || value.length > MAX_FLIGHT_RECORDER_VALUES * 2) {
39
- throw new TypeError(`${label} must contain no more than ${MAX_FLIGHT_RECORDER_VALUES * 2} bounded values.`);
40
- }
266
+ const input = boundedDataArray(value, label, 0, MAX_FLIGHT_RECORDER_INPUT_VALUES);
41
267
  const output = new Set();
42
- for (const [index, item] of value.entries()) {
43
- const normalized = text(item, `${label}[${index}]`, 500).toLowerCase();
44
- if (normalized)
268
+ for (let index = 0; index < input.length; index += 1) {
269
+ const normalized = text(input[index], `${label}[${index}]`, MAX_FLIGHT_RECORDER_VALUE_LENGTH).toLowerCase();
270
+ if (normalized && output.size < MAX_FLIGHT_RECORDER_VALUES)
45
271
  output.add(normalized);
46
- if (output.size >= MAX_FLIGHT_RECORDER_VALUES)
47
- break;
48
272
  }
49
- return [...output].sort();
273
+ return [...output].sort(ordinalCompare);
50
274
  }
51
275
  function observation(value, index) {
52
- const item = requireRecord(value, `observations[${index}]`);
53
- exactKeys(item, OBSERVATION_KEYS, `observations[${index}]`);
54
- if (!Array.isArray(item.fields) || item.fields.length > MAX_FLIGHT_RECORDER_FIELDS) {
55
- throw new TypeError(`observations[${index}].fields must contain no more than ${MAX_FLIGHT_RECORDER_FIELDS} fields.`);
56
- }
276
+ const item = exactRecord(value, OBSERVATION_KEYS, `observations[${index}]`);
277
+ const inputFields = boundedDataArray(item.fields, `observations[${index}].fields`, 0, MAX_FLIGHT_RECORDER_FIELDS);
57
278
  const seen = new Set();
58
- const fields = item.fields.map((raw, fieldIndex) => {
59
- const field = requireRecord(raw, `observations[${index}].fields[${fieldIndex}]`);
60
- exactKeys(field, FIELD_KEYS, `observations[${index}].fields[${fieldIndex}]`);
279
+ const fields = inputFields.map((raw, fieldIndex) => {
280
+ const field = exactRecord(raw, FIELD_KEYS, `observations[${index}].fields[${fieldIndex}]`);
61
281
  if (typeof field.id !== 'string' || !FIELD_IDS.has(field.id) || seen.has(field.id)) {
62
282
  throw new TypeError(`observations[${index}] contains an unsupported or duplicate field.`);
63
283
  }
@@ -67,11 +287,11 @@ function observation(value, index) {
67
287
  seen.add(field.id);
68
288
  return Object.freeze({
69
289
  id: field.id,
70
- source: text(field.source, `observations[${index}].fields[${fieldIndex}].source`, 120),
290
+ source: text(field.source, `observations[${index}].fields[${fieldIndex}].source`, MAX_FLIGHT_RECORDER_SOURCE_LENGTH),
71
291
  state: field.state,
72
292
  values: Object.freeze(values(field.values, `observations[${index}].fields[${fieldIndex}].values`)),
73
293
  });
74
- }).sort((left, right) => left.id.localeCompare(right.id));
294
+ }).sort((left, right) => ordinalCompare(left.id, right.id));
75
295
  const collectionDepth = item.collectionDepth === 'deep' || item.collectionDepth === 'fast'
76
296
  ? item.collectionDepth
77
297
  : item.collectionDepth === 'unknown'
@@ -87,29 +307,26 @@ function observation(value, index) {
87
307
  });
88
308
  }
89
309
  function approvedWindow(value, index) {
90
- const item = requireRecord(value, `approvedWindows[${index}]`);
91
- exactKeys(item, WINDOW_KEYS, `approvedWindows[${index}]`);
92
- if (!Array.isArray(item.fields) || item.fields.length < 1 || item.fields.length > MAX_FLIGHT_RECORDER_FIELDS) {
93
- throw new TypeError(`approvedWindows[${index}].fields must contain from 1 to ${MAX_FLIGHT_RECORDER_FIELDS} fields.`);
94
- }
95
- const fields = [...new Set(item.fields.map((field) => {
310
+ const item = exactRecord(value, WINDOW_KEYS, `approvedWindows[${index}]`);
311
+ const inputFields = boundedDataArray(item.fields, `approvedWindows[${index}].fields`, 1, MAX_FLIGHT_RECORDER_FIELDS);
312
+ const fields = [...new Set(inputFields.map((field) => {
96
313
  if (typeof field !== 'string' || !FIELD_IDS.has(field)) {
97
314
  throw new TypeError(`approvedWindows[${index}] contains an unsupported field.`);
98
315
  }
99
316
  return field;
100
- }))].sort();
317
+ }))].sort(ordinalCompare);
101
318
  const startsAt = requireIsoTimestamp(item.startsAt, `approvedWindows[${index}].startsAt`);
102
319
  const endsAt = requireIsoTimestamp(item.endsAt, `approvedWindows[${index}].endsAt`);
103
320
  if (Date.parse(endsAt) <= Date.parse(startsAt)) {
104
321
  throw new TypeError(`approvedWindows[${index}].endsAt must be later than startsAt.`);
105
322
  }
106
323
  return Object.freeze({
107
- id: text(item.id, `approvedWindows[${index}].id`, 64),
324
+ id: text(item.id, `approvedWindows[${index}].id`, MAX_FLIGHT_RECORDER_WINDOW_ID_LENGTH),
108
325
  domain: requireDomainName(item.domain, `approvedWindows[${index}].domain`),
109
326
  startsAt,
110
327
  endsAt,
111
328
  fields: Object.freeze(fields),
112
- reason: text(item.reason, `approvedWindows[${index}].reason`, 400),
329
+ reason: text(item.reason, `approvedWindows[${index}].reason`, MAX_FLIGHT_RECORDER_WINDOW_REASON_LENGTH),
113
330
  });
114
331
  }
115
332
  function sameValues(left, right) {
@@ -123,26 +340,26 @@ function matchingWindow(windows, domain, field, observedAt) {
123
340
  && time <= Date.parse(window.endsAt))) ?? null;
124
341
  }
125
342
  export function buildDomainControlFlightRecorder(inputRaw, generatedAtValue = new Date().toISOString()) {
126
- const input = requireRecord(inputRaw, 'Domain control flight-recorder input');
127
- if (input.schema !== DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA || input.version !== 1) {
128
- throw new TypeError(`Domain control flight-recorder input must use ${DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA} version 1.`);
343
+ const inputRoot = exactRecord(inputRaw, ROOT_KEYS, 'Domain control flight-recorder input');
344
+ if (inputRoot.schema !== DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA
345
+ || inputRoot.version !== DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION) {
346
+ throw new TypeError(`Domain control flight-recorder input must use ${DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA} version ${DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION}.`);
129
347
  }
130
- exactKeys(input, ROOT_KEYS, 'Domain control flight-recorder input');
131
- if (!Array.isArray(input.observations)
132
- || input.observations.length < 1
133
- || input.observations.length > MAX_FLIGHT_RECORDER_OBSERVATIONS) {
134
- throw new TypeError(`Domain control flight-recorder input must contain from 1 to ${MAX_FLIGHT_RECORDER_OBSERVATIONS} observations.`);
135
- }
136
- if (!Array.isArray(input.approvedWindows) || input.approvedWindows.length > MAX_FLIGHT_RECORDER_WINDOWS) {
137
- throw new TypeError(`approvedWindows must contain no more than ${MAX_FLIGHT_RECORDER_WINDOWS} entries.`);
138
- }
139
- const observations = input.observations.map(observation)
140
- .sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt) || left.domain.localeCompare(right.domain));
141
- const windows = input.approvedWindows.map(approvedWindow);
348
+ const input = snapshotFlightRecorderInput(inputRoot);
349
+ assertPrettyJsonByteBudget(input, MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_BYTES, 'Domain control flight-recorder input');
350
+ const inputObservations = input.observations;
351
+ const inputWindows = input.approvedWindows;
352
+ const observations = inputObservations.map(observation)
353
+ .sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt)
354
+ || ordinalCompare(left.domain, right.domain));
355
+ const windows = inputWindows.map(approvedWindow);
142
356
  if (new Set(windows.map((window) => window.id)).size !== windows.length) {
143
357
  throw new TypeError('Approved change-window ids must be unique.');
144
358
  }
145
359
  const generatedAt = requireIsoTimestamp(generatedAtValue, 'generatedAt');
360
+ if (observations.some((item) => Date.parse(item.observedAt) > Date.parse(generatedAt))) {
361
+ throw new TypeError('Domain control flight-recorder observations cannot be later than generatedAt.');
362
+ }
146
363
  const latest = new Map();
147
364
  const events = [];
148
365
  for (const item of observations) {
@@ -220,10 +437,10 @@ export function buildDomainControlFlightRecorder(inputRaw, generatedAtValue = ne
220
437
  }
221
438
  }
222
439
  }
223
- const domains = [...new Set(observations.map((item) => item.domain))].sort();
440
+ const domains = [...new Set(observations.map((item) => item.domain))].sort(ordinalCompare);
224
441
  const observedChanges = events.filter((event) => event.kind === 'observed_change');
225
442
  const collectionChanges = events.filter((event) => event.kind === 'collection_change');
226
- return Object.freeze({
443
+ const document = Object.freeze({
227
444
  schema: DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA,
228
445
  version: DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
229
446
  generatedAt,
@@ -238,13 +455,160 @@ export function buildDomainControlFlightRecorder(inputRaw, generatedAtValue = ne
238
455
  collectionChanges: collectionChanges.length,
239
456
  recoveredSources: events.filter((event) => event.kind === 'recovered').length,
240
457
  }),
241
- limitations: Object.freeze([
242
- 'The flight recorder compares only supplied bounded observations and performs no collection or configuration change.',
243
- 'Source fields remain separate. A collection failure, partial result, unsupported source, or missing field never becomes evidence that a prior value disappeared.',
244
- 'Approved windows label expected timing but do not delete evidence or establish that a change was authorised, successful, safe, or complete.',
245
- 'Observed changes can reflect publication lag or different collection conditions and require analyst review.',
246
- ]),
458
+ limitations: DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS,
459
+ });
460
+ serializeDomainControlFlightRecorder(document);
461
+ return document;
462
+ }
463
+ function exactRecorderText(value, label, maximum) {
464
+ const normalized = text(value, label, maximum);
465
+ if (normalized !== value)
466
+ throw new TypeError(`${label} must use its canonical text form.`);
467
+ return normalized;
468
+ }
469
+ function exactRecorderInteger(value, minimum, maximum, label) {
470
+ if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum) {
471
+ throw new TypeError(`${label} must be an integer from ${minimum} to ${maximum}.`);
472
+ }
473
+ return Number(value);
474
+ }
475
+ function exactRecorderValues(value, label) {
476
+ const input = boundedDataArray(value, label, 0, MAX_FLIGHT_RECORDER_VALUES);
477
+ const normalized = input.map((item, index) => exactRecorderText(item, `${label}[${index}]`, MAX_FLIGHT_RECORDER_VALUE_LENGTH));
478
+ if (new Set(normalized).size !== normalized.length
479
+ || normalized.some((item, index) => index > 0 && normalized[index - 1] >= item)) {
480
+ throw new TypeError(`${label} must contain unique canonically ordered values.`);
481
+ }
482
+ return Object.freeze(normalized);
483
+ }
484
+ export function validateDomainControlFlightRecorderDocument(value) {
485
+ const root = exactRecord(value, DOCUMENT_ROOT_KEYS, 'Domain control flight-recorder document');
486
+ if (root.schema !== DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA
487
+ || root.version !== DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION) {
488
+ throw new TypeError(`Domain control flight-recorder document must use ${DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA} version ${DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION}.`);
489
+ }
490
+ const generatedAt = requireIsoTimestamp(root.generatedAt, 'Domain control flight-recorder document generatedAt');
491
+ if (generatedAt !== root.generatedAt) {
492
+ throw new TypeError('Domain control flight-recorder document generatedAt must use its canonical timestamp form.');
493
+ }
494
+ const domainInput = boundedDataArray(root.domains, 'Domain control flight-recorder document domains', 1, MAX_FLIGHT_RECORDER_OBSERVATIONS);
495
+ const domains = domainInput.map((item, index) => {
496
+ const normalized = requireDomainName(item, `Domain control flight-recorder document domains[${index}]`);
497
+ if (normalized !== item) {
498
+ throw new TypeError(`Domain control flight-recorder document domains[${index}] must use its canonical form.`);
499
+ }
500
+ return normalized;
247
501
  });
502
+ if (new Set(domains).size !== domains.length
503
+ || domains.some((item, index) => index > 0 && domains[index - 1] >= item)) {
504
+ throw new TypeError('Domain control flight-recorder document domains must be unique and canonically ordered.');
505
+ }
506
+ const observationCount = exactRecorderInteger(root.observationCount, MIN_FLIGHT_RECORDER_OBSERVATIONS, MAX_FLIGHT_RECORDER_OBSERVATIONS, 'Domain control flight-recorder document observationCount');
507
+ if (observationCount < domains.length) {
508
+ throw new TypeError('Domain control flight-recorder document observationCount is inconsistent with its domains.');
509
+ }
510
+ const eventInput = boundedDataArray(root.events, 'Domain control flight-recorder document events', 0, MAX_FLIGHT_RECORDER_EVENTS);
511
+ const kindCounts = {
512
+ first_observation: 0,
513
+ observed_change: 0,
514
+ collection_change: 0,
515
+ recovered: 0,
516
+ approved_change: 0,
517
+ };
518
+ const events = eventInput.map((eventValue, index) => {
519
+ const label = `Domain control flight-recorder document event ${index + 1}`;
520
+ const event = exactRecord(eventValue, EVENT_KEYS, label);
521
+ const eventDomain = requireDomainName(event.domain, `${label}.domain`);
522
+ if (eventDomain !== event.domain || !domains.includes(eventDomain)) {
523
+ throw new TypeError(`${label}.domain must be present in the document domain list.`);
524
+ }
525
+ if (typeof event.field !== 'string' || !FIELD_IDS.has(event.field)) {
526
+ throw new TypeError(`${label}.field is unsupported.`);
527
+ }
528
+ const observedAt = requireIsoTimestamp(event.observedAt, `${label}.observedAt`);
529
+ if (observedAt !== event.observedAt || Date.parse(observedAt) > Date.parse(generatedAt)) {
530
+ throw new TypeError(`${label}.observedAt must not be later than document generation.`);
531
+ }
532
+ if (event.kind !== 'first_observation'
533
+ && event.kind !== 'observed_change'
534
+ && event.kind !== 'collection_change'
535
+ && event.kind !== 'recovered') {
536
+ throw new TypeError(`${label}.kind is unsupported.`);
537
+ }
538
+ const expectedState = event.kind === 'collection_change' ? 'partial' : 'observed';
539
+ if (event.state !== expectedState)
540
+ throw new TypeError(`${label}.state is inconsistent.`);
541
+ const before = exactRecorderValues(event.before, `${label}.before`);
542
+ const after = exactRecorderValues(event.after, `${label}.after`);
543
+ if (((event.kind === 'first_observation' || event.kind === 'recovered') && before.length)
544
+ || (event.kind === 'collection_change' && after.length)) {
545
+ throw new TypeError(`${label} before/after values are inconsistent.`);
546
+ }
547
+ let approvedWindow = null;
548
+ if (event.approvedWindow !== null) {
549
+ if (event.kind !== 'observed_change')
550
+ throw new TypeError(`${label}.approvedWindow is inconsistent.`);
551
+ const window = exactRecord(event.approvedWindow, EVENT_APPROVED_WINDOW_KEYS, `${label}.approvedWindow`);
552
+ approvedWindow = Object.freeze({
553
+ id: exactRecorderText(window.id, `${label}.approvedWindow.id`, MAX_FLIGHT_RECORDER_WINDOW_ID_LENGTH),
554
+ reason: exactRecorderText(window.reason, `${label}.approvedWindow.reason`, MAX_FLIGHT_RECORDER_WINDOW_REASON_LENGTH),
555
+ });
556
+ kindCounts.approved_change += 1;
557
+ }
558
+ kindCounts[event.kind] += 1;
559
+ return Object.freeze({
560
+ id: exactRecorderText(event.id, `${label}.id`, 400),
561
+ domain: eventDomain,
562
+ field: event.field,
563
+ observedAt,
564
+ kind: event.kind,
565
+ state: expectedState,
566
+ before,
567
+ after,
568
+ source: exactRecorderText(event.source, `${label}.source`, MAX_FLIGHT_RECORDER_SOURCE_LENGTH),
569
+ approvedWindow,
570
+ explanation: exactRecorderText(event.explanation, `${label}.explanation`, MAX_FLIGHT_RECORDER_VALUE_LENGTH),
571
+ });
572
+ });
573
+ const summaryInput = exactRecord(root.summary, SUMMARY_KEYS, 'Domain control flight-recorder document summary');
574
+ const expectedSummary = Object.freeze({
575
+ firstObservations: kindCounts.first_observation,
576
+ observedChanges: kindCounts.observed_change,
577
+ approvedChanges: kindCounts.approved_change,
578
+ unexpectedChanges: kindCounts.observed_change - kindCounts.approved_change,
579
+ collectionChanges: kindCounts.collection_change,
580
+ recoveredSources: kindCounts.recovered,
581
+ });
582
+ for (const key of DOMAIN_CONTROL_FLIGHT_RECORDER_SUMMARY_KEYS) {
583
+ const supplied = exactRecorderInteger(summaryInput[key], 0, MAX_FLIGHT_RECORDER_EVENTS, `Domain control flight-recorder document summary ${key}`);
584
+ if (supplied !== expectedSummary[key]) {
585
+ throw new TypeError('Domain control flight-recorder document summary is inconsistent.');
586
+ }
587
+ }
588
+ const limitationsInput = boundedDataArray(root.limitations, 'Domain control flight-recorder document limitations', DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS.length, DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS.length);
589
+ if (limitationsInput.some((item, index) => item !== DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS[index])) {
590
+ throw new TypeError('Domain control flight-recorder document limitations are invalid.');
591
+ }
592
+ const document = Object.freeze({
593
+ schema: DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA,
594
+ version: DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
595
+ generatedAt,
596
+ domains: Object.freeze(domains),
597
+ observationCount,
598
+ events: Object.freeze(events),
599
+ summary: expectedSummary,
600
+ limitations: DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS,
601
+ });
602
+ assertPrettyJsonByteBudget(document, MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_OUTPUT_BYTES, 'Domain control flight-recorder document');
603
+ return document;
604
+ }
605
+ export function serializeDomainControlFlightRecorder(document) {
606
+ assertPrettyJsonByteBudget(document, MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_OUTPUT_BYTES, 'Domain-control flight-recorder output');
607
+ const serialised = `${JSON.stringify(document, null, 2)}\n`;
608
+ if (Buffer.byteLength(serialised, 'utf8') > MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_OUTPUT_BYTES) {
609
+ throw new TypeError(`Domain-control flight-recorder output exceeds ${MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_OUTPUT_BYTES} serialised bytes.`);
610
+ }
611
+ return serialised;
248
612
  }
249
613
  export function formatDomainControlFlightRecorder(document) {
250
614
  const lines = [