@broberg/lens-daemon 0.1.5 → 0.1.6
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/package.json +3 -3
- package/src/flow.ts +9 -3
- package/src/secret-fill.ts +15 -13
- package/src/service.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@broberg/lens-daemon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "The Lens module of the cardmem daemon (flows, critic, coverage, capture sessions, the /lens/* routes and the cardmem-lens MCP) as a package. The daemon mounts createLensRouter(deps) + createLensMcpRouter(deps). Bun runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"sharp": "^0.33.5",
|
|
16
16
|
"turndown": "^7.2.4",
|
|
17
17
|
"zod": "^3.23.8",
|
|
18
|
-
"@broberg/lens-
|
|
19
|
-
"@broberg/lens-
|
|
18
|
+
"@broberg/lens-svg": "0.1.0",
|
|
19
|
+
"@broberg/lens-engine": "0.10.2"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@happy-dom/global-registrator": "^20.10.6",
|
package/src/flow.ts
CHANGED
|
@@ -1667,7 +1667,7 @@ async function sampleMotion(
|
|
|
1667
1667
|
const MOTION_SAMPLE_MS = 250;
|
|
1668
1668
|
|
|
1669
1669
|
|
|
1670
|
-
async function execStep(page: Page, step: FlowStep, base: string, vars: Record<string, unknown>, critic: CriticMode, localPath?: string, sink?: CriticSink, pwSink?: FilledPassword[], readySink?: NotListeningFinding[]): Promise<Buffer | null | { __inspect: string } | { __via: string; __build?: string; __redirect?: string }> {
|
|
1670
|
+
async function execStep(page: Page, step: FlowStep, base: string, vars: Record<string, unknown>, critic: CriticMode, localPath?: string, sink?: CriticSink, pwSink?: FilledPassword[], readySink?: NotListeningFinding[], secretRoot?: string): Promise<Buffer | null | { __inspect: string } | { __via: string; __build?: string; __redirect?: string }> {
|
|
1671
1671
|
switch (step.type) {
|
|
1672
1672
|
case 'goto': {
|
|
1673
1673
|
const p = subst(step.path, vars);
|
|
@@ -1706,7 +1706,7 @@ async function execStep(page: Page, step: FlowStep, base: string, vars: Record<s
|
|
|
1706
1706
|
// used to pass as success, which is the half of the fleet save-rule this
|
|
1707
1707
|
// instrument could not perform.
|
|
1708
1708
|
if (step.valueFrom) {
|
|
1709
|
-
await fillSecret(loc, step.valueFrom,
|
|
1709
|
+
await fillSecret(loc, step.valueFrom, secretRoot, remaining, pwSink, anchorOf(step));
|
|
1710
1710
|
return via ? { __via: via } : null;
|
|
1711
1711
|
}
|
|
1712
1712
|
const filledValue = subst(step.value ?? '', vars);
|
|
@@ -2207,6 +2207,12 @@ export interface RunFlowOptions {
|
|
|
2207
2207
|
/** F213.6 — repo root for resolving an uploadFile step's fixture path. The
|
|
2208
2208
|
* path is sandboxed to this dir (no `../` escape). Absent → only data: URIs. */
|
|
2209
2209
|
localPath?: string;
|
|
2210
|
+
/** F009.121 — the repo root a `fill valueFrom {path}` may read from, ALREADY
|
|
2211
|
+
* checked against the daemon sandbox by the caller. Absent → only {env} works.
|
|
2212
|
+
* Kept apart from localPath on purpose: a secret file is typed into whatever
|
|
2213
|
+
* page the caller named, so an unsandboxed root would let a flow post any file
|
|
2214
|
+
* the daemon can read to any URL. */
|
|
2215
|
+
secretRoot?: string;
|
|
2210
2216
|
/** F255.5 — record the whole driven run to a .webm, returned as FlowResult.video.
|
|
2211
2217
|
* Off by default: recording costs disk + a little time on every run, and most flows
|
|
2212
2218
|
* only need the per-step stills. */
|
|
@@ -2468,7 +2474,7 @@ export async function runFlow(flow: Flow, env: AuthEnv, opts: RunFlowOptions = {
|
|
|
2468
2474
|
// matter most, and a sink scoped to the try is unreachable from the catch.
|
|
2469
2475
|
const sink: CriticSink = { critic: null };
|
|
2470
2476
|
try {
|
|
2471
|
-
const out = await execStep(page, step, base, vars, opts.critic ?? 'off', opts.localPath, sink, filledPasswords, notListening);
|
|
2477
|
+
const out = await execStep(page, step, base, vars, opts.critic ?? 'off', opts.localPath, sink, filledPasswords, notListening, opts.secretRoot);
|
|
2472
2478
|
const obj = out !== null && !Buffer.isBuffer(out) && typeof out === 'object' ? out : null;
|
|
2473
2479
|
const inspect = obj && '__inspect' in obj ? (obj as { __inspect: string }).__inspect : null;
|
|
2474
2480
|
const via = obj && '__via' in obj ? (obj as { __via: string }).__via : null;
|
package/src/secret-fill.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// login form with a test account, and the only way before was the plaintext
|
|
5
5
|
// password in the manuscript.
|
|
6
6
|
import { readFile } from 'node:fs/promises';
|
|
7
|
-
import {
|
|
7
|
+
import { resolve, sep } from 'node:path';
|
|
8
8
|
import type { Locator } from 'playwright';
|
|
9
9
|
import { fillAndVerify } from './fill.ts';
|
|
10
10
|
import type { FilledPassword } from './password-in-url.ts';
|
|
@@ -13,7 +13,7 @@ import type { ValueFrom } from './flow.ts';
|
|
|
13
13
|
/** Read the secret. Every error names WHERE it looked and never WHAT it found. */
|
|
14
14
|
export async function readSecret(
|
|
15
15
|
vf: ValueFrom,
|
|
16
|
-
|
|
16
|
+
secretRoot: string | undefined,
|
|
17
17
|
env: Record<string, string | undefined> = process.env,
|
|
18
18
|
): Promise<string> {
|
|
19
19
|
if ('env' in vf) {
|
|
@@ -21,15 +21,17 @@ export async function readSecret(
|
|
|
21
21
|
if (!v) throw new Error(`fill valueFrom: env ${vf.env} is not set in the daemon`);
|
|
22
22
|
return v;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
// The file is typed into whatever page the caller named, so it must live
|
|
25
|
+
// inside a repo the daemon sandbox allows (secretRoot is checked by the
|
|
26
|
+
// caller) — never anywhere else the daemon user can read. Same containment
|
|
27
|
+
// rule as an uploadFile fixture.
|
|
28
|
+
if (!secretRoot) {
|
|
29
|
+
throw new Error('fill valueFrom {path} needs local_path inside the daemon sandbox — the secret file must live in that repo (use {env} otherwise)');
|
|
30
|
+
}
|
|
31
|
+
const root = resolve(secretRoot);
|
|
32
|
+
const p = resolve(root, vf.path);
|
|
33
|
+
if (p !== root && !p.startsWith(root + sep)) {
|
|
34
|
+
throw new Error(`fill valueFrom: path escapes the repo root: ${vf.path}`);
|
|
33
35
|
}
|
|
34
36
|
let raw: string;
|
|
35
37
|
try {
|
|
@@ -50,7 +52,7 @@ export async function readSecret(
|
|
|
50
52
|
export async function fillSecret(
|
|
51
53
|
loc: Locator,
|
|
52
54
|
vf: ValueFrom,
|
|
53
|
-
|
|
55
|
+
secretRoot: string | undefined,
|
|
54
56
|
timeout: number,
|
|
55
57
|
pwSink: FilledPassword[] | undefined,
|
|
56
58
|
anchor: string,
|
|
@@ -64,7 +66,7 @@ export async function fillSecret(
|
|
|
64
66
|
if (!isPassword) {
|
|
65
67
|
throw new Error('fill valueFrom only fills a password input (type="password") — any other field would show the secret in every later screenshot');
|
|
66
68
|
}
|
|
67
|
-
const secret = await readSecret(vf,
|
|
69
|
+
const secret = await readSecret(vf, secretRoot, env);
|
|
68
70
|
try {
|
|
69
71
|
await fillAndVerify(loc, secret, timeout);
|
|
70
72
|
} catch {
|
package/src/service.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// dashboard + cc point <img>/links at the daemon, the same way the rest of
|
|
9
9
|
// the SPA already talks to 127.0.0.1:7475.
|
|
10
10
|
|
|
11
|
+
import { resolveSandboxedPath } from './sandbox.ts';
|
|
11
12
|
import { randomUUID } from 'node:crypto';
|
|
12
13
|
import sharp from 'sharp';
|
|
13
14
|
import { durabilityOf } from './durability.ts';
|
|
@@ -299,6 +300,19 @@ function projectLocale(deps: LensDeps, project: string): { locale?: string; time
|
|
|
299
300
|
return row?.local_path ? manifestLocale(row.local_path, deps.sandboxRoots) : null;
|
|
300
301
|
}
|
|
301
302
|
|
|
303
|
+
/** F009.121 — the root a `fill valueFrom {path}` may read under: the caller's
|
|
304
|
+
* local_path, but only once the daemon sandbox has accepted it. Outside the
|
|
305
|
+
* sandbox (or absent) there is no root, so only {env} works — a secret file is
|
|
306
|
+
* typed into a page the caller chose, and must not be any file the daemon can read. */
|
|
307
|
+
export function secretRootOf(localPath: string | undefined, sandboxRoots: string[]): string | undefined {
|
|
308
|
+
if (!localPath) return undefined;
|
|
309
|
+
try {
|
|
310
|
+
return resolveSandboxedPath(localPath, sandboxRoots);
|
|
311
|
+
} catch {
|
|
312
|
+
return undefined; // refused by the sandbox; fill reports "needs local_path inside the daemon sandbox"
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
302
316
|
export function resolveVerifyAuth(
|
|
303
317
|
deps: LensDeps,
|
|
304
318
|
project: string,
|
|
@@ -926,6 +940,7 @@ export async function lensRunFlow(deps: LensDeps, input: RunFlowInput) {
|
|
|
926
940
|
viewport: coerceViewport(input.viewport, input.device),
|
|
927
941
|
critic: input.critic,
|
|
928
942
|
localPath: input.local_path, // F213.6 — sandbox root for uploadFile fixture paths
|
|
943
|
+
secretRoot: secretRootOf(input.local_path, deps.sandboxRoots), // F009.121
|
|
929
944
|
record: input.record === true, // F255.5 — opt-in .webm of the whole run
|
|
930
945
|
storage: coerceStorageSeed(input.storage), // F074.97 — seed saved client state
|
|
931
946
|
});
|
|
@@ -988,6 +1003,7 @@ export async function lensRunManuscript(deps: LensDeps, input: RunManuscriptInpu
|
|
|
988
1003
|
reducedMotion: input.reduced_motion,
|
|
989
1004
|
viewport: coerceViewport(input.viewport, input.device), // F252.2 — mobile-only UI E2E in manuscripts
|
|
990
1005
|
localPath: input.local_path, // F213.6 — uploadFile fixture root
|
|
1006
|
+
secretRoot: secretRootOf(input.local_path, deps.sandboxRoots), // F009.121
|
|
991
1007
|
record: input.record === true, // F255.5 — opt-in .webm of the whole run
|
|
992
1008
|
storage: coerceStorageSeed(input.storage), // F074.97 — seed saved client state
|
|
993
1009
|
});
|