rechrome 1.37.2 → 1.38.1
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/README.md +35 -0
- package/fill-secret.js +405 -0
- package/fill-secret.ts +399 -0
- package/listeners.js +2 -2
- package/listeners.ts +2 -2
- package/package.json +4 -2
- package/rechrome.js +200 -70
- package/rechrome.ts +200 -70
- package/serve.js +102 -10
- package/serve.ts +102 -10
package/README.md
CHANGED
|
@@ -64,6 +64,39 @@ verbatim: `rech --version` prints rechrome's version, `rech pw --version` playwr
|
|
|
64
64
|
Commands from the same git worktree share one browser session, so tabs you open persist
|
|
65
65
|
between calls; another worktree gets its own. `-s=<name>` opens a named sub-session.
|
|
66
66
|
|
|
67
|
+
#### Passwords and 2FA codes: `fill-secret`
|
|
68
|
+
|
|
69
|
+
`rech fill <ref> <password>` puts the password in the command line, the daemon log and the output.
|
|
70
|
+
`fill-secret` keeps it out of all three:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
rech fill-secret e7 --from-env SITE_PASSWORD --allow-domain '*.app.example.com'
|
|
74
|
+
rech fill-secret e9 --totp-from-env SITE_TOTP_SEED --env-file ~/secrets/.env --allow-domain '*.app.example.com' --submit
|
|
75
|
+
printf '%s\n' "$PW" | rech fill-secret e7 --from-stdin
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The client reads the value (or computes the TOTP code from a base32 seed: SHA1, 6 digits, 30 s; the
|
|
79
|
+
seed never leaves the client) and sends it outside the command's arguments. The daemon hands it to
|
|
80
|
+
the browser over the session's local socket. `--allow-domain` is checked against the host of the
|
|
81
|
+
field's own frame (an allowed page can't lend its host to a cross-origin iframe), and the fill goes
|
|
82
|
+
to that exact element, so it fails rather than types into a page that navigated after the check.
|
|
83
|
+
Snapshots echo input values, so the daemon shows `***` for the value in this and every later output,
|
|
84
|
+
log line and saved snapshot file: passwords until the daemon restarts, TOTP codes for 15 minutes.
|
|
85
|
+
Profile-scoped listeners accept `fill-secret` with its own flags.
|
|
86
|
+
|
|
87
|
+
Every password field is masked in snapshots, whoever filled it (you, Chrome's autofill, or
|
|
88
|
+
`fill-secret`): when an output carries a snapshot, the daemon reads the live values of all
|
|
89
|
+
`input[type=password]` fields in every frame (plus fields marked as passwords by `autocomplete` or a
|
|
90
|
+
name/id containing "pass", which covers a "show password" toggle) and masks them, and also blanks
|
|
91
|
+
any textbox whose name reads like a password field (password, PIN, パスワード…). If that check can't
|
|
92
|
+
be completed, the snapshot is withheld (inline and as a file) instead of passed through.
|
|
93
|
+
|
|
94
|
+
Limits: the masking stops accidental echoes, not someone who can run `eval`/`run-code` on the
|
|
95
|
+
session and deliberately reads the field back encoded. A snapshot file is scrubbed right after the
|
|
96
|
+
browser writes it, not before (downloads wait for the scrub). The check reads the page right after
|
|
97
|
+
the snapshot, so a page script that clears or replaces a neutrally-labelled password field in
|
|
98
|
+
between can slip past; closing that needs redaction inside playwright's snapshot code. Not yet supported on a Windows daemon.
|
|
99
|
+
|
|
67
100
|
### 3. Where things are kept
|
|
68
101
|
|
|
69
102
|
| Where | What |
|
|
@@ -280,6 +313,8 @@ prompting for stdin, then uses the default configuration save location.
|
|
|
280
313
|
> the extension auto-loaded and the token auto-seeded — zero GUI, zero TTY. It is *not* your real
|
|
281
314
|
> Chrome (branded Google Chrome 149+ rejects `--load-extension`), so it has no logins/cookies; use it
|
|
282
315
|
> for clean QA fixtures, and `rech setup` for your real, logged-in Chrome.
|
|
316
|
+
> After provisioning, `rech --profile <name> open https://example.com` automatically launches
|
|
317
|
+
> that managed browser with the bridge loaded; no separate browser launch is needed.
|
|
283
318
|
|
|
284
319
|
## Configuration
|
|
285
320
|
|
package/fill-secret.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// fill-secret.ts
|
|
3
|
+
import { createHmac } from "crypto";
|
|
4
|
+
import { connect } from "net";
|
|
5
|
+
import { readdirSync, readFileSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
var SECRET_MASK = "***";
|
|
9
|
+
var SECRET_MASK_TTL_MS = 15 * 60000;
|
|
10
|
+
var FILL_SECRET_USAGE = `Usage: rech fill-secret <ref> (--from-env VAR | --from-stdin | --totp-from-env VAR | --totp-from-stdin)
|
|
11
|
+
[--env-file <path>] [--allow-domain <glob>]... [--submit]
|
|
12
|
+
Fills <ref> (from \`rech snapshot\`) with a secret the daemon never logs or echoes.
|
|
13
|
+
--from-env VAR the value of env var VAR on this machine
|
|
14
|
+
--from-stdin the first line of stdin
|
|
15
|
+
--totp-from-env VAR the current 6-digit TOTP code (RFC 6238, SHA1, 30s) for the base32 seed in VAR;
|
|
16
|
+
only the code leaves this machine, never the seed
|
|
17
|
+
--totp-from-stdin the same, seed read from stdin
|
|
18
|
+
--env-file PATH look VAR up in this dotenv file instead of the environment
|
|
19
|
+
--allow-domain GLOB refuse unless the page host matches, e.g. '*.app.example.com' (repeatable)
|
|
20
|
+
--submit press Enter after filling`;
|
|
21
|
+
function parseFillSecretArgs(args) {
|
|
22
|
+
let ref;
|
|
23
|
+
let source;
|
|
24
|
+
let totp = false;
|
|
25
|
+
const allowDomains = [];
|
|
26
|
+
let submit = false;
|
|
27
|
+
let envFile;
|
|
28
|
+
const setSource = (s, isTotp) => {
|
|
29
|
+
if (source)
|
|
30
|
+
throw new Error("fill-secret takes exactly one of --from-env, --from-stdin, --totp-from-env, --totp-from-stdin");
|
|
31
|
+
source = s;
|
|
32
|
+
totp = isTotp;
|
|
33
|
+
};
|
|
34
|
+
for (let i = 0;i < args.length; i++) {
|
|
35
|
+
const a = args[i];
|
|
36
|
+
const eq = a.indexOf("=");
|
|
37
|
+
const flag = a.startsWith("--") && eq > 0 ? a.slice(0, eq) : a;
|
|
38
|
+
const value = () => {
|
|
39
|
+
if (a.startsWith("--") && eq > 0)
|
|
40
|
+
return a.slice(eq + 1);
|
|
41
|
+
const next = args[++i];
|
|
42
|
+
if (next === undefined || next.startsWith("--"))
|
|
43
|
+
throw new Error(`${flag} needs a value`);
|
|
44
|
+
return next;
|
|
45
|
+
};
|
|
46
|
+
if (flag === "--from-env")
|
|
47
|
+
setSource({ kind: "env", name: value() }, false);
|
|
48
|
+
else if (flag === "--totp-from-env")
|
|
49
|
+
setSource({ kind: "env", name: value() }, true);
|
|
50
|
+
else if (flag === "--from-stdin")
|
|
51
|
+
setSource({ kind: "stdin" }, false);
|
|
52
|
+
else if (flag === "--totp-from-stdin")
|
|
53
|
+
setSource({ kind: "stdin" }, true);
|
|
54
|
+
else if (flag === "--allow-domain") {
|
|
55
|
+
const globs = value().split(",").map((s) => s.trim());
|
|
56
|
+
if (globs.some((g) => !g))
|
|
57
|
+
throw new Error("--allow-domain got an empty host");
|
|
58
|
+
allowDomains.push(...globs);
|
|
59
|
+
} else if (flag === "--submit")
|
|
60
|
+
submit = true;
|
|
61
|
+
else if (flag === "--env-file")
|
|
62
|
+
envFile = value();
|
|
63
|
+
else if (a.startsWith("-"))
|
|
64
|
+
throw new Error(`fill-secret: unknown option ${a}`);
|
|
65
|
+
else if (ref === undefined)
|
|
66
|
+
ref = a;
|
|
67
|
+
else
|
|
68
|
+
throw new Error("fill-secret takes the secret from --from-env/--from-stdin, never as an argument");
|
|
69
|
+
}
|
|
70
|
+
if (!ref)
|
|
71
|
+
throw new Error("fill-secret needs a target ref, e.g. e12 (see `rech snapshot`)");
|
|
72
|
+
if (!source)
|
|
73
|
+
throw new Error("fill-secret needs --from-env VAR, --from-stdin, --totp-from-env VAR or --totp-from-stdin");
|
|
74
|
+
if (envFile) {
|
|
75
|
+
if (source.kind !== "env")
|
|
76
|
+
throw new Error("--env-file goes with --from-env / --totp-from-env");
|
|
77
|
+
source.envFile = envFile;
|
|
78
|
+
}
|
|
79
|
+
if (source.kind === "env" && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(source.name))
|
|
80
|
+
throw new Error(`fill-secret: "${source.name}" is not an environment variable name`);
|
|
81
|
+
for (const d of allowDomains)
|
|
82
|
+
domainGlobToRegExp(d);
|
|
83
|
+
return { ref, source, totp, allowDomains, submit };
|
|
84
|
+
}
|
|
85
|
+
function fillSecretWireArgs(req) {
|
|
86
|
+
return ["fill-secret", req.ref, ...req.allowDomains.map((d) => `--allow-domain=${d}`), ...req.submit ? ["--submit"] : [], ...req.totp ? ["--totp"] : []];
|
|
87
|
+
}
|
|
88
|
+
async function readSecretSource(source, env = process.env, stdin = () => Bun.stdin.text()) {
|
|
89
|
+
if (source.kind === "env") {
|
|
90
|
+
const v = source.envFile ? parseDotenv(await Bun.file(source.envFile).text().catch(() => {
|
|
91
|
+
throw new Error(`fill-secret: cannot read ${source.envFile}`);
|
|
92
|
+
}))[source.name] : env[source.name];
|
|
93
|
+
if (!v)
|
|
94
|
+
throw new Error(`fill-secret: ${source.name} is not set or empty${source.envFile ? ` in ${source.envFile}` : ""}`);
|
|
95
|
+
return v;
|
|
96
|
+
}
|
|
97
|
+
const text = await stdin();
|
|
98
|
+
const v = text.replace(/\r?\n[\s\S]*$/, "");
|
|
99
|
+
if (!v)
|
|
100
|
+
throw new Error("fill-secret: nothing on stdin");
|
|
101
|
+
return v;
|
|
102
|
+
}
|
|
103
|
+
function parseDotenv(text) {
|
|
104
|
+
const out = {};
|
|
105
|
+
for (const line of text.split(/\r?\n/)) {
|
|
106
|
+
const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
107
|
+
if (!m)
|
|
108
|
+
continue;
|
|
109
|
+
let v = m[2];
|
|
110
|
+
const q = v[0];
|
|
111
|
+
if ((q === '"' || q === "'") && v.lastIndexOf(q) > 0) {
|
|
112
|
+
v = v.slice(1, v.lastIndexOf(q));
|
|
113
|
+
if (q === '"')
|
|
114
|
+
v = v.replace(/\\n/g, `
|
|
115
|
+
`).replace(/\\(["\\])/g, "$1");
|
|
116
|
+
} else
|
|
117
|
+
v = v.replace(/\s+#.*$/, "");
|
|
118
|
+
out[m[1]] = v;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
function base32Decode(input) {
|
|
123
|
+
const s = input.replace(/[\s-]/g, "").replace(/=+$/, "").toUpperCase();
|
|
124
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
125
|
+
let bits = 0, acc = 0;
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const ch of s) {
|
|
128
|
+
const v = alphabet.indexOf(ch);
|
|
129
|
+
if (v < 0)
|
|
130
|
+
throw new Error("TOTP seed is not valid base32");
|
|
131
|
+
acc = acc << 5 | v;
|
|
132
|
+
bits += 5;
|
|
133
|
+
if (bits >= 8) {
|
|
134
|
+
bits -= 8;
|
|
135
|
+
out.push(acc >>> bits & 255);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!out.length)
|
|
139
|
+
throw new Error("TOTP seed is empty");
|
|
140
|
+
return Buffer.from(out);
|
|
141
|
+
}
|
|
142
|
+
function totpCode(seedBase32, timeMs = Date.now(), opts = {}) {
|
|
143
|
+
const { digits = 6, period = 30, algorithm = "sha1" } = opts;
|
|
144
|
+
const counter = Math.floor(timeMs / 1000 / period);
|
|
145
|
+
const msg = Buffer.alloc(8);
|
|
146
|
+
msg.writeBigUInt64BE(BigInt(counter));
|
|
147
|
+
const mac = createHmac(algorithm, base32Decode(seedBase32)).update(msg).digest();
|
|
148
|
+
const offset = mac[mac.length - 1] & 15;
|
|
149
|
+
const bin = (mac[offset] & 127) << 24 | mac[offset + 1] << 16 | mac[offset + 2] << 8 | mac[offset + 3];
|
|
150
|
+
return String(bin % 10 ** digits).padStart(digits, "0");
|
|
151
|
+
}
|
|
152
|
+
function totpWaitMs(timeMs = Date.now(), period = 30, minLeftMs = 5000) {
|
|
153
|
+
const left = period * 1000 - timeMs % (period * 1000);
|
|
154
|
+
return left < minLeftMs ? left + 50 : 0;
|
|
155
|
+
}
|
|
156
|
+
function domainGlobToRegExp(glob) {
|
|
157
|
+
const g = glob.trim().toLowerCase();
|
|
158
|
+
if (!/^(\*\.)?[a-z0-9-]+(\.[a-z0-9-]+)*$/.test(g))
|
|
159
|
+
throw new Error(`--allow-domain "${glob}" is not a host or *.host glob`);
|
|
160
|
+
const esc = (s) => s.replace(/[.]/g, "\\.");
|
|
161
|
+
return g.startsWith("*.") ? new RegExp(`^(?:[a-z0-9-]+\\.)+${esc(g.slice(2))}$`, "i") : new RegExp(`^${esc(g)}$`, "i");
|
|
162
|
+
}
|
|
163
|
+
function hostAllowed(host, globs) {
|
|
164
|
+
return globs.length === 0 || globs.some((g) => domainGlobToRegExp(g).test(host));
|
|
165
|
+
}
|
|
166
|
+
function parseFillSecretWire(args) {
|
|
167
|
+
const [cmd, ref, ...rest] = args;
|
|
168
|
+
if (cmd !== "fill-secret" || !ref || ref.startsWith("-"))
|
|
169
|
+
throw new Error("fill-secret needs a target ref");
|
|
170
|
+
const allowDomains = [];
|
|
171
|
+
let submit = false, totp = false;
|
|
172
|
+
for (const a of rest) {
|
|
173
|
+
if (a === "--submit")
|
|
174
|
+
submit = true;
|
|
175
|
+
else if (a === "--totp")
|
|
176
|
+
totp = true;
|
|
177
|
+
else if (a.startsWith("--allow-domain=")) {
|
|
178
|
+
const d = a.slice("--allow-domain=".length);
|
|
179
|
+
domainGlobToRegExp(d);
|
|
180
|
+
allowDomains.push(d);
|
|
181
|
+
} else
|
|
182
|
+
throw new Error(`fill-secret: unexpected argument ${a.startsWith("-") ? a : "(positional)"}`);
|
|
183
|
+
}
|
|
184
|
+
return { ref, allowDomains, submit, totp };
|
|
185
|
+
}
|
|
186
|
+
function buildFillCode(ref, value, allowDomains, submit) {
|
|
187
|
+
const patterns = allowDomains.map((d) => domainGlobToRegExp(d).source);
|
|
188
|
+
const locator = /^(f\d+)?e\d+$/.test(ref) ? `page.locator(${JSON.stringify(`aria-ref=${ref}`)})` : `page.locator(${JSON.stringify(ref)})`;
|
|
189
|
+
return `async page => {
|
|
190
|
+
const el = await ${locator}.elementHandle({ timeout: 10000 });
|
|
191
|
+
try {
|
|
192
|
+
const host = String(await el.evaluate(e => e.ownerDocument.location.hostname)).toLowerCase();
|
|
193
|
+
const allowed = ${JSON.stringify(patterns)};
|
|
194
|
+
if (allowed.length && !allowed.some(p => new RegExp(p, "i").test(host)))
|
|
195
|
+
throw new Error("fill-secret refused: the field's page host " + JSON.stringify(host) + " is not allowed by --allow-domain ${allowDomains.join(",").replace(/[^a-z0-9*.,-]/gi, "")}");
|
|
196
|
+
await el.fill(${JSON.stringify(value)}, { timeout: 10000 });
|
|
197
|
+
${submit ? `await el.press("Enter", { timeout: 10000 });` : ""}
|
|
198
|
+
return "filled " + ${JSON.stringify(ref)} + " on " + host;
|
|
199
|
+
} finally {
|
|
200
|
+
await el.dispose().catch(() => {});
|
|
201
|
+
}
|
|
202
|
+
}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
class SecretMasker {
|
|
206
|
+
ttlMs;
|
|
207
|
+
now;
|
|
208
|
+
values = new Map;
|
|
209
|
+
constructor(ttlMs = SECRET_MASK_TTL_MS, now = () => Date.now()) {
|
|
210
|
+
this.ttlMs = ttlMs;
|
|
211
|
+
this.now = now;
|
|
212
|
+
}
|
|
213
|
+
add(value, ttlMs = this.ttlMs) {
|
|
214
|
+
if (value)
|
|
215
|
+
this.values.set(value, this.now() + ttlMs);
|
|
216
|
+
}
|
|
217
|
+
get active() {
|
|
218
|
+
this.prune();
|
|
219
|
+
return this.values.size > 0;
|
|
220
|
+
}
|
|
221
|
+
mask(text) {
|
|
222
|
+
if (!text)
|
|
223
|
+
return text;
|
|
224
|
+
this.prune();
|
|
225
|
+
for (const v of [...this.values.keys()].sort((a, b) => b.length - a.length)) {
|
|
226
|
+
text = text.replaceAll(v, SECRET_MASK);
|
|
227
|
+
const lit = JSON.stringify(v).slice(1, -1);
|
|
228
|
+
if (lit !== v)
|
|
229
|
+
text = text.replaceAll(lit, SECRET_MASK);
|
|
230
|
+
}
|
|
231
|
+
return text;
|
|
232
|
+
}
|
|
233
|
+
prune() {
|
|
234
|
+
const t = this.now();
|
|
235
|
+
for (const [v, exp] of this.values)
|
|
236
|
+
if (exp <= t)
|
|
237
|
+
this.values.delete(v);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function sessionSocketCandidates(socketRoot, session) {
|
|
241
|
+
let names = [];
|
|
242
|
+
try {
|
|
243
|
+
names = readdirSync(socketRoot);
|
|
244
|
+
} catch {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
return names.filter((n) => n === `${session}.sock` || n.length === session.length + 22 && /^[0-9a-f]{16}-/.test(n) && n.slice(17) === `${session}.sock`).map((n) => join(socketRoot, n));
|
|
248
|
+
}
|
|
249
|
+
function daemonSessionSockets(session, env = process.env) {
|
|
250
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
251
|
+
const bases = env.PLAYWRIGHT_DAEMON_SESSION_DIR ? [env.PLAYWRIGHT_DAEMON_SESSION_DIR] : process.platform === "win32" ? [...new Set([
|
|
252
|
+
join(env.USERPROFILE || home, "AppData", "Local", "ms-playwright", "daemon"),
|
|
253
|
+
...env.LOCALAPPDATA ? [join(env.LOCALAPPDATA, "ms-playwright", "daemon")] : []
|
|
254
|
+
])] : process.platform === "darwin" ? [join(home, "Library", "Caches", "ms-playwright", "daemon")] : [join(env.XDG_CACHE_HOME || join(home, ".cache"), "ms-playwright", "daemon")];
|
|
255
|
+
const out = [];
|
|
256
|
+
for (const base of bases) {
|
|
257
|
+
let hashes = [];
|
|
258
|
+
try {
|
|
259
|
+
hashes = readdirSync(base);
|
|
260
|
+
} catch {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
for (const h of hashes) {
|
|
264
|
+
if (!/^[0-9a-f]{16}$/.test(h))
|
|
265
|
+
continue;
|
|
266
|
+
try {
|
|
267
|
+
const cfg = JSON.parse(readFileSync(join(base, h, `${session}.session`), "utf8"));
|
|
268
|
+
if ((cfg?.name ?? session) === session && typeof cfg?.socketPath === "string" && cfg.socketPath)
|
|
269
|
+
out.push(cfg.socketPath);
|
|
270
|
+
} catch {}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return [...new Set(out)];
|
|
274
|
+
}
|
|
275
|
+
function socketRequest(path, method, params, timeoutMs = 30000) {
|
|
276
|
+
return new Promise((resolve, reject) => {
|
|
277
|
+
const sock = connect(path);
|
|
278
|
+
let buf = "";
|
|
279
|
+
const timer = setTimeout(() => {
|
|
280
|
+
sock.destroy();
|
|
281
|
+
reject(new Error("timeout"));
|
|
282
|
+
}, timeoutMs);
|
|
283
|
+
sock.setEncoding("utf8");
|
|
284
|
+
sock.on("connect", () => sock.write(`${JSON.stringify({ id: 1, method, params })}
|
|
285
|
+
`));
|
|
286
|
+
sock.on("data", (d) => {
|
|
287
|
+
buf += d;
|
|
288
|
+
const nl = buf.indexOf(`
|
|
289
|
+
`);
|
|
290
|
+
if (nl < 0)
|
|
291
|
+
return;
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
sock.destroy();
|
|
294
|
+
try {
|
|
295
|
+
resolve(JSON.parse(buf.slice(0, nl)));
|
|
296
|
+
} catch {
|
|
297
|
+
reject(new Error("unreadable cliDaemon response"));
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
sock.on("error", (e) => {
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
reject(e);
|
|
303
|
+
});
|
|
304
|
+
sock.on("close", () => {
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
reject(new Error("cliDaemon closed the connection"));
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async function runCodeOnSession(opts) {
|
|
311
|
+
const candidates = [...new Set([...sessionSocketCandidates(opts.socketRoot, opts.session), ...daemonSessionSockets(opts.session)])];
|
|
312
|
+
if (!candidates.length)
|
|
313
|
+
throw new Error("no browser is open in this session; run `rech open <url>` first");
|
|
314
|
+
const errors = [];
|
|
315
|
+
for (const path of candidates) {
|
|
316
|
+
let reply;
|
|
317
|
+
try {
|
|
318
|
+
reply = await socketRequest(path, "run", { args: { _: ["run-code", opts.code] }, cwd: opts.cwd, ...opts.json ? { json: true } : {} });
|
|
319
|
+
} catch (e) {
|
|
320
|
+
errors.push(e instanceof Error ? e.message : String(e));
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (reply.error)
|
|
324
|
+
throw new Error(reply.error);
|
|
325
|
+
const r = reply.result;
|
|
326
|
+
return { isError: !!r?.isError, text: typeof r === "string" ? r : typeof r?.text === "string" ? r.text : "" };
|
|
327
|
+
}
|
|
328
|
+
throw new Error(`could not reach this session's browser (${errors.join("; ")}); run \`rech open <url>\` first`);
|
|
329
|
+
}
|
|
330
|
+
function fillSecretOnSession(opts) {
|
|
331
|
+
return runCodeOnSession({ ...opts, code: buildFillCode(opts.ref, opts.value, opts.allowDomains, opts.submit) });
|
|
332
|
+
}
|
|
333
|
+
var PASSWORD_FIELDS = 'input[type=password], input[autocomplete~="current-password" i], input[autocomplete~="new-password" i], input[name*="pass" i], input[id*="pass" i]';
|
|
334
|
+
var PASSWORD_VALUES_CODE = `async page => {
|
|
335
|
+
const values = [];
|
|
336
|
+
let failedFrames = 0;
|
|
337
|
+
for (const frame of page.frames()) {
|
|
338
|
+
try { values.push(...await frame.$$eval(${JSON.stringify(PASSWORD_FIELDS)}, els => els.map(e => e.value))); } catch { failedFrames++; }
|
|
339
|
+
}
|
|
340
|
+
return { values: values.filter(Boolean), failedFrames };
|
|
341
|
+
}`;
|
|
342
|
+
async function passwordValuesOnSession(opts) {
|
|
343
|
+
const { isError, text } = await runCodeOnSession({ ...opts, code: PASSWORD_VALUES_CODE, json: true });
|
|
344
|
+
if (isError)
|
|
345
|
+
throw new Error("password scan failed");
|
|
346
|
+
const { values, failedFrames } = parseScanResult(text);
|
|
347
|
+
if (failedFrames)
|
|
348
|
+
throw new Error(`password scan could not read ${failedFrames} frame(s)`);
|
|
349
|
+
return values;
|
|
350
|
+
}
|
|
351
|
+
function parseScanResult(text) {
|
|
352
|
+
let raw;
|
|
353
|
+
try {
|
|
354
|
+
raw = JSON.parse(text).result;
|
|
355
|
+
} catch {
|
|
356
|
+
raw = text.match(/### Result\n([^\n]*)/)?.[1];
|
|
357
|
+
}
|
|
358
|
+
let value = raw;
|
|
359
|
+
if (typeof raw === "string") {
|
|
360
|
+
try {
|
|
361
|
+
value = JSON.parse(raw);
|
|
362
|
+
} catch {
|
|
363
|
+
value = undefined;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (!value || !Array.isArray(value.values) || typeof value.failedFrames !== "number")
|
|
367
|
+
throw new Error("password scan returned no result");
|
|
368
|
+
return { values: value.values.filter((v) => typeof v === "string" && v.length > 0), failedFrames: value.failedFrames };
|
|
369
|
+
}
|
|
370
|
+
function withholdSnapshots(text, reason) {
|
|
371
|
+
return text.replace(/### Snapshot\n[\s\S]*?(?=\n### |$)/g, `### Snapshot
|
|
372
|
+
[rech] snapshot withheld: ${reason}. Retry the command.`);
|
|
373
|
+
}
|
|
374
|
+
var hasSnapshot = (text) => /### Snapshot|\.ya?ml\b/.test(text);
|
|
375
|
+
var PASSWORD_LINE = /^(\s*-\s*textbox\s+"[^"\n]*(?:password|passwd|passcode|\bpin\b|\u30D1\u30B9\u30EF\u30FC\u30C9|\u6697\u8A3C)[^"\n]*"[^\n:]*):[ \t]+\S.*$/gim;
|
|
376
|
+
function maskPasswordLines(text) {
|
|
377
|
+
return text.replace(PASSWORD_LINE, `$1: ${SECRET_MASK}`);
|
|
378
|
+
}
|
|
379
|
+
export {
|
|
380
|
+
FILL_SECRET_USAGE,
|
|
381
|
+
SECRET_MASK,
|
|
382
|
+
SECRET_MASK_TTL_MS,
|
|
383
|
+
SecretMasker,
|
|
384
|
+
base32Decode,
|
|
385
|
+
buildFillCode,
|
|
386
|
+
daemonSessionSockets,
|
|
387
|
+
domainGlobToRegExp,
|
|
388
|
+
fillSecretOnSession,
|
|
389
|
+
fillSecretWireArgs,
|
|
390
|
+
hasSnapshot,
|
|
391
|
+
hostAllowed,
|
|
392
|
+
maskPasswordLines,
|
|
393
|
+
parseDotenv,
|
|
394
|
+
parseFillSecretArgs,
|
|
395
|
+
parseFillSecretWire,
|
|
396
|
+
parseScanResult,
|
|
397
|
+
passwordValuesOnSession,
|
|
398
|
+
readSecretSource,
|
|
399
|
+
runCodeOnSession,
|
|
400
|
+
sessionSocketCandidates,
|
|
401
|
+
socketRequest,
|
|
402
|
+
totpCode,
|
|
403
|
+
totpWaitMs,
|
|
404
|
+
withholdSnapshots
|
|
405
|
+
};
|