@crouton-kit/capture 1.4.2 → 1.4.11
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 +187 -0
- package/bin/capture +31278 -7312
- package/package.json +3 -1
- package/src/capture.ts +21 -18
- package/src/cdp/args.ts +269 -49
- package/src/cdp/bridge/server.ts +6 -5
- package/src/cdp/bridge/spawn.ts +41 -97
- package/src/cdp/commands/bridge-serve.ts +11 -21
- package/src/cdp/commands/cdp.ts +44 -22
- package/src/cdp/commands/heap/census.ts +61 -0
- package/src/cdp/commands/heap/common.ts +77 -0
- package/src/cdp/commands/heap/diff.ts +92 -0
- package/src/cdp/commands/heap/index.ts +38 -0
- package/src/cdp/commands/heap/objects.ts +42 -0
- package/src/cdp/commands/heap/retainers.ts +52 -0
- package/src/cdp/commands/heap/snapshot.ts +94 -0
- package/src/cdp/commands/lib.ts +9 -17
- package/src/cdp/commands/lighthouse.ts +414 -0
- package/src/cdp/commands/measure/census.ts +2 -1
- package/src/cdp/commands/measure/check.ts +70 -16
- package/src/cdp/commands/measure/diff.ts +1 -2
- package/src/cdp/commands/measure/explain.ts +44 -7
- package/src/cdp/commands/measure/index.ts +24 -32
- package/src/cdp/commands/measure/map-ax.ts +2 -1
- package/src/cdp/commands/measure/map-focus.ts +2 -1
- package/src/cdp/commands/measure/map-layers.ts +5 -3
- package/src/cdp/commands/measure/map-scroll.ts +2 -1
- package/src/cdp/commands/measure/snap.ts +5 -4
- package/src/cdp/commands/measure/sweep.ts +16 -11
- package/src/cdp/commands/measure/text.ts +147 -0
- package/src/cdp/commands/motion/index.ts +10 -18
- package/src/cdp/commands/motion/jank.ts +13 -22
- package/src/cdp/commands/motion/mask.ts +10 -9
- package/src/cdp/commands/motion/rec.ts +228 -26
- package/src/cdp/commands/motion/timeline.ts +1 -1
- package/src/cdp/commands/page/click.ts +15 -5
- package/src/cdp/commands/page/elements.ts +43 -11
- package/src/cdp/commands/page/exec.ts +53 -18
- package/src/cdp/commands/page/index.ts +20 -20
- package/src/cdp/commands/page/inspect.ts +338 -0
- package/src/cdp/commands/page/navigate.ts +83 -38
- package/src/cdp/commands/page/repeat.ts +176 -0
- package/src/cdp/commands/page/scroll.ts +10 -5
- package/src/cdp/commands/page/shot.ts +66 -31
- package/src/cdp/commands/page/type.ts +6 -2
- package/src/cdp/commands/perf/index.ts +32 -0
- package/src/cdp/commands/perf/insights.ts +214 -0
- package/src/cdp/commands/perf/trace.ts +235 -0
- package/src/cdp/commands/perf/vitals.ts +76 -0
- package/src/cdp/commands/session/collectors.ts +54 -0
- package/src/cdp/commands/stub.ts +10 -0
- package/src/cdp/commands/tab/close.ts +2 -2
- package/src/cdp/commands/tab/index.ts +16 -23
- package/src/cdp/commands/tab/launch.ts +1 -1
- package/src/cdp/commands/tab/mock/index.ts +24 -0
- package/src/cdp/commands/tab/mock/lifecycle.ts +88 -0
- package/src/cdp/commands/tab/mock/start.ts +65 -0
- package/src/cdp/commands/tab/mock/stop.ts +39 -0
- package/src/cdp/commands/tab/network.ts +22 -13
- package/src/cdp/commands/tab/reset.ts +11 -12
- package/src/cdp/connection.ts +19 -39
- package/src/cdp/detect.ts +27 -7
- package/src/cdp/dispatch.ts +25 -4
- package/src/cdp/domains.ts +2 -2
- package/src/cdp/har-recorder.ts +0 -1
- package/src/cdp/heap-snapshot.ts +647 -0
- package/src/cdp/host/client.ts +22 -0
- package/src/cdp/host/collector.ts +74 -0
- package/src/cdp/host/collectors/heap.ts +79 -0
- package/src/cdp/host/collectors/intercept.ts +247 -0
- package/src/cdp/{recorder-bridge.ts → host/collectors/motion.ts} +388 -473
- package/src/cdp/host/collectors/trace.ts +108 -0
- package/src/cdp/host/core.ts +217 -0
- package/src/cdp/host/handle.ts +47 -0
- package/src/cdp/host/held-client.ts +91 -0
- package/src/cdp/host/kinds.ts +97 -0
- package/src/cdp/host/lifecycle.ts +123 -0
- package/src/cdp/host/server.ts +88 -0
- package/src/cdp/measure/check.ts +181 -16
- package/src/cdp/measure/collectors/hittest.ts +16 -10
- package/src/cdp/measure/collectors/scroll.ts +15 -7
- package/src/cdp/measure/collectors/style-provenance.ts +11 -0
- package/src/cdp/measure/collectors/styles.ts +4 -4
- package/src/cdp/measure/explain.ts +14 -2
- package/src/cdp/measure/map-layers.ts +52 -8
- package/src/cdp/measure/snapshot.ts +5 -3
- package/src/cdp/measure/sweep.ts +29 -6
- package/src/cdp/measure/text.ts +227 -0
- package/src/cdp/motion/jank.ts +29 -83
- package/src/cdp/motion/mask.ts +18 -9
- package/src/cdp/motion/recorder.ts +86 -731
- package/src/cdp/scope-lock.ts +9 -9
- package/src/cdp/screenshot.ts +51 -21
- package/src/cdp/timing.ts +5 -5
- package/src/cdp/trace-analysis.ts +320 -0
- package/src/cdp/types.ts +31 -1
- package/src/cdp.ts +4 -6
- package/src/har-manager.ts +2 -4
- package/src/interact.ts +50 -23
- package/src/output/artifact.ts +49 -5
- package/src/output/render.ts +60 -9
- package/src/output/selection.ts +6 -0
- package/src/session/artifacts.ts +122 -12
- package/src/session/commands.ts +348 -113
- package/src/session/coordinator.ts +0 -93
- package/src/session/har-redact.ts +29 -23
- package/src/session-context.ts +13 -9
- package/src/cdp/bridge/protocol.ts +0 -224
- package/src/cdp/recorder-client.ts +0 -326
- package/src/cdp/tab-lock.ts +0 -132
|
@@ -161,103 +161,10 @@ export async function beginSessionStop(sessionDir: string): Promise<SessionStopA
|
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
// Neutral recorder-handle scan + lifecycle transaction. Owned here so tab/reset
|
|
166
|
-
// and recorder start never import a motion feature (A2). Motion CONSEQUENCES
|
|
167
|
-
// (orphan finalize, markers/meta, viewport restore) stay in recorder.ts.
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
export interface RecorderHandleRecord {
|
|
171
|
-
recId: string; pid: number; socketPath: string; targetId: string;
|
|
172
|
-
url: string | null; startedAt: string; state: string; birth: PidBirth; markers: unknown;
|
|
173
|
-
/** The recording's control-socket admission token (64 lowercase hex chars,
|
|
174
|
-
* server-generated by the recorder bridge at boot). Required — a handle
|
|
175
|
-
* without a structurally valid nonce is malformed; there is no
|
|
176
|
-
* unauthenticated or legacy handle shape. */
|
|
177
|
-
nonce: string;
|
|
178
|
-
}
|
|
179
|
-
export type RecorderHandleClassification = 'live' | 'dead' | 'malformed' | 'unknown';
|
|
180
|
-
export interface ScannedRecorderHandle {
|
|
181
|
-
recId: string; recDir: string; classification: RecorderHandleClassification;
|
|
182
|
-
record: RecorderHandleRecord | null; // null iff malformed
|
|
183
|
-
}
|
|
184
164
|
export interface LifecycleSeams {
|
|
185
165
|
pidBirthProvider?: PidBirthProvider; nowNs?: () => bigint;
|
|
186
166
|
sleep?: (ms: number) => Promise<void>; token?: () => string;
|
|
187
167
|
acquireTimeoutMs?: number; leaseMs?: number;
|
|
188
|
-
/** How long a graceful recorder stop waits for the bridge process's own
|
|
189
|
-
* verified exit before escalating to an identity-checked SIGTERM
|
|
190
|
-
* (default ~2000ms) — injectable so tests with never-exiting placeholder
|
|
191
|
-
* children stay fast. */
|
|
192
|
-
stopExitTimeoutMs?: number;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Structural gate for a recorder control-socket nonce — the exact shape the
|
|
196
|
-
* bridge generates (`crypto.randomBytes(32).toString('hex')`). */
|
|
197
|
-
export const RECORDER_NONCE = /^[0-9a-f]{64}$/;
|
|
198
|
-
|
|
199
|
-
function parseRecorderHandle(raw: unknown): RecorderHandleRecord | null {
|
|
200
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
201
|
-
const r = raw as Record<string, unknown>;
|
|
202
|
-
if (typeof r.recId !== 'string' || !r.recId) return null;
|
|
203
|
-
if (!Number.isSafeInteger(r.pid) || (r.pid as number) <= 0) return null;
|
|
204
|
-
if (typeof r.socketPath !== 'string' || !r.socketPath) return null;
|
|
205
|
-
if (typeof r.targetId !== 'string' || !r.targetId) return null;
|
|
206
|
-
if (!(r.url === null || typeof r.url === 'string')) return null;
|
|
207
|
-
if (typeof r.startedAt !== 'string' || !r.startedAt) return null;
|
|
208
|
-
if (typeof r.state !== 'string' || !r.state) return null;
|
|
209
|
-
if (!isBirth(r.birth)) return null;
|
|
210
|
-
if (typeof r.nonce !== 'string' || !RECORDER_NONCE.test(r.nonce)) return null;
|
|
211
|
-
return { recId: r.recId, pid: r.pid as number, socketPath: r.socketPath, targetId: r.targetId,
|
|
212
|
-
url: r.url as string | null, startedAt: r.startedAt, state: r.state, birth: r.birth as PidBirth, markers: r.markers,
|
|
213
|
-
nonce: r.nonce };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** True iff the recorded process is the same birth-identity still alive. */
|
|
217
|
-
export function recorderProcessAlive(record: Pick<RecorderHandleRecord, 'pid' | 'birth'>, provider: PidBirthProvider = processPidBirthProvider): boolean {
|
|
218
|
-
const now = provider.read(record.pid);
|
|
219
|
-
return now.status === 'found' && sameBirth(now.identity, record.birth);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function scanRecorderHandles(sessionDir: string, seams: LifecycleSeams = {}): ScannedRecorderHandle[] {
|
|
223
|
-
const provider = seams.pidBirthProvider ?? processPidBirthProvider;
|
|
224
|
-
const recsRoot = path.join(assertUnderCaptureRoot(sessionDir), 'motion', 'recs');
|
|
225
|
-
let entries: fs.Dirent[];
|
|
226
|
-
try { entries = fs.readdirSync(recsRoot, { withFileTypes: true }); }
|
|
227
|
-
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; throw error; }
|
|
228
|
-
const out: ScannedRecorderHandle[] = [];
|
|
229
|
-
for (const entry of entries) {
|
|
230
|
-
if (!entry.isDirectory()) continue;
|
|
231
|
-
const recDir = path.join(recsRoot, entry.name);
|
|
232
|
-
let raw: unknown;
|
|
233
|
-
try { raw = JSON.parse(readPrivateFile(path.join(recDir, 'recorder.json')).toString('utf-8')); }
|
|
234
|
-
catch (error) {
|
|
235
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue; // no handle
|
|
236
|
-
out.push({ recId: entry.name, recDir, classification: 'malformed', record: null }); continue;
|
|
237
|
-
}
|
|
238
|
-
const record = parseRecorderHandle(raw);
|
|
239
|
-
if (!record) { out.push({ recId: entry.name, recDir, classification: 'malformed', record: null }); continue; }
|
|
240
|
-
const read = provider.read(record.pid);
|
|
241
|
-
const classification: RecorderHandleClassification =
|
|
242
|
-
read.status === 'unknown' ? 'unknown'
|
|
243
|
-
: (read.status === 'found' && sameBirth(read.identity, record.birth) && record.state === 'recording') ? 'live'
|
|
244
|
-
: 'dead';
|
|
245
|
-
out.push({ recId: entry.name, recDir, classification, record });
|
|
246
|
-
}
|
|
247
|
-
return out;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/** Reset-time pointer hygiene: reset does NOT remove dead handles — it only
|
|
251
|
-
* clears a session pointer that no longer names a LIVE recorder. Dead handles
|
|
252
|
-
* are left intact so motion's own reapDeadHandles / session-stop teardown
|
|
253
|
-
* orphan-finalizes them on the next start/stop, preserving their partial frames
|
|
254
|
-
* as meta.json in the bundle. */
|
|
255
|
-
export async function clearDanglingRecorderPointer(sessionDir: string, scan: ScannedRecorderHandle[]): Promise<void> {
|
|
256
|
-
const active = getActiveSession();
|
|
257
|
-
if (active && active.dir === sessionDir && active.activeRecId
|
|
258
|
-
&& !scan.some(h => h.recId === active.activeRecId && h.classification === 'live')) {
|
|
259
|
-
await updateSessionState(sessionDir, { activeRecId: null });
|
|
260
|
-
}
|
|
261
168
|
}
|
|
262
169
|
|
|
263
170
|
export async function withSessionLifecycle<T>(sessionDir: string, action: () => Promise<T>, seams: LifecycleSeams = {}): Promise<T> {
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Render-time credential redaction for HAR request URLs
|
|
3
|
-
* carve-out in Capture's otherwise no-redaction posture.
|
|
2
|
+
* Render-time credential redaction for HAR request URLs and header values —
|
|
3
|
+
* the ONE narrow carve-out in Capture's otherwise no-redaction posture.
|
|
4
4
|
*
|
|
5
5
|
* Capture preserves browser evidence rather than redacting it, and the HAR
|
|
6
6
|
* artifact on disk remains full fidelity: this module is applied only where a
|
|
7
|
-
* URL is rendered into a `session har` block, never on collection,
|
|
8
|
-
* the stored `har.json`/live NDJSON, and never on the value a
|
|
9
|
-
* pattern is matched against. Its whole job is to keep a
|
|
10
|
-
* token out of an agent's transcript and terminal
|
|
11
|
-
* request
|
|
12
|
-
* non-credential value survive untouched, so the row still joins to the
|
|
7
|
+
* URL or header is rendered into a `session har` block, never on collection,
|
|
8
|
+
* never on the stored `har.json`/live NDJSON, and never on the value a
|
|
9
|
+
* `--filter-url` pattern is matched against. Its whole job is to keep a
|
|
10
|
+
* dashboard/API session token out of an agent's transcript and terminal
|
|
11
|
+
* scrollback while leaving the request identifiable — names, ordering, and
|
|
12
|
+
* every non-credential value survive untouched, so the row still joins to the
|
|
13
13
|
* artifact record it came from.
|
|
14
14
|
*
|
|
15
|
-
* Matching is on the
|
|
16
|
-
*
|
|
15
|
+
* Matching is on the name only; no value is ever inspected, so this cannot
|
|
16
|
+
* depend on the shape of any observed secret.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/** Replacement text for a credential-like parameter value in rendered output. */
|
|
20
20
|
export const REDACTED_VALUE = 'REDACTED';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Credential-bearing names. Compared after normalization (lowercased, every
|
|
24
|
+
* non-alphanumeric character dropped), so `api_key`, `API-Key`, and `apiKey`
|
|
25
|
+
* all reduce to `apikey`.
|
|
26
26
|
*/
|
|
27
|
-
const
|
|
27
|
+
const CREDENTIAL_NAMES: ReadonlySet<string> = new Set([
|
|
28
28
|
'key',
|
|
29
29
|
'apikey',
|
|
30
30
|
'accesskey',
|
|
@@ -49,11 +49,11 @@ const CREDENTIAL_PARAM_NAMES: ReadonlySet<string> = new Set([
|
|
|
49
49
|
]);
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Fragments that make ANY containing
|
|
52
|
+
* Fragments that make ANY containing name credential-bearing —
|
|
53
53
|
* `access_token`, `refresh-token`, `clientSecret`, `x-amz-signature`, and the
|
|
54
54
|
* rest of the long tail no exact list keeps up with.
|
|
55
55
|
*/
|
|
56
|
-
const
|
|
56
|
+
const CREDENTIAL_NAME_FRAGMENTS: readonly string[] = [
|
|
57
57
|
'token',
|
|
58
58
|
'secret',
|
|
59
59
|
'password',
|
|
@@ -65,9 +65,10 @@ const CREDENTIAL_PARAM_FRAGMENTS: readonly string[] = [
|
|
|
65
65
|
'signature',
|
|
66
66
|
'authorization',
|
|
67
67
|
'sessionid',
|
|
68
|
+
'loid',
|
|
68
69
|
];
|
|
69
70
|
|
|
70
|
-
function
|
|
71
|
+
function normalizeCredentialName(raw: string): string {
|
|
71
72
|
let decoded = raw;
|
|
72
73
|
try {
|
|
73
74
|
decoded = decodeURIComponent(raw.replace(/\+/g, ' '));
|
|
@@ -77,12 +78,17 @@ function normalizeParamName(raw: string): string {
|
|
|
77
78
|
return decoded.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
/** Whether a query
|
|
81
|
-
export function
|
|
82
|
-
const name =
|
|
81
|
+
/** Whether a query parameter or header name names a credential. */
|
|
82
|
+
export function isCredentialName(raw: string): boolean {
|
|
83
|
+
const name = normalizeCredentialName(raw);
|
|
83
84
|
if (name === '') return false;
|
|
84
|
-
if (
|
|
85
|
-
return
|
|
85
|
+
if (CREDENTIAL_NAMES.has(name)) return true;
|
|
86
|
+
return CREDENTIAL_NAME_FRAGMENTS.some((fragment) => name.includes(fragment));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Render a credential-valued header without exposing its value. */
|
|
90
|
+
export function redactHeaderCredential(name: string, value: string): string {
|
|
91
|
+
return isCredentialName(name) ? `redacted · ${value.length} chars` : value;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
/** Redacts credential values in one `a=1&b=2` parameter section. */
|
|
@@ -93,7 +99,7 @@ function redactParamSection(section: string): string {
|
|
|
93
99
|
const eq = pair.indexOf('=');
|
|
94
100
|
if (eq < 0) return pair;
|
|
95
101
|
const name = pair.slice(0, eq);
|
|
96
|
-
return
|
|
102
|
+
return isCredentialName(name) ? `${name}=${REDACTED_VALUE}` : pair;
|
|
97
103
|
})
|
|
98
104
|
.join('&');
|
|
99
105
|
}
|
package/src/session-context.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ensurePrivateDir,
|
|
17
17
|
parseRegisteredLogTailer,
|
|
18
18
|
readPrivateFile,
|
|
19
|
+
registerArtifactRoot,
|
|
19
20
|
removeArtifactTree,
|
|
20
21
|
unlinkPrivateFile,
|
|
21
22
|
writeJsonPrivate,
|
|
@@ -165,6 +166,7 @@ export function isActiveStateCandidate(value: unknown): value is ActiveSessionSt
|
|
|
165
166
|
&& validLogs
|
|
166
167
|
&& (record.port === undefined || record.port === null || (Number.isInteger(record.port) && (record.port as number) > 0 && (record.port as number) <= 65535))
|
|
167
168
|
&& nullableString(record.bridgeSocket)
|
|
169
|
+
&& (record.networkOffline === undefined || typeof record.networkOffline === 'boolean')
|
|
168
170
|
&& nullablePid(record.bridgePid)
|
|
169
171
|
&& nullableString(record.activeRecId)
|
|
170
172
|
&& nullableString(record.stoppedAt)
|
|
@@ -202,6 +204,14 @@ function clearActiveEntry(expected: ActiveEntryIdentity | null): void {
|
|
|
202
204
|
* preserves a stale pointer for the invocation error path. Normal command
|
|
203
205
|
* consumers retain the default eager stale-index cleanup.
|
|
204
206
|
*/
|
|
207
|
+
export function readSessionState(sessionDir: string): ActiveSessionState {
|
|
208
|
+
const canonical = assertUnderCaptureRoot(path.resolve(sessionDir));
|
|
209
|
+
const raw = readPrivateFile(sessionMetaPath(canonical)).toString('utf-8');
|
|
210
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
211
|
+
if (!isActiveStateCandidate(parsed) || parsed.dir !== canonical) throw new Error(`invalid session metadata: ${sessionMetaPath(canonical)}`);
|
|
212
|
+
return parsed;
|
|
213
|
+
}
|
|
214
|
+
|
|
205
215
|
export function getActiveSession({ cleanStale = true }: { cleanStale?: boolean } = {}): ActiveSessionState | null {
|
|
206
216
|
const pointer = readActivePointer();
|
|
207
217
|
const clean = (): void => { if (cleanStale) clearActiveEntry(pointer.entry); };
|
|
@@ -212,6 +222,7 @@ export function getActiveSession({ cleanStale = true }: { cleanStale?: boolean }
|
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
try {
|
|
225
|
+
registerArtifactRoot(path.dirname(path.resolve(index.dir)));
|
|
215
226
|
const sessionDir = assertUnderCaptureRoot(path.resolve(index.dir));
|
|
216
227
|
if (sessionDir !== index.dir) throw new Error('active session directory escaped capture root');
|
|
217
228
|
|
|
@@ -223,15 +234,8 @@ export function getActiveSession({ cleanStale = true }: { cleanStale?: boolean }
|
|
|
223
234
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
224
235
|
}
|
|
225
236
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
if (!isActiveStateCandidate(parsed)) {
|
|
229
|
-
clean();
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const state = parsed as ActiveSessionState;
|
|
234
|
-
if (state.sessionId !== index.sessionId || state.dir !== sessionDir) {
|
|
237
|
+
const state = readSessionState(sessionDir);
|
|
238
|
+
if (state.sessionId !== index.sessionId) {
|
|
235
239
|
clean();
|
|
236
240
|
return null;
|
|
237
241
|
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wire protocol between `capture cdp --browser` and the held bridge process
|
|
3
|
-
* (`capture __bridge-serve`). Newline-delimited JSON over a Unix domain
|
|
4
|
-
* socket — one request per connection, one response, then the socket closes.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface BridgeRequest {
|
|
8
|
-
reqId: number;
|
|
9
|
-
/** CDP method, e.g. "Browser.grantPermissions". Omit when only waiting for an event. */
|
|
10
|
-
method?: string;
|
|
11
|
-
params?: Record<string, unknown>;
|
|
12
|
-
/**
|
|
13
|
-
* Target (page or worker) to scope this call to, via a flattened
|
|
14
|
-
* `Target.attachToTarget` session on the SAME held browser websocket.
|
|
15
|
-
* Required for domains that live on a target rather than the browser
|
|
16
|
-
* itself (`ServiceWorker.*`, `Page.*`, ...) — omit for pure browser-level
|
|
17
|
-
* domains (`Browser.*`, `Target.*`).
|
|
18
|
-
*/
|
|
19
|
-
targetId?: string;
|
|
20
|
-
/** CDP event name to observe after this request is armed. Observation is future-only and scoped to this request's actual flattened target session (`undefined` without `targetId`); one event broadcasts to all already-armed same-key requests. */
|
|
21
|
-
waitEvent?: string;
|
|
22
|
-
timeoutMs?: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface BridgeResponseOk {
|
|
26
|
-
reqId: number;
|
|
27
|
-
ok: true;
|
|
28
|
-
result?: unknown;
|
|
29
|
-
event?: unknown;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface BridgeResponseErr {
|
|
33
|
-
reqId: number;
|
|
34
|
-
ok: false;
|
|
35
|
-
error: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type BridgeResponse = BridgeResponseOk | BridgeResponseErr;
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Recorder-mode protocol — the SAME NDJSON-over-unix-socket wire format as
|
|
42
|
-
// above, specialized for `capture motion rec` (see ../recorder-bridge.ts).
|
|
43
|
-
// The generic `BridgeRequest`/`BridgeResponse` are unchanged and still
|
|
44
|
-
// govern `capture cdp --browser` / `session start --hold`; a recorder-mode
|
|
45
|
-
// bridge process (spawned in recorder mode, see ../bridge/spawn.ts) speaks
|
|
46
|
-
// this union instead, one request per connection, one response, same as
|
|
47
|
-
// the plain bridge.
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The three-way clock baseline the design's "Recorder timing model" requires to convert
|
|
52
|
-
* screencast-frame and Tracing timestamps into the authoritative `performance.now()` domain.
|
|
53
|
-
* `performanceNowMs`/`wallClockMs` are read synchronously when the recorder arms (`rec-start`);
|
|
54
|
-
* `firstScreencastTimestampSec`/`firstTraceEventTsUs` cannot be — no frame or trace batch has
|
|
55
|
-
* necessarily arrived yet at that instant — so they are filled in opportunistically as the
|
|
56
|
-
* recorder's own screencast/tracing handlers observe the first occurrence of each, and stay
|
|
57
|
-
* `null` (with `baselinesPending: true`) until then. `rec-stop`'s response carries the same
|
|
58
|
-
* object read again at stop time — the flush path a caller uses to get the completed baselines
|
|
59
|
-
* for `markers.json` even when `rec-start`'s own response returned it still pending.
|
|
60
|
-
*/
|
|
61
|
-
export interface RecorderClockBaselines {
|
|
62
|
-
performanceNowMs: number;
|
|
63
|
-
wallClockMs: number;
|
|
64
|
-
/** First `Page.screencastFrame`'s `metadata.timestamp` (wall-clock seconds), or `null` if none had arrived yet at the moment this snapshot was taken. */
|
|
65
|
-
firstScreencastTimestampSec: number | null;
|
|
66
|
-
/** First `Tracing.dataCollected` batch's earliest event `ts` (trace-clock microseconds), or `null` if none had arrived yet at the moment this snapshot was taken. */
|
|
67
|
-
firstTraceEventTsUs: number | null;
|
|
68
|
-
/** `true` while either timestamp above is still `null`. */
|
|
69
|
-
baselinesPending: boolean;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Arms the recorder on its held tab connection: enables the motion-rec CDP
|
|
74
|
-
* domains, starts `Page.startScreencast` + `Tracing`, injects the
|
|
75
|
-
* Mutation/Resize/Performance observers, and captures the clock baseline.
|
|
76
|
-
* A recorder process only accepts one `rec-start` while `state==="idle"`.
|
|
77
|
-
*
|
|
78
|
-
* `nonce` (here and on every other recorder request shape) is the
|
|
79
|
-
* per-recording control-socket admission token: the recorder bridge process
|
|
80
|
-
* generates it at boot (64 lowercase hex chars, 256 bits) and hands it back
|
|
81
|
-
* to its starter over a private boot file; every request line must carry it
|
|
82
|
-
* verbatim, and the server constant-time-compares it BEFORE any handler
|
|
83
|
-
* runs. There is no unauthenticated request shape and no compatibility
|
|
84
|
-
* lane — a missing or mismatched nonce is answered `ok:false error:
|
|
85
|
-
* 'unauthorized'` with no side effects.
|
|
86
|
-
*/
|
|
87
|
-
export interface RecStartRequest {
|
|
88
|
-
reqId: number;
|
|
89
|
-
type: 'rec-start';
|
|
90
|
-
/** Per-recording control-socket admission token — see the doc comment above. */
|
|
91
|
-
nonce: string;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export interface RecStartResponseOk {
|
|
95
|
-
reqId: number;
|
|
96
|
-
ok: true;
|
|
97
|
-
type: 'rec-start';
|
|
98
|
-
/** Returned for the caller to persist into `markers.json` — the recorder itself does not write that file. */
|
|
99
|
-
markers: RecorderClockBaselines;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Stops screencast + tracing, flushes the injected observers, and tears
|
|
104
|
-
* down the recorder's subscriptions (not the socket — the caller closes
|
|
105
|
-
* the bridge process once it has read the response). Returns counts for
|
|
106
|
-
* the caller's `meta.json`; the recorder itself does not write that file.
|
|
107
|
-
*/
|
|
108
|
-
export interface RecStopRequest {
|
|
109
|
-
reqId: number;
|
|
110
|
-
type: 'rec-stop';
|
|
111
|
-
/** Per-recording control-socket admission token — see `RecStartRequest`. */
|
|
112
|
-
nonce: string;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface RecStopResponseOk {
|
|
116
|
-
reqId: number;
|
|
117
|
-
ok: true;
|
|
118
|
-
type: 'rec-stop';
|
|
119
|
-
frameCount: number;
|
|
120
|
-
eventCount: number;
|
|
121
|
-
durationMs: number;
|
|
122
|
-
/** The clock baselines re-read at stop time — the flush path for a `rec-start` response whose baselines were still pending. */
|
|
123
|
-
markers: RecorderClockBaselines;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* A CDP request routed through the recorder's held tab connection — the
|
|
128
|
-
* mechanism intervening session commands (`click`, `type`, `navigate`, ...)
|
|
129
|
-
* use during a composed recording. Setting `mark` brackets the dispatch
|
|
130
|
-
* with two performance.now() reads taken in the page's execution context
|
|
131
|
-
* (`../timing.ts`'s `withDocumentPerformanceNow`) and appends a labeled
|
|
132
|
-
* input-landmark record to `events.jsonl`, tying the action to the frame
|
|
133
|
-
* timeline — this is the "marked CDP request message" the protocol
|
|
134
|
-
* supports. Omit `mark` for a plain passthrough call (still observed
|
|
135
|
-
* natively by the recorder's own event subscriptions, just not logged as a
|
|
136
|
-
* distinct input landmark). Omit `method` for a wait-event-only request;
|
|
137
|
-
* the bridge arms a future-only wait on the recorder tab websocket's
|
|
138
|
-
* unscoped event envelope and returns the matched event in one response.
|
|
139
|
-
*/
|
|
140
|
-
export interface RecCdpDispatchRequest {
|
|
141
|
-
reqId: number;
|
|
142
|
-
type: 'cdp';
|
|
143
|
-
/** Per-recording control-socket admission token — see `RecStartRequest`. */
|
|
144
|
-
nonce: string;
|
|
145
|
-
method: string;
|
|
146
|
-
params?: Record<string, unknown>;
|
|
147
|
-
mark?: string;
|
|
148
|
-
waitEvent?: string;
|
|
149
|
-
timeoutMs?: number;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export interface RecCdpWaitEventRequest {
|
|
153
|
-
reqId: number;
|
|
154
|
-
type: 'cdp';
|
|
155
|
-
/** Per-recording control-socket admission token — see `RecStartRequest`. */
|
|
156
|
-
nonce: string;
|
|
157
|
-
method?: undefined;
|
|
158
|
-
waitEvent: string;
|
|
159
|
-
timeoutMs?: number;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export type RecCdpRequest = RecCdpDispatchRequest | RecCdpWaitEventRequest;
|
|
163
|
-
|
|
164
|
-
export interface RecCdpResponseOk {
|
|
165
|
-
reqId: number;
|
|
166
|
-
ok: true;
|
|
167
|
-
type: 'cdp';
|
|
168
|
-
result?: unknown;
|
|
169
|
-
event?: unknown;
|
|
170
|
-
/**
|
|
171
|
-
* Present iff this request armed a `waitEvent` AND carried a `method`. It
|
|
172
|
-
* reports the load-wait outcome SEPARATELY from the method result: a method
|
|
173
|
-
* dispatch that succeeds is always `ok:true` with its `result` preserved,
|
|
174
|
-
* regardless of whether the paired wait observed its event
|
|
175
|
-
* (`'observed'`, with `event` set) or the wait's own bounded timer elapsed
|
|
176
|
-
* first (`'bounded-timeout'`, no `event`). A wait-event-ONLY request (no
|
|
177
|
-
* `method`) whose wait times out is still `ok:false` — that path has no
|
|
178
|
-
* method result to preserve, so its timeout is a genuine failure.
|
|
179
|
-
*/
|
|
180
|
-
waitOutcome?: 'observed' | 'bounded-timeout';
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* The action-return HAR flush/health barrier. A recorder-routed page action
|
|
185
|
-
* (`click`, `type`, `scroll`, `exec`, `navigate`) sends this after its settle
|
|
186
|
-
* window and before claiming success, so every HAR entry/body/append the
|
|
187
|
-
* recorder's streaming collector had already completed by then is durably in
|
|
188
|
-
* the owning session's live HAR first. Deliberately NON-attributing: the
|
|
189
|
-
* response reports store health only — `ok:true` means "all work completed at
|
|
190
|
-
* request time has appended and no fatal store error is latched"; it never
|
|
191
|
-
* says which entries belong to which action and carries no counts (the
|
|
192
|
-
* collector cannot truthfully attribute concurrent traffic to one action).
|
|
193
|
-
* Once the terminal `rec-stop` latch is claimed, HAR finalization belongs to
|
|
194
|
-
* that one owner and a later `har-flush` is answered with a deterministic
|
|
195
|
-
* `ok:false` rejection instead of sharing or re-driving the drain.
|
|
196
|
-
*/
|
|
197
|
-
export interface RecHarFlushRequest {
|
|
198
|
-
reqId: number;
|
|
199
|
-
type: 'har-flush';
|
|
200
|
-
/** Per-recording control-socket admission token — see `RecStartRequest`. */
|
|
201
|
-
nonce: string;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** Health-only: success carries no result fields beyond the envelope — see `RecHarFlushRequest`. */
|
|
205
|
-
export interface RecHarFlushResponseOk {
|
|
206
|
-
reqId: number;
|
|
207
|
-
ok: true;
|
|
208
|
-
type: 'har-flush';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export type RecorderRequest = RecStartRequest | RecStopRequest | RecCdpRequest | RecHarFlushRequest;
|
|
212
|
-
|
|
213
|
-
export interface RecorderResponseErr {
|
|
214
|
-
reqId: number;
|
|
215
|
-
ok: false;
|
|
216
|
-
type: RecorderRequest['type'];
|
|
217
|
-
error: string;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export type RecStartResponse = RecStartResponseOk | RecorderResponseErr;
|
|
221
|
-
export type RecStopResponse = RecStopResponseOk | RecorderResponseErr;
|
|
222
|
-
export type RecCdpResponse = RecCdpResponseOk | RecorderResponseErr;
|
|
223
|
-
export type RecHarFlushResponse = RecHarFlushResponseOk | RecorderResponseErr;
|
|
224
|
-
export type RecorderResponse = RecStartResponse | RecStopResponse | RecCdpResponse | RecHarFlushResponse;
|