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,139 @@
|
|
|
1
|
+
// View-hierarchy → Surface mapping. This is the native analogue of the web driver's
|
|
2
|
+
// EXTRACT_INTERACTIVE / classify pipeline: it flattens a NativeView tree into the target-agnostic
|
|
3
|
+
// `SurfaceNode[]` the core exercises, classifies each control's `kind`, and derives the stable
|
|
4
|
+
// per-screen structural signature the driver uses for drift/restore detection.
|
|
5
|
+
//
|
|
6
|
+
// Dependency arrow: imports `core` contract types + the sibling `maestro` transport types ONLY.
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
/** Kinds whose primary purpose is changing native UI state. They are activated as branches;
|
|
9
|
+
* `navigates` remains only for legacy coverage/report compatibility. */
|
|
10
|
+
const BRANCH_KINDS = new Set(["tab", "link", "cell"]);
|
|
11
|
+
const RE_INPUT = /(edittext|textfield|textview.*editable|securetextfield|searchfield|uitextfield|input)/i;
|
|
12
|
+
const RE_TOGGLE = /(switch|checkbox|toggle|radiobutton|uiswitch)/i;
|
|
13
|
+
const RE_TAB = /(tab|bottomnav|segment)/i;
|
|
14
|
+
const RE_BUTTON = /(button|imagebutton|fab|uibutton)/i;
|
|
15
|
+
const RE_CELL = /(recyclerview\.|listview|tablerow|cell|uitableviewcell|uicollectionviewcell|row)/i;
|
|
16
|
+
const RE_LINK = /(textview.*link|hyperlink|link)/i;
|
|
17
|
+
/** Map a native node's type/flags to a control kind. Text entry > toggle > tab > cell > button. */
|
|
18
|
+
export function classifyControl(node) {
|
|
19
|
+
const type = node.type ?? "";
|
|
20
|
+
if (node.focusable && RE_INPUT.test(type))
|
|
21
|
+
return "text-input";
|
|
22
|
+
if (RE_INPUT.test(type))
|
|
23
|
+
return "text-input";
|
|
24
|
+
if (RE_TOGGLE.test(type))
|
|
25
|
+
return "toggle";
|
|
26
|
+
if (RE_TAB.test(type))
|
|
27
|
+
return "tab";
|
|
28
|
+
if (RE_LINK.test(type))
|
|
29
|
+
return "link";
|
|
30
|
+
if (RE_CELL.test(type))
|
|
31
|
+
return "cell";
|
|
32
|
+
if (RE_BUTTON.test(type))
|
|
33
|
+
return "button";
|
|
34
|
+
return "control";
|
|
35
|
+
}
|
|
36
|
+
/** A node is an exercisable CONTROL when it is clickable/focusable or its type reads as a control,
|
|
37
|
+
* AND it carries an identity (a stable test id OR visible text) the driver can target. */
|
|
38
|
+
function isControlNode(node) {
|
|
39
|
+
const type = node.type ?? "";
|
|
40
|
+
const looksInteractive = node.clickable === true ||
|
|
41
|
+
node.focusable === true ||
|
|
42
|
+
RE_INPUT.test(type) ||
|
|
43
|
+
RE_TOGGLE.test(type) ||
|
|
44
|
+
RE_TAB.test(type) ||
|
|
45
|
+
RE_BUTTON.test(type) ||
|
|
46
|
+
RE_CELL.test(type) ||
|
|
47
|
+
RE_LINK.test(type);
|
|
48
|
+
const hasIdentity = Boolean(node.id) || Boolean((node.text ?? "").trim());
|
|
49
|
+
return looksInteractive && hasIdentity;
|
|
50
|
+
}
|
|
51
|
+
/** Depth-first flatten of the tree, retaining a structural path so node ids are stable+unique. */
|
|
52
|
+
function* walk(node, path) {
|
|
53
|
+
yield { node, path };
|
|
54
|
+
const kids = node.children ?? [];
|
|
55
|
+
for (let i = 0; i < kids.length; i++) {
|
|
56
|
+
yield* walk(kids[i], `${path}/${i}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** The stable cross-state identity for a control: its test id when present, else a text+path key
|
|
60
|
+
* (text is i18n-fragile but the only handle a id-less node has; path disambiguates duplicates). */
|
|
61
|
+
function nodeIdentity(node, path) {
|
|
62
|
+
if (node.id)
|
|
63
|
+
return node.id;
|
|
64
|
+
const text = (node.text ?? "").trim().toLowerCase().slice(0, 40);
|
|
65
|
+
return `text:${text}@${path}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Flatten a NativeView into target-agnostic SurfaceNodes:
|
|
69
|
+
* • kind = native control type (classifyControl)
|
|
70
|
+
* • id = stable test identifier (resource-id / a11y id / testTag), else text+path
|
|
71
|
+
* • label = visible text / accessibility label
|
|
72
|
+
* • handle = the raw NativeViewNode (round-tripped into exercise(); the core never reads it)
|
|
73
|
+
* • disabled = enabled === false
|
|
74
|
+
* • intent = branch for native nav-affordance kinds (tab/link/cell), exercise otherwise
|
|
75
|
+
* • navigates= legacy flag retained for branch nav-affordance kinds
|
|
76
|
+
*/
|
|
77
|
+
export function viewToSurfaceNodes(view) {
|
|
78
|
+
const out = [];
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
for (const { node, path } of walk(view.root, "0")) {
|
|
81
|
+
if (!isControlNode(node))
|
|
82
|
+
continue;
|
|
83
|
+
const id = nodeIdentity(node, path);
|
|
84
|
+
if (seen.has(id))
|
|
85
|
+
continue;
|
|
86
|
+
seen.add(id);
|
|
87
|
+
const kind = classifyControl(node);
|
|
88
|
+
const isBranch = BRANCH_KINDS.has(kind);
|
|
89
|
+
out.push({
|
|
90
|
+
id,
|
|
91
|
+
kind,
|
|
92
|
+
label: (node.text ?? "").trim(),
|
|
93
|
+
handle: node,
|
|
94
|
+
disabled: node.enabled === false,
|
|
95
|
+
intent: isBranch ? "branch" : "exercise",
|
|
96
|
+
navigates: isBranch,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/** Count subtrees the runner saw but couldn't describe (a node with no id, no text, and no type
|
|
102
|
+
* but with children is an opaque container — an honest coverage blind spot, never folded into 100%). */
|
|
103
|
+
export function blindSpotsOf(view) {
|
|
104
|
+
return blindSpotDetailsOf(view, screenNameOf(view, "native")).length;
|
|
105
|
+
}
|
|
106
|
+
export function blindSpotDetailsOf(view, scope) {
|
|
107
|
+
const out = [];
|
|
108
|
+
let index = 0;
|
|
109
|
+
for (const { node } of walk(view.root, "0")) {
|
|
110
|
+
if (!node.id && !(node.text ?? "").trim() && !node.type && (node.children?.length ?? 0) > 0) {
|
|
111
|
+
index++;
|
|
112
|
+
out.push({
|
|
113
|
+
kind: "native-opaque-subtree",
|
|
114
|
+
severity: "blocker-for-exhaustive",
|
|
115
|
+
scope,
|
|
116
|
+
message: `native view subtree ${index} could not be described by accessibility metadata`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Structural screen signature — the driver's PRIVATE drift/restore fingerprint (distinct from the
|
|
124
|
+
* core's stateFingerprint). Hashes the sorted control set so a navigation that changes the screen
|
|
125
|
+
* is detectable, while a benign re-render (same controls) is not. Screen name is included so two
|
|
126
|
+
* structurally-identical screens with different titles still read as different.
|
|
127
|
+
*/
|
|
128
|
+
export function screenSignature(view) {
|
|
129
|
+
const nodes = viewToSurfaceNodes(view);
|
|
130
|
+
const detail = nodes
|
|
131
|
+
.map((n) => `${n.kind}:${n.id}:${(n.label || "").toLowerCase().slice(0, 40)}:${n.disabled ? 1 : 0}`)
|
|
132
|
+
.sort()
|
|
133
|
+
.join("|");
|
|
134
|
+
return createHash("sha1").update(`${view.screen ?? ""}|${detail}`).digest("hex").slice(0, 16);
|
|
135
|
+
}
|
|
136
|
+
/** Best-effort human screen name for the report (inferred screen, else a scheme-less hint). */
|
|
137
|
+
export function screenNameOf(view, fallback) {
|
|
138
|
+
return (view.screen ?? "").trim() || fallback;
|
|
139
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/native/view.ts"],"names":[],"mappings":"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"}
|
package/dist/profile.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
function flag(argv, name) {
|
|
3
|
+
const hit = argv.find((arg) => arg.startsWith(`--${name}=`));
|
|
4
|
+
if (hit)
|
|
5
|
+
return hit.slice(name.length + 3);
|
|
6
|
+
const idx = argv.indexOf(`--${name}`);
|
|
7
|
+
if (idx >= 0) {
|
|
8
|
+
const next = argv[idx + 1];
|
|
9
|
+
if (next && !next.startsWith("--"))
|
|
10
|
+
return next;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
async function fileExists(path) {
|
|
15
|
+
try {
|
|
16
|
+
await access(path);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function asObject(value, label) {
|
|
24
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
25
|
+
throw new Error(`${label} must be a JSON object`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function optionalString(obj, key) {
|
|
30
|
+
const value = obj[key];
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
34
|
+
throw new Error(`${key} must be a non-empty string`);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function optionalNumber(obj, key) {
|
|
38
|
+
const value = obj[key];
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
42
|
+
throw new Error(`${key} must be a number`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function optionalBoolean(obj, key) {
|
|
46
|
+
const value = obj[key];
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
if (typeof value !== "boolean")
|
|
50
|
+
throw new Error(`${key} must be a boolean`);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function optionalTarget(obj, key) {
|
|
54
|
+
const value = obj[key];
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (value !== "web" && value !== "api" && value !== "native" && value !== "desktop") {
|
|
58
|
+
throw new Error(`${key} must be "web", "api", "native", or "desktop"`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function optionalPlatform(obj, key) {
|
|
63
|
+
const value = obj[key];
|
|
64
|
+
if (value === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
if (value !== "android" && value !== "ios" && value !== "macos" && value !== "windows") {
|
|
67
|
+
throw new Error(`${key} must be "android", "ios", "macos", or "windows"`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function optionalAutomationBackend(obj, key) {
|
|
72
|
+
const value = obj[key];
|
|
73
|
+
if (value === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (value !== "accessibility" && value !== "xctest" && value !== "uia" && value !== "unknown") {
|
|
76
|
+
throw new Error(`${key} must be "accessibility", "xctest", "uia", or "unknown"`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function optionalStringArray(obj, key) {
|
|
81
|
+
const value = obj[key];
|
|
82
|
+
if (value === undefined)
|
|
83
|
+
return undefined;
|
|
84
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
85
|
+
throw new Error(`${key} must be an array of non-empty strings`);
|
|
86
|
+
}
|
|
87
|
+
return [...value];
|
|
88
|
+
}
|
|
89
|
+
/** A flat object of string→string (role→token for the multi-role auth set). */
|
|
90
|
+
function optionalStringRecord(obj, key) {
|
|
91
|
+
const value = obj[key];
|
|
92
|
+
if (value === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
95
|
+
throw new Error(`${key} must be an object of string values`);
|
|
96
|
+
}
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [k, v] of Object.entries(value)) {
|
|
99
|
+
if (typeof v !== "string")
|
|
100
|
+
throw new Error(`${key}.${k} must be a string`);
|
|
101
|
+
out[k] = v;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
/** Parse the `connectors` array. Each entry needs a string `use` (built-in name or module
|
|
106
|
+
* specifier); `on` (if present) must be a valid severity gate; all other keys pass through as
|
|
107
|
+
* non-secret connector settings. Secrets are never accepted here — they come from env. */
|
|
108
|
+
function optionalConnectors(obj, key) {
|
|
109
|
+
const value = obj[key];
|
|
110
|
+
if (value === undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
if (!Array.isArray(value))
|
|
113
|
+
throw new Error(`${key} must be an array of connector objects`);
|
|
114
|
+
return value.map((raw, i) => {
|
|
115
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
116
|
+
throw new Error(`${key}[${i}] must be an object`);
|
|
117
|
+
}
|
|
118
|
+
const entry = raw;
|
|
119
|
+
if (typeof entry.use !== "string" || entry.use.trim() === "") {
|
|
120
|
+
throw new Error(`${key}[${i}].use must be a non-empty string (a built-in name or module specifier)`);
|
|
121
|
+
}
|
|
122
|
+
if (entry.on !== undefined && entry.on !== "always" && entry.on !== "warn" && entry.on !== "fail") {
|
|
123
|
+
throw new Error(`${key}[${i}].on must be "always", "warn", or "fail"`);
|
|
124
|
+
}
|
|
125
|
+
return { ...entry, use: entry.use, on: entry.on };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function firstString(obj, keys) {
|
|
129
|
+
for (const key of keys) {
|
|
130
|
+
const value = optionalString(obj, key);
|
|
131
|
+
if (value !== undefined)
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
function firstNumber(obj, keys) {
|
|
137
|
+
for (const key of keys) {
|
|
138
|
+
const value = optionalNumber(obj, key);
|
|
139
|
+
if (value !== undefined)
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
export function parseProfile(value, name) {
|
|
145
|
+
const obj = asObject(value, name ? `profile ${name}` : "profile");
|
|
146
|
+
return {
|
|
147
|
+
name,
|
|
148
|
+
target: optionalTarget(obj, "target"),
|
|
149
|
+
startUrl: firstString(obj, ["startUrl", "url"]),
|
|
150
|
+
openapi: firstString(obj, ["openapi", "openapiSource"]),
|
|
151
|
+
graphql: firstString(obj, ["graphql", "graphqlUrl"]),
|
|
152
|
+
har: firstString(obj, ["har", "harPath"]),
|
|
153
|
+
apiBase: firstString(obj, ["apiBase", "base"]),
|
|
154
|
+
seed: firstString(obj, ["seed", "apiSeedDataPath", "seedDataPath"]),
|
|
155
|
+
apiAuthProfiles: optionalStringRecord(obj, "apiAuthProfiles"),
|
|
156
|
+
ownerScoped: optionalStringArray(obj, "ownerScoped"),
|
|
157
|
+
roleSeed: firstString(obj, ["roleSeed", "roleSeedPath"]),
|
|
158
|
+
app: firstString(obj, ["app", "appId", "nativeApp"]),
|
|
159
|
+
appPath: optionalString(obj, "appPath"),
|
|
160
|
+
processName: optionalString(obj, "processName"),
|
|
161
|
+
platform: optionalPlatform(obj, "platform"),
|
|
162
|
+
provider: optionalString(obj, "provider"),
|
|
163
|
+
providerRunId: optionalString(obj, "providerRunId"),
|
|
164
|
+
launchCommand: optionalString(obj, "launchCommand"),
|
|
165
|
+
verificationCommand: optionalString(obj, "verificationCommand"),
|
|
166
|
+
automationBackend: optionalAutomationBackend(obj, "automationBackend"),
|
|
167
|
+
evidenceAccessibilityTrees: optionalStringArray(obj, "evidenceAccessibilityTrees"),
|
|
168
|
+
evidenceAutomationTrees: optionalStringArray(obj, "evidenceAutomationTrees"),
|
|
169
|
+
evidenceScreenshots: optionalStringArray(obj, "evidenceScreenshots"),
|
|
170
|
+
evidenceVideos: optionalStringArray(obj, "evidenceVideos"),
|
|
171
|
+
evidenceLogs: optionalStringArray(obj, "evidenceLogs"),
|
|
172
|
+
evidenceCrashLogs: optionalStringArray(obj, "evidenceCrashLogs"),
|
|
173
|
+
accessibilityPermissionVerified: optionalBoolean(obj, "accessibilityPermissionVerified"),
|
|
174
|
+
egressProxyVerified: optionalBoolean(obj, "egressProxyVerified"),
|
|
175
|
+
egressProxyProvider: optionalString(obj, "egressProxyProvider"),
|
|
176
|
+
egressProxyCommand: optionalString(obj, "egressProxyCommand"),
|
|
177
|
+
egressProxyArtifact: optionalString(obj, "egressProxyArtifact"),
|
|
178
|
+
egressProxyProof: optionalString(obj, "egressProxyProof"),
|
|
179
|
+
device: firstString(obj, ["device", "deviceId"]),
|
|
180
|
+
deeplinks: optionalStringArray(obj, "deeplinks"),
|
|
181
|
+
allowedOrigins: optionalStringArray(obj, "allowedOrigins"),
|
|
182
|
+
maxStates: optionalNumber(obj, "maxStates"),
|
|
183
|
+
maxActions: optionalNumber(obj, "maxActions"),
|
|
184
|
+
maxWallClockMs: firstNumber(obj, ["maxWallClockMs", "maxMs"]),
|
|
185
|
+
maxTokenBudget: firstNumber(obj, ["maxTokenBudget", "maxTokens"]),
|
|
186
|
+
maxActionsPerState: firstNumber(obj, ["maxActionsPerState", "maxPerState"]),
|
|
187
|
+
maxRegotosPerState: firstNumber(obj, ["maxRegotosPerState", "maxRegotos"]),
|
|
188
|
+
maxVisitsPerRoute: optionalNumber(obj, "maxVisitsPerRoute"),
|
|
189
|
+
nearDupThreshold: firstNumber(obj, ["nearDupThreshold", "nearDup"]),
|
|
190
|
+
externalLinks: optionalBoolean(obj, "externalLinks"),
|
|
191
|
+
maxExternalLinks: optionalNumber(obj, "maxExternalLinks"),
|
|
192
|
+
linkTimeoutMs: optionalNumber(obj, "linkTimeoutMs"),
|
|
193
|
+
linkConcurrency: optionalNumber(obj, "linkConcurrency"),
|
|
194
|
+
crossOrigin: optionalBoolean(obj, "crossOrigin"),
|
|
195
|
+
llm: optionalBoolean(obj, "llm"),
|
|
196
|
+
deterministic: optionalBoolean(obj, "deterministic"),
|
|
197
|
+
dryRun: optionalBoolean(obj, "dryRun"),
|
|
198
|
+
allowWrites: optionalBoolean(obj, "allowWrites"),
|
|
199
|
+
storageStatePath: firstString(obj, ["storageStatePath", "storageState"]),
|
|
200
|
+
baselinePath: firstString(obj, ["baselinePath", "baseline"]),
|
|
201
|
+
prCommentPath: firstString(obj, ["prCommentPath", "prComment"]),
|
|
202
|
+
headless: optionalBoolean(obj, "headless"),
|
|
203
|
+
outDir: optionalString(obj, "outDir"),
|
|
204
|
+
denylist: optionalStringArray(obj, "denylist"),
|
|
205
|
+
destructiveHrefDenylist: optionalStringArray(obj, "destructiveHrefDenylist"),
|
|
206
|
+
replay: firstString(obj, ["replay", "replayPlan", "replayPlanPath"]),
|
|
207
|
+
replayOut: firstString(obj, ["replayOut", "replayOutPath"]),
|
|
208
|
+
connectors: optionalConnectors(obj, "connectors"),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export async function loadProfileFromArgv(argv) {
|
|
212
|
+
const explicitConfigPath = flag(argv, "config");
|
|
213
|
+
const profileName = flag(argv, "profile");
|
|
214
|
+
const configPath = explicitConfigPath ?? "comber.config.json";
|
|
215
|
+
if (!(await fileExists(configPath))) {
|
|
216
|
+
if (explicitConfigPath || profileName)
|
|
217
|
+
throw new Error(`Comber config not found: ${configPath}`);
|
|
218
|
+
return { profile: {} };
|
|
219
|
+
}
|
|
220
|
+
const raw = await readFile(configPath, "utf8");
|
|
221
|
+
const root = asObject(JSON.parse(raw), configPath);
|
|
222
|
+
const profilesValue = root.profiles;
|
|
223
|
+
if (profilesValue === undefined) {
|
|
224
|
+
return { configPath, profile: parseProfile(root) };
|
|
225
|
+
}
|
|
226
|
+
const profiles = asObject(profilesValue, "profiles");
|
|
227
|
+
const selected = profileName ?? optionalString(root, "defaultProfile");
|
|
228
|
+
if (!selected)
|
|
229
|
+
return { configPath, profile: {} };
|
|
230
|
+
// Own-property check (not `in`) so a profile named like a prototype key
|
|
231
|
+
// ("constructor", "toString", "__proto__") resolves correctly / reports
|
|
232
|
+
// not-found instead of matching the inherited member.
|
|
233
|
+
if (!Object.prototype.hasOwnProperty.call(profiles, selected)) {
|
|
234
|
+
throw new Error(`Profile not found in ${configPath}: ${selected}`);
|
|
235
|
+
}
|
|
236
|
+
return { configPath, profile: parseProfile(profiles[selected], selected) };
|
|
237
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile.js","sourceRoot":"","sources":["../src/profile.ts"],"names":[],"mappings":"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"}
|
package/dist/sink.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { redactText, redactUrl } from "./core/redact.js";
|
|
2
|
+
function counts(result) {
|
|
3
|
+
return {
|
|
4
|
+
fail: result.findings.filter((f) => f.severity === "fail").length,
|
|
5
|
+
warn: result.findings.filter((f) => f.severity === "warn").length,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** Writes qa_runs + qa_findings via Supabase PostgREST (service-role key). */
|
|
9
|
+
class SupabaseSink {
|
|
10
|
+
url;
|
|
11
|
+
key;
|
|
12
|
+
name = "supabase";
|
|
13
|
+
constructor(url, key) {
|
|
14
|
+
this.url = url;
|
|
15
|
+
this.key = key;
|
|
16
|
+
}
|
|
17
|
+
async recordRun(result, ctx) {
|
|
18
|
+
const headers = {
|
|
19
|
+
apikey: this.key,
|
|
20
|
+
Authorization: `Bearer ${this.key}`,
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
Prefer: "resolution=merge-duplicates",
|
|
23
|
+
};
|
|
24
|
+
const cov = result.coverage;
|
|
25
|
+
const c = counts(result);
|
|
26
|
+
const run = {
|
|
27
|
+
id: ctx.runId,
|
|
28
|
+
app: ctx.app,
|
|
29
|
+
target_url: redactUrl(result.startUrl),
|
|
30
|
+
finished_at: new Date().toISOString(),
|
|
31
|
+
mode: ctx.mode,
|
|
32
|
+
states: result.visitedStates,
|
|
33
|
+
actions: result.actionsTaken,
|
|
34
|
+
element_coverage_pct: cov?.elementCoveragePct,
|
|
35
|
+
controls: cov?.extra.controlsExercised,
|
|
36
|
+
modals: cov?.extra.modalsOpened,
|
|
37
|
+
forms_tested: cov?.extra.formsTested,
|
|
38
|
+
links_checked: cov?.extra.linksChecked,
|
|
39
|
+
links_broken: cov?.extra.linksBroken,
|
|
40
|
+
frame_blind_spots: cov?.frameBlindSpots,
|
|
41
|
+
fail_count: c.fail,
|
|
42
|
+
warn_count: c.warn,
|
|
43
|
+
token_in: result.tokenSpend.input,
|
|
44
|
+
token_out: result.tokenSpend.output,
|
|
45
|
+
capped_by: result.cappedBy,
|
|
46
|
+
git_sha: ctx.gitSha,
|
|
47
|
+
};
|
|
48
|
+
const r1 = await fetch(`${this.url}/rest/v1/qa_runs`, { method: "POST", headers, body: JSON.stringify(run) });
|
|
49
|
+
if (!r1.ok)
|
|
50
|
+
throw new Error(`qa_runs ${r1.status} ${(await r1.text()).slice(0, 120)}`);
|
|
51
|
+
if (result.findings.length) {
|
|
52
|
+
const rows = result.findings.map((f) => ({
|
|
53
|
+
run_id: ctx.runId,
|
|
54
|
+
severity: f.severity,
|
|
55
|
+
kind: f.kind,
|
|
56
|
+
signature: f.signature,
|
|
57
|
+
url: redactUrl(f.url),
|
|
58
|
+
message: redactText(f.message),
|
|
59
|
+
count: f.count,
|
|
60
|
+
verdict_real: f.verdict?.real,
|
|
61
|
+
verdict_reason: f.verdict?.reason ? redactText(f.verdict.reason) : undefined,
|
|
62
|
+
}));
|
|
63
|
+
const r2 = await fetch(`${this.url}/rest/v1/qa_findings`, { method: "POST", headers, body: JSON.stringify(rows) });
|
|
64
|
+
if (!r2.ok)
|
|
65
|
+
throw new Error(`qa_findings ${r2.status} ${(await r2.text()).slice(0, 120)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Fires a comber_run event to PostHog. */
|
|
70
|
+
class PostHogSink {
|
|
71
|
+
host;
|
|
72
|
+
key;
|
|
73
|
+
name = "posthog";
|
|
74
|
+
constructor(host, key) {
|
|
75
|
+
this.host = host;
|
|
76
|
+
this.key = key;
|
|
77
|
+
}
|
|
78
|
+
async recordRun(result, ctx) {
|
|
79
|
+
const c = counts(result);
|
|
80
|
+
await fetch(`${this.host.replace(/\/$/, "")}/i/v0/e/`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "Content-Type": "application/json" },
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
api_key: this.key,
|
|
85
|
+
event: "comber_run",
|
|
86
|
+
distinct_id: ctx.app ?? "comber",
|
|
87
|
+
properties: {
|
|
88
|
+
target: redactUrl(result.startUrl),
|
|
89
|
+
fails: c.fail,
|
|
90
|
+
warns: c.warn,
|
|
91
|
+
coverage_pct: result.coverage?.elementCoveragePct,
|
|
92
|
+
states: result.visitedStates,
|
|
93
|
+
links_broken: result.coverage?.extra.linksBroken,
|
|
94
|
+
mode: ctx.mode,
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Fan-out sink. Opt-in via env; an empty MultiSink is a no-op so env-unset runs stay
|
|
102
|
+
* byte-identical to local-only today. Each sink is error-swallowed individually —
|
|
103
|
+
* persistence NEVER breaks a crawl. SupabaseSink fails LOUD on an anon-shaped key
|
|
104
|
+
* (silent write-loss is worse than a crash). R2 (screenshots/trace) + Sentry (one issue
|
|
105
|
+
* per fail signature) are scaffolded — wire when the dashboard needs them.
|
|
106
|
+
*/
|
|
107
|
+
export class MultiSink {
|
|
108
|
+
sinks;
|
|
109
|
+
constructor(sinks) {
|
|
110
|
+
this.sinks = sinks;
|
|
111
|
+
}
|
|
112
|
+
static fromEnv() {
|
|
113
|
+
const sinks = [];
|
|
114
|
+
const su = process.env.COMBER_SUPABASE_URL;
|
|
115
|
+
const sk = process.env.COMBER_SUPABASE_SERVICE_KEY;
|
|
116
|
+
if (su && sk) {
|
|
117
|
+
if (sk.length < 40 || /\banon\b/i.test(sk)) {
|
|
118
|
+
throw new Error("COMBER_SUPABASE_SERVICE_KEY looks like an anon key — refusing (writes would silently no-op under RLS)");
|
|
119
|
+
}
|
|
120
|
+
sinks.push(new SupabaseSink(su.replace(/\/$/, ""), sk));
|
|
121
|
+
}
|
|
122
|
+
const ph = process.env.COMBER_POSTHOG_KEY;
|
|
123
|
+
if (ph)
|
|
124
|
+
sinks.push(new PostHogSink(process.env.COMBER_POSTHOG_HOST ?? "https://us.i.posthog.com", ph));
|
|
125
|
+
return new MultiSink(sinks);
|
|
126
|
+
}
|
|
127
|
+
get active() {
|
|
128
|
+
return this.sinks.map((s) => s.name);
|
|
129
|
+
}
|
|
130
|
+
async recordRun(result, ctx) {
|
|
131
|
+
await Promise.all(this.sinks.map((s) => s.recordRun(result, ctx).catch((e) => console.error(`[sink:${s.name}] ${String(e).slice(0, 160)}`))));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=sink.js.map
|
package/dist/sink.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sink.js","sourceRoot":"","sources":["../src/sink.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAczD,SAAS,MAAM,CAAC,MAAmB;IACjC,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,MAAM;QACjE,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,MAAM;KAClE,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,MAAM,YAAY;IAGN;IACA;IAHD,IAAI,GAAG,UAAU,CAAC;IAC3B,YACU,GAAW,EACX,GAAW;QADX,QAAG,GAAH,GAAG,CAAQ;QACX,QAAG,GAAH,GAAG,CAAQ;IAClB,CAAC;IAEJ,KAAK,CAAC,SAAS,CAAC,MAAmB,EAAE,GAAe;QAClD,MAAM,OAAO,GAAG;YACd,MAAM,EAAE,IAAI,CAAC,GAAG;YAChB,aAAa,EAAE,UAAU,IAAI,CAAC,GAAG,EAAE;YACnC,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,6BAA6B;SACtC,CAAC;QACF,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC;QAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QACzB,MAAM,GAAG,GAAG;YACV,EAAE,EAAE,GAAG,CAAC,KAAK;YACb,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC;YACtC,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACrC,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,MAAM,EAAE,MAAM,CAAC,aAAa;YAC5B,OAAO,EAAE,MAAM,CAAC,YAAY;YAC5B,oBAAoB,EAAE,GAAG,EAAE,kBAAkB;YAC7C,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,iBAAiB;YACtC,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,YAAY;YAC/B,YAAY,EAAE,GAAG,EAAE,KAAK,CAAC,WAAW;YACpC,aAAa,EAAE,GAAG,EAAE,KAAK,CAAC,YAAY;YACtC,YAAY,EAAE,GAAG,EAAE,KAAK,CAAC,WAAW;YACpC,iBAAiB,EAAE,GAAG,EAAE,eAAe;YACvC,UAAU,EAAE,CAAC,CAAC,IAAI;YAClB,UAAU,EAAE,CAAC,CAAC,IAAI;YAClB,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,KAAK;YACjC,SAAS,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM;YACnC,SAAS,EAAE,MAAM,CAAC,QAAQ;YAC1B,OAAO,EAAE,GAAG,CAAC,MAAM;SACpB,CAAC;QACF,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,kBAAkB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9G,IAAI,CAAC,EAAE,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAEvF,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACvC,MAAM,EAAE,GAAG,CAAC,KAAK;gBACjB,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,SAAS,EAAE,CAAC,CAAC,SAAS;gBACtB,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC;gBACrB,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;gBAC9B,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,YAAY,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI;gBAC7B,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS;aAC7E,CAAC,CAAC,CAAC;YACJ,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,sBAAsB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACnH,IAAI,CAAC,EAAE,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,eAAe,EAAE,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7F,CAAC;IACH,CAAC;CACF;AAED,2CAA2C;AAC3C,MAAM,WAAW;IAGL;IACA;IAHD,IAAI,GAAG,SAAS,CAAC;IAC1B,YACU,IAAY,EACZ,GAAW;QADX,SAAI,GAAJ,IAAI,CAAQ;QACZ,QAAG,GAAH,GAAG,CAAQ;IAClB,CAAC;IAEJ,KAAK,CAAC,SAAS,CAAC,MAAmB,EAAE,GAAe;QAClD,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QACzB,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,EAAE;YACrD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,OAAO,EAAE,IAAI,CAAC,GAAG;gBACjB,KAAK,EAAE,YAAY;gBACnB,WAAW,EAAE,GAAG,CAAC,GAAG,IAAI,QAAQ;gBAChC,UAAU,EAAE;oBACV,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC;oBAClC,KAAK,EAAE,CAAC,CAAC,IAAI;oBACb,KAAK,EAAE,CAAC,CAAC,IAAI;oBACb,YAAY,EAAE,MAAM,CAAC,QAAQ,EAAE,kBAAkB;oBACjD,MAAM,EAAE,MAAM,CAAC,aAAa;oBAC5B,YAAY,EAAE,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,WAAW;oBAChD,IAAI,EAAE,GAAG,CAAC,IAAI;iBACf;aACF,CAAC;SACH,CAAC,CAAC;IACL,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,SAAS;IACA;IAApB,YAAoB,KAAa;QAAb,UAAK,GAAL,KAAK,CAAQ;IAAG,CAAC;IAErC,MAAM,CAAC,OAAO;QACZ,MAAM,KAAK,GAAW,EAAE,CAAC;QACzB,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;QAC3C,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC;QACnD,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;YACb,IAAI,EAAE,CAAC,MAAM,GAAG,EAAE,IAAI,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC3C,MAAM,IAAI,KAAK,CAAC,uGAAuG,CAAC,CAAC;YAC3H,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAC1D,CAAC;QACD,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;QAC1C,IAAI,EAAE;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,0BAA0B,EAAE,EAAE,CAAC,CAAC,CAAC;QACvG,OAAO,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,MAAmB,EAAE,GAAe;QAClD,MAAM,OAAO,CAAC,GAAG,CACf,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACnB,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CACpG,CACF,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { appendFile, copyFile, mkdir, readdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { buildRunRecord } from "./record.js";
|
|
4
|
+
import { resolveConfiguredPath, safeJoinWithin } from "./paths.js";
|
|
5
|
+
/**
|
|
6
|
+
* Dependency-free local sink. Appends one redacted run-summary record as a JSON line to
|
|
7
|
+
* a configurable JSONL file, and copies the run's `*.png` screenshots into a configurable
|
|
8
|
+
* directory. Both paths are normalized up front and every copied filename is
|
|
9
|
+
* traversal-guarded. Unconfigured (neither path set) it is never constructed by `fromEnv`.
|
|
10
|
+
*/
|
|
11
|
+
export class FileSink {
|
|
12
|
+
name = "file";
|
|
13
|
+
filePath;
|
|
14
|
+
screenshotDir;
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
this.filePath = opts.filePath ? resolveConfiguredPath(opts.filePath) : undefined;
|
|
17
|
+
this.screenshotDir = opts.screenshotDir ? resolveConfiguredPath(opts.screenshotDir) : undefined;
|
|
18
|
+
}
|
|
19
|
+
async recordRun(result, meta) {
|
|
20
|
+
if (this.filePath) {
|
|
21
|
+
const record = buildRunRecord(result, meta);
|
|
22
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
23
|
+
await appendFile(this.filePath, JSON.stringify(record) + "\n");
|
|
24
|
+
}
|
|
25
|
+
if (this.screenshotDir && meta.runDir) {
|
|
26
|
+
await this.copyScreenshots(meta.runDir, meta.runId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async copyScreenshots(runDir, runId) {
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = await readdir(runDir);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return; // run dir missing/unreadable — nothing to copy
|
|
36
|
+
}
|
|
37
|
+
const shots = entries.filter((f) => /\.png$/i.test(f));
|
|
38
|
+
if (!shots.length)
|
|
39
|
+
return;
|
|
40
|
+
// Per-run subfolder so screenshots from many runs never collide.
|
|
41
|
+
const destDir = safeJoinWithin(this.screenshotDir, runId);
|
|
42
|
+
await mkdir(destDir, { recursive: true });
|
|
43
|
+
for (const shot of shots) {
|
|
44
|
+
const src = safeJoinWithin(runDir, shot);
|
|
45
|
+
const dest = safeJoinWithin(destDir, shot);
|
|
46
|
+
await copyFile(src, dest).catch(() => { });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/sinks/file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AASnE;;;;;GAKG;AACH,MAAM,OAAO,QAAQ;IACV,IAAI,GAAG,MAAM,CAAC;IACN,QAAQ,CAAU;IAClB,aAAa,CAAU;IAExC,YAAY,IAAqB;QAC/B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACjF,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,qBAAqB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAClG,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,MAAmB,EAAE,IAAa;QAChD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,MAAM,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;QACjE,CAAC;QACD,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACtC,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,MAAc,EAAE,KAAa;QACzD,IAAI,OAAiB,CAAC;QACtB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,+CAA+C;QACzD,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK,CAAC,MAAM;YAAE,OAAO;QAC1B,iEAAiE;QACjE,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,CAAC,aAAc,EAAE,KAAK,CAAC,CAAC;QAC3D,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,GAAG,GAAG,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YACzC,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAC3C,MAAM,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { buildRunRecord } from "./record.js";
|
|
2
|
+
/**
|
|
3
|
+
* Dependency-free webhook sink (stdlib `fetch`). POSTs the canonical run record as JSON
|
|
4
|
+
* to a configurable URL with an optional Bearer auth header. Also covers the
|
|
5
|
+
* "error-tracking hook" use — a backend / Sentry-style collector consumes the same shape.
|
|
6
|
+
*
|
|
7
|
+
* CRITICAL: the POSTed body is built by `buildRunRecord`, which redacts the result first,
|
|
8
|
+
* so target secrets never cross the wire. The auth token is attached only as a header and
|
|
9
|
+
* never appears in the body or any log/error message.
|
|
10
|
+
*/
|
|
11
|
+
export class HttpSink {
|
|
12
|
+
name = "http";
|
|
13
|
+
url;
|
|
14
|
+
token;
|
|
15
|
+
timeoutMs;
|
|
16
|
+
fetchImpl;
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this.url = opts.url;
|
|
19
|
+
this.token = opts.token;
|
|
20
|
+
this.timeoutMs = opts.timeoutMs ?? 15_000;
|
|
21
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
22
|
+
}
|
|
23
|
+
async recordRun(result, meta) {
|
|
24
|
+
const record = buildRunRecord(result, meta);
|
|
25
|
+
const headers = { "Content-Type": "application/json" };
|
|
26
|
+
if (this.token)
|
|
27
|
+
headers.Authorization = `Bearer ${this.token}`;
|
|
28
|
+
const res = await this.fetchImpl(this.url, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers,
|
|
31
|
+
body: JSON.stringify(record),
|
|
32
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
33
|
+
});
|
|
34
|
+
// Surface non-2xx so the MultiSink logs it — but never echo the token or body.
|
|
35
|
+
if (!res.ok)
|
|
36
|
+
throw new Error(`http sink POST ${res.status}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/sinks/http.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAW7C;;;;;;;;GAQG;AACH,MAAM,OAAO,QAAQ;IACV,IAAI,GAAG,MAAM,CAAC;IACN,GAAG,CAAS;IACZ,KAAK,CAAU;IACf,SAAS,CAAe;IAEzC,YAAY,IAAqB;QAC/B,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACpB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,MAAmB,EAAE,IAAa;QAChD,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC5C,MAAM,OAAO,GAA2B,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;QAC/E,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,aAAa,GAAG,UAAU,IAAI,CAAC,KAAK,EAAE,CAAC;QAC/D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE;YACzC,MAAM,EAAE,MAAM;YACd,OAAO;YACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;SAC7B,CAAC,CAAC;QACH,+EAA+E;QAC/E,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAC/D,CAAC;CACF"}
|