@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
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { canonicalArtifactJsonFor, resolveArtifactCanonicalization, } from "./artifact-integrity.mjs";
|
|
2
|
+
import { normalizeDomain } from "./domain-name.mjs";
|
|
3
|
+
import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "./observation.mjs";
|
|
4
|
+
import { DOMAIN_CONTROL_CAA_RECORD_KEYS, DOMAIN_CONTROL_DIGEST_SHA256_HEX_LENGTH, DOMAIN_CONTROL_DS_RECORD_KEYS, DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS, DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA as DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA, DOMAIN_CONTROL_MANIFEST_INPUT_KEYS, DOMAIN_CONTROL_MANIFEST_INPUT_VERSION, DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, DOMAIN_CONTROL_MANIFEST_INTEGRITY_KEYS, DOMAIN_CONTROL_MANIFEST_LIMITATIONS, DOMAIN_CONTROL_MANIFEST_ROOT_KEYS, DOMAIN_CONTROL_MANIFEST_SCHEMA as DOMAIN_CONTROL_PASSPORT_SCHEMA, DOMAIN_CONTROL_MANIFEST_VERSION, DOMAIN_CONTROL_MX_RECORD_KEYS, DOMAIN_CONTROL_RECORD_LIST_FIELDS, DOMAIN_CONTROL_SPKI_SHA256_HEX_LENGTH, DOMAIN_CONTROL_TEXT_INPUT_BOUND_FACTOR, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_CAA_FLAGS, MAX_DOMAIN_CONTROL_CAA_PRESENTATION_LENGTH, MAX_DOMAIN_CONTROL_CAA_TAG_LENGTH, MAX_DOMAIN_CONTROL_CAA_VALUE_LENGTH, MAX_DOMAIN_CONTROL_DOMAIN_LENGTH, MAX_DOMAIN_CONTROL_DS_ALGORITHM, MAX_DOMAIN_CONTROL_DS_DIGEST_LENGTH, MAX_DOMAIN_CONTROL_DS_DIGEST_TYPE, MAX_DOMAIN_CONTROL_DS_KEY_TAG, MAX_DOMAIN_CONTROL_DS_PRESENTATION_LENGTH, MAX_DOMAIN_CONTROL_INPUT_RECORDS, MAX_DOMAIN_CONTROL_JSON_DEPTH, MAX_DOMAIN_CONTROL_JSON_VALUES, MAX_DOMAIN_CONTROL_MANIFEST_BYTES, MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES, MAX_DOMAIN_CONTROL_MX_PRIORITY, MAX_DOMAIN_CONTROL_MX_TEXT_LENGTH, MAX_DOMAIN_CONTROL_NAME_INPUT_LENGTH, MAX_DOMAIN_CONTROL_NOTE_LENGTH, MAX_DOMAIN_CONTROL_RECORD_INTEGER_TEXT_LENGTH, MAX_DOMAIN_CONTROL_TEXT_LENGTH, MAX_DOMAIN_CONTROL_TIMESTAMP_LENGTH, MIN_DOMAIN_CONTROL_DS_DIGEST_LENGTH, MIN_DOMAIN_CONTROL_MANIFEST_ENTRIES, MIN_DOMAIN_CONTROL_RECORDS, } from "../contracts/domain-control-manifest.mjs";
|
|
5
|
+
const DOMAIN_CONTROL_PASSPORT_VERSION = DOMAIN_CONTROL_MANIFEST_INPUT_VERSION;
|
|
6
|
+
export { DOMAIN_CONTROL_MANIFEST_VERSION, DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA, DOMAIN_CONTROL_PASSPORT_SCHEMA, DOMAIN_CONTROL_PASSPORT_VERSION, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_MANIFEST_BYTES, };
|
|
7
|
+
export const MAX_DOMAIN_CONTROL_PASSPORT_BYTES = MAX_DOMAIN_CONTROL_MANIFEST_BYTES;
|
|
8
|
+
export const MAX_DOMAIN_CONTROL_PASSPORT_ENTRIES = MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES;
|
|
9
|
+
export const DOMAIN_CONTROL_PASSPORT_LIMITATIONS = DOMAIN_CONTROL_MANIFEST_LIMITATIONS;
|
|
10
|
+
const INPUT_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_INPUT_KEYS);
|
|
11
|
+
const MANIFEST_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_ROOT_KEYS);
|
|
12
|
+
const ENTRY_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS);
|
|
13
|
+
const INTEGRITY_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_INTEGRITY_KEYS);
|
|
14
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
15
|
+
const RECORD_LIST_FIELDS = DOMAIN_CONTROL_RECORD_LIST_FIELDS;
|
|
16
|
+
function compareCodeUnits(left, right) {
|
|
17
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
18
|
+
}
|
|
19
|
+
function boundedDataRecord(value, allowed, label) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
21
|
+
return null;
|
|
22
|
+
const prototype = Object.getPrototypeOf(value);
|
|
23
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
24
|
+
throw new TypeError(`${label} must be a plain object.`);
|
|
25
|
+
}
|
|
26
|
+
const serialiser = Object.getOwnPropertyDescriptor(value, 'toJSON');
|
|
27
|
+
if (serialiser)
|
|
28
|
+
throw new TypeError(`${label} contains an unknown field: toJSON.`);
|
|
29
|
+
const result = Object.create(null);
|
|
30
|
+
for (const key of allowed) {
|
|
31
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
32
|
+
if (!descriptor)
|
|
33
|
+
continue;
|
|
34
|
+
if (!descriptor.enumerable || !('value' in descriptor)) {
|
|
35
|
+
throw new TypeError(`${label} must use ordinary data fields.`);
|
|
36
|
+
}
|
|
37
|
+
result[key] = descriptor.value;
|
|
38
|
+
}
|
|
39
|
+
let visibleKeys = 0;
|
|
40
|
+
for (const key in value) {
|
|
41
|
+
if (!Object.hasOwn(value, key) || !allowed.has(key)) {
|
|
42
|
+
const detail = key.length <= 80 ? `: ${key}` : '';
|
|
43
|
+
throw new TypeError(`${label} contains an unknown field${detail}.`);
|
|
44
|
+
}
|
|
45
|
+
visibleKeys += 1;
|
|
46
|
+
if (visibleKeys > allowed.size)
|
|
47
|
+
throw new TypeError(`${label} contains too many fields.`);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function boundedDataArray(value, label, minimum, maximum, maximumMessage) {
|
|
52
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
53
|
+
throw new TypeError(`${label} must contain between ${minimum} and ${maximum} entries.`);
|
|
54
|
+
}
|
|
55
|
+
if (Object.getOwnPropertyDescriptor(value, Symbol.iterator)
|
|
56
|
+
|| Object.getOwnPropertyDescriptor(value, 'toJSON')) {
|
|
57
|
+
throw new TypeError(`${label} must contain ordinary indexed data entries without custom behaviour.`);
|
|
58
|
+
}
|
|
59
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
60
|
+
const length = lengthDescriptor && 'value' in lengthDescriptor
|
|
61
|
+
? lengthDescriptor.value
|
|
62
|
+
: null;
|
|
63
|
+
if (Number.isSafeInteger(length) && Number(length) > maximum && maximumMessage) {
|
|
64
|
+
throw new TypeError(maximumMessage);
|
|
65
|
+
}
|
|
66
|
+
if (!lengthDescriptor
|
|
67
|
+
|| lengthDescriptor.enumerable
|
|
68
|
+
|| !Number.isSafeInteger(length)
|
|
69
|
+
|| Number(length) < minimum
|
|
70
|
+
|| Number(length) > maximum) {
|
|
71
|
+
throw new TypeError(`${label} must contain between ${minimum} and ${maximum} entries.`);
|
|
72
|
+
}
|
|
73
|
+
const boundedLength = Number(length);
|
|
74
|
+
const result = [];
|
|
75
|
+
for (let index = 0; index < boundedLength; index += 1) {
|
|
76
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
77
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
78
|
+
throw new TypeError(`${label} must contain ordinary indexed data entries.`);
|
|
79
|
+
}
|
|
80
|
+
result.push(descriptor.value);
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
const MX_RECORD_KEYS = new Set(DOMAIN_CONTROL_MX_RECORD_KEYS);
|
|
85
|
+
const CAA_RECORD_KEYS = new Set(DOMAIN_CONTROL_CAA_RECORD_KEYS);
|
|
86
|
+
const DS_RECORD_KEYS = new Set(DOMAIN_CONTROL_DS_RECORD_KEYS);
|
|
87
|
+
const DS_DIGEST_PATTERN = new RegExp(`^[a-f0-9]{${MIN_DOMAIN_CONTROL_DS_DIGEST_LENGTH},${MAX_DOMAIN_CONTROL_DS_DIGEST_LENGTH}}$`, 'u');
|
|
88
|
+
const SPKI_DIGEST_PATTERN = new RegExp(`^[a-f0-9]{${DOMAIN_CONTROL_SPKI_SHA256_HEX_LENGTH}}$`, 'u');
|
|
89
|
+
const MANIFEST_DIGEST_PATTERN = new RegExp(`^sha256:[a-f0-9]{${DOMAIN_CONTROL_DIGEST_SHA256_HEX_LENGTH}}$`, 'u');
|
|
90
|
+
function structuredRecord(value, keys, label) {
|
|
91
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
92
|
+
return null;
|
|
93
|
+
return boundedDataRecord(value, keys, label);
|
|
94
|
+
}
|
|
95
|
+
function recordText(value, maximum = MAX_DOMAIN_CONTROL_MX_TEXT_LENGTH) {
|
|
96
|
+
if (typeof value !== 'string'
|
|
97
|
+
|| value.length > maximum * DOMAIN_CONTROL_TEXT_INPUT_BOUND_FACTOR
|
|
98
|
+
|| /[\u0000-\u001f\u007f]/u.test(value))
|
|
99
|
+
return '';
|
|
100
|
+
return value.replace(/\s+/gu, ' ').trim().slice(0, maximum).trim();
|
|
101
|
+
}
|
|
102
|
+
function recordInteger(value, maximum) {
|
|
103
|
+
const candidate = typeof value === 'number'
|
|
104
|
+
? value
|
|
105
|
+
: typeof value === 'string'
|
|
106
|
+
&& value.length <= MAX_DOMAIN_CONTROL_RECORD_INTEGER_TEXT_LENGTH
|
|
107
|
+
&& /^\d+$/u.test(value.trim())
|
|
108
|
+
? Number(value)
|
|
109
|
+
: NaN;
|
|
110
|
+
return Number.isSafeInteger(candidate) && candidate >= 0 && candidate <= maximum
|
|
111
|
+
? candidate
|
|
112
|
+
: null;
|
|
113
|
+
}
|
|
114
|
+
function domainControlName(value) {
|
|
115
|
+
return typeof value === 'string' && value.length <= MAX_DOMAIN_CONTROL_NAME_INPUT_LENGTH
|
|
116
|
+
? normalizeDomain(value)
|
|
117
|
+
: '';
|
|
118
|
+
}
|
|
119
|
+
function exchange(value) {
|
|
120
|
+
const candidate = recordText(value, MAX_DOMAIN_CONTROL_DOMAIN_LENGTH).toLowerCase();
|
|
121
|
+
if (candidate === '.')
|
|
122
|
+
return '.';
|
|
123
|
+
return domainControlName(candidate);
|
|
124
|
+
}
|
|
125
|
+
function suppliedAliasValues(source, keys) {
|
|
126
|
+
const values = [];
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
if (!Object.hasOwn(source, key))
|
|
129
|
+
continue;
|
|
130
|
+
const value = source[key];
|
|
131
|
+
if (value !== null && value !== undefined)
|
|
132
|
+
values.push(value);
|
|
133
|
+
}
|
|
134
|
+
return values;
|
|
135
|
+
}
|
|
136
|
+
function canonicalStringAlias(source, keys, normalise, label) {
|
|
137
|
+
const values = suppliedAliasValues(source, keys);
|
|
138
|
+
if (!values.length)
|
|
139
|
+
return '';
|
|
140
|
+
const normalised = values.map(normalise);
|
|
141
|
+
if (normalised.length > 1
|
|
142
|
+
&& (normalised.some((value) => !value) || normalised.some((value) => value !== normalised[0]))) {
|
|
143
|
+
throw new TypeError(`${label} aliases must resolve to one valid canonical value.`);
|
|
144
|
+
}
|
|
145
|
+
return normalised[0] ?? '';
|
|
146
|
+
}
|
|
147
|
+
function canonicalIntegerAlias(source, keys, maximum, label, fallback = null) {
|
|
148
|
+
const values = suppliedAliasValues(source, keys);
|
|
149
|
+
if (!values.length)
|
|
150
|
+
return fallback;
|
|
151
|
+
const normalised = values.map((value) => recordInteger(value, maximum));
|
|
152
|
+
if (normalised.length > 1
|
|
153
|
+
&& (normalised.some((value) => value === null) || normalised.some((value) => value !== normalised[0]))) {
|
|
154
|
+
throw new TypeError(`${label} aliases must resolve to one valid canonical value.`);
|
|
155
|
+
}
|
|
156
|
+
return normalised[0] ?? null;
|
|
157
|
+
}
|
|
158
|
+
export function canonicalMxRecord(value) {
|
|
159
|
+
const item = structuredRecord(value, MX_RECORD_KEYS, 'Domain control MX record');
|
|
160
|
+
if (item) {
|
|
161
|
+
const host = canonicalStringAlias(item, ['exchange', 'host', 'value'], exchange, 'Domain control MX host');
|
|
162
|
+
const priority = canonicalIntegerAlias(item, ['priority', 'preference'], MAX_DOMAIN_CONTROL_MX_PRIORITY, 'Domain control MX priority');
|
|
163
|
+
return host && priority !== null ? `${priority} ${host}` : '';
|
|
164
|
+
}
|
|
165
|
+
const candidate = recordText(value, MAX_DOMAIN_CONTROL_MX_TEXT_LENGTH).toLowerCase();
|
|
166
|
+
const match = /^(\d{1,5})\s+(.+)$/u.exec(candidate);
|
|
167
|
+
if (match) {
|
|
168
|
+
const priority = recordInteger(match[1], MAX_DOMAIN_CONTROL_MX_PRIORITY);
|
|
169
|
+
const host = exchange(match[2]);
|
|
170
|
+
return priority !== null && host ? `${priority} ${host}` : '';
|
|
171
|
+
}
|
|
172
|
+
return exchange(candidate);
|
|
173
|
+
}
|
|
174
|
+
function caaValue(value) {
|
|
175
|
+
return recordText(value, MAX_DOMAIN_CONTROL_CAA_VALUE_LENGTH).replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u, '$1$2').trim().toLowerCase();
|
|
176
|
+
}
|
|
177
|
+
export function canonicalCaaRecord(value) {
|
|
178
|
+
const item = structuredRecord(value, CAA_RECORD_KEYS, 'Domain control CAA record');
|
|
179
|
+
if (item) {
|
|
180
|
+
const flags = canonicalIntegerAlias(item, ['critical', 'flags'], MAX_DOMAIN_CONTROL_CAA_FLAGS, 'Domain control CAA flags', 0);
|
|
181
|
+
const tag = recordText(item.tag, MAX_DOMAIN_CONTROL_CAA_TAG_LENGTH).toLowerCase();
|
|
182
|
+
const payload = caaValue(item.value);
|
|
183
|
+
return flags !== null && /^(?:issue|issuewild|iodef)$/u.test(tag) && payload
|
|
184
|
+
? `${flags} ${tag} ${payload}`
|
|
185
|
+
: '';
|
|
186
|
+
}
|
|
187
|
+
const candidate = recordText(value, MAX_DOMAIN_CONTROL_CAA_PRESENTATION_LENGTH);
|
|
188
|
+
const match = /^(\d{1,3})\s+(issue|issuewild|iodef)\s+(.+)$/iu.exec(candidate);
|
|
189
|
+
if (!match)
|
|
190
|
+
return '';
|
|
191
|
+
const flags = recordInteger(match[1], MAX_DOMAIN_CONTROL_CAA_FLAGS);
|
|
192
|
+
const tag = match[2]?.toLowerCase() ?? '';
|
|
193
|
+
const payload = caaValue(match[3]);
|
|
194
|
+
return flags !== null && payload ? `${flags} ${tag} ${payload}` : '';
|
|
195
|
+
}
|
|
196
|
+
export function canonicalDsRecord(value) {
|
|
197
|
+
const item = structuredRecord(value, DS_RECORD_KEYS, 'Domain control DS record');
|
|
198
|
+
if (item) {
|
|
199
|
+
const keyTag = canonicalIntegerAlias(item, ['keyTag', 'key_tag'], MAX_DOMAIN_CONTROL_DS_KEY_TAG, 'Domain control DS key tag');
|
|
200
|
+
const algorithm = recordInteger(item.algorithm, MAX_DOMAIN_CONTROL_DS_ALGORITHM);
|
|
201
|
+
const digestType = canonicalIntegerAlias(item, ['digestType', 'digest_type'], MAX_DOMAIN_CONTROL_DS_DIGEST_TYPE, 'Domain control DS digest type');
|
|
202
|
+
const digest = recordText(item.digest, MAX_DOMAIN_CONTROL_DS_DIGEST_LENGTH).replace(/\s+/gu, '').toLowerCase();
|
|
203
|
+
return keyTag !== null && algorithm !== null && digestType !== null && DS_DIGEST_PATTERN.test(digest)
|
|
204
|
+
? `${keyTag} ${algorithm} ${digestType} ${digest}`
|
|
205
|
+
: '';
|
|
206
|
+
}
|
|
207
|
+
const candidate = recordText(value, MAX_DOMAIN_CONTROL_DS_PRESENTATION_LENGTH);
|
|
208
|
+
const match = /^(\d{1,5})\s+(\d{1,3})\s+(\d{1,3})\s+([a-f0-9\s]+)$/iu.exec(candidate);
|
|
209
|
+
if (!match)
|
|
210
|
+
return '';
|
|
211
|
+
return canonicalDsRecord({ keyTag: match[1], algorithm: match[2], digestType: match[3], digest: match[4] });
|
|
212
|
+
}
|
|
213
|
+
export function canonicalDomainControlRecordList(value, kind) {
|
|
214
|
+
let normalise;
|
|
215
|
+
if (kind === 'mx')
|
|
216
|
+
normalise = canonicalMxRecord;
|
|
217
|
+
else if (kind === 'caa')
|
|
218
|
+
normalise = canonicalCaaRecord;
|
|
219
|
+
else if (kind === 'ds')
|
|
220
|
+
normalise = canonicalDsRecord;
|
|
221
|
+
else
|
|
222
|
+
throw new TypeError('Domain control record kind is unsupported.');
|
|
223
|
+
if (!Array.isArray(value))
|
|
224
|
+
return [];
|
|
225
|
+
const source = boundedDataArray(value, `Domain control ${kind.toUpperCase()} records`, MIN_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_INPUT_RECORDS);
|
|
226
|
+
const retained = [];
|
|
227
|
+
for (const item of source) {
|
|
228
|
+
const canonical = normalise(item);
|
|
229
|
+
if (canonical)
|
|
230
|
+
retained.push(canonical);
|
|
231
|
+
}
|
|
232
|
+
return [...new Set(retained)]
|
|
233
|
+
.sort(compareCodeUnits)
|
|
234
|
+
.slice(0, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS);
|
|
235
|
+
}
|
|
236
|
+
class DomainControlJsonBudgetError extends Error {
|
|
237
|
+
}
|
|
238
|
+
class DomainControlJsonStructureError extends Error {
|
|
239
|
+
}
|
|
240
|
+
function addJsonBytes(current, addition) {
|
|
241
|
+
const total = current + addition;
|
|
242
|
+
if (!Number.isSafeInteger(total) || total > MAX_DOMAIN_CONTROL_PASSPORT_BYTES) {
|
|
243
|
+
throw new DomainControlJsonBudgetError();
|
|
244
|
+
}
|
|
245
|
+
return total;
|
|
246
|
+
}
|
|
247
|
+
function jsonStringBytes(value) {
|
|
248
|
+
let bytes = 2;
|
|
249
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
250
|
+
const code = value.charCodeAt(index);
|
|
251
|
+
let addition = 0;
|
|
252
|
+
if (code === 0x22 || code === 0x5c)
|
|
253
|
+
addition = 2;
|
|
254
|
+
else if (code <= 0x1f)
|
|
255
|
+
addition = code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d ? 2 : 6;
|
|
256
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
257
|
+
const next = value.charCodeAt(index + 1);
|
|
258
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
259
|
+
addition = 4;
|
|
260
|
+
index += 1;
|
|
261
|
+
}
|
|
262
|
+
else
|
|
263
|
+
addition = 6;
|
|
264
|
+
}
|
|
265
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
266
|
+
addition = 6;
|
|
267
|
+
else if (code <= 0x7f)
|
|
268
|
+
addition = 1;
|
|
269
|
+
else if (code <= 0x7ff)
|
|
270
|
+
addition = 2;
|
|
271
|
+
else
|
|
272
|
+
addition = 3;
|
|
273
|
+
bytes = addJsonBytes(bytes, addition);
|
|
274
|
+
}
|
|
275
|
+
return bytes;
|
|
276
|
+
}
|
|
277
|
+
function boundedPrettyJsonBytes(value) {
|
|
278
|
+
const active = new Set();
|
|
279
|
+
let values = 0;
|
|
280
|
+
const measure = (input, depth, arrayElement) => {
|
|
281
|
+
values += 1;
|
|
282
|
+
if (values > MAX_DOMAIN_CONTROL_JSON_VALUES || depth > MAX_DOMAIN_CONTROL_JSON_DEPTH) {
|
|
283
|
+
throw new DomainControlJsonStructureError();
|
|
284
|
+
}
|
|
285
|
+
if (input === null)
|
|
286
|
+
return 4;
|
|
287
|
+
if (typeof input === 'string')
|
|
288
|
+
return jsonStringBytes(input);
|
|
289
|
+
if (typeof input === 'boolean')
|
|
290
|
+
return input ? 4 : 5;
|
|
291
|
+
if (typeof input === 'number')
|
|
292
|
+
return Number.isFinite(input) ? (JSON.stringify(input)?.length ?? 4) : 4;
|
|
293
|
+
if (typeof input === 'undefined' || typeof input === 'function' || typeof input === 'symbol') {
|
|
294
|
+
return arrayElement ? 4 : null;
|
|
295
|
+
}
|
|
296
|
+
if (typeof input === 'bigint' || typeof input !== 'object')
|
|
297
|
+
throw new DomainControlJsonStructureError();
|
|
298
|
+
if (active.has(input))
|
|
299
|
+
throw new DomainControlJsonStructureError();
|
|
300
|
+
active.add(input);
|
|
301
|
+
try {
|
|
302
|
+
const childIndent = (depth + 1) * 2;
|
|
303
|
+
const closingIndent = depth * 2;
|
|
304
|
+
if (Array.isArray(input)) {
|
|
305
|
+
const items = boundedDataArray(input, 'Domain control JSON array', 0, MAX_DOMAIN_CONTROL_JSON_VALUES);
|
|
306
|
+
if (!items.length)
|
|
307
|
+
return 2;
|
|
308
|
+
let bytes = 1;
|
|
309
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
310
|
+
if (index > 0)
|
|
311
|
+
bytes = addJsonBytes(bytes, 1);
|
|
312
|
+
bytes = addJsonBytes(bytes, 1 + childIndent);
|
|
313
|
+
bytes = addJsonBytes(bytes, measure(items[index], depth + 1, true) ?? 4);
|
|
314
|
+
}
|
|
315
|
+
return addJsonBytes(bytes, 1 + closingIndent + 1);
|
|
316
|
+
}
|
|
317
|
+
const prototype = Object.getPrototypeOf(input);
|
|
318
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
319
|
+
throw new DomainControlJsonStructureError();
|
|
320
|
+
if (Object.prototype.hasOwnProperty.call(input, 'toJSON'))
|
|
321
|
+
throw new DomainControlJsonStructureError();
|
|
322
|
+
let bytes = 1;
|
|
323
|
+
let properties = 0;
|
|
324
|
+
for (const key in input) {
|
|
325
|
+
if (!Object.prototype.hasOwnProperty.call(input, key))
|
|
326
|
+
continue;
|
|
327
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
328
|
+
if (!descriptor || !descriptor.enumerable)
|
|
329
|
+
continue;
|
|
330
|
+
if (!('value' in descriptor))
|
|
331
|
+
throw new DomainControlJsonStructureError();
|
|
332
|
+
const measured = measure(descriptor.value, depth + 1, false);
|
|
333
|
+
if (measured === null)
|
|
334
|
+
continue;
|
|
335
|
+
if (properties > 0)
|
|
336
|
+
bytes = addJsonBytes(bytes, 1);
|
|
337
|
+
bytes = addJsonBytes(bytes, 1 + childIndent);
|
|
338
|
+
bytes = addJsonBytes(bytes, jsonStringBytes(key));
|
|
339
|
+
bytes = addJsonBytes(bytes, 2);
|
|
340
|
+
bytes = addJsonBytes(bytes, measured);
|
|
341
|
+
properties += 1;
|
|
342
|
+
}
|
|
343
|
+
return properties ? addJsonBytes(bytes, 1 + closingIndent + 1) : 2;
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
active.delete(input);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
const bytes = measure(value, 0, false);
|
|
350
|
+
if (bytes === null)
|
|
351
|
+
throw new DomainControlJsonStructureError();
|
|
352
|
+
return addJsonBytes(bytes, 1);
|
|
353
|
+
}
|
|
354
|
+
export function domainControlPassportSerialisedBytes(value, label = 'Domain control manifest') {
|
|
355
|
+
try {
|
|
356
|
+
return boundedPrettyJsonBytes(value);
|
|
357
|
+
}
|
|
358
|
+
catch (cause) {
|
|
359
|
+
if (cause instanceof DomainControlJsonBudgetError) {
|
|
360
|
+
throw new TypeError(`${label} exceeds ${MAX_DOMAIN_CONTROL_PASSPORT_BYTES} serialised bytes.`);
|
|
361
|
+
}
|
|
362
|
+
throw new TypeError(`${label} must be serialisable as bounded JSON.`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
export function assertDomainControlPassportByteBudget(value, label = 'Domain control manifest') {
|
|
366
|
+
domainControlPassportSerialisedBytes(value, label);
|
|
367
|
+
}
|
|
368
|
+
function boundedDomainControlPassportEntries(value, label) {
|
|
369
|
+
const entries = boundedDataArray(value, label, MIN_DOMAIN_CONTROL_MANIFEST_ENTRIES, MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES);
|
|
370
|
+
return entries.map((entry) => {
|
|
371
|
+
const source = boundedDataRecord(entry, ENTRY_KEYS, `${label} entry`);
|
|
372
|
+
if (!source)
|
|
373
|
+
return entry;
|
|
374
|
+
for (const field of RECORD_LIST_FIELDS) {
|
|
375
|
+
if (!Object.hasOwn(source, field))
|
|
376
|
+
continue;
|
|
377
|
+
source[field] = boundedDataArray(source[field], `${label} entry ${field}`, MIN_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_INPUT_RECORDS, `${label} entry ${field} exceeds ${MAX_DOMAIN_CONTROL_INPUT_RECORDS} input records.`);
|
|
378
|
+
}
|
|
379
|
+
return source;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
function text(value, maximum = MAX_DOMAIN_CONTROL_TEXT_LENGTH) {
|
|
383
|
+
if (typeof value !== 'string'
|
|
384
|
+
|| value.length > maximum * DOMAIN_CONTROL_TEXT_INPUT_BOUND_FACTOR
|
|
385
|
+
|| CONTROL_RE.test(value))
|
|
386
|
+
return null;
|
|
387
|
+
return value.replace(/\s+/gu, ' ').trim().slice(0, maximum) || null;
|
|
388
|
+
}
|
|
389
|
+
function timestamp(value, legacy = false) {
|
|
390
|
+
const candidate = text(value, MAX_DOMAIN_CONTROL_TIMESTAMP_LENGTH);
|
|
391
|
+
return candidate
|
|
392
|
+
? normalizeExplicitIsoTimestamp(candidate) ?? (legacy ? normalizeLegacyIsoTimestamp(candidate) : null)
|
|
393
|
+
: null;
|
|
394
|
+
}
|
|
395
|
+
function hostnames(value) {
|
|
396
|
+
if (value === undefined)
|
|
397
|
+
return [];
|
|
398
|
+
const source = boundedDataArray(value, 'Domain control nameservers', MIN_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_INPUT_RECORDS);
|
|
399
|
+
const names = [];
|
|
400
|
+
for (const item of source) {
|
|
401
|
+
const normalized = domainControlName(item);
|
|
402
|
+
if (normalized)
|
|
403
|
+
names.push(normalized);
|
|
404
|
+
}
|
|
405
|
+
return [...new Set(names)].sort(compareCodeUnits).slice(0, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS);
|
|
406
|
+
}
|
|
407
|
+
function digest(value) {
|
|
408
|
+
const candidate = text(value, DOMAIN_CONTROL_SPKI_SHA256_HEX_LENGTH)?.toLowerCase() ?? '';
|
|
409
|
+
return SPKI_DIGEST_PATTERN.test(candidate) ? candidate : null;
|
|
410
|
+
}
|
|
411
|
+
function normalizeEntry(value) {
|
|
412
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
413
|
+
return null;
|
|
414
|
+
const source = value;
|
|
415
|
+
const domain = domainControlName(source.domain);
|
|
416
|
+
if (!domain)
|
|
417
|
+
return null;
|
|
418
|
+
return Object.freeze({
|
|
419
|
+
domain,
|
|
420
|
+
nameservers: Object.freeze(hostnames(source.nameservers)),
|
|
421
|
+
ds: Object.freeze(canonicalDomainControlRecordList(source.ds, 'ds')),
|
|
422
|
+
mx: Object.freeze(canonicalDomainControlRecordList(source.mx, 'mx')),
|
|
423
|
+
caa: Object.freeze(canonicalDomainControlRecordList(source.caa, 'caa')),
|
|
424
|
+
tlsIssuer: text(source.tlsIssuer, MAX_DOMAIN_CONTROL_TEXT_LENGTH)?.toLowerCase() ?? null,
|
|
425
|
+
tlsSpkiSha256: digest(source.tlsSpkiSha256),
|
|
426
|
+
registrarLock: source.registrarLock === 'required' || source.registrarLock === 'not_required'
|
|
427
|
+
? source.registrarLock
|
|
428
|
+
: null,
|
|
429
|
+
renewalReviewAt: timestamp(source.renewalReviewAt, true),
|
|
430
|
+
note: text(source.note, MAX_DOMAIN_CONTROL_NOTE_LENGTH),
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function buildUnsignedDomainControlPassportInternal(input, generatedAtValue, options = {}) {
|
|
434
|
+
const source = boundedDataRecord(input, INPUT_KEYS, 'Domain control manifest input');
|
|
435
|
+
if (!source || source.schema !== DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA || source.version !== DOMAIN_CONTROL_MANIFEST_INPUT_VERSION) {
|
|
436
|
+
throw new TypeError(`Domain control manifest input must use ${DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA} version ${DOMAIN_CONTROL_MANIFEST_INPUT_VERSION}.`);
|
|
437
|
+
}
|
|
438
|
+
const inputEntries = boundedDomainControlPassportEntries(source.entries, 'Domain control manifest input');
|
|
439
|
+
source.entries = inputEntries;
|
|
440
|
+
assertDomainControlPassportByteBudget(source, 'Domain control manifest input');
|
|
441
|
+
const generatedAt = timestamp(generatedAtValue, options.legacyGeneratedAt === true);
|
|
442
|
+
const expiresAt = timestamp(source.expiresAt, true);
|
|
443
|
+
if (!generatedAt) {
|
|
444
|
+
throw new TypeError('Domain control manifest generation time must be valid and include an explicit timezone.');
|
|
445
|
+
}
|
|
446
|
+
if (!expiresAt || Date.parse(expiresAt) <= Date.parse(generatedAt)) {
|
|
447
|
+
throw new TypeError('Domain control manifest expiry must be a valid time after generation.');
|
|
448
|
+
}
|
|
449
|
+
const entries = inputEntries.map(normalizeEntry);
|
|
450
|
+
if (entries.some((entry) => entry === null))
|
|
451
|
+
throw new TypeError('Domain control manifest contains an invalid entry.');
|
|
452
|
+
const normalizedEntries = entries;
|
|
453
|
+
if (new Set(normalizedEntries.map((entry) => entry.domain)).size !== normalizedEntries.length) {
|
|
454
|
+
throw new TypeError('Domain control manifest entries must use unique domains.');
|
|
455
|
+
}
|
|
456
|
+
const orderedEntries = options.preserveEntryOrder === true
|
|
457
|
+
? [...normalizedEntries]
|
|
458
|
+
: [...normalizedEntries].sort((left, right) => compareCodeUnits(left.domain, right.domain));
|
|
459
|
+
return Object.freeze({
|
|
460
|
+
schema: DOMAIN_CONTROL_PASSPORT_SCHEMA,
|
|
461
|
+
version: DOMAIN_CONTROL_MANIFEST_VERSION,
|
|
462
|
+
generatedAt,
|
|
463
|
+
expiresAt,
|
|
464
|
+
entries: Object.freeze(orderedEntries),
|
|
465
|
+
limitations: DOMAIN_CONTROL_PASSPORT_LIMITATIONS,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
export function buildUnsignedDomainControlPassport(input, generatedAtValue, options = {}) {
|
|
469
|
+
return buildUnsignedDomainControlPassportInternal(input, generatedAtValue, options);
|
|
470
|
+
}
|
|
471
|
+
function normalizeDomainControlPassportDocumentInternal(value) {
|
|
472
|
+
const source = boundedDataRecord(value, MANIFEST_KEYS, 'Domain control manifest');
|
|
473
|
+
const integrity = boundedDataRecord(source?.integrity, INTEGRITY_KEYS, 'Domain control manifest integrity');
|
|
474
|
+
if (!source
|
|
475
|
+
|| source.schema !== DOMAIN_CONTROL_PASSPORT_SCHEMA
|
|
476
|
+
|| !integrity
|
|
477
|
+
|| integrity.algorithm !== 'SHA-256'
|
|
478
|
+
|| typeof integrity.digestSha256 !== 'string'
|
|
479
|
+
|| !MANIFEST_DIGEST_PATTERN.test(integrity.digestSha256)) {
|
|
480
|
+
throw new TypeError('Domain control manifest has an unsupported or malformed structure.');
|
|
481
|
+
}
|
|
482
|
+
let limitations;
|
|
483
|
+
try {
|
|
484
|
+
limitations = boundedDataArray(source.limitations, 'Domain control manifest limitations', DOMAIN_CONTROL_PASSPORT_LIMITATIONS.length, DOMAIN_CONTROL_PASSPORT_LIMITATIONS.length);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
throw new TypeError('Domain control manifest has an unsupported or malformed structure.');
|
|
488
|
+
}
|
|
489
|
+
if (limitations.some((item, index) => item !== DOMAIN_CONTROL_PASSPORT_LIMITATIONS[index])) {
|
|
490
|
+
throw new TypeError('Domain control manifest has an unsupported or malformed structure.');
|
|
491
|
+
}
|
|
492
|
+
source.limitations = limitations;
|
|
493
|
+
const manifestEntries = boundedDomainControlPassportEntries(source.entries, 'Domain control manifest');
|
|
494
|
+
source.entries = manifestEntries;
|
|
495
|
+
source.integrity = integrity;
|
|
496
|
+
assertDomainControlPassportByteBudget(source);
|
|
497
|
+
const canonicalization = resolveArtifactCanonicalization(source.version, integrity.canonicalization, DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, 'Domain control manifest');
|
|
498
|
+
const baseUnsigned = buildUnsignedDomainControlPassportInternal({
|
|
499
|
+
schema: DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA,
|
|
500
|
+
version: DOMAIN_CONTROL_MANIFEST_INPUT_VERSION,
|
|
501
|
+
expiresAt: source.expiresAt,
|
|
502
|
+
entries: manifestEntries,
|
|
503
|
+
}, source.generatedAt, {
|
|
504
|
+
preserveEntryOrder: true,
|
|
505
|
+
});
|
|
506
|
+
const unsigned = baseUnsigned;
|
|
507
|
+
const { integrity: _integrity, ...suppliedUnsigned } = source;
|
|
508
|
+
const canonicalUnsigned = canonicalArtifactJsonFor(unsigned, canonicalization);
|
|
509
|
+
if (canonicalArtifactJsonFor(suppliedUnsigned, canonicalization) !== canonicalUnsigned) {
|
|
510
|
+
throw new TypeError('Domain control manifest must use its canonical normalised content.');
|
|
511
|
+
}
|
|
512
|
+
const serialisableManifest = Object.freeze({
|
|
513
|
+
...unsigned,
|
|
514
|
+
integrity: Object.freeze({
|
|
515
|
+
algorithm: 'SHA-256',
|
|
516
|
+
canonicalization,
|
|
517
|
+
digestSha256: integrity.digestSha256,
|
|
518
|
+
}),
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
manifest: serialisableManifest,
|
|
522
|
+
unsigned,
|
|
523
|
+
canonicalUnsigned,
|
|
524
|
+
canonicalization,
|
|
525
|
+
serialisableManifest,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
export function normalizeDomainControlPassportDocument(value) {
|
|
529
|
+
const normalized = normalizeDomainControlPassportDocumentInternal(value);
|
|
530
|
+
return Object.freeze({
|
|
531
|
+
manifest: normalized.manifest,
|
|
532
|
+
unsigned: normalized.unsigned,
|
|
533
|
+
canonicalUnsigned: normalized.canonicalUnsigned,
|
|
534
|
+
canonicalization: normalized.canonicalization,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Produces the portable two-space JSON representation of a structurally valid
|
|
539
|
+
* manifest. This does not authenticate the retained digest; callers that need
|
|
540
|
+
* integrity assurance must use the Node or browser verifier separately.
|
|
541
|
+
*/
|
|
542
|
+
export function serializeDomainControlManifest(value) {
|
|
543
|
+
const normalized = normalizeDomainControlPassportDocumentInternal(value);
|
|
544
|
+
assertDomainControlPassportByteBudget(normalized.serialisableManifest);
|
|
545
|
+
return `${JSON.stringify(normalized.serialisableManifest, null, 2)}\n`;
|
|
546
|
+
}
|
|
547
|
+
export { DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA as DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA, DOMAIN_CONTROL_PASSPORT_SCHEMA as DOMAIN_CONTROL_MANIFEST_SCHEMA, DOMAIN_CONTROL_PASSPORT_LIMITATIONS as DOMAIN_CONTROL_MANIFEST_LIMITATIONS, MAX_DOMAIN_CONTROL_PASSPORT_ENTRIES as MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES, assertDomainControlPassportByteBudget as assertDomainControlManifestByteBudget, buildUnsignedDomainControlPassport as buildUnsignedDomainControlManifest, domainControlPassportSerialisedBytes as domainControlManifestSerialisedBytes, normalizeDomainControlPassportDocument as normalizeDomainControlManifestDocument, };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Dependency-neutral canonicalisation for analyst-supplied domain names.
|
|
2
|
+
// Callers that accept untrusted values must apply their own type and raw-input
|
|
3
|
+
// bounds before using this compatibility normaliser.
|
|
4
|
+
import { MAX_DOMAIN_LABEL_LENGTH, MAX_DOMAIN_NAME_LENGTH, } from "../contracts/domain-name.mjs";
|
|
5
|
+
/**
|
|
6
|
+
* Strict, canonical domain normalisation. Parses through the WHATWG URL host
|
|
7
|
+
* (which strips scheme/path/port/userinfo and applies IDNA/punycode so Unicode
|
|
8
|
+
* and its punycode form collapse to one value), lowercases, drops a single
|
|
9
|
+
* terminal root dot, and validates LDH hostname labels. Rejects IPs, ASNs,
|
|
10
|
+
* whitespace/control characters, underscores, empty/overlong/hyphen-edged
|
|
11
|
+
* labels, and undotted names. Returns '' for anything unusable.
|
|
12
|
+
*/
|
|
13
|
+
export function normalizeDomain(value) {
|
|
14
|
+
const raw = String(value == null ? '' : value).trim();
|
|
15
|
+
if (!raw || /[\s\x00-\x1f\x7f]/u.test(raw))
|
|
16
|
+
return '';
|
|
17
|
+
let hostname;
|
|
18
|
+
try {
|
|
19
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//iu.test(raw);
|
|
20
|
+
hostname = new URL(hasScheme ? raw : `http://${raw}`).hostname;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
hostname = hostname.toLowerCase().replace(/\.$/u, '');
|
|
26
|
+
if (!hostname || hostname.length > MAX_DOMAIN_NAME_LENGTH)
|
|
27
|
+
return '';
|
|
28
|
+
if (hostname.includes(':') || hostname.startsWith('['))
|
|
29
|
+
return '';
|
|
30
|
+
const labels = hostname.split('.');
|
|
31
|
+
if (labels.length < 2)
|
|
32
|
+
return '';
|
|
33
|
+
const labelPattern = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;
|
|
34
|
+
for (const label of labels) {
|
|
35
|
+
if (!label || label.length > MAX_DOMAIN_LABEL_LENGTH || !labelPattern.test(label))
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
if (/^[0-9]+$/u.test(labels.at(-1) ?? ''))
|
|
39
|
+
return '';
|
|
40
|
+
return hostname;
|
|
41
|
+
}
|