@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.
- package/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
|
@@ -1,33 +1,261 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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 (
|
|
43
|
-
const normalized = text(
|
|
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 =
|
|
53
|
-
|
|
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 =
|
|
59
|
-
const field =
|
|
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`,
|
|
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
|
|
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 =
|
|
91
|
-
|
|
92
|
-
|
|
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`,
|
|
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`,
|
|
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
|
|
127
|
-
if (
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 = [
|