@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,369 +0,0 @@
|
|
|
1
|
-
import { scaleBand, scaleLinear, scalePoint } from 'd3-scale';
|
|
2
|
-
import { boundedVisualizationId as boundedId, boundedVisualizationNumber as boundedNumber, boundedVisualizationText as boundedText, validVisualizationDate as validDate, } from "./visualization-bounds.js";
|
|
3
|
-
export { FORCE_GRAPH_LINK_KINDS, MAX_FORCE_GRAPH_LINKS, MAX_FORCE_GRAPH_NODES, projectBoundedForceGraph, } from "./visualization-force-graph.js";
|
|
4
|
-
export { MAX_VISUAL_MATRIX_COLUMNS, MAX_VISUAL_MATRIX_ROWS, projectEvidenceMatrix, } from "./visualization-matrix.js";
|
|
5
|
-
export { MAX_COLLECTION_TIMING_SOURCES, MAX_LIFECYCLE_EVENTS, MAX_TREND_POINTS, projectCollectionTiming, projectLifecycleEvents, projectTrendPoints, } from "./visualization-time-series.js";
|
|
6
|
-
export const MAX_REDIRECT_NODES = 9;
|
|
7
|
-
export const MAX_TRIAGE_PLOT_POINTS = 300;
|
|
8
|
-
export const WATCHLIST_ACTIVITY_DAYS = 28;
|
|
9
|
-
export const MAX_SCORE_FACTORS = 16;
|
|
10
|
-
export const MAX_COVERAGE_BAR_GROUPS = 18;
|
|
11
|
-
export const MAX_MONITOR_TIMELINE_EVENTS = 12;
|
|
12
|
-
export const MAX_MONITOR_TIMELINE_LANES = 6;
|
|
13
|
-
function boundedScore(value) {
|
|
14
|
-
if (value === null || value === undefined || value === '')
|
|
15
|
-
return null;
|
|
16
|
-
const score = Number(value);
|
|
17
|
-
return Number.isFinite(score) ? Math.max(0, Math.min(100, score)) : null;
|
|
18
|
-
}
|
|
19
|
-
function redirectLabel(value) {
|
|
20
|
-
const text = boundedText(value, 320);
|
|
21
|
-
try {
|
|
22
|
-
const parsed = new URL(text);
|
|
23
|
-
const path = parsed.pathname === '/' ? '' : parsed.pathname;
|
|
24
|
-
return `${parsed.hostname}${path}`.slice(0, 72);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return text.slice(0, 72);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
export function projectRedirectPath(rawRedirects) {
|
|
31
|
-
const redirects = (Array.isArray(rawRedirects) ? rawRedirects : [])
|
|
32
|
-
.slice(0, MAX_REDIRECT_NODES - 1)
|
|
33
|
-
.map((redirect) => ({
|
|
34
|
-
status: boundedText(redirect?.status, 12) || '—',
|
|
35
|
-
from: redirectLabel(redirect?.from),
|
|
36
|
-
to: redirectLabel(redirect?.to),
|
|
37
|
-
queryOmitted: Boolean(redirect?.queryOmitted),
|
|
38
|
-
}))
|
|
39
|
-
.filter((redirect) => redirect.from && redirect.to);
|
|
40
|
-
const rawCount = Array.isArray(rawRedirects) ? rawRedirects.length : 0;
|
|
41
|
-
if (!redirects.length) {
|
|
42
|
-
return { width: 900, height: 150, nodes: [], edges: [], truncated: rawCount > 0 };
|
|
43
|
-
}
|
|
44
|
-
const firstRedirect = redirects[0];
|
|
45
|
-
if (!firstRedirect) {
|
|
46
|
-
return { width: 900, height: 150, nodes: [], edges: [], truncated: rawCount > 0 };
|
|
47
|
-
}
|
|
48
|
-
const nodes = [
|
|
49
|
-
{ id: 'redirect-start', label: firstRedirect.from, status: 'start', queryOmitted: false },
|
|
50
|
-
...redirects.map((redirect, index) => ({
|
|
51
|
-
id: `redirect-${index + 1}`,
|
|
52
|
-
label: redirect.to,
|
|
53
|
-
status: redirect.status,
|
|
54
|
-
queryOmitted: redirect.queryOmitted,
|
|
55
|
-
})),
|
|
56
|
-
];
|
|
57
|
-
const x = scalePoint().domain(nodes.map((node) => node.id)).range([62, 838]).padding(0.35);
|
|
58
|
-
const projected = nodes.map((node) => ({ ...node, x: x(node.id) ?? 450, y: 70 }));
|
|
59
|
-
const edges = redirects.flatMap((redirect, index) => {
|
|
60
|
-
const from = projected[index];
|
|
61
|
-
const to = projected[index + 1];
|
|
62
|
-
return from && to ? [{
|
|
63
|
-
id: `redirect-edge-${index}`,
|
|
64
|
-
fromX: from.x,
|
|
65
|
-
toX: to.x,
|
|
66
|
-
y: 70,
|
|
67
|
-
status: redirect.status,
|
|
68
|
-
}] : [];
|
|
69
|
-
});
|
|
70
|
-
return {
|
|
71
|
-
width: 900,
|
|
72
|
-
height: 150,
|
|
73
|
-
nodes: projected,
|
|
74
|
-
edges,
|
|
75
|
-
truncated: rawCount > redirects.length,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
function deterministicSample(items, limit) {
|
|
79
|
-
const boundedLimit = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 0;
|
|
80
|
-
if (boundedLimit === 0)
|
|
81
|
-
return [];
|
|
82
|
-
if (items.length <= boundedLimit)
|
|
83
|
-
return items;
|
|
84
|
-
if (boundedLimit === 1)
|
|
85
|
-
return items.slice(0, 1);
|
|
86
|
-
return Array.from({ length: boundedLimit }, (_, index) => {
|
|
87
|
-
const sourceIndex = Math.round(index * (items.length - 1) / (boundedLimit - 1));
|
|
88
|
-
return items[sourceIndex];
|
|
89
|
-
}).filter((item) => item !== undefined);
|
|
90
|
-
}
|
|
91
|
-
export function projectTriagePoints(rawPoints) {
|
|
92
|
-
const eligible = (Array.isArray(rawPoints) ? rawPoints : [])
|
|
93
|
-
.map((point) => ({
|
|
94
|
-
domain: boundedText(point?.domain, 253).toLowerCase(),
|
|
95
|
-
risk: boundedScore(point?.risk),
|
|
96
|
-
opportunity: boundedScore(point?.opportunity),
|
|
97
|
-
availability: boundedText(point?.availability, 30).toLowerCase() || 'unknown',
|
|
98
|
-
trusted: Boolean(point?.trusted),
|
|
99
|
-
}))
|
|
100
|
-
.filter((point) => point.domain && point.risk !== null && point.opportunity !== null)
|
|
101
|
-
.sort((a, b) => a.domain.localeCompare(b.domain));
|
|
102
|
-
const sampled = deterministicSample(eligible, MAX_TRIAGE_PLOT_POINTS);
|
|
103
|
-
const quadrants = eligible.reduce((counts, point) => {
|
|
104
|
-
if (point.risk >= 50) {
|
|
105
|
-
if (point.opportunity >= 50)
|
|
106
|
-
counts.priorityReview += 1;
|
|
107
|
-
else
|
|
108
|
-
counts.riskLedReview += 1;
|
|
109
|
-
}
|
|
110
|
-
else if (point.opportunity >= 50) {
|
|
111
|
-
counts.availableReview += 1;
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
counts.lowerScores += 1;
|
|
115
|
-
}
|
|
116
|
-
return counts;
|
|
117
|
-
}, {
|
|
118
|
-
availableReview: 0,
|
|
119
|
-
priorityReview: 0,
|
|
120
|
-
lowerScores: 0,
|
|
121
|
-
riskLedReview: 0,
|
|
122
|
-
});
|
|
123
|
-
const x = scaleLinear().domain([0, 100]).range([58, 842]).clamp(true);
|
|
124
|
-
const y = scaleLinear().domain([0, 100]).range([308, 28]).clamp(true);
|
|
125
|
-
const points = sampled.map((point) => ({
|
|
126
|
-
...point,
|
|
127
|
-
risk: point.risk,
|
|
128
|
-
opportunity: point.opportunity,
|
|
129
|
-
x: x(point.risk),
|
|
130
|
-
y: y(point.opportunity),
|
|
131
|
-
tone: point.trusted
|
|
132
|
-
? 'trusted'
|
|
133
|
-
: point.availability === 'available'
|
|
134
|
-
? 'available'
|
|
135
|
-
: point.availability === 'error'
|
|
136
|
-
? 'error'
|
|
137
|
-
: 'registered',
|
|
138
|
-
}));
|
|
139
|
-
return {
|
|
140
|
-
width: 900,
|
|
141
|
-
height: 360,
|
|
142
|
-
points,
|
|
143
|
-
eligibleCount: eligible.length,
|
|
144
|
-
omittedCount: Math.max(0, (Array.isArray(rawPoints) ? rawPoints.length : 0) - eligible.length),
|
|
145
|
-
sampled: eligible.length > sampled.length,
|
|
146
|
-
quadrants,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
function utcDayKey(milliseconds) {
|
|
150
|
-
return new Date(milliseconds).toISOString().slice(0, 10);
|
|
151
|
-
}
|
|
152
|
-
export function projectWatchlistActivity(rawEvents) {
|
|
153
|
-
const valid = (Array.isArray(rawEvents) ? rawEvents : [])
|
|
154
|
-
.map((event) => {
|
|
155
|
-
const date = validDate(event?.checkedAt);
|
|
156
|
-
if (!date)
|
|
157
|
-
return null;
|
|
158
|
-
const changeCount = Math.max(0, Math.min(10_000, Math.trunc(Number(event.changeCount) || 0)));
|
|
159
|
-
const resultCount = Math.max(0, Math.min(10_000, Math.trunc(Number(event.resultCount) || 0)));
|
|
160
|
-
const conclusiveCount = Math.max(0, Math.min(resultCount, Math.trunc(Number(event.conclusiveCount) || 0)));
|
|
161
|
-
return { ...date, changeCount, resultCount, conclusiveCount };
|
|
162
|
-
})
|
|
163
|
-
.filter((event) => Boolean(event));
|
|
164
|
-
if (!valid.length) {
|
|
165
|
-
return { width: 620, height: 190, days: [], maxChanges: 0, totalChecks: 0, totalChanges: 0 };
|
|
166
|
-
}
|
|
167
|
-
const latest = Math.max(...valid.map((event) => event.milliseconds));
|
|
168
|
-
const latestDay = Date.UTC(new Date(latest).getUTCFullYear(), new Date(latest).getUTCMonth(), new Date(latest).getUTCDate());
|
|
169
|
-
const start = latestDay - (WATCHLIST_ACTIVITY_DAYS - 1) * 86_400_000;
|
|
170
|
-
const windowEnd = latestDay + 86_400_000;
|
|
171
|
-
const windowEvents = valid.filter((event) => event.milliseconds >= start && event.milliseconds < windowEnd);
|
|
172
|
-
const byDay = new Map();
|
|
173
|
-
for (const event of windowEvents) {
|
|
174
|
-
const key = utcDayKey(event.milliseconds);
|
|
175
|
-
const current = byDay.get(key) ?? { checks: 0, changes: 0, results: 0, conclusive: 0 };
|
|
176
|
-
current.checks += 1;
|
|
177
|
-
current.changes += event.changeCount;
|
|
178
|
-
current.results += event.resultCount;
|
|
179
|
-
current.conclusive += event.conclusiveCount;
|
|
180
|
-
byDay.set(key, current);
|
|
181
|
-
}
|
|
182
|
-
const weekScale = scaleBand().domain([0, 1, 2, 3]).range([72, 580]).padding(0.12);
|
|
183
|
-
const dayScale = scaleBand().domain([0, 1, 2, 3, 4, 5, 6]).range([26, 165]).padding(0.12);
|
|
184
|
-
const dayWidth = weekScale.bandwidth();
|
|
185
|
-
const dayHeight = dayScale.bandwidth();
|
|
186
|
-
const days = Array.from({ length: WATCHLIST_ACTIVITY_DAYS }, (_, index) => {
|
|
187
|
-
const milliseconds = start + index * 86_400_000;
|
|
188
|
-
const key = utcDayKey(milliseconds);
|
|
189
|
-
const activity = byDay.get(key) ?? { checks: 0, changes: 0, results: 0, conclusive: 0 };
|
|
190
|
-
return {
|
|
191
|
-
date: key,
|
|
192
|
-
label: new Date(milliseconds).toLocaleDateString('en-AU', { day: 'numeric', month: 'short', timeZone: 'UTC' }),
|
|
193
|
-
week: Math.floor(index / 7),
|
|
194
|
-
weekday: index % 7,
|
|
195
|
-
x: weekScale(Math.floor(index / 7)) ?? 0,
|
|
196
|
-
y: dayScale(index % 7) ?? 0,
|
|
197
|
-
width: dayWidth,
|
|
198
|
-
height: dayHeight,
|
|
199
|
-
...activity,
|
|
200
|
-
};
|
|
201
|
-
});
|
|
202
|
-
return {
|
|
203
|
-
width: 620,
|
|
204
|
-
height: 190,
|
|
205
|
-
days,
|
|
206
|
-
maxChanges: Math.max(0, ...days.map((day) => day.changes)),
|
|
207
|
-
totalChecks: windowEvents.length,
|
|
208
|
-
totalChanges: windowEvents.reduce((sum, event) => sum + event.changeCount, 0),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
export function projectScoreFactors(rawFactors) {
|
|
212
|
-
const candidates = (Array.isArray(rawFactors) ? rawFactors : [])
|
|
213
|
-
.map((factor, index) => ({
|
|
214
|
-
id: `${boundedId(factor?.label) || 'factor'}-${index}`,
|
|
215
|
-
label: boundedText(factor?.label, 56) || `Factor ${index + 1}`,
|
|
216
|
-
delta: boundedNumber(factor?.delta, -100, 100),
|
|
217
|
-
}));
|
|
218
|
-
const factors = candidates.slice(0, MAX_SCORE_FACTORS);
|
|
219
|
-
const maximum = Math.max(1, ...factors.map((factor) => Math.abs(factor.delta)));
|
|
220
|
-
const x = scaleLinear().domain([-maximum, maximum]).range([220, 860]).clamp(true);
|
|
221
|
-
const zeroX = x(0);
|
|
222
|
-
const rowHeight = 28;
|
|
223
|
-
return {
|
|
224
|
-
width: 900,
|
|
225
|
-
height: Math.max(74, 42 + factors.length * rowHeight),
|
|
226
|
-
zeroX,
|
|
227
|
-
maximum,
|
|
228
|
-
factors: factors.map((factor, index) => {
|
|
229
|
-
const valueX = x(factor.delta);
|
|
230
|
-
return {
|
|
231
|
-
...factor,
|
|
232
|
-
x: Math.min(zeroX, valueX),
|
|
233
|
-
width: factor.delta === 0 ? 0 : Math.max(2, Math.abs(valueX - zeroX)),
|
|
234
|
-
y: 24 + index * rowHeight,
|
|
235
|
-
};
|
|
236
|
-
}),
|
|
237
|
-
truncated: candidates.length > factors.length,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
export function projectCoverageBars(rawGroups) {
|
|
241
|
-
const candidates = (Array.isArray(rawGroups) ? rawGroups : [])
|
|
242
|
-
.map((group, index) => {
|
|
243
|
-
const values = {
|
|
244
|
-
protected: Math.trunc(boundedNumber(group?.protected, 0, 100_000)),
|
|
245
|
-
registered: Math.trunc(boundedNumber(group?.registered, 0, 100_000)),
|
|
246
|
-
available: Math.trunc(boundedNumber(group?.available, 0, 100_000)),
|
|
247
|
-
unknown: Math.trunc(boundedNumber(group?.unknown, 0, 100_000)),
|
|
248
|
-
};
|
|
249
|
-
return {
|
|
250
|
-
id: boundedId(group?.id) || `group-${index}`,
|
|
251
|
-
label: boundedText(group?.label, 56) || `Group ${index + 1}`,
|
|
252
|
-
...values,
|
|
253
|
-
total: Object.values(values).reduce((sum, value) => sum + value, 0),
|
|
254
|
-
};
|
|
255
|
-
})
|
|
256
|
-
.filter((group) => group.total > 0)
|
|
257
|
-
.sort((a, b) => b.total - a.total || a.label.localeCompare(b.label));
|
|
258
|
-
const groups = candidates.slice(0, MAX_COVERAGE_BAR_GROUPS);
|
|
259
|
-
const maximum = Math.max(1, ...groups.map((group) => group.total));
|
|
260
|
-
const x = scaleLinear().domain([0, maximum]).range([210, 860]).clamp(true);
|
|
261
|
-
const rowHeight = 31;
|
|
262
|
-
return {
|
|
263
|
-
width: 900,
|
|
264
|
-
height: Math.max(76, 40 + groups.length * rowHeight),
|
|
265
|
-
groups: groups.map((group, index) => {
|
|
266
|
-
let offset = 0;
|
|
267
|
-
const segments = ['protected', 'registered', 'available', 'unknown'].map((state) => {
|
|
268
|
-
const value = group[state];
|
|
269
|
-
const start = offset;
|
|
270
|
-
offset += value;
|
|
271
|
-
return {
|
|
272
|
-
state,
|
|
273
|
-
value,
|
|
274
|
-
x: x(start),
|
|
275
|
-
width: Math.max(value ? 1 : 0, x(offset) - x(start)),
|
|
276
|
-
};
|
|
277
|
-
});
|
|
278
|
-
return { ...group, y: 20 + index * rowHeight, segments };
|
|
279
|
-
}),
|
|
280
|
-
truncated: candidates.length > groups.length,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
export function projectMonitorTimeline(rawEvents) {
|
|
284
|
-
const candidates = (Array.isArray(rawEvents) ? rawEvents : [])
|
|
285
|
-
.map((event, index) => {
|
|
286
|
-
const date = validDate(event?.checkedAt);
|
|
287
|
-
const id = boundedId(event?.id) || `event-${index}`;
|
|
288
|
-
if (!date)
|
|
289
|
-
return null;
|
|
290
|
-
return {
|
|
291
|
-
id,
|
|
292
|
-
checkedAt: date.text,
|
|
293
|
-
milliseconds: date.milliseconds,
|
|
294
|
-
mode: boundedText(event?.mode, 20).toLowerCase() || 'saved',
|
|
295
|
-
groups: (Array.isArray(event.groups) ? event.groups : [])
|
|
296
|
-
.map((group) => ({
|
|
297
|
-
key: boundedId(group?.key),
|
|
298
|
-
label: boundedText(group?.label, 40),
|
|
299
|
-
changeCount: Math.trunc(boundedNumber(group?.changeCount, 0, 10_000)),
|
|
300
|
-
}))
|
|
301
|
-
.filter((group) => group.key && group.label),
|
|
302
|
-
};
|
|
303
|
-
})
|
|
304
|
-
.filter((event) => Boolean(event))
|
|
305
|
-
.sort((a, b) => a.milliseconds - b.milliseconds || a.id.localeCompare(b.id));
|
|
306
|
-
const events = candidates.slice(-MAX_MONITOR_TIMELINE_EVENTS);
|
|
307
|
-
const laneMap = new Map();
|
|
308
|
-
for (const event of events) {
|
|
309
|
-
for (const group of event.groups) {
|
|
310
|
-
if (!laneMap.has(group.key) && laneMap.size < MAX_MONITOR_TIMELINE_LANES) {
|
|
311
|
-
laneMap.set(group.key, group.label);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
const lanes = [...laneMap].map(([key, label]) => ({ key, label }));
|
|
316
|
-
const x = scaleBand().domain(events.map((event) => event.id)).range([170, 865]).padding(0.18);
|
|
317
|
-
const y = scaleBand().domain(lanes.map((lane) => lane.key)).range([42, 42 + lanes.length * 40]).padding(0.18);
|
|
318
|
-
const maxChanges = Math.max(1, ...events.flatMap((event) => event.groups.map((group) => group.changeCount)));
|
|
319
|
-
return {
|
|
320
|
-
width: 900,
|
|
321
|
-
height: Math.max(110, 64 + lanes.length * 40),
|
|
322
|
-
maxChanges,
|
|
323
|
-
events: events.map((event) => ({
|
|
324
|
-
...event,
|
|
325
|
-
x: x(event.id) ?? 170,
|
|
326
|
-
width: x.bandwidth(),
|
|
327
|
-
cells: lanes.map((lane) => ({
|
|
328
|
-
key: lane.key,
|
|
329
|
-
label: lane.label,
|
|
330
|
-
count: event.groups.find((group) => group.key === lane.key)?.changeCount ?? 0,
|
|
331
|
-
y: y(lane.key) ?? 42,
|
|
332
|
-
height: y.bandwidth(),
|
|
333
|
-
})),
|
|
334
|
-
})),
|
|
335
|
-
lanes: lanes.map((lane) => ({ ...lane, y: y(lane.key) ?? 42, height: y.bandwidth() })),
|
|
336
|
-
truncated: candidates.length > events.length
|
|
337
|
-
|| events.some((event) => event.groups.some((group) => !laneMap.has(group.key))),
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
export function projectCertificateValidity(input) {
|
|
341
|
-
const validFrom = validDate(input.validFrom);
|
|
342
|
-
const validTo = validDate(input.validTo);
|
|
343
|
-
const observed = validDate(input.observedAt);
|
|
344
|
-
if (!validFrom || !validTo || validTo.milliseconds <= validFrom.milliseconds) {
|
|
345
|
-
return { available: false, width: 900, height: 110 };
|
|
346
|
-
}
|
|
347
|
-
const observedMilliseconds = observed?.milliseconds ?? validFrom.milliseconds;
|
|
348
|
-
const span = validTo.milliseconds - validFrom.milliseconds;
|
|
349
|
-
const padding = Math.max(86_400_000, span * 0.08);
|
|
350
|
-
const x = scaleLinear()
|
|
351
|
-
.domain([validFrom.milliseconds - padding, validTo.milliseconds + padding])
|
|
352
|
-
.range([72, 828])
|
|
353
|
-
.clamp(true);
|
|
354
|
-
return {
|
|
355
|
-
available: true,
|
|
356
|
-
width: 900,
|
|
357
|
-
height: 110,
|
|
358
|
-
validFrom: validFrom.text,
|
|
359
|
-
validTo: validTo.text,
|
|
360
|
-
observedAt: observed?.text ?? '',
|
|
361
|
-
hasObservation: Boolean(observed),
|
|
362
|
-
fromX: x(validFrom.milliseconds),
|
|
363
|
-
toX: x(validTo.milliseconds),
|
|
364
|
-
observedX: x(observedMilliseconds),
|
|
365
|
-
observedWithinValidity: observedMilliseconds >= validFrom.milliseconds
|
|
366
|
-
&& observedMilliseconds <= validTo.milliseconds,
|
|
367
|
-
elapsedPercent: Math.max(0, Math.min(100, ((observedMilliseconds - validFrom.milliseconds) / span) * 100)),
|
|
368
|
-
};
|
|
369
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { scaleLinear, scalePoint } from 'd3-scale';
|
|
2
|
-
import { boundedVisualizationId as boundedId, boundedVisualizationNumber as boundedNumber, boundedVisualizationText as boundedText, validVisualizationDate as validDate, } from "./visualization-bounds.js";
|
|
3
|
-
export const MAX_LIFECYCLE_EVENTS = 8;
|
|
4
|
-
export const MAX_COLLECTION_TIMING_SOURCES = 16;
|
|
5
|
-
export const MAX_TREND_POINTS = 24;
|
|
6
|
-
export function projectLifecycleEvents(rawEvents) {
|
|
7
|
-
const seen = new Set();
|
|
8
|
-
const candidates = (Array.isArray(rawEvents) ? rawEvents : [])
|
|
9
|
-
.map((event) => {
|
|
10
|
-
const id = boundedId(event?.id);
|
|
11
|
-
const label = boundedText(event?.label, 40);
|
|
12
|
-
const date = validDate(event?.date);
|
|
13
|
-
if (!id || !label || !date)
|
|
14
|
-
return null;
|
|
15
|
-
const kind = event.kind === 'certificate' || event.kind === 'observation'
|
|
16
|
-
? event.kind
|
|
17
|
-
: 'registry';
|
|
18
|
-
return {
|
|
19
|
-
id,
|
|
20
|
-
label,
|
|
21
|
-
date: date.text,
|
|
22
|
-
milliseconds: date.milliseconds,
|
|
23
|
-
detail: boundedText(event.detail, 100),
|
|
24
|
-
kind,
|
|
25
|
-
};
|
|
26
|
-
})
|
|
27
|
-
.filter((event) => Boolean(event))
|
|
28
|
-
.sort((a, b) => a.milliseconds - b.milliseconds || a.id.localeCompare(b.id))
|
|
29
|
-
.filter((event) => {
|
|
30
|
-
if (seen.has(event.id))
|
|
31
|
-
return false;
|
|
32
|
-
seen.add(event.id);
|
|
33
|
-
return true;
|
|
34
|
-
});
|
|
35
|
-
const accepted = candidates.slice(0, MAX_LIFECYCLE_EVENTS);
|
|
36
|
-
const x = scalePoint()
|
|
37
|
-
.domain(accepted.map((event) => event.id))
|
|
38
|
-
.range([70, 830])
|
|
39
|
-
.padding(accepted.length > 1 ? 0.35 : 0.5);
|
|
40
|
-
const laneY = [48, 158, 27];
|
|
41
|
-
const events = accepted.map((event, index) => ({
|
|
42
|
-
...event,
|
|
43
|
-
x: x(event.id) ?? 450,
|
|
44
|
-
labelY: laneY[index % laneY.length] ?? 102,
|
|
45
|
-
anchor: index === 0 ? 'start' : index === accepted.length - 1 ? 'end' : 'middle',
|
|
46
|
-
}));
|
|
47
|
-
return {
|
|
48
|
-
width: 900,
|
|
49
|
-
height: 205,
|
|
50
|
-
axisY: 102,
|
|
51
|
-
events,
|
|
52
|
-
truncated: candidates.length > accepted.length,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
export function projectCollectionTiming(rawSources, rawTotalMs) {
|
|
56
|
-
const candidates = (Array.isArray(rawSources) ? rawSources : [])
|
|
57
|
-
.map((source, index) => {
|
|
58
|
-
const id = boundedId(source?.source) || `source-${index}`;
|
|
59
|
-
const durationMs = boundedNumber(source?.durationMs, 0, 300_000);
|
|
60
|
-
const completedAfterMs = boundedNumber(source?.completedAfterMs, 0, 300_000);
|
|
61
|
-
return {
|
|
62
|
-
id,
|
|
63
|
-
label: boundedText(source?.source, 48) || `Source ${index + 1}`,
|
|
64
|
-
outcome: boundedText(source?.outcome, 24).toLowerCase() || 'unknown',
|
|
65
|
-
durationMs,
|
|
66
|
-
completedAfterMs,
|
|
67
|
-
startedAfterMs: Math.max(0, completedAfterMs - durationMs),
|
|
68
|
-
};
|
|
69
|
-
})
|
|
70
|
-
.sort((a, b) => a.completedAfterMs - b.completedAfterMs || a.id.localeCompare(b.id));
|
|
71
|
-
const sources = candidates.slice(0, MAX_COLLECTION_TIMING_SOURCES);
|
|
72
|
-
const longestSource = [...sources]
|
|
73
|
-
.sort((a, b) => b.durationMs - a.durationMs
|
|
74
|
-
|| b.completedAfterMs - a.completedAfterMs
|
|
75
|
-
|| a.id.localeCompare(b.id))[0] ?? null;
|
|
76
|
-
const lastSettledSource = sources.at(-1) ?? null;
|
|
77
|
-
const requestErrorCount = sources.filter((source) => source.outcome === 'rejected').length;
|
|
78
|
-
const totalMs = Math.max(1, boundedNumber(rawTotalMs, 0, 300_000), ...sources.map((source) => source.completedAfterMs));
|
|
79
|
-
const x = scaleLinear().domain([0, totalMs]).range([190, 860]).clamp(true);
|
|
80
|
-
const rowHeight = 30;
|
|
81
|
-
return {
|
|
82
|
-
width: 900,
|
|
83
|
-
height: Math.max(92, 50 + sources.length * rowHeight),
|
|
84
|
-
totalMs,
|
|
85
|
-
longestSource,
|
|
86
|
-
lastSettledSource,
|
|
87
|
-
requestErrorCount,
|
|
88
|
-
ticks: x.ticks(5).map((value) => ({ value, x: x(value) })),
|
|
89
|
-
sources: sources.map((source, index) => ({
|
|
90
|
-
...source,
|
|
91
|
-
x: x(source.startedAfterMs),
|
|
92
|
-
width: Math.max(3, x(source.completedAfterMs) - x(source.startedAfterMs)),
|
|
93
|
-
y: 30 + index * rowHeight,
|
|
94
|
-
})),
|
|
95
|
-
truncated: candidates.length > sources.length,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
export function projectTrendPoints(rawPoints) {
|
|
99
|
-
const seen = new Set();
|
|
100
|
-
const candidates = (Array.isArray(rawPoints) ? rawPoints : [])
|
|
101
|
-
.map((point, index) => {
|
|
102
|
-
const date = validDate(point?.date);
|
|
103
|
-
const id = boundedId(point?.id) || `point-${index}`;
|
|
104
|
-
if (!date || seen.has(id))
|
|
105
|
-
return null;
|
|
106
|
-
seen.add(id);
|
|
107
|
-
return {
|
|
108
|
-
id,
|
|
109
|
-
date: date.text,
|
|
110
|
-
milliseconds: date.milliseconds,
|
|
111
|
-
total: Math.trunc(boundedNumber(point?.total, 0, 100_000)),
|
|
112
|
-
added: Math.trunc(boundedNumber(point?.added, 0, 100_000)),
|
|
113
|
-
partial: Boolean(point?.partial),
|
|
114
|
-
};
|
|
115
|
-
})
|
|
116
|
-
.filter((point) => Boolean(point))
|
|
117
|
-
.sort((a, b) => a.milliseconds - b.milliseconds || a.id.localeCompare(b.id));
|
|
118
|
-
const points = candidates.slice(-MAX_TREND_POINTS);
|
|
119
|
-
const x = scalePoint().domain(points.map((point) => point.id)).range([64, 850]).padding(0.45);
|
|
120
|
-
const maximum = Math.max(1, ...points.map((point) => point.total));
|
|
121
|
-
const y = scaleLinear().domain([0, maximum]).range([190, 24]).nice().clamp(true);
|
|
122
|
-
const first = points[0] ?? null;
|
|
123
|
-
const latest = points.at(-1) ?? null;
|
|
124
|
-
return {
|
|
125
|
-
width: 900,
|
|
126
|
-
height: 225,
|
|
127
|
-
maximum,
|
|
128
|
-
ticks: y.ticks(4).map((value) => ({ value, y: y(value) })),
|
|
129
|
-
points: points.map((point) => ({
|
|
130
|
-
...point,
|
|
131
|
-
x: x(point.id) ?? 450,
|
|
132
|
-
y: y(point.total),
|
|
133
|
-
addedY: y(point.added),
|
|
134
|
-
})),
|
|
135
|
-
summary: {
|
|
136
|
-
firstTotal: first?.total ?? 0,
|
|
137
|
-
latestTotal: latest?.total ?? 0,
|
|
138
|
-
peakTotal: points.reduce((peak, point) => Math.max(peak, point.total), 0),
|
|
139
|
-
newlyObserved: points.reduce((total, point) => total + point.added, 0),
|
|
140
|
-
partialChecks: points.filter((point) => point.partial).length,
|
|
141
|
-
},
|
|
142
|
-
truncated: candidates.length > points.length,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
// Environment-neutral browser/server JSON response boundary. It enforces a
|
|
2
|
-
// declared and streamed byte ceiling before parsing and can apply one deadline
|
|
3
|
-
// across both the fetch and body-read phases.
|
|
4
|
-
export const SMALL_JSON_RESPONSE_BYTES = 64 * 1024;
|
|
5
|
-
export const STANDARD_JSON_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
6
|
-
export const LARGE_JSON_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
7
|
-
export const DEFAULT_JSON_RESPONSE_TIMEOUT_MS = 40_000;
|
|
8
|
-
const MAX_JSON_RESPONSE_TIMEOUT_MS = 120_000;
|
|
9
|
-
export class BoundedJsonResponseError extends Error {
|
|
10
|
-
code;
|
|
11
|
-
constructor(code, message) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.name = 'BoundedJsonResponseError';
|
|
14
|
-
this.code = code;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
async function raceWithAbort(promise, signal, onLateResolution) {
|
|
18
|
-
if (signal.aborted)
|
|
19
|
-
throw signal.reason ?? new Error('Request aborted.');
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
let aborted = false;
|
|
22
|
-
const onAbort = () => {
|
|
23
|
-
aborted = true;
|
|
24
|
-
reject(signal.reason ?? new Error('Request aborted.'));
|
|
25
|
-
};
|
|
26
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
27
|
-
void promise.then((value) => {
|
|
28
|
-
signal.removeEventListener('abort', onAbort);
|
|
29
|
-
if (aborted) {
|
|
30
|
-
void Promise.resolve(onLateResolution?.(value)).catch(() => { });
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
resolve(value);
|
|
34
|
-
}, (cause) => {
|
|
35
|
-
signal.removeEventListener('abort', onAbort);
|
|
36
|
-
reject(cause);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
function boundedPositiveInteger(value, fallback, maximum) {
|
|
41
|
-
return typeof value === 'number' && Number.isFinite(value)
|
|
42
|
-
? Math.max(1, Math.min(maximum, Math.round(value)))
|
|
43
|
-
: fallback;
|
|
44
|
-
}
|
|
45
|
-
export async function readJsonResponseCapped(response, maximumBytes = STANDARD_JSON_RESPONSE_BYTES, signal) {
|
|
46
|
-
const maxBytes = boundedPositiveInteger(maximumBytes, STANDARD_JSON_RESPONSE_BYTES, LARGE_JSON_RESPONSE_BYTES);
|
|
47
|
-
const declared = response.headers.get('content-length');
|
|
48
|
-
if (declared && /^\d+$/u.test(declared)) {
|
|
49
|
-
const bytes = Number(declared);
|
|
50
|
-
if (!Number.isSafeInteger(bytes) || bytes > maxBytes) {
|
|
51
|
-
await response.body?.cancel().catch(() => { });
|
|
52
|
-
throw new BoundedJsonResponseError('response_too_large', `JSON response exceeded ${maxBytes} bytes.`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (!response.body)
|
|
56
|
-
return null;
|
|
57
|
-
const reader = response.body.getReader();
|
|
58
|
-
const cancelOnAbort = () => { void reader.cancel(signal?.reason).catch(() => { }); };
|
|
59
|
-
signal?.addEventListener('abort', cancelOnAbort, { once: true });
|
|
60
|
-
const chunks = [];
|
|
61
|
-
let total = 0;
|
|
62
|
-
try {
|
|
63
|
-
while (true) {
|
|
64
|
-
const read = reader.read();
|
|
65
|
-
const { done, value } = signal ? await raceWithAbort(read, signal) : await read;
|
|
66
|
-
if (done)
|
|
67
|
-
break;
|
|
68
|
-
if (!value)
|
|
69
|
-
continue;
|
|
70
|
-
total += value.byteLength;
|
|
71
|
-
if (total > maxBytes) {
|
|
72
|
-
await reader.cancel().catch(() => { });
|
|
73
|
-
throw new BoundedJsonResponseError('response_too_large', `JSON response exceeded ${maxBytes} bytes.`);
|
|
74
|
-
}
|
|
75
|
-
chunks.push(value);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
finally {
|
|
79
|
-
signal?.removeEventListener('abort', cancelOnAbort);
|
|
80
|
-
reader.releaseLock();
|
|
81
|
-
}
|
|
82
|
-
const bytes = new Uint8Array(total);
|
|
83
|
-
let offset = 0;
|
|
84
|
-
for (const chunk of chunks) {
|
|
85
|
-
bytes.set(chunk, offset);
|
|
86
|
-
offset += chunk.byteLength;
|
|
87
|
-
}
|
|
88
|
-
try {
|
|
89
|
-
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
throw new BoundedJsonResponseError('invalid_json', 'Response body was not valid UTF-8 JSON.');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
export async function requestJsonCapped(input, init = {}, options = {}) {
|
|
96
|
-
const timeoutMs = boundedPositiveInteger(options.timeoutMs, DEFAULT_JSON_RESPONSE_TIMEOUT_MS, MAX_JSON_RESPONSE_TIMEOUT_MS);
|
|
97
|
-
const controller = new AbortController();
|
|
98
|
-
const upstreamSignal = init.signal;
|
|
99
|
-
const forwardAbort = () => controller.abort(upstreamSignal?.reason);
|
|
100
|
-
if (upstreamSignal?.aborted)
|
|
101
|
-
forwardAbort();
|
|
102
|
-
else
|
|
103
|
-
upstreamSignal?.addEventListener('abort', forwardAbort, { once: true });
|
|
104
|
-
const timeoutReason = Object.freeze({ type: 'bounded-json-timeout' });
|
|
105
|
-
const timeout = setTimeout(() => controller.abort(timeoutReason), timeoutMs);
|
|
106
|
-
try {
|
|
107
|
-
if (controller.signal.aborted) {
|
|
108
|
-
throw new BoundedJsonResponseError('aborted', 'Request was cancelled.');
|
|
109
|
-
}
|
|
110
|
-
const fetchRequest = (options.fetchImpl ?? fetch)(input, {
|
|
111
|
-
...init,
|
|
112
|
-
signal: controller.signal,
|
|
113
|
-
});
|
|
114
|
-
const response = await raceWithAbort(fetchRequest, controller.signal, async (lateResponse) => lateResponse.body?.cancel().catch(() => { }));
|
|
115
|
-
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
|
|
116
|
-
const plausibleJson = !contentType
|
|
117
|
-
|| contentType.includes('application/json')
|
|
118
|
-
|| contentType.includes('+json');
|
|
119
|
-
if (options.allowNonJsonErrorResponse !== false && !response.ok && !plausibleJson) {
|
|
120
|
-
await response.body?.cancel().catch(() => { });
|
|
121
|
-
return { response, body: null };
|
|
122
|
-
}
|
|
123
|
-
let body;
|
|
124
|
-
try {
|
|
125
|
-
body = await readJsonResponseCapped(response, options.maximumBytes, controller.signal);
|
|
126
|
-
}
|
|
127
|
-
catch (cause) {
|
|
128
|
-
// Reverse proxies and hosting adapters can replace an upstream JSON
|
|
129
|
-
// error with a bounded HTML or plain-text response. Preserve the HTTP
|
|
130
|
-
// status so callers can apply retry and generic error handling, while
|
|
131
|
-
// continuing to require valid JSON for successful responses.
|
|
132
|
-
if (options.allowNonJsonErrorResponse !== false
|
|
133
|
-
&& !response.ok
|
|
134
|
-
&& cause instanceof BoundedJsonResponseError
|
|
135
|
-
&& cause.code === 'invalid_json') {
|
|
136
|
-
body = null;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
throw cause;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return { response, body };
|
|
143
|
-
}
|
|
144
|
-
catch (cause) {
|
|
145
|
-
if (controller.signal.aborted) {
|
|
146
|
-
const timedOut = controller.signal.reason === timeoutReason;
|
|
147
|
-
throw new BoundedJsonResponseError(timedOut ? 'timeout' : 'aborted', timedOut ? `Request timed out after ${timeoutMs} ms.` : 'Request was cancelled.');
|
|
148
|
-
}
|
|
149
|
-
if (cause instanceof BoundedJsonResponseError)
|
|
150
|
-
throw cause;
|
|
151
|
-
throw cause;
|
|
152
|
-
}
|
|
153
|
-
finally {
|
|
154
|
-
clearTimeout(timeout);
|
|
155
|
-
upstreamSignal?.removeEventListener('abort', forwardAbort);
|
|
156
|
-
}
|
|
157
|
-
}
|