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.
Files changed (158) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +732 -0
  3. package/action.yml +279 -0
  4. package/comber.config.example.json +64 -0
  5. package/dist/api/classify.js +99 -0
  6. package/dist/api/classify.js.map +1 -0
  7. package/dist/api/driver.js +235 -0
  8. package/dist/api/driver.js.map +1 -0
  9. package/dist/api/graphql-driver.js +150 -0
  10. package/dist/api/graphql-driver.js.map +1 -0
  11. package/dist/api/graphql.js +115 -0
  12. package/dist/api/graphql.js.map +1 -0
  13. package/dist/api/har-driver.js +167 -0
  14. package/dist/api/har-driver.js.map +1 -0
  15. package/dist/api/har.js +100 -0
  16. package/dist/api/har.js.map +1 -0
  17. package/dist/api/load.js +83 -0
  18. package/dist/api/load.js.map +1 -0
  19. package/dist/api/openapi.js +159 -0
  20. package/dist/api/openapi.js.map +1 -0
  21. package/dist/api/request.js +141 -0
  22. package/dist/api/request.js.map +1 -0
  23. package/dist/api/schema.js +126 -0
  24. package/dist/api/schema.js.map +1 -0
  25. package/dist/cli.js +352 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/comment.js +164 -0
  28. package/dist/comment.js.map +1 -0
  29. package/dist/config.js +664 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/connectors/builtin/discord.js +55 -0
  32. package/dist/connectors/builtin/github-checks.js +53 -0
  33. package/dist/connectors/builtin/junit.js +70 -0
  34. package/dist/connectors/builtin/linear.js +58 -0
  35. package/dist/connectors/builtin/slack.js +60 -0
  36. package/dist/connectors/builtin/webhook.js +39 -0
  37. package/dist/connectors/index.js +3 -0
  38. package/dist/connectors/loader.js +72 -0
  39. package/dist/connectors/registry.js +28 -0
  40. package/dist/connectors/types.js +23 -0
  41. package/dist/core/action-gate.js +48 -0
  42. package/dist/core/baseline.js +85 -0
  43. package/dist/core/baseline.js.map +1 -0
  44. package/dist/core/blind-spots.js +22 -0
  45. package/dist/core/blind-spots.js.map +1 -0
  46. package/dist/core/contract-extras.js +1 -0
  47. package/dist/core/contract-extras.js.map +1 -0
  48. package/dist/core/contract.js +1 -0
  49. package/dist/core/contract.js.map +1 -0
  50. package/dist/core/coverage.js +70 -0
  51. package/dist/core/coverage.js.map +1 -0
  52. package/dist/core/crawl.js +229 -0
  53. package/dist/core/crawl.js.map +1 -0
  54. package/dist/core/egress.js +128 -0
  55. package/dist/core/egress.js.map +1 -0
  56. package/dist/core/ledger.js +41 -0
  57. package/dist/core/ledger.js.map +1 -0
  58. package/dist/core/net.js +39 -0
  59. package/dist/core/pipeline.js +191 -0
  60. package/dist/core/pipeline.js.map +1 -0
  61. package/dist/core/plan.js +121 -0
  62. package/dist/core/plan.js.map +1 -0
  63. package/dist/core/redact.js +240 -0
  64. package/dist/core/redact.js.map +1 -0
  65. package/dist/core/replay.js +263 -0
  66. package/dist/core/replay.js.map +1 -0
  67. package/dist/core/report.js +253 -0
  68. package/dist/core/report.js.map +1 -0
  69. package/dist/core/safety-judge.js +116 -0
  70. package/dist/core/safety.js +92 -0
  71. package/dist/core/safety.js.map +1 -0
  72. package/dist/core/signal-normalize.js +190 -0
  73. package/dist/core/signal-normalize.js.map +1 -0
  74. package/dist/core/signals.js +5 -0
  75. package/dist/core/signals.js.map +1 -0
  76. package/dist/core/triage.js +104 -0
  77. package/dist/core/triage.js.map +1 -0
  78. package/dist/core/types.js +1 -0
  79. package/dist/core/types.js.map +1 -0
  80. package/dist/desktop/desktop-profile.js +6 -0
  81. package/dist/desktop/desktop-profile.js.map +1 -0
  82. package/dist/desktop/driver.js +433 -0
  83. package/dist/desktop/driver.js.map +1 -0
  84. package/dist/desktop/evidence.js +206 -0
  85. package/dist/desktop/evidence.js.map +1 -0
  86. package/dist/desktop/macos/accessibility-runner.js +118 -0
  87. package/dist/desktop/macos/accessibility-runner.js.map +1 -0
  88. package/dist/desktop/macos/types.js +1 -0
  89. package/dist/desktop/macos/types.js.map +1 -0
  90. package/dist/desktop/preflight.js +107 -0
  91. package/dist/desktop/preflight.js.map +1 -0
  92. package/dist/desktop/proxy-proof.js +185 -0
  93. package/dist/desktop/proxy-proof.js.map +1 -0
  94. package/dist/desktop/view.js +348 -0
  95. package/dist/desktop/view.js.map +1 -0
  96. package/dist/desktop/windows/types.js +1 -0
  97. package/dist/desktop/windows/types.js.map +1 -0
  98. package/dist/desktop/windows/uia-runner.js +127 -0
  99. package/dist/desktop/windows/uia-runner.js.map +1 -0
  100. package/dist/native/apple.js +14 -0
  101. package/dist/native/apple.js.map +1 -0
  102. package/dist/native/device-profile.js +6 -0
  103. package/dist/native/device-profile.js.map +1 -0
  104. package/dist/native/driver.js +416 -0
  105. package/dist/native/driver.js.map +1 -0
  106. package/dist/native/evidence.js +164 -0
  107. package/dist/native/evidence.js.map +1 -0
  108. package/dist/native/load.js +9 -0
  109. package/dist/native/load.js.map +1 -0
  110. package/dist/native/maestro.js +153 -0
  111. package/dist/native/maestro.js.map +1 -0
  112. package/dist/native/preflight.js +72 -0
  113. package/dist/native/preflight.js.map +1 -0
  114. package/dist/native/proxy-proof.js +182 -0
  115. package/dist/native/proxy-proof.js.map +1 -0
  116. package/dist/native/samsung.js +34 -0
  117. package/dist/native/samsung.js.map +1 -0
  118. package/dist/native/signals.js +50 -0
  119. package/dist/native/signals.js.map +1 -0
  120. package/dist/native/view.js +139 -0
  121. package/dist/native/view.js.map +1 -0
  122. package/dist/profile.js +237 -0
  123. package/dist/profile.js.map +1 -0
  124. package/dist/sink.js +134 -0
  125. package/dist/sink.js.map +1 -0
  126. package/dist/sinks/file.js +49 -0
  127. package/dist/sinks/file.js.map +1 -0
  128. package/dist/sinks/http.js +38 -0
  129. package/dist/sinks/http.js.map +1 -0
  130. package/dist/sinks/index.js +5 -0
  131. package/dist/sinks/index.js.map +1 -0
  132. package/dist/sinks/multi.js +40 -0
  133. package/dist/sinks/multi.js.map +1 -0
  134. package/dist/sinks/paths.js +32 -0
  135. package/dist/sinks/paths.js.map +1 -0
  136. package/dist/sinks/record.js +58 -0
  137. package/dist/sinks/record.js.map +1 -0
  138. package/dist/sinks/types.js +1 -0
  139. package/dist/sinks/types.js.map +1 -0
  140. package/dist/web/breakage.js +210 -0
  141. package/dist/web/breakage.js.map +1 -0
  142. package/dist/web/controls.js +102 -0
  143. package/dist/web/controls.js.map +1 -0
  144. package/dist/web/driver.js +803 -0
  145. package/dist/web/driver.js.map +1 -0
  146. package/dist/web/elements.js +151 -0
  147. package/dist/web/elements.js.map +1 -0
  148. package/dist/web/fingerprint.js +13 -0
  149. package/dist/web/fingerprint.js.map +1 -0
  150. package/dist/web/forms.js +74 -0
  151. package/dist/web/forms.js.map +1 -0
  152. package/dist/web/links.js +194 -0
  153. package/dist/web/links.js.map +1 -0
  154. package/dist/web/types.js +1 -0
  155. package/dist/web/types.js.map +1 -0
  156. package/package.json +80 -0
  157. package/runtime/macos-accessibility-runner.js +275 -0
  158. package/runtime/windows-uia-runner.ps1 +539 -0
@@ -0,0 +1,39 @@
1
+ import { checkNetworkEgress } from "./egress.js";
2
+ /** Thrown when the egress guard blocks a target. The message names only the REASON,
3
+ * never the URL, so a token-bearing URL can't leak through an error string/log. */
4
+ export class EgressBlockedError extends Error {
5
+ constructor(reason) {
6
+ super(`network egress blocked: ${reason}`);
7
+ this.name = "EgressBlockedError";
8
+ }
9
+ }
10
+ const DEFAULT_TIMEOUT_MS = 15_000;
11
+ const MAX_REDIRECTS = 5;
12
+ /**
13
+ * `fetch` with the two baseline invariants applied. Follows redirects manually (bounded)
14
+ * when guarding so each hop is egress-checked; delegates to the platform's own redirect
15
+ * handling only in the `allowPrivate` escape hatch.
16
+ */
17
+ export async function guardedFetch(url, init = {}, opts = {}) {
18
+ const fetchImpl = opts.fetchImpl ?? fetch;
19
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
20
+ if (opts.allowPrivate) {
21
+ return await fetchImpl(url, { ...init, signal: init.signal ?? AbortSignal.timeout(timeoutMs) });
22
+ }
23
+ let current = url;
24
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
25
+ const decision = await checkNetworkEgress(current, opts.resolveHost);
26
+ if (!decision.allowed)
27
+ throw new EgressBlockedError(decision.reason ?? "blocked");
28
+ const res = await fetchImpl(current, {
29
+ ...init,
30
+ redirect: "manual",
31
+ signal: init.signal ?? AbortSignal.timeout(timeoutMs),
32
+ });
33
+ const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
34
+ if (!location)
35
+ return res;
36
+ current = new URL(location, current).toString();
37
+ }
38
+ throw new Error("too many redirects");
39
+ }
@@ -0,0 +1,191 @@
1
+ import { fallbackBlindSpots } from "./blind-spots.js";
2
+ import { actionKey, elementSignature, surfaceFingerprint, traversalIntentOf } from "./safety.js";
3
+ import { evaluateAction } from "./action-gate.js";
4
+ /**
5
+ * THE merge point: exercise EVERY safe node in a surface exactly once via the driver's
6
+ * type-correct `exercise`, draining breakage and restoring baseline between each. The
7
+ * ElKind dispatch now lives behind `driver.exercise`; the core only partitions
8
+ * (discover / skip / navigate / exercise) and restores. `navigates` nodes (web: links)
9
+ * are counted and FOLLOWED by the frontier, never clicked. When an exercised node REPORTS
10
+ * it navigated somewhere new (`ExerciseResult.discovered` — native tap-only screens), those
11
+ * Addresses are handed to the frontier via `deps.enqueue` (inScope + dedup applied there).
12
+ */
13
+ export async function exerciseState(snap, scope, deps) {
14
+ const { driver, cfg, triedSignatures, performedMutations, coverage, ledger } = deps;
15
+ coverage.blind(snap.blindSpots, snap.blindSpotDetails, snap.address.scope);
16
+ const blindSpotDetails = snap.blindSpotDetails ?? fallbackBlindSpots(snap.blindSpots, snap.address.scope, "surface could not be inspected");
17
+ for (const blindSpot of blindSpotDetails)
18
+ ledger?.recordBlindSpot(snap.address, blindSpot);
19
+ const baseAddr = snap.address;
20
+ const baselineFp = surfaceFingerprint(snap);
21
+ let acted = 0;
22
+ let regotoCount = 0;
23
+ for (const node of snap.nodes) {
24
+ if (deps.overBudget())
25
+ break;
26
+ const sig = elementSignature(node, scope);
27
+ if (triedSignatures.has(sig))
28
+ continue;
29
+ triedSignatures.add(sig);
30
+ coverage.discover(1);
31
+ const intent = traversalIntentOf(node);
32
+ // Partition (a partition, never a silent drop) — the effect-based action-floor. `deny`
33
+ // hard-skips (subsumes the old label-only denylist); `escalate`/`allow` fall through to
34
+ // exercise. In `advisory` mode `escalate` proceeds (coverage-preserving); in `strict`
35
+ // mode the floor already resolved it to `deny` (fail-closed). A future increment routes
36
+ // `escalate` to the LLM safety-judge before this fall-through.
37
+ const gate = evaluateAction(node, cfg);
38
+ if (gate.verdict === "deny") {
39
+ coverage.skipElement(gate.reason);
40
+ ledger?.recordNode(baseAddr, node, intent, "skipped", { reason: gate.reason });
41
+ continue;
42
+ }
43
+ // Escalation — an ambiguous mutation. Route to the out-of-band safety judge (Fable,
44
+ // fail-closed). No judge configured → advisory proceeds (permissive), strict fails closed.
45
+ // Symmetric with the `deny` path: a judge-denied node never reaches `driver.exercise`.
46
+ if (gate.verdict === "escalate") {
47
+ let allow;
48
+ let reason = gate.reason;
49
+ if (deps.safetyJudge) {
50
+ try {
51
+ const d = await deps.safetyJudge(node);
52
+ allow = d.allow;
53
+ if (!allow)
54
+ reason = d.reason;
55
+ }
56
+ catch {
57
+ allow = false; // a judge that throws is treated as unsafe — fail closed, skip one node
58
+ reason = "escalate-judge-threw";
59
+ }
60
+ }
61
+ else {
62
+ allow = cfg.actionGateMode !== "strict";
63
+ reason = "escalate-no-judge-fail-closed";
64
+ }
65
+ if (!allow) {
66
+ // A judge that is DEGRADED (infra error / refused-twice / threw) silently denies every
67
+ // ambiguous mutation while CI stays green — surface it as one deduped warn so a broken
68
+ // judge fails loudly. A genuine "unsafe" verdict is expected and stays quiet.
69
+ if (reason === "judge-error" || reason === "judge-refused" || reason === "escalate-judge-threw") {
70
+ deps.onSignal({
71
+ kind: "safety-judge-degraded",
72
+ severity: "warn",
73
+ message: "safety judge unavailable — ambiguous mutations are being denied (fail-closed); mutation coverage is degraded",
74
+ url: baseAddr.label ?? baseAddr.id,
75
+ signature: "safety-judge-degraded",
76
+ });
77
+ }
78
+ coverage.skipElement(reason);
79
+ ledger?.recordNode(baseAddr, node, intent, "skipped", { reason });
80
+ continue;
81
+ }
82
+ }
83
+ // Known-disabled (the web driver also folds file-inputs in here — never drive a
84
+ // native file picker) → counted as a skip, not a gap.
85
+ if (node.disabled) {
86
+ coverage.skipElement("disabled");
87
+ ledger?.recordNode(baseAddr, node, intent, "skipped", { reason: "disabled" });
88
+ continue;
89
+ }
90
+ if (intent === "follow") {
91
+ coverage.bumpExtra("followTargetsDiscovered");
92
+ coverage.bumpExtra("linksDiscovered");
93
+ ledger?.recordNode(baseAddr, node, intent, "followed");
94
+ continue;
95
+ }
96
+ if (intent === "branch")
97
+ coverage.bumpExtra("branchesDiscovered");
98
+ if (cfg.dryRun) {
99
+ coverage.skipElement("dry-run");
100
+ ledger?.recordNode(baseAddr, node, intent, "skipped", { reason: "dry-run" });
101
+ continue;
102
+ }
103
+ // Idempotency — a mutating key fires once even across near-duplicate states.
104
+ const akey = actionKey(node, scope);
105
+ if (performedMutations.has(akey)) {
106
+ coverage.skipElement("idempotent-skip");
107
+ ledger?.recordNode(baseAddr, node, intent, "skipped", { reason: "idempotent-skip" });
108
+ continue;
109
+ }
110
+ if (acted >= cfg.maxActionsPerState) {
111
+ coverage.skipElement("per-state-cap");
112
+ coverage.cappedNotExhaustive = true;
113
+ ledger?.recordNode(baseAddr, node, intent, "blocked", { reason: "per-state-cap" });
114
+ continue;
115
+ }
116
+ try {
117
+ const res = await driver.exercise(node);
118
+ if (res && res.signals)
119
+ res.signals.forEach(deps.onSignal);
120
+ const childTargetsDiscovered = res?.discovered?.length ?? 0;
121
+ // Frontier expansion from ACTING: states the tap/click navigated to that aren't
122
+ // reachable by link-following (native tap-only screens). enqueue applies inScope +
123
+ // dedup; the global caps bound it at the loop head.
124
+ if (res && res.discovered) {
125
+ coverage.bumpExtra("branchTargetsDiscovered", res.discovered.length);
126
+ if (deps.enqueue)
127
+ for (const a of res.discovered)
128
+ deps.enqueue(a);
129
+ }
130
+ performedMutations.add(akey);
131
+ acted++;
132
+ coverage.exercise();
133
+ if (intent === "branch")
134
+ coverage.bumpExtra("branchesExercised");
135
+ ledger?.recordNode(baseAddr, node, intent, intent === "branch" ? "branched" : "exercised", {
136
+ childTargetsDiscovered,
137
+ });
138
+ deps.recordExercise?.(node.id);
139
+ driver.drainSignals().forEach(deps.onSignal);
140
+ }
141
+ catch (e) {
142
+ deps.onSignal({
143
+ kind: "interaction-threw",
144
+ severity: "warn",
145
+ message: `${node.kind} "${node.label}" threw: ${String(e).slice(0, 120)}`,
146
+ url: baseAddr.label ?? baseAddr.id,
147
+ signature: `threw-${node.id}`.slice(0, 80),
148
+ });
149
+ ledger?.recordNode(baseAddr, node, intent, "blocked", { reason: "interaction-threw" });
150
+ }
151
+ const r = await restoreBaseline(driver, { address: baseAddr, fingerprint: baselineFp }, cfg);
152
+ if (r.mode === "regoto" && ++regotoCount > cfg.maxRegotosPerState) {
153
+ coverage.cappedNotExhaustive = true;
154
+ break;
155
+ }
156
+ if (!r.ok) {
157
+ deps.onSignal({
158
+ kind: "irrecoverable-state",
159
+ severity: "warn",
160
+ message: `could not restore baseline after "${node.label}" (${node.kind})`,
161
+ url: baseAddr.label ?? baseAddr.id,
162
+ signature: `restore-fail-${scope}`,
163
+ });
164
+ break; // frontier re-goto will re-establish this scope cleanly
165
+ }
166
+ }
167
+ return acted;
168
+ }
169
+ /**
170
+ * Restore to the baseline state with escalating cost: probe (none) → soft reset (back) →
171
+ * goto (regoto backstop). Uses EXACT surfaceFingerprint equality so a partial restore
172
+ * still escalates to the deterministic regoto. The soft-reset tier (web: Escape) is an
173
+ * optional driver capability; without it the loop falls straight through to regoto.
174
+ */
175
+ export async function restoreBaseline(driver, baseline, _cfg) {
176
+ if (driver.currentAddress().id !== baseline.address.id) {
177
+ await driver.goto(baseline.address);
178
+ return { ok: true, mode: "regoto" };
179
+ }
180
+ let snap = await driver.snapshot().catch(() => null);
181
+ if (snap && surfaceFingerprint(snap) === baseline.fingerprint)
182
+ return { ok: true, mode: "none" };
183
+ if (driver.softReset)
184
+ await driver.softReset();
185
+ snap = await driver.snapshot().catch(() => null);
186
+ if (snap && surfaceFingerprint(snap) === baseline.fingerprint)
187
+ return { ok: true, mode: "back" };
188
+ await driver.goto(baseline.address);
189
+ snap = await driver.snapshot().catch(() => null);
190
+ return { ok: snap ? surfaceFingerprint(snap) === baseline.fingerprint : false, mode: "regoto" };
191
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../src/core/pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ // The deterministic replay PLAN — Comber's "discover once, replay cheaply" record.
2
+ // A plan is a small, driver-AGNOSTIC trace of a normal crawl: for each visited state,
3
+ // its `Address`, the ordered `SurfaceNode.id`s that were exercised, the outbound
4
+ // Addresses that were followed, and the named probes that ran. It carries NO driver
5
+ // handles (those are live, target-specific, and unserializable) — only ids + addresses —
6
+ // so the SAME plan replays against the web AND api drivers. `core` owns this; the
7
+ // dependency arrow still points one way (plan.ts imports nothing from a driver package).
8
+ import { readFile } from "node:fs/promises";
9
+ /** Bump when the on-disk shape changes incompatibly; `parsePlan` rejects other versions. */
10
+ export const PLAN_VERSION = 1;
11
+ /** Copy only the identity fields of an Address (no stray keys leak into the plan). */
12
+ function leanAddress(a) {
13
+ return { id: a.id, scope: a.scope, ...(a.label !== undefined ? { label: a.label } : {}) };
14
+ }
15
+ /**
16
+ * Threaded through the orchestrator/pipeline during a NORMAL crawl. It is fed
17
+ * (begin-state → record probes/outbound/exercises) in execution order, then serialized
18
+ * to `plan.json`. Cheap and allocation-light: ids and addresses only.
19
+ */
20
+ export class PlanRecorder {
21
+ meta;
22
+ states = [];
23
+ current;
24
+ constructor(meta) {
25
+ this.meta = meta;
26
+ }
27
+ /** Open a new state record; subsequent record* calls attach to it. */
28
+ beginState(address) {
29
+ this.current = { address: leanAddress(address), exercised: [], outbound: [], probes: [] };
30
+ this.states.push(this.current);
31
+ }
32
+ /** Note a named probe that fired at the current state (deduped, order-preserving). */
33
+ recordProbe(name) {
34
+ if (this.current && !this.current.probes.includes(name))
35
+ this.current.probes.push(name);
36
+ }
37
+ /** Note an outbound address followed from the current state. */
38
+ recordOutbound(addr) {
39
+ if (this.current)
40
+ this.current.outbound.push(leanAddress(addr));
41
+ }
42
+ /** Note (in order) a node that was actually exercised at the current state. */
43
+ recordExercise(nodeId) {
44
+ if (this.current)
45
+ this.current.exercised.push(nodeId);
46
+ }
47
+ toPlan() {
48
+ return {
49
+ version: PLAN_VERSION,
50
+ target: this.meta.target,
51
+ startUrl: this.meta.startUrl,
52
+ createdAt: new Date().toISOString(),
53
+ states: this.states,
54
+ };
55
+ }
56
+ }
57
+ // -- loading / validation (a malformed plan is a clear error, never a silent mis-replay) --
58
+ function parseAddress(value, label) {
59
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
60
+ throw new Error(`${label} must be an object`);
61
+ }
62
+ const o = value;
63
+ if (typeof o.id !== "string" || o.id === "")
64
+ throw new Error(`${label}.id must be a non-empty string`);
65
+ if (typeof o.scope !== "string")
66
+ throw new Error(`${label}.scope must be a string`);
67
+ if (o.label !== undefined && typeof o.label !== "string")
68
+ throw new Error(`${label}.label must be a string`);
69
+ return { id: o.id, scope: o.scope, ...(o.label !== undefined ? { label: o.label } : {}) };
70
+ }
71
+ function parseStringArray(value, label) {
72
+ if (!Array.isArray(value) || value.some((x) => typeof x !== "string")) {
73
+ throw new Error(`${label} must be an array of strings`);
74
+ }
75
+ return [...value];
76
+ }
77
+ function parseState(value, label) {
78
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
79
+ throw new Error(`${label} must be an object`);
80
+ }
81
+ const o = value;
82
+ return {
83
+ address: parseAddress(o.address, `${label}.address`),
84
+ exercised: parseStringArray(o.exercised, `${label}.exercised`),
85
+ outbound: Array.isArray(o.outbound)
86
+ ? o.outbound.map((a, i) => parseAddress(a, `${label}.outbound[${i}]`))
87
+ : [],
88
+ probes: o.probes === undefined ? [] : parseStringArray(o.probes, `${label}.probes`),
89
+ };
90
+ }
91
+ /** Validate an untrusted value into a ReplayPlan (or throw a precise error). */
92
+ export function parsePlan(value, label) {
93
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
94
+ throw new Error(`${label}: plan must be a JSON object`);
95
+ }
96
+ const o = value;
97
+ if (o.version !== PLAN_VERSION) {
98
+ throw new Error(`${label}: unsupported plan version ${String(o.version)} (expected ${PLAN_VERSION})`);
99
+ }
100
+ if (!Array.isArray(o.states))
101
+ throw new Error(`${label}: plan.states must be an array`);
102
+ return {
103
+ version: PLAN_VERSION,
104
+ target: typeof o.target === "string" ? o.target : "web",
105
+ startUrl: typeof o.startUrl === "string" ? o.startUrl : "",
106
+ createdAt: typeof o.createdAt === "string" ? o.createdAt : "",
107
+ states: o.states.map((s, i) => parseState(s, `${label}.states[${i}]`)),
108
+ };
109
+ }
110
+ /** Read + parse a plan.json from disk. */
111
+ export async function loadPlan(path) {
112
+ const raw = await readFile(path, "utf8");
113
+ let json;
114
+ try {
115
+ json = JSON.parse(raw);
116
+ }
117
+ catch (e) {
118
+ throw new Error(`replay plan is not valid JSON (${path}): ${String(e)}`);
119
+ }
120
+ return parsePlan(json, path);
121
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/core/plan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,240 @@
1
+ const URL_PATTERN = /\b(?:https?|wss?|data):[^\s"'<>]+/gi;
2
+ const EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
3
+ const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
4
+ const BASIC_AUTH_PATTERN = /\bBasic\s+[A-Za-z0-9+/=_-]{8,}/gi;
5
+ // A JSON Web Token / Supabase service_role|anon key: three base64url segments.
6
+ const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}/g;
7
+ // Secret-shaped key=value / key: value / "key":"value" (cookies, sessions, JWTs,
8
+ // CSRF, auth, api keys). The optional quote before the separator catches JSON form.
9
+ const KEY_VALUE_PATTERN = /\b(token|secret|secrets|password|passwd|pwd|api[_-]?keys?|access[_-]?tokens?|refresh[_-]?tokens?|id[_-]?token|session[_-]?id|sessionid|sid|jwt|auth|authorization|csrf[_-]?token|csrf|xsrf|cookie|set-cookie|client[_-]?secret|private[_-]?key|signature|sig)"?\s*[:=]\s*"?([^&\s"';,]+)/gi;
10
+ // Belt-and-suspenders: keyword-FREE, high-precision provider token shapes. These catch
11
+ // a bare secret pasted into a finding message / log line with no `token=`/`Bearer`
12
+ // context. Each is specific (prefix + charset + length) → near-zero false-positive.
13
+ // Single source of truth; add new providers here.
14
+ const PROVIDER_TOKEN_PATTERNS = [
15
+ /-----BEGIN (?:[A-Z]+ )*PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z]+ )*PRIVATE KEY-----/g, // PEM
16
+ /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, // AWS access key id
17
+ /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g, // GitHub PAT/OAuth/app/refresh
18
+ /\bgithub_pat_[0-9A-Za-z_]{22,}\b/g, // GitHub fine-grained PAT
19
+ /\bglpat-[0-9A-Za-z_-]{20,}\b/g, // GitLab PAT
20
+ /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/gi, // Slack
21
+ /\b[sprk]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, // Stripe sk/pk/rk live|test
22
+ /\bwhsec_[A-Za-z0-9]{16,}\b/g, // Stripe webhook secret
23
+ /\bsk-(?:ant-)?[A-Za-z0-9_-]{20,}\b/g, // OpenAI / Anthropic
24
+ /\bAIza[0-9A-Za-z_-]{35}\b/g, // Google API key
25
+ /\bya29\.[0-9A-Za-z_-]{20,}\b/g, // Google OAuth token
26
+ /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, // SendGrid
27
+ /\bshp(?:at|ss|ca|pa)_[A-Fa-f0-9]{32}\b/g, // Shopify
28
+ /\bnpm_[0-9A-Za-z]{36}\b/g, // npm
29
+ /\bhf_[A-Za-z0-9]{10,}\b/g, // HuggingFace
30
+ /\bappl_[A-Za-z0-9]{10,}\b/g, // RevenueCat
31
+ /\bkey-[0-9a-zA-Z]{32}\b/g, // Mailgun
32
+ /\bdop_v1_[a-f0-9]{64}\b/g, // DigitalOcean
33
+ ];
34
+ function stripProviderTokens(text) {
35
+ let out = text;
36
+ for (const re of PROVIDER_TOKEN_PATTERNS)
37
+ out = out.replace(re, "[redacted]");
38
+ return out;
39
+ }
40
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
41
+ // A bare opaque token candidate: a long base64url-ish run. Decided per-match by
42
+ // `looksLikeBareSecret` so legit identifiers (UUIDs, git SHAs, decimal ids) survive.
43
+ const BARE_TOKEN_CANDIDATE = /[A-Za-z0-9_\-+/=]{32,}/g;
44
+ /** Shannon entropy in bits/char — random secrets are high; words/repeats are low. */
45
+ function shannonBits(s) {
46
+ const freq = new Map();
47
+ for (const ch of s)
48
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
49
+ let h = 0;
50
+ for (const n of freq.values()) {
51
+ const p = n / s.length;
52
+ h -= p * Math.log2(p);
53
+ }
54
+ return h;
55
+ }
56
+ /** True for a long, high-entropy, mixed-charset opaque string no keyword/provider
57
+ * pattern named — a probable bare secret. EXCLUDES UUIDs, pure-hex (git SHAs/hashes),
58
+ * and pure-decimal ids so legitimate identifiers are never masked. */
59
+ function looksLikeBareSecret(s) {
60
+ if (s.length < 32)
61
+ return false;
62
+ if (UUID_RE.test(s))
63
+ return false;
64
+ if (/^[0-9a-f]+$/i.test(s))
65
+ return false; // hex: git SHAs, hashes, hex ids
66
+ if (/^[0-9]+$/.test(s))
67
+ return false; // long decimal id
68
+ if (!/[A-Za-z]/.test(s) || !/[0-9]/.test(s))
69
+ return false; // must be mixed alnum
70
+ return shannonBits(s) >= 3.5;
71
+ }
72
+ function stripBareSecrets(text) {
73
+ return text.replace(BARE_TOKEN_CANDIDATE, (m) => (looksLikeBareSecret(m) ? "[redacted]" : m));
74
+ }
75
+ /** Mask opaque high-entropy path segments (reset/magic-link tokens live in the path,
76
+ * not just the query — which redactUrl already strips). Conservative: only a long,
77
+ * contiguous alnum/token-charset segment carrying BOTH letters and digits, so human
78
+ * slugs and UUID segments are left intact. */
79
+ function maskPathSegment(seg) {
80
+ return seg.length >= 24 && /[0-9]/.test(seg) && /[A-Za-z]/.test(seg) && /^[A-Za-z0-9._~-]+$/.test(seg)
81
+ ? "[redacted]"
82
+ : seg;
83
+ }
84
+ export function redactUrl(rawUrl) {
85
+ try {
86
+ const url = new URL(rawUrl);
87
+ if (url.protocol === "data:")
88
+ return "data:[redacted]";
89
+ if (!/^(?:https?|wss?):$/.test(url.protocol))
90
+ return `${url.protocol}[redacted]`;
91
+ // Drop userinfo + query + fragment entirely (the common secret carriers) and
92
+ // mask token-shaped path segments. Build the string manually so the [redacted]
93
+ // placeholder is not percent-encoded by the URL pathname setter.
94
+ const path = url.pathname.split("/").map(maskPathSegment).join("/");
95
+ return `${url.protocol}//${url.host}${path}`;
96
+ }
97
+ catch {
98
+ return stripProviderTokens(rawUrl
99
+ .replace(KEY_VALUE_PATTERN, "$1=[redacted]")
100
+ .replace(BEARER_PATTERN, "Bearer [redacted]")
101
+ .replace(EMAIL_PATTERN, "[email]"));
102
+ }
103
+ }
104
+ export function redactText(text) {
105
+ const structured = text
106
+ .replace(URL_PATTERN, (url) => redactUrl(url))
107
+ .replace(BASIC_AUTH_PATTERN, "Basic [redacted]")
108
+ .replace(BEARER_PATTERN, "Bearer [redacted]")
109
+ .replace(JWT_PATTERN, "[jwt]")
110
+ .replace(KEY_VALUE_PATTERN, "$1=[redacted]")
111
+ .replace(EMAIL_PATTERN, "[email]");
112
+ // Then the keyword-free catches: named provider shapes, then a conservative
113
+ // generic high-entropy sweep over whatever opaque tokens remain.
114
+ return stripBareSecrets(stripProviderTokens(structured));
115
+ }
116
+ function redactScope(scope) {
117
+ if (scope.startsWith("/")) {
118
+ const redacted = redactUrl(`https://comber.local${scope}`);
119
+ return redacted.replace("https://comber.local", "");
120
+ }
121
+ return redactText(redactUrl(scope));
122
+ }
123
+ export function redactFinding(finding) {
124
+ return {
125
+ ...finding,
126
+ url: redactUrl(finding.url),
127
+ message: redactText(finding.message),
128
+ verdict: finding.verdict
129
+ ? { real: finding.verdict.real, reason: redactText(finding.verdict.reason) }
130
+ : undefined,
131
+ };
132
+ }
133
+ function redactAuditRow(r) {
134
+ return {
135
+ ...r,
136
+ url: r.url ? redactUrl(r.url) : r.url,
137
+ foundOnUrl: r.foundOnUrl ? redactUrl(r.foundOnUrl) : r.foundOnUrl,
138
+ finalUrl: r.finalUrl ? redactUrl(r.finalUrl) : r.finalUrl,
139
+ reason: r.reason ? redactText(r.reason) : r.reason,
140
+ redirectChain: r.redirectChain ? r.redirectChain.map(redactUrl) : r.redirectChain,
141
+ };
142
+ }
143
+ function redactLedgerEntry(entry) {
144
+ return {
145
+ ...entry,
146
+ addressId: redactText(redactUrl(entry.addressId)),
147
+ addressScope: redactScope(entry.addressScope),
148
+ stateLabel: entry.stateLabel ? redactText(entry.stateLabel) : entry.stateLabel,
149
+ nodeLabel: entry.nodeLabel ? redactText(entry.nodeLabel) : entry.nodeLabel,
150
+ message: entry.message ? redactText(entry.message) : entry.message,
151
+ };
152
+ }
153
+ function redactCoverage(coverage) {
154
+ if (!coverage)
155
+ return coverage;
156
+ const blindSpots = Array.isArray(coverage.blindSpots) ? coverage.blindSpots : [];
157
+ return {
158
+ ...coverage,
159
+ blindSpots: blindSpots.map((blindSpot) => ({
160
+ ...blindSpot,
161
+ scope: redactScope(blindSpot.scope),
162
+ message: redactText(blindSpot.message),
163
+ })),
164
+ };
165
+ }
166
+ function redactEvidenceString(value) {
167
+ const pathMasked = value.includes("/") || value.includes("\\")
168
+ ? value
169
+ .split(/([/\\])/)
170
+ .map((segment) => (segment === "/" || segment === "\\" ? segment : maskPathSegment(segment)))
171
+ .join("")
172
+ : maskPathSegment(value);
173
+ return redactText(pathMasked);
174
+ }
175
+ function redactDeviceEvidence(value) {
176
+ if (typeof value === "string")
177
+ return redactEvidenceString(value);
178
+ if (Array.isArray(value))
179
+ return value.map((entry) => redactDeviceEvidence(entry));
180
+ if (value && typeof value === "object") {
181
+ const out = {};
182
+ for (const [key, nested] of Object.entries(value)) {
183
+ out[key] = redactDeviceEvidence(nested);
184
+ }
185
+ return out;
186
+ }
187
+ return value;
188
+ }
189
+ /**
190
+ * Deep-redact a driver's `extra` bag. `linkAudit` rows carry URLs, so they get URL-aware
191
+ * row redaction; EVERY OTHER key is deep-redacted at the string leaves by default. This is
192
+ * fail-closed: numeric audit bags (apiAudit/graphqlAudit counts) pass through untouched,
193
+ * while any string-bearing sub-bag — deviceEvidence, desktopEvidence, nativeReadiness,
194
+ * desktopReadiness, and anything a future driver adds — has its launch/verify commands and
195
+ * app paths scrubbed before it can reach the HTML report or the (possibly public) PR comment.
196
+ * (Previously only `linkAudit` + `deviceEvidence` were handled, so desktop/readiness bags
197
+ * leaked through unredacted.)
198
+ */
199
+ function redactExtra(extra) {
200
+ if (!extra)
201
+ return extra;
202
+ const out = {};
203
+ for (const [key, value] of Object.entries(extra)) {
204
+ if (key === "linkAudit" && Array.isArray(value)) {
205
+ out[key] = value.map(redactAuditRow);
206
+ }
207
+ else {
208
+ out[key] = redactDeviceEvidence(value);
209
+ }
210
+ }
211
+ return out;
212
+ }
213
+ /**
214
+ * Return a deep-redacted copy of a crawl result, safe to persist (result.json),
215
+ * render (HTML report), or post to a (possibly public) PR comment. Every
216
+ * field that can carry crawled-app secrets — finding urls/messages, the start
217
+ * URL, visited URLs, link-audit URLs, and the baseline diff's findings — is
218
+ * scrubbed. `signature` is intentionally preserved so baseline diffing across
219
+ * runs (which keys on signature, not url/message) still works against a
220
+ * redacted baseline.
221
+ */
222
+ export function redactResult(result) {
223
+ return {
224
+ ...result,
225
+ startUrl: redactUrl(result.startUrl),
226
+ visitedUrls: result.visitedUrls.map(redactUrl),
227
+ findings: result.findings.map(redactFinding),
228
+ coverage: redactCoverage(result.coverage),
229
+ exhaustivenessLedger: (result.exhaustivenessLedger ?? []).map(redactLedgerEntry),
230
+ extra: redactExtra(result.extra),
231
+ baseline: result.baseline
232
+ ? {
233
+ ...result.baseline,
234
+ newFindings: result.baseline.newFindings.map(redactFinding),
235
+ resolvedFindings: result.baseline.resolvedFindings.map(redactFinding),
236
+ unchangedFindings: result.baseline.unchangedFindings.map(redactFinding),
237
+ }
238
+ : result.baseline,
239
+ };
240
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/core/redact.ts"],"names":[],"mappings":"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"}