@slicedearth/whoisleuth-cli 2.5.1 → 2.7.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 +9 -6
- package/NOTICE +3 -2
- package/README.md +60 -67
- package/cli/artifact-validation/case-response.mjs +6 -814
- package/cli/artifact-validation/lookup-evidence.mjs +30 -3
- package/cli/artifact-validation/signed-review.mjs +2 -51
- package/cli/artifact-verify.mjs +41 -3
- package/cli/bulk-command-runner.mjs +4 -6
- package/cli/case-command.mjs +92 -16
- package/cli/case-pack.mjs +14 -3
- package/cli/ci-report.mjs +17 -5
- package/cli/command-definition.mjs +7 -6
- package/cli/discovery-scan-command-runner.mjs +4 -6
- package/cli/doctor.mjs +37 -3
- package/cli/domain-feed.mjs +61 -0
- package/cli/formatters/html.mjs +3 -0
- package/cli/formatters/json.mjs +9 -14
- package/cli/formatters/terminal-lookup.mjs +24 -1
- package/cli/history-command-definitions.mjs +1 -1
- package/cli/indicator-set-command.mjs +1 -2
- package/cli/intake-command.mjs +43 -3
- package/cli/investigation-artifacts.mjs +2 -3
- package/cli/investigation-package-review.mjs +2 -0
- package/cli/investigation-plan.mjs +15 -0
- package/cli/local-mmdb-review.mjs +27 -4
- package/cli/lookup-brief.mjs +11 -5
- package/cli/lookup-command-runner.mjs +21 -13
- package/cli/offline-evidence-review.mjs +51 -5
- package/cli/review-arguments.mjs +3 -2
- package/cli/review-command-definitions.mjs +23 -17
- package/cli/review-command-runner.mjs +6 -3
- package/cli/risk-calibration.mjs +6 -4
- package/cli/saved-lookup.mjs +22 -7
- package/cli/selected-input-worker.mjs +12 -2
- package/cli/source-reliability.mjs +12 -10
- package/cli/support-arguments.mjs +1 -1
- package/cli/support-command-definitions.mjs +5 -4
- package/cli/support-command-runner.mjs +9 -7
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +14 -0
- package/cli/workflow-command-definitions.mjs +27 -1
- package/cli/workflow-command-runner.mjs +32 -0
- package/docs/cli-reference.md +49 -11
- package/docs/cli.md +200 -26
- package/docs/contextual-reviews.md +38 -5
- package/docs/infrastructure-observations.md +135 -0
- package/docs/offline-intake-context.md +213 -0
- package/lib/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- package/lib/dmarc-discovery.mjs +4 -2
- package/lib/dns-delegation-health.mjs +23 -7
- package/lib/dns-inheritance-review.mjs +14 -1
- package/lib/dns-intelligence.mjs +18 -4
- package/lib/domain-posture-analysis.mjs +53 -2
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +29 -10
- package/lib/evidence-export-privacy.mjs +5 -2
- package/lib/evidence-export.mjs +10 -4
- package/lib/evidence-report.mjs +3 -1
- package/lib/favicon.mjs +13 -4
- package/lib/generated/cisa-kev-catalog.mjs +17 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +178 -5
- package/lib/html-signals.mjs +6 -0
- package/lib/idn-confusables.mjs +5 -1
- package/lib/lookup-child-profile-contract.mjs +28 -11
- package/lib/lookup-collection-receipt.mjs +52 -0
- package/lib/lookup-diagnostics.mjs +2 -12
- package/lib/lookup-progress.mjs +29 -31
- package/lib/lookup-response-contract.mjs +10 -0
- package/lib/lookup-response.mjs +15 -14
- package/lib/lookup-source-progress.mjs +4 -2
- package/lib/lookup.mjs +33 -2
- package/lib/operation-budget.mjs +4 -0
- package/lib/rdap-capabilities.mjs +43 -9
- package/lib/registry-insights.mjs +17 -6
- package/lib/service-binding-dns.mjs +19 -2
- package/lib/sslbl-certificates.generated.mjs +142 -140
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/technology-evidence-role.mjs +1 -51
- package/lib/tls-intelligence.mjs +30 -5
- package/lib/website-enrichment.mjs +39 -23
- package/lib/whois-authority.mjs +56 -1
- package/lib/whois-chain.mjs +14 -4
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-contracts.mjs +2 -0
- package/lib/whois-normalization.mjs +4 -4
- package/package.json +9 -7
- package/packages/analysis/bounded-json.mjs +12 -2
- package/packages/analysis/risk-scoring.mjs +12 -9
- package/packages/cases/case-attachment-model.mjs +36 -1
- package/packages/cases/case-checkpoint-receipts.mjs +68 -0
- package/packages/cases/case-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +44 -0
- package/packages/cases/case-evidence-model.mjs +51 -7
- package/packages/cases/case-migration-model.mjs +9 -1
- package/packages/cases/case-object-coverage.mjs +119 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +35 -5
- package/packages/cases/case-record-operations.mjs +87 -9
- package/packages/cases/case-record-projection.mjs +43 -5
- package/packages/cases/case-record-version-input.mjs +23 -1
- package/packages/cases/case-report.mjs +35 -4
- package/packages/cases/case-requested-evidence.mjs +1 -1
- package/packages/cases/case-response-actions.mjs +57 -1
- package/packages/cases/case-response-model.mjs +7 -0
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +81 -14
- package/packages/cases/case-response-packet-render.mjs +22 -3
- package/packages/cases/case-response-packet-validation.mjs +880 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +12 -1
- package/packages/cases/case-response-packet.mjs +32 -17
- package/packages/cases/case-response-review-inputs.mjs +66 -8
- package/packages/cases/case-type-presentation.mjs +10 -0
- package/packages/cases/case-workflow-metadata.mjs +1 -17
- package/packages/comparison/capture-context.mjs +3 -2
- package/packages/contracts/analyst-interchange.mjs +7 -5
- package/packages/contracts/analyst-review-state-contract.mjs +1 -1
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +292 -43
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-doctor.mjs +4 -0
- package/packages/contracts/cli-lookup.mjs +57 -46
- package/packages/contracts/context-review.mjs +2 -1
- package/packages/contracts/external-observation-interchange.mjs +96 -28
- package/packages/contracts/lookup-evidence.mjs +13 -2
- package/packages/contracts/message-intake.mjs +7 -1
- package/packages/contracts/registry-insights.mjs +6 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +1 -1
- package/packages/contracts/workspace-portability.mjs +86 -16
- package/packages/evidence/collection-quality.mjs +1 -1
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dmarc-policy-context.mjs +80 -0
- package/packages/evidence/image-regions.mjs +15 -0
- package/packages/evidence/lookup-collection-receipt.mjs +140 -0
- package/packages/evidence/lookup-collection-review.mjs +74 -0
- package/packages/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/external-findings-import.mjs +40 -21
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +3 -2
- package/packages/interchange/managed-indicator-set.mjs +13 -0
- package/packages/investigation/acquisition-decision-packet.mjs +78 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -2
- package/packages/investigation/context-review.mjs +3 -3
- package/packages/investigation/document-intake.mjs +9 -3
- package/packages/investigation/domain-history-review.mjs +50 -12
- package/packages/investigation/domain-transition-review.mjs +2 -0
- package/packages/investigation/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +2 -1
- package/packages/investigation/investigation-manifest.mjs +33 -7
- package/packages/investigation/link-intake.mjs +38 -8
- package/packages/investigation/message-intake.mjs +55 -15
- package/packages/monitoring/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +241 -0
- package/packages/workspace/brand-profile-model.mjs +46 -2
- package/packages/workspace/bulk-session-model.mjs +12 -0
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +10 -2
- package/packages/workspace/shortlist-model.mjs +10 -2
- package/packages/workspace/watchlist-history.mjs +31 -4
- package/packages/workspace/watchlist-store.mjs +136 -7
- package/third-party-notices.txt +30 -30
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { LOOKUP_SOURCE_IDS, LOOKUP_CHOICE_KEYS, LOOKUP_CHOICE_SOURCES, LOOKUP_SOURCE_STATES, LOOKUP_COLLECTION_RECEIPT_VERSION, lookupCollectionChoices, parseLookupCollectionReceipt, } from "../packages/evidence/lookup-collection-receipt.mjs";
|
|
2
|
+
import { plannedLookupSources } from "./lookup-progress.mjs";
|
|
3
|
+
import { normalizeLookupSourceSettlement } from "./lookup-source-progress.mjs";
|
|
4
|
+
/** Operation-local observer. It neither admits nor starts collection. */
|
|
5
|
+
export function createLookupCollectionReceiptTracker(classified, options) {
|
|
6
|
+
const requestedDepth = options.fast ? 'fast' : 'deep';
|
|
7
|
+
const originalChoices = lookupCollectionChoices(options);
|
|
8
|
+
const eligible = new Set(plannedLookupSources(classified.type, requestedDepth, originalChoices));
|
|
9
|
+
const requested = new Set(eligible);
|
|
10
|
+
for (const choice of LOOKUP_CHOICE_KEYS)
|
|
11
|
+
if (originalChoices[choice])
|
|
12
|
+
requested.add(LOOKUP_CHOICE_SOURCES[choice]);
|
|
13
|
+
const sources = new Map(LOOKUP_SOURCE_IDS.map(source => [source, {
|
|
14
|
+
source,
|
|
15
|
+
intent: requested.has(source) ? 'requested' : 'not_requested',
|
|
16
|
+
admission: eligible.has(source) ? 'unknown' : 'not_applicable',
|
|
17
|
+
attempt: 'not_started', settlement: 'unknown', complete: null, truncated: null,
|
|
18
|
+
}]));
|
|
19
|
+
function admission(source, value) {
|
|
20
|
+
const current = sources.get(source);
|
|
21
|
+
if (current.attempt !== 'started')
|
|
22
|
+
sources.set(source, { ...current, admission: value });
|
|
23
|
+
}
|
|
24
|
+
function started(source) {
|
|
25
|
+
sources.set(source, { ...sources.get(source), admission: 'admitted', attempt: 'started' });
|
|
26
|
+
}
|
|
27
|
+
function settled(source, outcome, value) {
|
|
28
|
+
const current = sources.get(source);
|
|
29
|
+
if (current.attempt !== 'started')
|
|
30
|
+
return;
|
|
31
|
+
const summary = normalizeLookupSourceSettlement(source, outcome, value, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined);
|
|
32
|
+
const raw = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
33
|
+
// Preserve final distinctions that the provisional progress vocabulary folds
|
|
34
|
+
// into skipped. The current source's explicit quality still takes precedence.
|
|
35
|
+
const status = typeof raw.status === 'string'
|
|
36
|
+
&& LOOKUP_SOURCE_STATES.includes(raw.status) ? raw.status : null;
|
|
37
|
+
const settlement = outcome === 'fulfilled' && status && ['disabled', 'not_applicable', 'stale'].includes(status)
|
|
38
|
+
? status : summary.state;
|
|
39
|
+
sources.set(source, { ...current, settlement,
|
|
40
|
+
complete: value == null ? null : ['success', 'complete', 'not_found'].includes(settlement) && summary.complete && !summary.truncated,
|
|
41
|
+
truncated: value == null || outcome === 'rejected' ? null : summary.truncated,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function finish() {
|
|
45
|
+
return parseLookupCollectionReceipt({
|
|
46
|
+
version: LOOKUP_COLLECTION_RECEIPT_VERSION, requestedDepth, targetClass: classified.type,
|
|
47
|
+
webSelection: classified.type !== 'domain' ? 'not_applicable' : options.selectedUrl ? 'selected_url' : 'hostname',
|
|
48
|
+
originalChoices, sources: [...sources.values()],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze({ admission, started, settled, finish });
|
|
52
|
+
}
|
|
@@ -2,22 +2,12 @@
|
|
|
2
2
|
import { OPERATION_BUDGET_ERROR_CODE } from "./operation-budget.mjs";
|
|
3
3
|
import { FEATURE_DISABLED_ERROR_CODE } from "./feature-policy.mjs";
|
|
4
4
|
import { LOOKUP_SOURCE_STATES, lookupDiagnosticStates } from "./lookup-progress.mjs";
|
|
5
|
+
import { LOOKUP_SOURCE_IDS } from "../packages/evidence/lookup-collection-receipt.mjs";
|
|
5
6
|
const LOOKUP_DIAGNOSTICS_VERSION = 8;
|
|
6
7
|
const LOOKUP_LEGACY_DIAGNOSTICS_VERSION = 7;
|
|
7
8
|
const LOOKUP_TIMING_VERSION = 1;
|
|
8
9
|
const MAX_LOOKUP_TIMING_MS = 120_000;
|
|
9
|
-
const LOOKUP_TIMING_SOURCE_ORDER =
|
|
10
|
-
'rdap',
|
|
11
|
-
'whois',
|
|
12
|
-
'domain_evidence',
|
|
13
|
-
'reverse_dns',
|
|
14
|
-
'registrar_rdap',
|
|
15
|
-
'network_context',
|
|
16
|
-
'security_txt',
|
|
17
|
-
'external_intelligence',
|
|
18
|
-
'malware_host_intelligence',
|
|
19
|
-
'malware_ioc_intelligence',
|
|
20
|
-
]);
|
|
10
|
+
const LOOKUP_TIMING_SOURCE_ORDER = LOOKUP_SOURCE_IDS;
|
|
21
11
|
const LOOKUP_ERROR_CODES = Object.freeze({
|
|
22
12
|
AUTH_REQUIRED: 'AUTH_REQUIRED',
|
|
23
13
|
RATE_LIMITED: 'RATE_LIMITED',
|
package/lib/lookup-progress.mjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// presentation-only; only a validated final ordinary Lookup result may cross
|
|
3
3
|
// the persistence boundary.
|
|
4
4
|
import { scanBoundedJson, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES } from "../packages/analysis/bounded-json.mjs";
|
|
5
|
+
import { LOOKUP_SOURCE_IDS as SOURCE_IDS, LOOKUP_SOURCE_STATES } from "../packages/evidence/lookup-collection-receipt.mjs";
|
|
6
|
+
export { LOOKUP_SOURCE_STATES } from "../packages/evidence/lookup-collection-receipt.mjs";
|
|
5
7
|
export const LOOKUP_PROGRESS_SCHEMA = 'whoisleuth.lookup-progress';
|
|
6
8
|
export const LOOKUP_PROGRESS_VERSION = 1;
|
|
7
9
|
export const MAX_LOOKUP_PROGRESS_SOURCES = 16;
|
|
@@ -9,18 +11,6 @@ export const MAX_LOOKUP_PROGRESS_EVENTS = MAX_LOOKUP_PROGRESS_SOURCES + 2;
|
|
|
9
11
|
export const MAX_LOOKUP_PROGRESS_FRAGMENT_BYTES = 256 * 1024;
|
|
10
12
|
export const MAX_LOOKUP_PROGRESS_FINAL_BYTES = 8 * 1024 * 1024;
|
|
11
13
|
export const MAX_LOOKUP_PROGRESS_STREAM_BYTES = 12 * 1024 * 1024;
|
|
12
|
-
const SOURCE_IDS = Object.freeze([
|
|
13
|
-
'rdap',
|
|
14
|
-
'whois',
|
|
15
|
-
'domain_evidence',
|
|
16
|
-
'reverse_dns',
|
|
17
|
-
'registrar_rdap',
|
|
18
|
-
'network_context',
|
|
19
|
-
'security_txt',
|
|
20
|
-
'external_intelligence',
|
|
21
|
-
'malware_host_intelligence',
|
|
22
|
-
'malware_ioc_intelligence',
|
|
23
|
-
]);
|
|
24
14
|
const SOURCE_ID_SET = new Set(SOURCE_IDS);
|
|
25
15
|
const SOURCE_STATES = new Set([
|
|
26
16
|
'success',
|
|
@@ -33,28 +23,36 @@ const SOURCE_STATES = new Set([
|
|
|
33
23
|
'rate_limited',
|
|
34
24
|
]);
|
|
35
25
|
// Final diagnostic states are distinct from the provisional progress subset.
|
|
36
|
-
export const LOOKUP_SOURCE_STATES = Object.freeze([
|
|
37
|
-
'success', 'partial', 'not_found', 'skipped', 'error', 'unsupported',
|
|
38
|
-
'not_applicable', 'unavailable', 'rate_limited', 'complete', 'disabled', 'stale',
|
|
39
|
-
]);
|
|
40
26
|
const FINAL_SOURCE_STATES = new Set(LOOKUP_SOURCE_STATES);
|
|
41
|
-
|
|
27
|
+
const DIAGNOSTIC_SOURCE_PATHS = Object.freeze({
|
|
28
|
+
rdap: ['rdap'], registrar_rdap: ['rdap', 'registrar'], whois: ['whois'],
|
|
29
|
+
availability: ['availability'], reverse_dns: ['reverseDns'], network_context: ['network'],
|
|
30
|
+
security_txt: ['securityTxt'], sslbl: ['sslbl'],
|
|
31
|
+
});
|
|
32
|
+
export function lookupDiagnosticEntries(value) {
|
|
42
33
|
const record = (input) => input && typeof input === 'object' && !Array.isArray(input)
|
|
43
34
|
? input : {};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
35
|
+
return Object.freeze(Object.entries(DIAGNOSTIC_SOURCE_PATHS).map(([source, keys]) => {
|
|
36
|
+
let current = value;
|
|
37
|
+
let present = true;
|
|
38
|
+
for (const key of keys) {
|
|
39
|
+
const parent = record(current);
|
|
40
|
+
if (!Object.hasOwn(parent, key)) {
|
|
41
|
+
present = false;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
current = parent[key];
|
|
45
|
+
}
|
|
46
|
+
const diagnostic = record(current);
|
|
47
|
+
const state = present && Object.hasOwn(diagnostic, 'status') ? diagnostic.status : undefined;
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
source, path: `diagnostics.${keys.join('.')}.status`, present,
|
|
50
|
+
state: typeof state === 'string' && FINAL_SOURCE_STATES.has(state) ? state : null,
|
|
51
|
+
});
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
export function lookupDiagnosticStates(value) {
|
|
55
|
+
return Object.freeze(Object.fromEntries(lookupDiagnosticEntries(value).map(({ source, state }) => [source, state])));
|
|
58
56
|
}
|
|
59
57
|
const SOURCE_ID_RE = /^[a-z][a-z0-9_]{0,39}$/u;
|
|
60
58
|
/** The existing Lookup recipe, shared by offline plans and source progress.
|
|
@@ -13,6 +13,7 @@ import { assertBoundedJsonStructure } from "../packages/analysis/bounded-json.mj
|
|
|
13
13
|
import { canonicalRegistrableDomain } from "../packages/analysis/registrable-domain.mjs";
|
|
14
14
|
import { validLookupObservationScope } from "../packages/evidence/lookup-target.mjs";
|
|
15
15
|
import { isWebCollectionQuality } from "../packages/evidence/collection-quality.mjs";
|
|
16
|
+
import { parseLookupCollectionReceipt } from "../packages/evidence/lookup-collection-receipt.mjs";
|
|
16
17
|
import { MAX_LOOKUP_DNS_RECORDS_PER_TYPE, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS, MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, } from "./lookup-network-evidence-bounds.mjs";
|
|
17
18
|
import { MAX_SECURITY_POSTURE_FINDINGS, sanitizeLookupChildProfiles, validSecurityPostureFinding, validTlsChainCertificate, validTlsFinding, } from "./lookup-child-profile-contract.mjs";
|
|
18
19
|
import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "./homepage-metadata-contract.mjs";
|
|
@@ -646,6 +647,7 @@ function validAvailabilityScalars(value) {
|
|
|
646
647
|
&& (value.hasNullMx === undefined || value.hasNullMx === null || typeof value.hasNullMx === 'boolean')
|
|
647
648
|
&& (value.hasSpf === undefined || value.hasSpf === null || typeof value.hasSpf === 'boolean')
|
|
648
649
|
&& (value.hasDmarc === undefined || value.hasDmarc === null || typeof value.hasDmarc === 'boolean')
|
|
650
|
+
&& (value.hasExternalPasswordForm === undefined || value.hasExternalPasswordForm === null || typeof value.hasExternalPasswordForm === 'boolean')
|
|
649
651
|
&& validOptionalStringArray(value.mxHosts, MAX_LOOKUP_DNS_RECORDS_PER_TYPE, 253);
|
|
650
652
|
}
|
|
651
653
|
function validLookupDomainIdentity(value) {
|
|
@@ -824,6 +826,14 @@ function parseLookupHttpResponse(value) {
|
|
|
824
826
|
return invalidLookupResponse();
|
|
825
827
|
}
|
|
826
828
|
const lookupResponse = sanitizeLookupChildProfiles(value);
|
|
829
|
+
try {
|
|
830
|
+
const receipt = parseLookupCollectionReceipt(lookupResponse.collectionReceipt);
|
|
831
|
+
if (receipt && receipt.targetClass !== lookupResponse.type)
|
|
832
|
+
return invalidLookupResponse();
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
return invalidLookupResponse();
|
|
836
|
+
}
|
|
827
837
|
if (lookupResponse.registrarStanding !== undefined
|
|
828
838
|
&& (!validRegistrarStanding(lookupResponse.registrarStanding)
|
|
829
839
|
|| lookupResponse.registrarStanding.ianaId !== resolveRegistrarIanaId(lookupResponse.rdap.parsed, lookupResponse.whois.parsed)))
|
package/lib/lookup-response.mjs
CHANGED
|
@@ -76,12 +76,16 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
76
76
|
? { skipped: true, detail: 'RDAP is disabled by deployment policy.' }
|
|
77
77
|
: rdapRecord
|
|
78
78
|
? { ...rdapRecord, ...(registrarRdap ? { registrarRdap } : {}) }
|
|
79
|
-
:
|
|
80
|
-
error: rdapResult.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
: rdapResult.status === 'rejected'
|
|
80
|
+
? { error: boundedSourceDetail(rdapResult.reason, 'RDAP lookup failed'), attempts: rdapAttempts }
|
|
81
|
+
: { status: 'unsupported', detail: 'No RDAP service is listed for this target in the IANA bootstrap.', attempts: rdapAttempts };
|
|
82
|
+
const whoisStatus = !whoisEnabled
|
|
83
|
+
? 'disabled'
|
|
84
|
+
: skipWhois
|
|
85
|
+
? 'skipped'
|
|
86
|
+
: whoisResult.status === 'rejected'
|
|
87
|
+
? 'error'
|
|
88
|
+
: whoisCollectionStatus(whoisChain, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined);
|
|
85
89
|
let whois;
|
|
86
90
|
if (!whoisEnabled) {
|
|
87
91
|
whois = { skipped: true, detail: 'WHOIS is disabled by deployment policy.' };
|
|
@@ -90,7 +94,11 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
90
94
|
whois = { skipped: true, detail: 'WHOIS is omitted in fast RDAP-only mode.' };
|
|
91
95
|
}
|
|
92
96
|
else if (Array.isArray(whoisChain)) {
|
|
93
|
-
whois =
|
|
97
|
+
whois = whoisStatus === 'unsupported' || whoisStatus === 'skipped'
|
|
98
|
+
? { status: whoisStatus, chain: whoisChain,
|
|
99
|
+
detail: whoisChain[0]?.detail ?? 'IANA did not publish a domain WHOIS referral for this target.' }
|
|
100
|
+
: { chain: whoisChain, parsed: parseWhoisChain(whoisChain, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined),
|
|
101
|
+
...(whoisStatus === 'error' ? { error: boundedSourceDetail({ message: whoisChain[0]?.error }, 'WHOIS lookup failed') } : {}) };
|
|
94
102
|
}
|
|
95
103
|
else {
|
|
96
104
|
whois = {
|
|
@@ -138,13 +146,6 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
138
146
|
: !rdapRecord
|
|
139
147
|
? 'unsupported'
|
|
140
148
|
: rdapRecord.upstreamStatus === 404 ? 'not_found' : 'success';
|
|
141
|
-
const whoisStatus = !whoisEnabled
|
|
142
|
-
? 'disabled'
|
|
143
|
-
: skipWhois
|
|
144
|
-
? 'skipped'
|
|
145
|
-
: whoisResult.status === 'rejected'
|
|
146
|
-
? 'error'
|
|
147
|
-
: whoisCollectionStatus(whoisChain, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined);
|
|
148
149
|
const availabilityStatus = classified.type !== 'domain'
|
|
149
150
|
? 'not_applicable'
|
|
150
151
|
: !availabilityEnabled ? 'disabled'
|
|
@@ -100,9 +100,11 @@ function normalizeLookupSourceSettlement(source, outcome, value, requestedDomain
|
|
|
100
100
|
const limitation = outcome === 'rejected'
|
|
101
101
|
? 'This source did not complete. No absence or safety conclusion was inferred.'
|
|
102
102
|
: state === 'skipped'
|
|
103
|
-
? 'This source was not
|
|
103
|
+
? 'This source was not queried: it was not requested, did not apply to the target, or was excluded by an access policy.'
|
|
104
104
|
: state === 'unsupported'
|
|
105
|
-
?
|
|
105
|
+
? source === 'rdap' || source === 'whois'
|
|
106
|
+
? 'No service is published through the applicable registry discovery source for this target; this is not a failed lookup or an absence finding.'
|
|
107
|
+
: 'This source could not provide authoritative evidence for this target.'
|
|
106
108
|
: state === 'partial'
|
|
107
109
|
? 'This source returned incomplete evidence; missing fields remain unknown.'
|
|
108
110
|
: null;
|
package/lib/lookup.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { normalizeLookupSourceSettlement, plannedLookupProgressSources, } from "
|
|
|
22
22
|
import { buildUnifiedLookupResponse } from "./lookup-response.mjs";
|
|
23
23
|
import { createLookupTimingTracker } from "./lookup-diagnostics.mjs";
|
|
24
24
|
import { prepareSelectedLookupUrl } from "../packages/evidence/lookup-target.mjs";
|
|
25
|
+
import { createLookupCollectionReceiptTracker } from "./lookup-collection-receipt.mjs";
|
|
25
26
|
async function runUnifiedLookup(classified, options = {}) {
|
|
26
27
|
const fetchRdap = options.fetchRdapRecord || fetchRdapRecord;
|
|
27
28
|
const fetchRegistrarRdap = options.fetchRegistrarRdapRecord || fetchRegistrarRdapRecord;
|
|
@@ -45,18 +46,42 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
45
46
|
const securityTxtRequested = options.securityTxt === true;
|
|
46
47
|
const featurePolicy = options.featurePolicy || networkFeaturePolicy();
|
|
47
48
|
const timing = createLookupTimingTracker(!fast && !compact, options.now || Date.now);
|
|
49
|
+
const receipt = !compact && options.onCollectionReceipt
|
|
50
|
+
? createLookupCollectionReceiptTracker(classified, options) : null;
|
|
48
51
|
const startedCollectors = [];
|
|
49
52
|
const measure = (source, operation) => (timing.measure(source, () => abortable(() => {
|
|
53
|
+
receipt?.started(source);
|
|
50
54
|
const running = Promise.resolve(operation());
|
|
51
55
|
if (options.waitForStartedCollectors)
|
|
52
56
|
startedCollectors.push(running.catch(() => { }));
|
|
53
57
|
return running;
|
|
54
|
-
}, options.signal))
|
|
58
|
+
}, options.signal)).then(value => {
|
|
59
|
+
receipt?.settled(source, 'fulfilled', value);
|
|
60
|
+
return value;
|
|
61
|
+
}, error => {
|
|
62
|
+
receipt?.settled(source, 'rejected', error);
|
|
63
|
+
throw error;
|
|
64
|
+
}));
|
|
55
65
|
const rdapEnabled = featureDecision('rdap', featurePolicy).enabled;
|
|
56
66
|
const whoisEnabled = featureDecision('whois', featurePolicy).enabled;
|
|
57
67
|
const availabilityEnabled = featureDecision('availability', featurePolicy).enabled;
|
|
58
68
|
const websiteProbeEnabled = featureDecision('website_probe', featurePolicy).enabled;
|
|
59
69
|
const dnsIntelligenceEnabled = featureDecision('dns_intelligence', featurePolicy).enabled;
|
|
70
|
+
if (!rdapEnabled) {
|
|
71
|
+
receipt?.admission('rdap', 'disabled');
|
|
72
|
+
receipt?.admission('registrar_rdap', 'disabled');
|
|
73
|
+
receipt?.admission('network_context', 'disabled');
|
|
74
|
+
}
|
|
75
|
+
if (!whoisEnabled)
|
|
76
|
+
receipt?.admission('whois', 'disabled');
|
|
77
|
+
if (!availabilityEnabled) {
|
|
78
|
+
receipt?.admission('domain_evidence', 'disabled');
|
|
79
|
+
receipt?.admission('network_context', 'disabled');
|
|
80
|
+
}
|
|
81
|
+
if (!dnsIntelligenceEnabled)
|
|
82
|
+
receipt?.admission('reverse_dns', 'disabled');
|
|
83
|
+
if (!websiteProbeEnabled)
|
|
84
|
+
receipt?.admission('security_txt', 'disabled');
|
|
60
85
|
const skipWhois = fast || !whoisEnabled;
|
|
61
86
|
const selectedUrl = options.selectedUrl === undefined ? undefined
|
|
62
87
|
: prepareSelectedLookupUrl(options.selectedUrl, classified.inputHostname ?? classified.value);
|
|
@@ -72,6 +97,8 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
72
97
|
// Registrar RDAP is a separately attributed deep-lookup enrichment. It may
|
|
73
98
|
// overlap the WHOIS chain, but it never joins the promises used to decide
|
|
74
99
|
// availability and can add up to its own bounded timeout to a deep lookup.
|
|
100
|
+
if (classified.type === 'domain' && rdapEnabled && !fast && !compact)
|
|
101
|
+
receipt?.admission('registrar_rdap', 'prerequisite_unavailable');
|
|
75
102
|
const registrarRdapPromise = classified.type === 'domain' && rdapEnabled && !fast && !compact
|
|
76
103
|
? rdapPromise.then((record) => record && record.upstreamStatus === 200 && record.parsed
|
|
77
104
|
? measure('registrar_rdap', () => fetchRegistrarRdap(classified.value, record))
|
|
@@ -187,7 +214,7 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
187
214
|
}
|
|
188
215
|
}
|
|
189
216
|
try {
|
|
190
|
-
|
|
217
|
+
const result = await buildUnifiedLookupResponse({
|
|
191
218
|
availabilityEnabled,
|
|
192
219
|
availabilityPromise,
|
|
193
220
|
classified,
|
|
@@ -211,6 +238,10 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
211
238
|
whoisEnabled,
|
|
212
239
|
whoisPromise,
|
|
213
240
|
});
|
|
241
|
+
options.signal?.throwIfAborted();
|
|
242
|
+
if (receipt)
|
|
243
|
+
options.onCollectionReceipt?.(receipt.finish());
|
|
244
|
+
return result;
|
|
214
245
|
}
|
|
215
246
|
finally {
|
|
216
247
|
if (options.waitForStartedCollectors)
|
package/lib/operation-budget.mjs
CHANGED
|
@@ -34,6 +34,7 @@ const OPERATION_FEATURES = Object.freeze({
|
|
|
34
34
|
AVAILABILITY_FAST: 'availability_fast',
|
|
35
35
|
AVAILABILITY_DEEP: 'availability_deep',
|
|
36
36
|
CERTIFICATE_TRANSPARENCY: 'certificate_transparency',
|
|
37
|
+
DOMAIN_FEED_SEARCH: 'domain_feed_search',
|
|
37
38
|
DOMAIN_POSTURE: 'domain_posture',
|
|
38
39
|
});
|
|
39
40
|
const OPERATION_FEATURE_CLASSES = Object.freeze({
|
|
@@ -47,6 +48,7 @@ const OPERATION_FEATURE_CLASSES = Object.freeze({
|
|
|
47
48
|
[OPERATION_FEATURES.AVAILABILITY_FAST]: OPERATION_CLASSES.REGISTRY_LIGHT,
|
|
48
49
|
[OPERATION_FEATURES.AVAILABILITY_DEEP]: OPERATION_CLASSES.REGISTRY_DEEP,
|
|
49
50
|
[OPERATION_FEATURES.CERTIFICATE_TRANSPARENCY]: OPERATION_CLASSES.CERTIFICATE_SEARCH,
|
|
51
|
+
[OPERATION_FEATURES.DOMAIN_FEED_SEARCH]: OPERATION_CLASSES.CERTIFICATE_SEARCH,
|
|
50
52
|
[OPERATION_FEATURES.DOMAIN_POSTURE]: OPERATION_CLASSES.POSTURE_AUDIT,
|
|
51
53
|
});
|
|
52
54
|
// The browser currently runs at most 12 fast Bulk workers, 4 deep Bulk
|
|
@@ -87,6 +89,8 @@ function operationFeatureFor(feature, { fast = false, compact = false } = {}) {
|
|
|
87
89
|
return OPERATION_FEATURES.WHOIS;
|
|
88
90
|
if (feature === 'certificate_transparency')
|
|
89
91
|
return OPERATION_FEATURES.CERTIFICATE_TRANSPARENCY;
|
|
92
|
+
if (feature === 'domain_feed_search')
|
|
93
|
+
return OPERATION_FEATURES.DOMAIN_FEED_SEARCH;
|
|
90
94
|
if (feature === 'domain_posture')
|
|
91
95
|
return OPERATION_FEATURES.DOMAIN_POSTURE;
|
|
92
96
|
return null;
|
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
// search and does not treat an absent extension declaration as proof that a
|
|
4
4
|
// server cannot support the related operation.
|
|
5
5
|
export const RDAP_CAPABILITY_INSPECTION_VERSION = 1;
|
|
6
|
-
export const RDAP_EXTENSION_CATALOG_REVIEWED_AT = '2026-
|
|
6
|
+
export const RDAP_EXTENSION_CATALOG_REVIEWED_AT = '2026-10-14';
|
|
7
7
|
export const MAX_RDAP_CAPABILITY_DECLARATIONS = 50;
|
|
8
8
|
export const MAX_RDAP_UNKNOWN_CAPABILITIES = 20;
|
|
9
9
|
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
const EXTENSION_CATALOG = Object.freeze({
|
|
10
|
+
// Published interpretation snapshot. Saved evidence selects this exact revision;
|
|
11
|
+
// later catalogue revisions extend or replace entries without changing it.
|
|
12
|
+
const JULY_2026_EXTENSION_CATALOG = Object.freeze({
|
|
14
13
|
rdap_level_0: {
|
|
15
14
|
label: 'Core RDAP response',
|
|
16
15
|
category: 'core',
|
|
@@ -164,8 +163,39 @@ const EXTENSION_CATALOG = Object.freeze({
|
|
|
164
163
|
capability: 'DNS time-to-live values in RDAP responses.',
|
|
165
164
|
},
|
|
166
165
|
});
|
|
166
|
+
const RDAP_EXTENSION_CATALOGS = Object.freeze({
|
|
167
|
+
'2026-07-31': JULY_2026_EXTENSION_CATALOG,
|
|
168
|
+
'2026-10-14': Object.freeze({
|
|
169
|
+
...JULY_2026_EXTENSION_CATALOG,
|
|
170
|
+
reliabilityassessment: {
|
|
171
|
+
label: 'Reliability assessment metadata',
|
|
172
|
+
category: 'operator',
|
|
173
|
+
capability: 'Operator-declared assessment metadata; no assessment is retrieved or verified.',
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
const CATALOG_EPOCHS = Object.keys(RDAP_EXTENSION_CATALOGS).sort();
|
|
178
|
+
export function resolveRdapCapabilityCatalogRevision(value) {
|
|
179
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/u.test(value)
|
|
180
|
+
|| value < CATALOG_EPOCHS[0] || value > RDAP_EXTENSION_CATALOG_REVIEWED_AT)
|
|
181
|
+
return null;
|
|
182
|
+
const time = Date.parse(`${value}T00:00:00.000Z`);
|
|
183
|
+
if (!Number.isFinite(time) || new Date(time).toISOString().slice(0, 10) !== value)
|
|
184
|
+
return null;
|
|
185
|
+
// Review-only renewals retain the same semantic epoch and the recorded date.
|
|
186
|
+
for (let index = CATALOG_EPOCHS.length - 1; index >= 0; index -= 1) {
|
|
187
|
+
const epoch = CATALOG_EPOCHS[index];
|
|
188
|
+
if (epoch <= value)
|
|
189
|
+
return epoch;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
167
193
|
export function reviewedRdapExtensionCatalog() {
|
|
168
|
-
|
|
194
|
+
const revision = resolveRdapCapabilityCatalogRevision(RDAP_EXTENSION_CATALOG_REVIEWED_AT);
|
|
195
|
+
if (revision === null)
|
|
196
|
+
throw new TypeError('Unsupported RDAP capability catalogue revision.');
|
|
197
|
+
const catalog = RDAP_EXTENSION_CATALOGS[revision];
|
|
198
|
+
return Object.entries(catalog)
|
|
169
199
|
.filter(([, entry]) => entry.category !== 'core')
|
|
170
200
|
.map(([identifier, entry]) => Object.freeze({
|
|
171
201
|
identifier,
|
|
@@ -232,7 +262,11 @@ function reverseSearchCapability(state, declared) {
|
|
|
232
262
|
],
|
|
233
263
|
};
|
|
234
264
|
}
|
|
235
|
-
export function inspectRdapCapabilities(parsedValue, status) {
|
|
265
|
+
export function inspectRdapCapabilities(parsedValue, status, catalogReviewedAt = RDAP_EXTENSION_CATALOG_REVIEWED_AT) {
|
|
266
|
+
const revision = resolveRdapCapabilityCatalogRevision(catalogReviewedAt);
|
|
267
|
+
if (revision === null)
|
|
268
|
+
throw new TypeError('Unsupported RDAP capability catalogue revision.');
|
|
269
|
+
const catalog = RDAP_EXTENSION_CATALOGS[revision];
|
|
236
270
|
const parsed = record(parsedValue);
|
|
237
271
|
const state = sourceState(parsed, status);
|
|
238
272
|
const rawDeclarations = Array.isArray(parsed?.conformance) ? parsed.conformance : [];
|
|
@@ -245,7 +279,7 @@ export function inspectRdapCapabilities(parsedValue, status) {
|
|
|
245
279
|
break;
|
|
246
280
|
}
|
|
247
281
|
const declarations = [...unique].sort().map((value) => {
|
|
248
|
-
const entry = Object.hasOwn(
|
|
282
|
+
const entry = Object.hasOwn(catalog, value) ? catalog[value] : undefined;
|
|
249
283
|
return entry
|
|
250
284
|
? {
|
|
251
285
|
identifier: value,
|
|
@@ -281,7 +315,7 @@ export function inspectRdapCapabilities(parsedValue, status) {
|
|
|
281
315
|
}
|
|
282
316
|
return {
|
|
283
317
|
version: RDAP_CAPABILITY_INSPECTION_VERSION,
|
|
284
|
-
catalogReviewedAt
|
|
318
|
+
catalogReviewedAt,
|
|
285
319
|
state,
|
|
286
320
|
declarations,
|
|
287
321
|
unknownIdentifiers,
|
|
@@ -9,7 +9,8 @@ import { inspectRdapCapabilities } from "./rdap-capabilities.mjs";
|
|
|
9
9
|
import { isRecord, recordOrEmpty } from "./json-record.mjs";
|
|
10
10
|
import { MAX_RDAP_REDACTIONS, MAX_RDAP_STATUSES } from "./rdap-types.mjs";
|
|
11
11
|
import { MAX_WHOIS_STATUSES } from "./whois-contracts.mjs";
|
|
12
|
-
|
|
12
|
+
import { LEGACY_REGISTRY_INSIGHTS_VERSION, REGISTRY_INSIGHTS_VERSION, isSupportedRegistryInsightsVersion, } from "../packages/contracts/registry-insights.mjs";
|
|
13
|
+
export { REGISTRY_INSIGHTS_VERSION } from "../packages/contracts/registry-insights.mjs";
|
|
13
14
|
export const MAX_REGISTRY_INSIGHT_STATUSES = 40;
|
|
14
15
|
export const MAX_REGISTRY_INSIGHT_ISSUES = 12;
|
|
15
16
|
export const MAX_REGISTRY_ABUSE_ROUTES = 8;
|
|
@@ -288,7 +289,10 @@ function contactRoute(kind, contact, source) {
|
|
|
288
289
|
],
|
|
289
290
|
}));
|
|
290
291
|
}
|
|
291
|
-
export function buildRegistryInsights(input) {
|
|
292
|
+
export function buildRegistryInsights(input, options = {}) {
|
|
293
|
+
const version = options.version ?? REGISTRY_INSIGHTS_VERSION;
|
|
294
|
+
if (!isSupportedRegistryInsightsVersion(version))
|
|
295
|
+
throw new TypeError('Unsupported registry insight version.');
|
|
292
296
|
const rdap = record(input.rdapParsed);
|
|
293
297
|
const whois = record(input.whoisParsed);
|
|
294
298
|
const registrarRdap = record(input.registrarRdapParsed);
|
|
@@ -322,7 +326,14 @@ export function buildRegistryInsights(input) {
|
|
|
322
326
|
];
|
|
323
327
|
const lifecycleCandidates = [
|
|
324
328
|
{ statuses: rdapStatuses.values, state: publications[0].state },
|
|
325
|
-
{
|
|
329
|
+
{
|
|
330
|
+
statuses: whoisStatuses.values,
|
|
331
|
+
// A completed response describes collection, not positive registration.
|
|
332
|
+
// Preserve the old derivation only when verifying a historical export.
|
|
333
|
+
state: version === LEGACY_REGISTRY_INSIGHTS_VERSION || whois.registrationStatus === 'registered'
|
|
334
|
+
? publications[1].state
|
|
335
|
+
: 'unavailable',
|
|
336
|
+
},
|
|
326
337
|
].filter((candidate) => candidate.state !== 'unavailable');
|
|
327
338
|
const lifecycleSource = lifecycleCandidates.find((candidate) => candidate.statuses.length)
|
|
328
339
|
?? lifecycleCandidates[0]
|
|
@@ -343,7 +354,7 @@ export function buildRegistryInsights(input) {
|
|
|
343
354
|
break;
|
|
344
355
|
}
|
|
345
356
|
return {
|
|
346
|
-
version
|
|
357
|
+
version,
|
|
347
358
|
contactDisclosure: {
|
|
348
359
|
registryRdap: disclosure('registry_rdap', input.rdapParsed, input.rdapStatus),
|
|
349
360
|
whois: disclosure('whois', input.whoisParsed, input.whoisStatus),
|
|
@@ -369,8 +380,8 @@ export function buildRegistryInsights(input) {
|
|
|
369
380
|
},
|
|
370
381
|
publications,
|
|
371
382
|
rdapCapabilities: {
|
|
372
|
-
registry: inspectRdapCapabilities(input.rdapParsed, input.rdapStatus),
|
|
373
|
-
registrar: inspectRdapCapabilities(input.registrarRdapParsed, input.registrarRdapStatus),
|
|
383
|
+
registry: inspectRdapCapabilities(input.rdapParsed, input.rdapStatus, options.rdapCapabilityCatalogReviewedAt),
|
|
384
|
+
registrar: inspectRdapCapabilities(input.registrarRdapParsed, input.registrarRdapStatus, options.rdapCapabilityCatalogReviewedAt),
|
|
374
385
|
},
|
|
375
386
|
abuseRouting: [...uniqueRoutes.values()],
|
|
376
387
|
};
|
|
@@ -526,6 +526,8 @@ function defaultTcpExchange(query, resolver, options) {
|
|
|
526
526
|
}
|
|
527
527
|
});
|
|
528
528
|
socket.once('connect', () => {
|
|
529
|
+
if (settled)
|
|
530
|
+
return;
|
|
529
531
|
const length = Buffer.alloc(2);
|
|
530
532
|
length.writeUInt16BE(query.length, 0);
|
|
531
533
|
socket.write(Buffer.concat([length, query]));
|
|
@@ -534,6 +536,7 @@ function defaultTcpExchange(query, resolver, options) {
|
|
|
534
536
|
}
|
|
535
537
|
function defaultDnsExchange(query, resolver, options) {
|
|
536
538
|
return new Promise((resolve, reject) => {
|
|
539
|
+
options.signal?.throwIfAborted();
|
|
537
540
|
const socket = createSocket(resolver.family === 6 ? 'udp6' : 'udp4');
|
|
538
541
|
let settled = false;
|
|
539
542
|
const timer = setTimeout(() => {
|
|
@@ -544,6 +547,7 @@ function defaultDnsExchange(query, resolver, options) {
|
|
|
544
547
|
return;
|
|
545
548
|
settled = true;
|
|
546
549
|
clearTimeout(timer);
|
|
550
|
+
options.signal?.removeEventListener('abort', cancelled);
|
|
547
551
|
try {
|
|
548
552
|
socket.close();
|
|
549
553
|
}
|
|
@@ -555,6 +559,12 @@ function defaultDnsExchange(query, resolver, options) {
|
|
|
555
559
|
else
|
|
556
560
|
resolve(response);
|
|
557
561
|
};
|
|
562
|
+
const cancelled = () => finish(options.signal?.reason ?? new Error('DNS UDP query cancelled'));
|
|
563
|
+
options.signal?.addEventListener('abort', cancelled, { once: true });
|
|
564
|
+
if (options.signal?.aborted) {
|
|
565
|
+
cancelled();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
558
568
|
socket.once('error', (error) => finish(error));
|
|
559
569
|
socket.once('message', (message) => {
|
|
560
570
|
if (message.length > MAX_DNS_MESSAGE_BYTES) {
|
|
@@ -565,6 +575,8 @@ function defaultDnsExchange(query, resolver, options) {
|
|
|
565
575
|
}
|
|
566
576
|
});
|
|
567
577
|
socket.connect(resolver.port, resolver.address, () => {
|
|
578
|
+
if (settled)
|
|
579
|
+
return;
|
|
568
580
|
socket.send(query, (error) => {
|
|
569
581
|
if (error)
|
|
570
582
|
finish(error);
|
|
@@ -592,6 +604,7 @@ function missingResponseError(rcode) {
|
|
|
592
604
|
return new ServiceBindingDnsError(`DNS resolver returned ${names[rcode] || `rcode ${rcode}`}`, 'EDNSFAIL');
|
|
593
605
|
}
|
|
594
606
|
async function resolveServiceBindingRecords(nameValue, type, options = {}) {
|
|
607
|
+
options.signal?.throwIfAborted();
|
|
595
608
|
const name = normalizeQueryName(nameValue);
|
|
596
609
|
const transactionId = options.transactionId ?? randomInt(0x1_0000);
|
|
597
610
|
const query = buildServiceBindingDnsQuery(name, type, transactionId);
|
|
@@ -608,17 +621,20 @@ async function resolveServiceBindingRecords(nameValue, type, options = {}) {
|
|
|
608
621
|
const deadline = Date.now() + timeoutMs;
|
|
609
622
|
let lastError = null;
|
|
610
623
|
for (const resolver of resolvers) {
|
|
624
|
+
options.signal?.throwIfAborted();
|
|
611
625
|
const udpTimeoutMs = deadline - Date.now();
|
|
612
626
|
if (udpTimeoutMs <= 0)
|
|
613
627
|
break;
|
|
614
628
|
try {
|
|
615
|
-
let response = await exchange(query, resolver, { timeoutMs: udpTimeoutMs });
|
|
629
|
+
let response = await exchange(query, resolver, { timeoutMs: udpTimeoutMs, ...(options.signal ? { signal: options.signal } : {}) });
|
|
630
|
+
options.signal?.throwIfAborted();
|
|
616
631
|
if (isTruncatedDnsResponse(response, transactionId)) {
|
|
617
632
|
const tcpTimeoutMs = deadline - Date.now();
|
|
618
633
|
if (tcpTimeoutMs <= 0) {
|
|
619
634
|
throw new ServiceBindingDnsError('DNS service-binding query timed out', 'ETIMEOUT');
|
|
620
635
|
}
|
|
621
|
-
response = await (options.tcpExchange || defaultTcpExchange)(query, resolver, { timeoutMs: tcpTimeoutMs });
|
|
636
|
+
response = await (options.tcpExchange || defaultTcpExchange)(query, resolver, { timeoutMs: tcpTimeoutMs, ...(options.signal ? { signal: options.signal } : {}) });
|
|
637
|
+
options.signal?.throwIfAborted();
|
|
622
638
|
}
|
|
623
639
|
const parsed = parseServiceBindingDnsResponse(response, { transactionId, name, type });
|
|
624
640
|
if (parsed.truncatedResponse)
|
|
@@ -631,6 +647,7 @@ async function resolveServiceBindingRecords(nameValue, type, options = {}) {
|
|
|
631
647
|
return { records: parsed.records, truncated: parsed.truncated };
|
|
632
648
|
}
|
|
633
649
|
catch (error) {
|
|
650
|
+
options.signal?.throwIfAborted();
|
|
634
651
|
lastError = error;
|
|
635
652
|
const code = error && typeof error === 'object' ? String(error.code || '') : '';
|
|
636
653
|
if (code === 'ENODATA' || code === 'ENOTFOUND')
|