@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.
Files changed (110) hide show
  1. package/README.md +187 -0
  2. package/bin/capture +31278 -7312
  3. package/package.json +3 -1
  4. package/src/capture.ts +21 -18
  5. package/src/cdp/args.ts +269 -49
  6. package/src/cdp/bridge/server.ts +6 -5
  7. package/src/cdp/bridge/spawn.ts +41 -97
  8. package/src/cdp/commands/bridge-serve.ts +11 -21
  9. package/src/cdp/commands/cdp.ts +44 -22
  10. package/src/cdp/commands/heap/census.ts +61 -0
  11. package/src/cdp/commands/heap/common.ts +77 -0
  12. package/src/cdp/commands/heap/diff.ts +92 -0
  13. package/src/cdp/commands/heap/index.ts +38 -0
  14. package/src/cdp/commands/heap/objects.ts +42 -0
  15. package/src/cdp/commands/heap/retainers.ts +52 -0
  16. package/src/cdp/commands/heap/snapshot.ts +94 -0
  17. package/src/cdp/commands/lib.ts +9 -17
  18. package/src/cdp/commands/lighthouse.ts +414 -0
  19. package/src/cdp/commands/measure/census.ts +2 -1
  20. package/src/cdp/commands/measure/check.ts +70 -16
  21. package/src/cdp/commands/measure/diff.ts +1 -2
  22. package/src/cdp/commands/measure/explain.ts +44 -7
  23. package/src/cdp/commands/measure/index.ts +24 -32
  24. package/src/cdp/commands/measure/map-ax.ts +2 -1
  25. package/src/cdp/commands/measure/map-focus.ts +2 -1
  26. package/src/cdp/commands/measure/map-layers.ts +5 -3
  27. package/src/cdp/commands/measure/map-scroll.ts +2 -1
  28. package/src/cdp/commands/measure/snap.ts +5 -4
  29. package/src/cdp/commands/measure/sweep.ts +16 -11
  30. package/src/cdp/commands/measure/text.ts +147 -0
  31. package/src/cdp/commands/motion/index.ts +10 -18
  32. package/src/cdp/commands/motion/jank.ts +13 -22
  33. package/src/cdp/commands/motion/mask.ts +10 -9
  34. package/src/cdp/commands/motion/rec.ts +228 -26
  35. package/src/cdp/commands/motion/timeline.ts +1 -1
  36. package/src/cdp/commands/page/click.ts +15 -5
  37. package/src/cdp/commands/page/elements.ts +43 -11
  38. package/src/cdp/commands/page/exec.ts +53 -18
  39. package/src/cdp/commands/page/index.ts +20 -20
  40. package/src/cdp/commands/page/inspect.ts +338 -0
  41. package/src/cdp/commands/page/navigate.ts +83 -38
  42. package/src/cdp/commands/page/repeat.ts +176 -0
  43. package/src/cdp/commands/page/scroll.ts +10 -5
  44. package/src/cdp/commands/page/shot.ts +66 -31
  45. package/src/cdp/commands/page/type.ts +6 -2
  46. package/src/cdp/commands/perf/index.ts +32 -0
  47. package/src/cdp/commands/perf/insights.ts +214 -0
  48. package/src/cdp/commands/perf/trace.ts +235 -0
  49. package/src/cdp/commands/perf/vitals.ts +76 -0
  50. package/src/cdp/commands/session/collectors.ts +54 -0
  51. package/src/cdp/commands/stub.ts +10 -0
  52. package/src/cdp/commands/tab/close.ts +2 -2
  53. package/src/cdp/commands/tab/index.ts +16 -23
  54. package/src/cdp/commands/tab/launch.ts +1 -1
  55. package/src/cdp/commands/tab/mock/index.ts +24 -0
  56. package/src/cdp/commands/tab/mock/lifecycle.ts +88 -0
  57. package/src/cdp/commands/tab/mock/start.ts +65 -0
  58. package/src/cdp/commands/tab/mock/stop.ts +39 -0
  59. package/src/cdp/commands/tab/network.ts +22 -13
  60. package/src/cdp/commands/tab/reset.ts +11 -12
  61. package/src/cdp/connection.ts +19 -39
  62. package/src/cdp/detect.ts +27 -7
  63. package/src/cdp/dispatch.ts +25 -4
  64. package/src/cdp/domains.ts +2 -2
  65. package/src/cdp/har-recorder.ts +0 -1
  66. package/src/cdp/heap-snapshot.ts +647 -0
  67. package/src/cdp/host/client.ts +22 -0
  68. package/src/cdp/host/collector.ts +74 -0
  69. package/src/cdp/host/collectors/heap.ts +79 -0
  70. package/src/cdp/host/collectors/intercept.ts +247 -0
  71. package/src/cdp/{recorder-bridge.ts → host/collectors/motion.ts} +388 -473
  72. package/src/cdp/host/collectors/trace.ts +108 -0
  73. package/src/cdp/host/core.ts +217 -0
  74. package/src/cdp/host/handle.ts +47 -0
  75. package/src/cdp/host/held-client.ts +91 -0
  76. package/src/cdp/host/kinds.ts +97 -0
  77. package/src/cdp/host/lifecycle.ts +123 -0
  78. package/src/cdp/host/server.ts +88 -0
  79. package/src/cdp/measure/check.ts +181 -16
  80. package/src/cdp/measure/collectors/hittest.ts +16 -10
  81. package/src/cdp/measure/collectors/scroll.ts +15 -7
  82. package/src/cdp/measure/collectors/style-provenance.ts +11 -0
  83. package/src/cdp/measure/collectors/styles.ts +4 -4
  84. package/src/cdp/measure/explain.ts +14 -2
  85. package/src/cdp/measure/map-layers.ts +52 -8
  86. package/src/cdp/measure/snapshot.ts +5 -3
  87. package/src/cdp/measure/sweep.ts +29 -6
  88. package/src/cdp/measure/text.ts +227 -0
  89. package/src/cdp/motion/jank.ts +29 -83
  90. package/src/cdp/motion/mask.ts +18 -9
  91. package/src/cdp/motion/recorder.ts +86 -731
  92. package/src/cdp/scope-lock.ts +9 -9
  93. package/src/cdp/screenshot.ts +51 -21
  94. package/src/cdp/timing.ts +5 -5
  95. package/src/cdp/trace-analysis.ts +320 -0
  96. package/src/cdp/types.ts +31 -1
  97. package/src/cdp.ts +4 -6
  98. package/src/har-manager.ts +2 -4
  99. package/src/interact.ts +50 -23
  100. package/src/output/artifact.ts +49 -5
  101. package/src/output/render.ts +60 -9
  102. package/src/output/selection.ts +6 -0
  103. package/src/session/artifacts.ts +122 -12
  104. package/src/session/commands.ts +348 -113
  105. package/src/session/coordinator.ts +0 -93
  106. package/src/session/har-redact.ts +29 -23
  107. package/src/session-context.ts +13 -9
  108. package/src/cdp/bridge/protocol.ts +0 -224
  109. package/src/cdp/recorder-client.ts +0 -326
  110. 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 — the ONE narrow
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, never on
8
- * the stored `har.json`/live NDJSON, and never on the value a `--filter-url`
9
- * pattern is matched against. Its whole job is to keep a dashboard/API session
10
- * token out of an agent's transcript and terminal scrollback while leaving the
11
- * request itself identifiable — the parameter name, ordering, and every
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 parameter NAME only; no value is ever inspected, so this
16
- * cannot depend on the shape of any observed secret.
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
- * Parameter names that are credential-bearing on their own. Compared after
24
- * normalization (lowercased, every non-alphanumeric character dropped), so
25
- * `api_key`, `API-Key`, and `apiKey` all reduce to `apikey`.
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 CREDENTIAL_PARAM_NAMES: ReadonlySet<string> = new Set([
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 parameter name credential-bearing —
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 CREDENTIAL_PARAM_FRAGMENTS: readonly string[] = [
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 normalizeParamName(raw: string): string {
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/fragment parameter name names a credential. */
81
- export function isCredentialParamName(raw: string): boolean {
82
- const name = normalizeParamName(raw);
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 (CREDENTIAL_PARAM_NAMES.has(name)) return true;
85
- return CREDENTIAL_PARAM_FRAGMENTS.some((fragment) => name.includes(fragment));
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 isCredentialParamName(name) ? `${name}=${REDACTED_VALUE}` : pair;
102
+ return isCredentialName(name) ? `${name}=${REDACTED_VALUE}` : pair;
97
103
  })
98
104
  .join('&');
99
105
  }
@@ -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 raw = readPrivateFile(sessionMetaPath(sessionDir)).toString('utf-8');
227
- const parsed = JSON.parse(raw) as unknown;
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;