comber 0.1.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/LICENSE +18 -0
- package/README.md +732 -0
- package/action.yml +279 -0
- package/comber.config.example.json +64 -0
- package/dist/api/classify.js +99 -0
- package/dist/api/classify.js.map +1 -0
- package/dist/api/driver.js +235 -0
- package/dist/api/driver.js.map +1 -0
- package/dist/api/graphql-driver.js +150 -0
- package/dist/api/graphql-driver.js.map +1 -0
- package/dist/api/graphql.js +115 -0
- package/dist/api/graphql.js.map +1 -0
- package/dist/api/har-driver.js +167 -0
- package/dist/api/har-driver.js.map +1 -0
- package/dist/api/har.js +100 -0
- package/dist/api/har.js.map +1 -0
- package/dist/api/load.js +83 -0
- package/dist/api/load.js.map +1 -0
- package/dist/api/openapi.js +159 -0
- package/dist/api/openapi.js.map +1 -0
- package/dist/api/request.js +141 -0
- package/dist/api/request.js.map +1 -0
- package/dist/api/schema.js +126 -0
- package/dist/api/schema.js.map +1 -0
- package/dist/cli.js +352 -0
- package/dist/cli.js.map +1 -0
- package/dist/comment.js +164 -0
- package/dist/comment.js.map +1 -0
- package/dist/config.js +664 -0
- package/dist/config.js.map +1 -0
- package/dist/connectors/builtin/discord.js +55 -0
- package/dist/connectors/builtin/github-checks.js +53 -0
- package/dist/connectors/builtin/junit.js +70 -0
- package/dist/connectors/builtin/linear.js +58 -0
- package/dist/connectors/builtin/slack.js +60 -0
- package/dist/connectors/builtin/webhook.js +39 -0
- package/dist/connectors/index.js +3 -0
- package/dist/connectors/loader.js +72 -0
- package/dist/connectors/registry.js +28 -0
- package/dist/connectors/types.js +23 -0
- package/dist/core/action-gate.js +48 -0
- package/dist/core/baseline.js +85 -0
- package/dist/core/baseline.js.map +1 -0
- package/dist/core/blind-spots.js +22 -0
- package/dist/core/blind-spots.js.map +1 -0
- package/dist/core/contract-extras.js +1 -0
- package/dist/core/contract-extras.js.map +1 -0
- package/dist/core/contract.js +1 -0
- package/dist/core/contract.js.map +1 -0
- package/dist/core/coverage.js +70 -0
- package/dist/core/coverage.js.map +1 -0
- package/dist/core/crawl.js +229 -0
- package/dist/core/crawl.js.map +1 -0
- package/dist/core/egress.js +128 -0
- package/dist/core/egress.js.map +1 -0
- package/dist/core/ledger.js +41 -0
- package/dist/core/ledger.js.map +1 -0
- package/dist/core/net.js +39 -0
- package/dist/core/pipeline.js +191 -0
- package/dist/core/pipeline.js.map +1 -0
- package/dist/core/plan.js +121 -0
- package/dist/core/plan.js.map +1 -0
- package/dist/core/redact.js +240 -0
- package/dist/core/redact.js.map +1 -0
- package/dist/core/replay.js +263 -0
- package/dist/core/replay.js.map +1 -0
- package/dist/core/report.js +253 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/safety-judge.js +116 -0
- package/dist/core/safety.js +92 -0
- package/dist/core/safety.js.map +1 -0
- package/dist/core/signal-normalize.js +190 -0
- package/dist/core/signal-normalize.js.map +1 -0
- package/dist/core/signals.js +5 -0
- package/dist/core/signals.js.map +1 -0
- package/dist/core/triage.js +104 -0
- package/dist/core/triage.js.map +1 -0
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +1 -0
- package/dist/desktop/desktop-profile.js +6 -0
- package/dist/desktop/desktop-profile.js.map +1 -0
- package/dist/desktop/driver.js +433 -0
- package/dist/desktop/driver.js.map +1 -0
- package/dist/desktop/evidence.js +206 -0
- package/dist/desktop/evidence.js.map +1 -0
- package/dist/desktop/macos/accessibility-runner.js +118 -0
- package/dist/desktop/macos/accessibility-runner.js.map +1 -0
- package/dist/desktop/macos/types.js +1 -0
- package/dist/desktop/macos/types.js.map +1 -0
- package/dist/desktop/preflight.js +107 -0
- package/dist/desktop/preflight.js.map +1 -0
- package/dist/desktop/proxy-proof.js +185 -0
- package/dist/desktop/proxy-proof.js.map +1 -0
- package/dist/desktop/view.js +348 -0
- package/dist/desktop/view.js.map +1 -0
- package/dist/desktop/windows/types.js +1 -0
- package/dist/desktop/windows/types.js.map +1 -0
- package/dist/desktop/windows/uia-runner.js +127 -0
- package/dist/desktop/windows/uia-runner.js.map +1 -0
- package/dist/native/apple.js +14 -0
- package/dist/native/apple.js.map +1 -0
- package/dist/native/device-profile.js +6 -0
- package/dist/native/device-profile.js.map +1 -0
- package/dist/native/driver.js +416 -0
- package/dist/native/driver.js.map +1 -0
- package/dist/native/evidence.js +164 -0
- package/dist/native/evidence.js.map +1 -0
- package/dist/native/load.js +9 -0
- package/dist/native/load.js.map +1 -0
- package/dist/native/maestro.js +153 -0
- package/dist/native/maestro.js.map +1 -0
- package/dist/native/preflight.js +72 -0
- package/dist/native/preflight.js.map +1 -0
- package/dist/native/proxy-proof.js +182 -0
- package/dist/native/proxy-proof.js.map +1 -0
- package/dist/native/samsung.js +34 -0
- package/dist/native/samsung.js.map +1 -0
- package/dist/native/signals.js +50 -0
- package/dist/native/signals.js.map +1 -0
- package/dist/native/view.js +139 -0
- package/dist/native/view.js.map +1 -0
- package/dist/profile.js +237 -0
- package/dist/profile.js.map +1 -0
- package/dist/sink.js +134 -0
- package/dist/sink.js.map +1 -0
- package/dist/sinks/file.js +49 -0
- package/dist/sinks/file.js.map +1 -0
- package/dist/sinks/http.js +38 -0
- package/dist/sinks/http.js.map +1 -0
- package/dist/sinks/index.js +5 -0
- package/dist/sinks/index.js.map +1 -0
- package/dist/sinks/multi.js +40 -0
- package/dist/sinks/multi.js.map +1 -0
- package/dist/sinks/paths.js +32 -0
- package/dist/sinks/paths.js.map +1 -0
- package/dist/sinks/record.js +58 -0
- package/dist/sinks/record.js.map +1 -0
- package/dist/sinks/types.js +1 -0
- package/dist/sinks/types.js.map +1 -0
- package/dist/web/breakage.js +210 -0
- package/dist/web/breakage.js.map +1 -0
- package/dist/web/controls.js +102 -0
- package/dist/web/controls.js.map +1 -0
- package/dist/web/driver.js +803 -0
- package/dist/web/driver.js.map +1 -0
- package/dist/web/elements.js +151 -0
- package/dist/web/elements.js.map +1 -0
- package/dist/web/fingerprint.js +13 -0
- package/dist/web/fingerprint.js.map +1 -0
- package/dist/web/forms.js +74 -0
- package/dist/web/forms.js.map +1 -0
- package/dist/web/links.js +194 -0
- package/dist/web/links.js.map +1 -0
- package/dist/web/types.js +1 -0
- package/dist/web/types.js.map +1 -0
- package/package.json +80 -0
- package/runtime/macos-accessibility-runner.js +275 -0
- package/runtime/windows-uia-runner.ps1 +539 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
function bySignature(findings) {
|
|
3
|
+
const out = new Map();
|
|
4
|
+
for (const finding of findings) {
|
|
5
|
+
out.set(finding.signature, finding);
|
|
6
|
+
}
|
|
7
|
+
return out;
|
|
8
|
+
}
|
|
9
|
+
export function compareBaseline(current, baseline, baselinePath) {
|
|
10
|
+
if (!baseline) {
|
|
11
|
+
const newFindings = [...current.findings];
|
|
12
|
+
return {
|
|
13
|
+
baselinePath,
|
|
14
|
+
baselineFound: false,
|
|
15
|
+
newFindings,
|
|
16
|
+
resolvedFindings: [],
|
|
17
|
+
unchangedFindings: [],
|
|
18
|
+
summary: {
|
|
19
|
+
newFail: newFindings.filter((finding) => finding.severity === "fail").length,
|
|
20
|
+
newWarn: newFindings.filter((finding) => finding.severity === "warn").length,
|
|
21
|
+
resolved: 0,
|
|
22
|
+
unchanged: 0,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const currentBySignature = bySignature(current.findings);
|
|
27
|
+
const baselineBySignature = bySignature(baseline.findings);
|
|
28
|
+
const newFindings = [];
|
|
29
|
+
const resolvedFindings = [];
|
|
30
|
+
const unchangedFindings = [];
|
|
31
|
+
for (const finding of current.findings) {
|
|
32
|
+
if (baselineBySignature.has(finding.signature))
|
|
33
|
+
unchangedFindings.push(finding);
|
|
34
|
+
else
|
|
35
|
+
newFindings.push(finding);
|
|
36
|
+
}
|
|
37
|
+
for (const finding of baseline.findings) {
|
|
38
|
+
if (!currentBySignature.has(finding.signature))
|
|
39
|
+
resolvedFindings.push(finding);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
baselinePath,
|
|
43
|
+
baselineFound: true,
|
|
44
|
+
newFindings,
|
|
45
|
+
resolvedFindings,
|
|
46
|
+
unchangedFindings,
|
|
47
|
+
summary: {
|
|
48
|
+
newFail: newFindings.filter((finding) => finding.severity === "fail").length,
|
|
49
|
+
newWarn: newFindings.filter((finding) => finding.severity === "warn").length,
|
|
50
|
+
resolved: resolvedFindings.length,
|
|
51
|
+
unchanged: unchangedFindings.length,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function readBaselineResult(path) {
|
|
56
|
+
// A missing OR malformed baseline must degrade to "no baseline" (the run still
|
|
57
|
+
// writes its report + result.json) — never abort the whole crawl after the fact.
|
|
58
|
+
// We warn loudly so a typo'd / corrupt baseline path is visible, not silent.
|
|
59
|
+
let text;
|
|
60
|
+
try {
|
|
61
|
+
text = await readFile(path, "utf8");
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err.code === "ENOENT")
|
|
65
|
+
return undefined;
|
|
66
|
+
console.warn(`comber: baseline unreadable (${path}): ${String(err).slice(0, 120)} — treating as no baseline`);
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(text);
|
|
71
|
+
if (!Array.isArray(parsed.findings)) {
|
|
72
|
+
console.warn(`comber: baseline has no findings array (${path}) — treating as no baseline`);
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.warn(`comber: baseline not valid JSON (${path}): ${String(err).slice(0, 120)} — treating as no baseline`);
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function compareBaselineFromPath(current, path) {
|
|
83
|
+
const baseline = await readBaselineResult(path);
|
|
84
|
+
return compareBaseline(current, baseline, path);
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/core/baseline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function fallbackBlindSpots(count, scope, message) {
|
|
2
|
+
const out = [];
|
|
3
|
+
for (let i = 0; i < count; i++) {
|
|
4
|
+
out.push({
|
|
5
|
+
kind: "unknown-surface",
|
|
6
|
+
severity: "blocker-for-exhaustive",
|
|
7
|
+
scope,
|
|
8
|
+
message,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
export function summarizeBlindSpots(blindSpots) {
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const blindSpot of blindSpots) {
|
|
16
|
+
out[blindSpot.kind] = (out[blindSpot.kind] ?? 0) + 1;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
export function blindSpotsBlockExhaustive(blindSpots) {
|
|
21
|
+
return blindSpots.some((blindSpot) => blindSpot.severity === "blocker-for-exhaustive");
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blind-spots.js","sourceRoot":"","sources":["../../src/core/blind-spots.ts"],"names":[],"mappings":"AA0BA,MAAM,UAAU,kBAAkB,CAAC,KAAa,EAAE,KAAa,EAAE,OAAe;IAC9E,MAAM,GAAG,GAAgB,EAAE,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,GAAG,CAAC,IAAI,CAAC;YACP,IAAI,EAAE,iBAAiB;YACvB,QAAQ,EAAE,wBAAwB;YAClC,KAAK;YACL,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,UAAgC;IAClE,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,UAAgC;IACxE,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,KAAK,wBAAwB,CAAC,CAAC;AACzF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-extras.js","sourceRoot":"","sources":["../../src/core/contract-extras.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/core/contract.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { blindSpotsBlockExhaustive, fallbackBlindSpots, summarizeBlindSpots } from "./blind-spots.js";
|
|
2
|
+
export class CoverageAccumulator {
|
|
3
|
+
statesVisited = 0;
|
|
4
|
+
elementsDiscovered = 0;
|
|
5
|
+
elementsExercised = 0;
|
|
6
|
+
frameBlindSpots = 0;
|
|
7
|
+
cappedNotExhaustive = false;
|
|
8
|
+
blindSpots = [];
|
|
9
|
+
skipLedger = {};
|
|
10
|
+
/** Driver sub-metrics + orchestrator-tracked link discovery. */
|
|
11
|
+
extra = {};
|
|
12
|
+
/** Element-partition skips only (denylist/disabled/file-input/idempotent/cap) — drives the
|
|
13
|
+
* element-coverage denominator. Form/route skips go in skipLedger but NOT here. */
|
|
14
|
+
elementSkips = 0;
|
|
15
|
+
noteState() {
|
|
16
|
+
this.statesVisited++;
|
|
17
|
+
}
|
|
18
|
+
discover(n) {
|
|
19
|
+
this.elementsDiscovered += n;
|
|
20
|
+
}
|
|
21
|
+
exercise() {
|
|
22
|
+
this.elementsExercised++;
|
|
23
|
+
}
|
|
24
|
+
/** Non-element skip (forms, route-visit-cap, near-dup-state) — ledger only. */
|
|
25
|
+
skip(reason) {
|
|
26
|
+
this.skipLedger[reason] = (this.skipLedger[reason] ?? 0) + 1;
|
|
27
|
+
}
|
|
28
|
+
/** Element-partition skip — ledger AND the coverage denominator. */
|
|
29
|
+
skipElement(reason) {
|
|
30
|
+
this.elementSkips++;
|
|
31
|
+
this.skip(reason);
|
|
32
|
+
}
|
|
33
|
+
blind(n, details, scope = "unknown") {
|
|
34
|
+
this.frameBlindSpots += n;
|
|
35
|
+
if (details && details.length > 0) {
|
|
36
|
+
this.blindSpots.push(...details);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.blindSpots.push(...fallbackBlindSpots(n, scope, "surface could not be inspected"));
|
|
40
|
+
}
|
|
41
|
+
/** Bump a single driver/orchestrator sub-metric. */
|
|
42
|
+
bumpExtra(key, n = 1) {
|
|
43
|
+
this.extra[key] = (this.extra[key] ?? 0) + n;
|
|
44
|
+
}
|
|
45
|
+
/** Merge a driver's end-of-run sub-metrics into `extra`. */
|
|
46
|
+
mergeExtra(record) {
|
|
47
|
+
for (const [k, v] of Object.entries(record))
|
|
48
|
+
this.extra[k] = (this.extra[k] ?? 0) + v;
|
|
49
|
+
}
|
|
50
|
+
summary() {
|
|
51
|
+
// Honest denominator: non-link exercisable = discovered − element-skips − links.
|
|
52
|
+
// Only ELEMENT-partition skips count (not form/route skips, which aren't elements).
|
|
53
|
+
const followTargets = this.extra.followTargetsDiscovered ?? this.extra.linksDiscovered ?? 0;
|
|
54
|
+
const denom = Math.max(this.elementsExercised, this.elementsDiscovered - this.elementSkips - followTargets);
|
|
55
|
+
return {
|
|
56
|
+
statesVisited: this.statesVisited,
|
|
57
|
+
elementsDiscovered: this.elementsDiscovered,
|
|
58
|
+
elementsExercised: this.elementsExercised,
|
|
59
|
+
elementsSkipped: this.elementSkips,
|
|
60
|
+
skipLedger: { ...this.skipLedger },
|
|
61
|
+
frameBlindSpots: this.frameBlindSpots,
|
|
62
|
+
blindSpots: this.blindSpots.map((blindSpot) => ({ ...blindSpot })),
|
|
63
|
+
blindSpotSummary: summarizeBlindSpots(this.blindSpots),
|
|
64
|
+
elementCoveragePct: denom > 0 ? Math.round((100 * this.elementsExercised) / denom) : 100,
|
|
65
|
+
exhaustiveClaimAllowed: !this.cappedNotExhaustive && !blindSpotsBlockExhaustive(this.blindSpots),
|
|
66
|
+
cappedNotExhaustive: this.cappedNotExhaustive,
|
|
67
|
+
extra: { ...this.extra },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/core/coverage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { exerciseState } from "./pipeline.js";
|
|
4
|
+
import { CoverageAccumulator } from "./coverage.js";
|
|
5
|
+
import { ExhaustivenessLedger } from "./ledger.js";
|
|
6
|
+
import { PlanRecorder } from "./plan.js";
|
|
7
|
+
import { makeSafetyJudge } from "./safety-judge.js";
|
|
8
|
+
import { invariants, jaccardNearDup, nameSet, stateFingerprint } from "./safety.js";
|
|
9
|
+
import { compareFindings, normalizeBreakageSignals } from "./signal-normalize.js";
|
|
10
|
+
/**
|
|
11
|
+
* Frontier BFS over in-scope Addresses. The frontier grows TWO ways: it FOLLOWS the
|
|
12
|
+
* driver's outbound addresses (links, never clicked) AND enqueues addresses the driver
|
|
13
|
+
* reports it NAVIGATED to while acting (`ExerciseResult.discovered` — native tap-only
|
|
14
|
+
* screens). `exerciseState` exhaustively interacts with every other node per state.
|
|
15
|
+
*
|
|
16
|
+
* TWO DISTINCT DEDUP LAYERS (contract v2), never conflated:
|
|
17
|
+
* • FRONTIER de-dup — keyed by the GOTO HANDLE (`addr.dedupeKey ?? addr.id`): the `queued`
|
|
18
|
+
* set, so the same handle is never `goto`'d twice.
|
|
19
|
+
* • VISITED-by-STATE — keyed by `stateFingerprint(snapshot())`: the `visited` set, so two
|
|
20
|
+
* handles that land on the same structural screen are processed once.
|
|
21
|
+
*
|
|
22
|
+
* The driver owns "what is a location" (Address) and "what is in scope" (inScope). Probes
|
|
23
|
+
* are driver-DECLARED with a lifecycle slot (`capabilities.probes`), and the orchestrator
|
|
24
|
+
* fires each driver's probes at the matching slot — reproducing web's original fixed
|
|
25
|
+
* ordering (after-goto: session · after-snapshot: axe/dom/links · after-exercise: forms ·
|
|
26
|
+
* finalize) while letting any driver register new phases (native: crash/logs). Hardened
|
|
27
|
+
* near-dup fingerprint + per-scope visit cap terminate infinite-feed/live pages; all global
|
|
28
|
+
* caps are authoritative backstops.
|
|
29
|
+
*/
|
|
30
|
+
export async function crawl(driver, cfg, runDir) {
|
|
31
|
+
const startedAt = Date.now();
|
|
32
|
+
const spend = { input: 0, output: 0 };
|
|
33
|
+
const visited = new Set(); // exact-ish state fingerprints
|
|
34
|
+
const visitedByScope = new Map(); // for Jaccard near-dup collapse
|
|
35
|
+
const queued = new Set(); // Address.id already in/through the frontier
|
|
36
|
+
const frontier = [];
|
|
37
|
+
const rawSignals = [];
|
|
38
|
+
const visitedUrls = [];
|
|
39
|
+
const triedSignatures = new Set();
|
|
40
|
+
const performedMutations = new Set();
|
|
41
|
+
const visitsPerScope = new Map();
|
|
42
|
+
const coverage = new CoverageAccumulator();
|
|
43
|
+
const ledger = new ExhaustivenessLedger();
|
|
44
|
+
// Record an ordered, driver-agnostic execution PLAN (addresses + node ids only) as the
|
|
45
|
+
// crawl runs — the cheap half of "discover once, replay cheaply". Persisted below.
|
|
46
|
+
const recorder = new PlanRecorder({ target: cfg.target, startUrl: cfg.startUrl });
|
|
47
|
+
// The LLM safety-judge is the HOSTED (strict) gate. In advisory (default) the deterministic
|
|
48
|
+
// floor's effect-based DENIES still apply, but ambiguous mutations proceed WITHOUT a per-action
|
|
49
|
+
// LLM call — preserving the LLM-free crawl and today's behavior for internal / BYO-key runs.
|
|
50
|
+
// Built once (one Anthropic client); undefined unless strict AND an LLM is configured.
|
|
51
|
+
const safetyJudge = cfg.actionGateMode === "strict" ? makeSafetyJudge(cfg) : undefined;
|
|
52
|
+
let states = 0;
|
|
53
|
+
let actions = 0;
|
|
54
|
+
let cappedBy;
|
|
55
|
+
// Hand the driver the coverage sink so its sub-metrics + target-specific skips land in
|
|
56
|
+
// the one honest ledger (BEFORE start, so any pass the driver wires at start can record).
|
|
57
|
+
driver.attachCoverage?.(coverage);
|
|
58
|
+
driver.attachRunDir?.(runDir);
|
|
59
|
+
// Driver-declared probes, fired at their lifecycle slot in registration order. `record`
|
|
60
|
+
// feeds the replay plan (no-op for the pre-beginState after-goto slot + the un-scoped
|
|
61
|
+
// finalize slot). This reproduces web's old fixed session/axe/dom/links/forms ordering
|
|
62
|
+
// exactly while letting any driver register new phases at the same slots.
|
|
63
|
+
const probesAt = (slot) => driver.capabilities.probes.filter((p) => p.slot === slot);
|
|
64
|
+
const runSlot = async (slot, record) => {
|
|
65
|
+
if (!driver.probe)
|
|
66
|
+
return [];
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const reg of probesAt(slot)) {
|
|
69
|
+
if (record)
|
|
70
|
+
recorder.recordProbe(reg.name); // no-op until a state is begun
|
|
71
|
+
out.push(...(await driver.probe(reg.name)));
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
};
|
|
75
|
+
// Traversal itself spends NO tokens (picker is heuristic); the only model call is the
|
|
76
|
+
// post-crawl Opus judge, which enforces its own input cap (see triage.ts). So the token
|
|
77
|
+
// ceiling is not a crawl-loop backstop — these gates are wall-clock / states / actions only.
|
|
78
|
+
const overBudget = () => {
|
|
79
|
+
if (states >= cfg.maxStates)
|
|
80
|
+
return `maxStates(${cfg.maxStates})`;
|
|
81
|
+
if (actions >= cfg.maxActions)
|
|
82
|
+
return `maxActions(${cfg.maxActions})`;
|
|
83
|
+
if (Date.now() - startedAt >= cfg.maxWallClockMs)
|
|
84
|
+
return "maxWallClockMs";
|
|
85
|
+
return undefined;
|
|
86
|
+
};
|
|
87
|
+
const overActionBudget = () => {
|
|
88
|
+
if (actions >= cfg.maxActions)
|
|
89
|
+
return `maxActions(${cfg.maxActions})`;
|
|
90
|
+
if (Date.now() - startedAt >= cfg.maxWallClockMs)
|
|
91
|
+
return "maxWallClockMs";
|
|
92
|
+
return undefined;
|
|
93
|
+
};
|
|
94
|
+
// FRONTIER de-dup is by GOTO HANDLE (`dedupeKey ?? id`) — never the structural state hash
|
|
95
|
+
// (that is the `visited` fingerprint layer below). Web/API leave `dedupeKey` unset, so the
|
|
96
|
+
// key is `id` and behavior is unchanged; native sets it to a destination-screen fingerprint
|
|
97
|
+
// so two tap-paths reaching the same screen enqueue once.
|
|
98
|
+
const enqueue = (addr) => {
|
|
99
|
+
const key = addr.dedupeKey ?? addr.id;
|
|
100
|
+
if (driver.inScope(addr) && !queued.has(key)) {
|
|
101
|
+
queued.add(key);
|
|
102
|
+
frontier.push(addr);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
// start() is INSIDE the try so a mid-start failure (e.g. WebDriver launches Chromium then
|
|
106
|
+
// throws in newContext()/route()) still hits the finally and closes the driver — otherwise a
|
|
107
|
+
// failed start leaks a headless browser process, compounding across CI runs.
|
|
108
|
+
try {
|
|
109
|
+
await driver.start();
|
|
110
|
+
const entry = driver.entryAddress();
|
|
111
|
+
enqueue(entry);
|
|
112
|
+
while (frontier.length > 0) {
|
|
113
|
+
cappedBy = overBudget();
|
|
114
|
+
if (cappedBy) {
|
|
115
|
+
coverage.cappedNotExhaustive = true;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
invariants({ actions, cfg });
|
|
119
|
+
const addr = frontier.shift();
|
|
120
|
+
const scope = addr.scope;
|
|
121
|
+
const visits = visitsPerScope.get(scope) ?? 0;
|
|
122
|
+
if (visits >= cfg.maxVisitsPerRoute) {
|
|
123
|
+
coverage.skip("route-visit-cap");
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
visitsPerScope.set(scope, visits + 1);
|
|
127
|
+
await driver.goto(addr);
|
|
128
|
+
rawSignals.push(...driver.drainSignals());
|
|
129
|
+
// AFTER-GOTO slot (web: session-bounce — emit, don't halt, avoids false-halt on
|
|
130
|
+
// legit /login routes). Fires before beginState, so it is not recorded into a state.
|
|
131
|
+
rawSignals.push(...(await runSlot("after-goto", true)));
|
|
132
|
+
const snap = await driver.snapshot();
|
|
133
|
+
const fp = stateFingerprint(snap);
|
|
134
|
+
if (visited.has(fp))
|
|
135
|
+
continue;
|
|
136
|
+
// Near-dup collapse, scoped to the same scope (so /user/1 vs /user/2 never merge).
|
|
137
|
+
const ns = nameSet(snap);
|
|
138
|
+
const priors = visitedByScope.get(scope) ?? [];
|
|
139
|
+
if (priors.some((p) => jaccardNearDup(ns, p) >= cfg.nearDupThreshold)) {
|
|
140
|
+
coverage.skip("near-dup-state");
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
visited.add(fp);
|
|
144
|
+
priors.push(ns);
|
|
145
|
+
visitedByScope.set(scope, priors);
|
|
146
|
+
states++;
|
|
147
|
+
coverage.noteState();
|
|
148
|
+
visitedUrls.push(snap.address.label ?? snap.address.id);
|
|
149
|
+
// Open this state's plan record BEFORE its probes/outbound/exercises so they attach to it.
|
|
150
|
+
recorder.beginState(snap.address);
|
|
151
|
+
// Screenshot the state for the report. Screenshots are RAW pixels of the target (on-screen
|
|
152
|
+
// secrets/PII can't be text-redacted), so --no-screenshots skips capture entirely.
|
|
153
|
+
if (cfg.screenshots) {
|
|
154
|
+
const shot = await driver.screenshot();
|
|
155
|
+
const shotPath = join(runDir, `state-${states}.png`);
|
|
156
|
+
if (shot.length)
|
|
157
|
+
await writeFile(shotPath, shot).catch(() => { });
|
|
158
|
+
}
|
|
159
|
+
// AFTER-SNAPSHOT (pre-exercise) slot. Web order = axe (bounded a11y, driver
|
|
160
|
+
// self-limits) → dom (every-state sweep) → links (collect for the post-pass);
|
|
161
|
+
// registration order is preserved.
|
|
162
|
+
rawSignals.push(...(await runSlot("after-snapshot", true)));
|
|
163
|
+
// Frontier expansion #1: FOLLOW (never click) the driver's in-scope outbound targets.
|
|
164
|
+
if (driver.outboundAddresses)
|
|
165
|
+
for (const a of driver.outboundAddresses()) {
|
|
166
|
+
if (driver.inScope(a))
|
|
167
|
+
recorder.recordOutbound(a);
|
|
168
|
+
enqueue(a);
|
|
169
|
+
}
|
|
170
|
+
const deps = {
|
|
171
|
+
driver,
|
|
172
|
+
cfg,
|
|
173
|
+
triedSignatures,
|
|
174
|
+
performedMutations,
|
|
175
|
+
overBudget: overActionBudget,
|
|
176
|
+
onSignal: (s) => rawSignals.push(s),
|
|
177
|
+
coverage,
|
|
178
|
+
ledger,
|
|
179
|
+
recordExercise: (id) => recorder.recordExercise(id),
|
|
180
|
+
// Frontier expansion #2: enqueue states the driver NAVIGATED to while acting
|
|
181
|
+
// (ExerciseResult.discovered), subject to inScope + caps/dedup (enqueue handles both).
|
|
182
|
+
enqueue,
|
|
183
|
+
safetyJudge,
|
|
184
|
+
};
|
|
185
|
+
actions += await exerciseState(snap, scope, deps);
|
|
186
|
+
// AFTER-EXERCISE slot (web: form testing — submits are egress-gated, state restored
|
|
187
|
+
// per-form). Gated behind dry-run / action budget exactly as the old forms call was.
|
|
188
|
+
if (!cfg.dryRun && !overActionBudget())
|
|
189
|
+
rawSignals.push(...(await runSlot("after-exercise", true)));
|
|
190
|
+
}
|
|
191
|
+
// FINALIZE slot (native: a final crash/ANR log scan) + the finalize extras (web:
|
|
192
|
+
// validate EVERY collected link) — both run while the session is still live.
|
|
193
|
+
rawSignals.push(...(await runSlot("finalize", false)));
|
|
194
|
+
if (driver.finalize)
|
|
195
|
+
rawSignals.push(...(await driver.finalize()));
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
await driver.close();
|
|
199
|
+
}
|
|
200
|
+
// Persist the recorded execution plan (always, cheap) for deterministic replay. Driver-
|
|
201
|
+
// agnostic (addresses + node ids only, no handles); honors --replay-out, else <runDir>/plan.json.
|
|
202
|
+
const planPath = cfg.replayOutPath ?? join(runDir, "plan.json");
|
|
203
|
+
await writeFile(planPath, JSON.stringify(recorder.toPlan(), null, 2)).catch(() => { });
|
|
204
|
+
// Pull any per-driver structured audit data (web: linkAudit) for the report/result.json.
|
|
205
|
+
const extra = driver.auditExtra ? driver.auditExtra() : undefined;
|
|
206
|
+
// Dedupe by signature → Finding[] with counts.
|
|
207
|
+
const bySig = new Map();
|
|
208
|
+
for (const s of normalizeBreakageSignals(rawSignals)) {
|
|
209
|
+
const existing = bySig.get(s.signature);
|
|
210
|
+
if (existing)
|
|
211
|
+
existing.count++;
|
|
212
|
+
else
|
|
213
|
+
bySig.set(s.signature, { ...s, count: 1 });
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
startUrl: cfg.startUrl,
|
|
217
|
+
visitedStates: states,
|
|
218
|
+
visitedUrls,
|
|
219
|
+
actionsTaken: actions,
|
|
220
|
+
findings: [...bySig.values()].sort(compareFindings),
|
|
221
|
+
durationMs: Date.now() - startedAt,
|
|
222
|
+
tokenSpend: spend,
|
|
223
|
+
cappedBy,
|
|
224
|
+
frameBlindSpots: coverage.frameBlindSpots,
|
|
225
|
+
coverage: coverage.summary(),
|
|
226
|
+
exhaustivenessLedger: ledger.snapshot(),
|
|
227
|
+
...(extra ? { extra } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crawl.js","sourceRoot":"","sources":["../../src/core/crawl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAKjC,OAAO,EAAE,aAAa,EAAqB,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAElF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,MAAkB,EAAE,GAAc,EAAE,MAAc;IAC5E,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACtC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC,CAAC,+BAA+B;IAClE,MAAM,cAAc,GAAG,IAAI,GAAG,EAAyB,CAAC,CAAC,gCAAgC;IACzF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC,CAAC,6CAA6C;IAC/E,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,UAAU,GAAqB,EAAE,CAAC;IACxC,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IAC1C,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC7C,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;IACjD,MAAM,QAAQ,GAAG,IAAI,mBAAmB,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC1C,uFAAuF;IACvF,mFAAmF;IACnF,MAAM,QAAQ,GAAG,IAAI,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;IAClF,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,QAA4B,CAAC;IAEjC,uFAAuF;IACvF,0FAA0F;IAC1F,MAAM,CAAC,cAAc,EAAE,CAAC,QAAQ,CAAC,CAAC;IAClC,MAAM,CAAC,YAAY,EAAE,CAAC,MAAM,CAAC,CAAC;IAE9B,wFAAwF;IACxF,sFAAsF;IACtF,uFAAuF;IACvF,0EAA0E;IAC1E,MAAM,QAAQ,GAAG,CAAC,IAAe,EAAgC,EAAE,CACjE,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,KAAK,EAAE,IAAe,EAAE,MAAe,EAA6B,EAAE;QACpF,IAAI,CAAC,MAAM,CAAC,KAAK;YAAE,OAAO,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAqB,EAAE,CAAC;QACjC,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,IAAI,MAAM;gBAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,+BAA+B;YAC3E,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;IAEF,MAAM,UAAU,GAAG,GAAuB,EAAE;QAC1C,IAAI,MAAM,IAAI,GAAG,CAAC,SAAS;YAAE,OAAO,aAAa,GAAG,CAAC,SAAS,GAAG,CAAC;QAClE,IAAI,OAAO,IAAI,GAAG,CAAC,UAAU;YAAE,OAAO,cAAc,GAAG,CAAC,UAAU,GAAG,CAAC;QACtE,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,IAAI,GAAG,CAAC,cAAc;YAAE,OAAO,gBAAgB,CAAC;QAC1E,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,cAAc;YAAE,OAAO,kBAAkB,GAAG,CAAC,cAAc,GAAG,CAAC;QACvF,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IACF,MAAM,gBAAgB,GAAG,GAAuB,EAAE;QAChD,IAAI,OAAO,IAAI,GAAG,CAAC,UAAU;YAAE,OAAO,cAAc,GAAG,CAAC,UAAU,GAAG,CAAC;QACtE,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,IAAI,GAAG,CAAC,cAAc;YAAE,OAAO,gBAAgB,CAAC;QAC1E,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,cAAc;YAAE,OAAO,kBAAkB,GAAG,CAAC,cAAc,GAAG,CAAC;QACvF,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IAEF,0FAA0F;IAC1F,2FAA2F;IAC3F,4FAA4F;IAC5F,0DAA0D;IAC1D,MAAM,OAAO,GAAG,CAAC,IAAa,EAAQ,EAAE;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,EAAE,CAAC;QACtC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7C,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAChB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACrB,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,EAAE,CAAC;IACpC,OAAO,CAAC,KAAK,CAAC,CAAC;IACf,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,QAAQ,GAAG,UAAU,EAAE,CAAC;YACxB,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,mBAAmB,GAAG,IAAI,CAAC;gBACpC,MAAM;YACR,CAAC;YACD,UAAU,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;YAE7B,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,EAAG,CAAC;YAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;YACzB,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC9C,IAAI,MAAM,IAAI,GAAG,CAAC,iBAAiB,EAAE,CAAC;gBACpC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;gBACjC,SAAS;YACX,CAAC;YACD,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC;YAEtC,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACxB,UAAU,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC;YAE1C,gFAAgF;YAChF,qFAAqF;YACrF,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;YAExD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC;YACrC,MAAM,EAAE,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,SAAS;YAE9B,mFAAmF;YACnF,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;YACzB,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAC/C,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBACtE,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;gBAChC,SAAS;YACX,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAChB,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAClC,MAAM,EAAE,CAAC;YACT,QAAQ,CAAC,SAAS,EAAE,CAAC;YACrB,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACxD,2FAA2F;YAC3F,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAElC,uCAAuC;YACvC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC;YACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,SAAS,MAAM,MAAM,CAAC,CAAC;YACrD,IAAI,IAAI,CAAC,MAAM;gBAAE,MAAM,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAEjE,4EAA4E;YAC5E,8EAA8E;YAC9E,mCAAmC;YACnC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;YAE5D,sFAAsF;YACtF,IAAI,MAAM,CAAC,iBAAiB;gBAC1B,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,iBAAiB,EAAE,EAAE,CAAC;oBAC3C,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;wBAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;oBAClD,OAAO,CAAC,CAAC,CAAC,CAAC;gBACb,CAAC;YAEH,MAAM,IAAI,GAAiB;gBACzB,MAAM;gBACN,GAAG;gBACH,eAAe;gBACf,kBAAkB;gBAClB,UAAU,EAAE,gBAAgB;gBAC5B,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;gBACnC,QAAQ;gBACR,MAAM;gBACN,cAAc,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;gBACnD,6EAA6E;gBAC7E,uFAAuF;gBACvF,OAAO;aACR,CAAC;YACF,OAAO,IAAI,MAAM,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YAElD,oFAAoF;YACpF,qFAAqF;YACrF,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,gBAAgB,EAAE;gBAAE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QACtG,CAAC;QAED,iFAAiF;QACjF,6EAA6E;QAC7E,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;QACvD,IAAI,MAAM,CAAC,QAAQ;YAAE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IACrE,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAED,wFAAwF;IACxF,kGAAkG;IAClG,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAChE,MAAM,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAEtF,yFAAyF;IACzF,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAElE,+CAA+C;IAC/C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAmB,CAAC;IACzC,KAAK,MAAM,CAAC,IAAI,wBAAwB,CAAC,UAAU,CAAC,EAAE,CAAC;QACrD,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACxC,IAAI,QAAQ;YAAE,QAAQ,CAAC,KAAK,EAAE,CAAC;;YAC1B,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,aAAa,EAAE,MAAM;QACrB,WAAW;QACX,YAAY,EAAE,OAAO;QACrB,QAAQ,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC;QACnD,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;QAClC,UAAU,EAAE,KAAK;QACjB,QAAQ;QACR,eAAe,EAAE,QAAQ,CAAC,eAAe;QACzC,QAAQ,EAAE,QAAQ,CAAC,OAAO,EAAE;QAC5B,oBAAoB,EAAE,MAAM,CAAC,QAAQ,EAAE;QACvC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
function normalizeHostname(hostname) {
|
|
4
|
+
return hostname.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
function isPrivateIpv4(address) {
|
|
7
|
+
const parts = address.split(".").map((part) => Number(part));
|
|
8
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const [a, b] = parts;
|
|
12
|
+
if (a === undefined || b === undefined)
|
|
13
|
+
return false;
|
|
14
|
+
return (a === 0 ||
|
|
15
|
+
a === 10 ||
|
|
16
|
+
a === 127 ||
|
|
17
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
18
|
+
(a === 169 && b === 254) ||
|
|
19
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
20
|
+
(a === 192 && b === 168) ||
|
|
21
|
+
(a === 198 && (b === 18 || b === 19)) ||
|
|
22
|
+
a >= 224);
|
|
23
|
+
}
|
|
24
|
+
function isPrivateIpv6(address) {
|
|
25
|
+
const value = normalizeHostname(address);
|
|
26
|
+
if (value === "::" || value === "::1" || value === "")
|
|
27
|
+
return true; // unspecified / loopback
|
|
28
|
+
if (value.startsWith("::ffff:"))
|
|
29
|
+
return isPrivateIp(value.slice("::ffff:".length)); // IPv4-mapped
|
|
30
|
+
const firstHextet = Number.parseInt(value.split(":")[0] || "", 16);
|
|
31
|
+
if (!Number.isFinite(firstHextet))
|
|
32
|
+
return false;
|
|
33
|
+
if (firstHextet >= 0xfe80 && firstHextet <= 0xfebf)
|
|
34
|
+
return true; // link-local fe80::/10 (was only fe80:*)
|
|
35
|
+
if ((firstHextet & 0xfe00) === 0xfc00)
|
|
36
|
+
return true; // unique-local fc00::/7
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
export function isPrivateIp(address) {
|
|
40
|
+
const value = normalizeHostname(address);
|
|
41
|
+
const family = isIP(value);
|
|
42
|
+
if (family === 4)
|
|
43
|
+
return isPrivateIpv4(value);
|
|
44
|
+
if (family === 6)
|
|
45
|
+
return isPrivateIpv6(value);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// Distinct sentinel so a DNS *timeout* is told apart from a genuine no-resolution.
|
|
49
|
+
class DnsTimeoutError extends Error {
|
|
50
|
+
}
|
|
51
|
+
async function defaultResolveHost(hostname) {
|
|
52
|
+
// 3s ceiling (was 1s — too tight, flagged legit hosts on a slow resolver). On
|
|
53
|
+
// timeout we REJECT (transient) rather than resolve [] (which read as NXDOMAIN).
|
|
54
|
+
return await Promise.race([
|
|
55
|
+
lookup(hostname, { all: true }).then((rows) => rows.map((row) => row.address)),
|
|
56
|
+
new Promise((_, reject) => setTimeout(() => reject(new DnsTimeoutError("dns timeout")), 3_000)),
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
export async function checkNetworkEgress(rawUrl, resolveHost = defaultResolveHost) {
|
|
60
|
+
let url;
|
|
61
|
+
try {
|
|
62
|
+
url = new URL(rawUrl);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { allowed: false, reason: "unparseable URL" };
|
|
66
|
+
}
|
|
67
|
+
// Network schemes only — http(s) and ws(s) (WebSocket connect targets are gated
|
|
68
|
+
// too). Everything else (mailto/tel/data/blob/javascript) is not a network hop.
|
|
69
|
+
if (!/^(?:https?|wss?):$/.test(url.protocol)) {
|
|
70
|
+
return { allowed: true };
|
|
71
|
+
}
|
|
72
|
+
const hostname = normalizeHostname(url.hostname);
|
|
73
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "metadata.google.internal") {
|
|
74
|
+
return { allowed: false, reason: "blocked local/metadata host" };
|
|
75
|
+
}
|
|
76
|
+
if (isIP(hostname)) {
|
|
77
|
+
return isPrivateIp(hostname) ? { allowed: false, reason: "blocked private IP" } : { allowed: true };
|
|
78
|
+
}
|
|
79
|
+
let addresses;
|
|
80
|
+
try {
|
|
81
|
+
addresses = await resolveHost(hostname);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const code = err.code;
|
|
85
|
+
// Permanent non-resolution → stable block. Timeout / temporary failure →
|
|
86
|
+
// fail-closed for this attempt but mark transient so it is not cached.
|
|
87
|
+
if (code === "ENOTFOUND" || code === "NXDOMAIN" || code === "EAI_NONAME") {
|
|
88
|
+
return { allowed: false, reason: "host did not resolve (NXDOMAIN)" };
|
|
89
|
+
}
|
|
90
|
+
return { allowed: false, reason: `dns resolution transient failure (${code ?? "timeout"})`, transient: true };
|
|
91
|
+
}
|
|
92
|
+
if (addresses.length === 0)
|
|
93
|
+
return { allowed: false, reason: "host did not resolve publicly" };
|
|
94
|
+
if (addresses.some((address) => isPrivateIp(address))) {
|
|
95
|
+
return { allowed: false, reason: "host resolves to private IP" };
|
|
96
|
+
}
|
|
97
|
+
return { allowed: true };
|
|
98
|
+
}
|
|
99
|
+
export class EgressGuard {
|
|
100
|
+
resolveHost;
|
|
101
|
+
decisions = new Map();
|
|
102
|
+
constructor(resolveHost = defaultResolveHost) {
|
|
103
|
+
this.resolveHost = resolveHost;
|
|
104
|
+
}
|
|
105
|
+
check(rawUrl) {
|
|
106
|
+
let cacheKey = rawUrl;
|
|
107
|
+
try {
|
|
108
|
+
const url = new URL(rawUrl);
|
|
109
|
+
cacheKey = `${url.protocol}//${normalizeHostname(url.hostname)}`;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* keep raw */
|
|
113
|
+
}
|
|
114
|
+
const existing = this.decisions.get(cacheKey);
|
|
115
|
+
if (existing)
|
|
116
|
+
return existing;
|
|
117
|
+
const key = cacheKey;
|
|
118
|
+
const pending = checkNetworkEgress(rawUrl, this.resolveHost).then((decision) => {
|
|
119
|
+
// Never cache a transient (DNS-timeout) verdict — re-resolve next time so a
|
|
120
|
+
// single slow lookup can't block a legitimate host for the whole run.
|
|
121
|
+
if (decision.transient)
|
|
122
|
+
this.decisions.delete(key);
|
|
123
|
+
return decision;
|
|
124
|
+
});
|
|
125
|
+
this.decisions.set(key, pending);
|
|
126
|
+
return pending;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress.js","sourceRoot":"","sources":["../../src/core/egress.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAahC,SAAS,iBAAiB,CAAC,QAAgB;IACzC,OAAO,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AACtE,CAAC;AAED,SAAS,aAAa,CAAC,OAAe;IACpC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QAClG,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC;IACrB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAErD,OAAO,CACL,CAAC,KAAK,CAAC;QACP,CAAC,KAAK,EAAE;QACR,CAAC,KAAK,GAAG;QACT,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,GAAG,CAAC;QAClC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC;QACxB,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;QACjC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC;QACxB,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACrC,CAAC,IAAI,GAAG,CACT,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,OAAe;IACpC,MAAM,KAAK,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,CAAC,yBAAyB;IAC7F,IAAI,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,cAAc;IAClG,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACnE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,KAAK,CAAC;IAChD,IAAI,WAAW,IAAI,MAAM,IAAI,WAAW,IAAI,MAAM;QAAE,OAAO,IAAI,CAAC,CAAC,yCAAyC;IAC1G,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC,CAAC,wBAAwB;IAC5E,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,MAAM,KAAK,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;IAC9C,OAAO,KAAK,CAAC;AACf,CAAC;AAED,mFAAmF;AACnF,MAAM,eAAgB,SAAQ,KAAK;CAAG;AAEtC,KAAK,UAAU,kBAAkB,CAAC,QAAgB;IAChD,8EAA8E;IAC9E,iFAAiF;IACjF,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC;QACxB,MAAM,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC9E,IAAI,OAAO,CAAW,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,eAAe,CAAC,aAAa,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;KAC1G,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAAc,EACd,cAA4B,kBAAkB;IAE9C,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IACvD,CAAC;IAED,gFAAgF;IAChF,gFAAgF;IAChF,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED,MAAM,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,QAAQ,KAAK,WAAW,IAAI,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,QAAQ,KAAK,0BAA0B,EAAE,CAAC;QAC3G,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,6BAA6B,EAAE,CAAC;IACnE,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnB,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACtG,CAAC;IAED,IAAI,SAAmB,CAAC;IACxB,IAAI,CAAC;QACH,SAAS,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,GAAI,GAAyB,CAAC,IAAI,CAAC;QAC7C,yEAAyE;QACzE,uEAAuE;QACvE,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;YACzE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,iCAAiC,EAAE,CAAC;QACvE,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,qCAAqC,IAAI,IAAI,SAAS,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAChH,CAAC;IACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,+BAA+B,EAAE,CAAC;IAC/F,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;QACtD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,6BAA6B,EAAE,CAAC;IACnE,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,MAAM,OAAO,WAAW;IAGO;IAFZ,SAAS,GAAG,IAAI,GAAG,EAAmC,CAAC;IAExE,YAA6B,cAA4B,kBAAkB;QAA9C,gBAAW,GAAX,WAAW,CAAmC;IAAG,CAAC;IAE/E,KAAK,CAAC,MAAc;QAClB,IAAI,QAAQ,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;YAC5B,QAAQ,GAAG,GAAG,GAAG,CAAC,QAAQ,KAAK,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnE,CAAC;QAAC,MAAM,CAAC;YACP,cAAc;QAChB,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC9C,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAE9B,MAAM,GAAG,GAAG,QAAQ,CAAC;QACrB,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;YAC7E,4EAA4E;YAC5E,sEAAsE;YACtE,IAAI,QAAQ,CAAC,SAAS;gBAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACnD,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACjC,OAAO,OAAO,CAAC;IACjB,CAAC;CACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class ExhaustivenessLedger {
|
|
2
|
+
entries = [];
|
|
3
|
+
recordNode(address, node, intent, disposition, opts = {}) {
|
|
4
|
+
this.entries.push({
|
|
5
|
+
addressId: address.id,
|
|
6
|
+
addressScope: address.scope,
|
|
7
|
+
stateLabel: address.label,
|
|
8
|
+
nodeId: node.id,
|
|
9
|
+
nodeKind: node.kind,
|
|
10
|
+
nodeLabel: node.label,
|
|
11
|
+
intent,
|
|
12
|
+
disposition,
|
|
13
|
+
...(opts.reason !== undefined ? { reason: opts.reason } : {}),
|
|
14
|
+
...(opts.childTargetsDiscovered !== undefined ? { childTargetsDiscovered: opts.childTargetsDiscovered } : {}),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
recordBlindSpot(address, blindSpot) {
|
|
18
|
+
this.entries.push({
|
|
19
|
+
addressId: address.id,
|
|
20
|
+
addressScope: address.scope,
|
|
21
|
+
stateLabel: address.label,
|
|
22
|
+
disposition: "blind-spot",
|
|
23
|
+
blindSpotKind: blindSpot.kind,
|
|
24
|
+
severity: blindSpot.severity,
|
|
25
|
+
message: blindSpot.message,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
recordMissingNode(address, nodeId, reason) {
|
|
29
|
+
this.entries.push({
|
|
30
|
+
addressId: address.id,
|
|
31
|
+
addressScope: address.scope,
|
|
32
|
+
stateLabel: address.label,
|
|
33
|
+
nodeId,
|
|
34
|
+
disposition: "blocked",
|
|
35
|
+
reason,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
snapshot() {
|
|
39
|
+
return this.entries.map((entry) => ({ ...entry }));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../src/core/ledger.ts"],"names":[],"mappings":"AAqBA,MAAM,OAAO,oBAAoB;IACd,OAAO,GAAgC,EAAE,CAAC;IAE3D,UAAU,CACR,OAAgB,EAChB,IAAiB,EACjB,MAAuB,EACvB,WAA6D,EAC7D,OAA6D,EAAE;QAE/D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YAChB,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,YAAY,EAAE,OAAO,CAAC,KAAK;YAC3B,UAAU,EAAE,OAAO,CAAC,KAAK;YACzB,MAAM,EAAE,IAAI,CAAC,EAAE;YACf,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,SAAS,EAAE,IAAI,CAAC,KAAK;YACrB,MAAM;YACN,WAAW;YACX,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,GAAG,CAAC,IAAI,CAAC,sBAAsB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9G,CAAC,CAAC;IACL,CAAC;IAED,eAAe,CAAC,OAAgB,EAAE,SAAoB;QACpD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YAChB,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,YAAY,EAAE,OAAO,CAAC,KAAK;YAC3B,UAAU,EAAE,OAAO,CAAC,KAAK;YACzB,WAAW,EAAE,YAAY;YACzB,aAAa,EAAE,SAAS,CAAC,IAAI;YAC7B,QAAQ,EAAE,SAAS,CAAC,QAAQ;YAC5B,OAAO,EAAE,SAAS,CAAC,OAAO;SAC3B,CAAC,CAAC;IACL,CAAC;IAED,iBAAiB,CAAC,OAAgB,EAAE,MAAc,EAAE,MAAc;QAChE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YAChB,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,YAAY,EAAE,OAAO,CAAC,KAAK;YAC3B,UAAU,EAAE,OAAO,CAAC,KAAK;YACzB,MAAM;YACN,WAAW,EAAE,SAAS;YACtB,MAAM;SACP,CAAC,CAAC;IACL,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;IACrD,CAAC;CACF"}
|