wb-browser-runtime 0.12.0 → 0.14.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.
@@ -0,0 +1,128 @@
1
+ // Verb-argument substitution: {{ env.X }} / {{ artifacts.X }} expansion plus a
2
+ // `\{{` escape for literal template braces. Extracted from the entry point so
3
+ // it's unit-testable without booting the sidecar.
4
+ //
5
+ // {{ env.NAME }} → process.env.NAME
6
+ // {{ artifacts.NAME }} → contents of $WB_ARTIFACTS_DIR/NAME.txt (or .../NAME)
7
+ // \{{ → literal "{{" (escape; braces are NOT re-scanned)
8
+ //
9
+ // `expand(value, collected, artifactCache)` walks strings/arrays/objects.
10
+ // Resolved secret-ish values (≥3 chars) are added to `collected` so the caller
11
+ // can scrub them out of error messages with `scrubSecrets`.
12
+
13
+ import { readFileSync } from "node:fs";
14
+ import { log } from "./io.js";
15
+ import { resolveInside } from "./util.js";
16
+
17
+ // One combined pattern, scanned left-to-right in a single pass so the escape
18
+ // branch consumes the braces before either substitution branch can see them.
19
+ // Alternation order matters: the escape must come first.
20
+ // \{{ → no capture group (escape)
21
+ // {{ env.NAME }} → group 1
22
+ // {{ artifacts.NAME }} → group 2
23
+ // Artifact names are bare identifiers — no dots, no slashes — so a name can't
24
+ // compose with WB_ARTIFACTS_DIR into a path-traversal read.
25
+ const SUBST_RE =
26
+ /\\\{\{|\{\{\s*env\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}|\{\{\s*artifacts\.([A-Za-z_][A-Za-z0-9_-]*)\s*\}\}/g;
27
+
28
+ let warnedInvalidPolicy = false;
29
+
30
+ // Resolve the missing-value policy fresh each call (cheap) so the behavior
31
+ // tracks the current env. `warn` matches historical behavior (log + empty
32
+ // string, runbook continues). `error` throws so a missing OTP fails the slice
33
+ // instead of silently sending an empty value into a Playwright action. `empty`
34
+ // is the silent variant.
35
+ function resolveOnMissing() {
36
+ const raw = (process.env.WB_SUBSTITUTION_ON_MISSING || "warn").trim().toLowerCase();
37
+ if (raw === "error" || raw === "empty" || raw === "warn") return raw;
38
+ if (!warnedInvalidPolicy) {
39
+ warnedInvalidPolicy = true;
40
+ log(
41
+ `[warn] WB_SUBSTITUTION_ON_MISSING=${raw} is not valid (warn|error|empty); defaulting to warn`,
42
+ );
43
+ }
44
+ return "warn";
45
+ }
46
+
47
+ function handleMissingSubstitution(kind, name) {
48
+ const msg = `${kind}.${name} is not set`;
49
+ if (resolveOnMissing() === "error") {
50
+ throw new Error(`substitution: ${msg}`);
51
+ }
52
+ if (resolveOnMissing() === "warn") {
53
+ log(`[warn] ${msg}; substituting empty string`);
54
+ }
55
+ return "";
56
+ }
57
+
58
+ function readArtifactRaw(name) {
59
+ const dir = (process.env.WB_ARTIFACTS_DIR || "").trim();
60
+ if (!dir) {
61
+ log(`[warn] artifacts.${name} referenced but WB_ARTIFACTS_DIR is not set`);
62
+ return null;
63
+ }
64
+ for (const candidate of [`${name}.txt`, name]) {
65
+ const full = resolveInside(dir, candidate);
66
+ if (!full) continue;
67
+ try {
68
+ return readFileSync(full, "utf8").trimEnd();
69
+ } catch {
70
+ // try next candidate
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+
76
+ function readArtifact(name, cache) {
77
+ if (cache && cache.has(name)) {
78
+ const hit = cache.get(name);
79
+ if (hit === null) return handleMissingSubstitution("artifacts", name);
80
+ return hit;
81
+ }
82
+ const v = readArtifactRaw(name);
83
+ if (cache) cache.set(name, v);
84
+ if (v === null) return handleMissingSubstitution("artifacts", name);
85
+ return v;
86
+ }
87
+
88
+ export function expand(value, collected, artifactCache) {
89
+ if (typeof value === "string") {
90
+ return value.replace(SUBST_RE, (m, envName, artName) => {
91
+ // Escape branch: `\{{` → literal `{{`. No capture group, so both
92
+ // envName and artName are undefined here.
93
+ if (envName === undefined && artName === undefined) return "{{";
94
+ if (envName !== undefined) {
95
+ const v = process.env[envName];
96
+ if (v === undefined) return handleMissingSubstitution("env", envName);
97
+ if (collected && v.length >= 3) collected.add(v);
98
+ return v;
99
+ }
100
+ const v = readArtifact(artName, artifactCache);
101
+ if (collected && v && v.length >= 3) collected.add(v);
102
+ return v;
103
+ });
104
+ }
105
+ if (Array.isArray(value))
106
+ return value.map((v) => expand(v, collected, artifactCache));
107
+ if (value && typeof value === "object") {
108
+ const out = {};
109
+ for (const [k, v] of Object.entries(value))
110
+ out[k] = expand(v, collected, artifactCache);
111
+ return out;
112
+ }
113
+ return value;
114
+ }
115
+
116
+ // Scrub any values that came from {{ env.X }} / {{ artifacts.X }} expansion out
117
+ // of error messages before they cross the stdio boundary — Playwright and fetch
118
+ // errors sometimes echo their inputs (URLs, script bodies, assertion text) and
119
+ // those inputs may contain credentials.
120
+ export function scrubSecrets(msg, secrets) {
121
+ let out = String(msg == null ? "" : msg);
122
+ if (!secrets) return out;
123
+ for (const s of secrets) {
124
+ if (!s) continue;
125
+ out = out.split(s).join("«***»");
126
+ }
127
+ return out;
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wb-browser-runtime",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Browser sidecar runtime for wb — Playwright over CDP (Browserbase, browser-use) via the wb-sidecar/1 line-framed JSON protocol.",
5
5
  "bin": {
6
6
  "wb-browser-runtime": "bin/wb-browser-runtime.js"