@vosjs/render-core 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { RecordingMeta, CursorEvent, Backdrop, RecordingArtifact, ProjectDoc } from '@vosjs/studio-core';
1
+ import { RecordingMeta, CursorEvent, RecordingSkip, RecordingOutput, Backdrop, RecordingArtifact, ProjectDoc, RecordingPageMedia } from '@vosjs/studio-core';
2
2
 
3
3
  /**
4
4
  * The recorder's driver: what it needs from a browser, as a STRUCTURAL
@@ -21,13 +21,43 @@ interface RecorderRect {
21
21
  }
22
22
  interface RecorderLocator {
23
23
  waitFor(opts: {
24
- state: 'visible';
24
+ state: 'visible' | 'attached';
25
25
  timeout: number;
26
26
  }): Promise<unknown>;
27
27
  scrollIntoViewIfNeeded(): Promise<unknown>;
28
28
  boundingBox(): Promise<RecorderRect | null>;
29
29
  click(): Promise<unknown>;
30
30
  fill(value: string): Promise<unknown>;
31
+ /** what an `expect` and an `await` read, and the disabled-click report */
32
+ isVisible(): Promise<boolean>;
33
+ isEnabled(opts?: {
34
+ timeout?: number;
35
+ }): Promise<boolean>;
36
+ /** a `download` src reads the media element's source through the locator engine */
37
+ evaluate<R>(fn: (el: RecorderMediaElement) => R): Promise<R>;
38
+ }
39
+ /** What a `download` src reads off the element its selector matched. */
40
+ interface RecorderMediaElement {
41
+ currentSrc?: string;
42
+ src?: string;
43
+ getAttribute(name: string): string | null;
44
+ }
45
+ /** A browser download, as Playwright's `Download` answers it. */
46
+ interface RecorderDownload {
47
+ suggestedFilename(): string;
48
+ createReadStream(): Promise<AsyncIterable<unknown> | null>;
49
+ }
50
+ /** The page's own request context (its cookies), as Playwright's `APIResponse` answers. */
51
+ interface RecorderApiResponse {
52
+ ok(): boolean;
53
+ status(): number;
54
+ headers(): Record<string, string>;
55
+ body(): Promise<Uint8Array>;
56
+ }
57
+ /** Which button a press is, and which press of a multi-click. */
58
+ interface RecorderButtonOpts {
59
+ button?: 'left' | 'right' | 'middle';
60
+ clickCount?: number;
31
61
  }
32
62
  interface RecorderLocatorRoot {
33
63
  first(): RecorderLocator;
@@ -41,23 +71,41 @@ interface RecorderConsoleMessage {
41
71
  }
42
72
  interface RecorderPage {
43
73
  goto(url: string, opts: {
44
- waitUntil: 'networkidle';
74
+ waitUntil: 'networkidle' | 'load';
45
75
  timeout: number;
46
76
  }): Promise<RecorderResponse | null>;
47
77
  url(): string;
48
78
  title(): Promise<string>;
79
+ /** An `await` with `reload: true`; absent, the recorder opens the page's URL again. */
80
+ reload?(opts: {
81
+ waitUntil: 'networkidle' | 'load';
82
+ timeout: number;
83
+ }): Promise<RecorderResponse | null>;
84
+ /** A `download` step's click; absent, the recorder says the browser reports none. */
85
+ waitForEvent?(event: 'download', opts: {
86
+ timeout: number;
87
+ }): Promise<RecorderDownload>;
88
+ /** A `download` step's `src` fetch, with the page's cookies; absent, it fetches in the page. */
89
+ request?: {
90
+ get(url: string, opts?: {
91
+ timeout?: number;
92
+ }): Promise<RecorderApiResponse>;
93
+ };
49
94
  evaluate<T>(pageFunction: string | (() => T)): Promise<T>;
50
95
  on(event: 'console', cb: (m: RecorderConsoleMessage) => void): unknown;
51
96
  locator(selector: string): RecorderLocatorRoot;
52
97
  mouse: {
53
98
  move(x: number, y: number): Promise<void>;
54
- down(): Promise<void>;
55
- up(): Promise<void>;
99
+ down(opts?: RecorderButtonOpts): Promise<void>;
100
+ up(opts?: RecorderButtonOpts): Promise<void>;
56
101
  wheel(dx: number, dy: number): Promise<void>;
57
102
  };
58
103
  keyboard: {
59
104
  type(text: string): Promise<void>;
60
105
  press(key: string): Promise<void>;
106
+ /** A drag's modifiers, held from the press to the release. */
107
+ down(key: string): Promise<void>;
108
+ up(key: string): Promise<void>;
61
109
  };
62
110
  }
63
111
  interface RecorderCdp {
@@ -91,6 +139,13 @@ interface RecorderContext {
91
139
  */
92
140
  route?(pattern: string, handler: (route: RecorderRoute) => Promise<unknown> | unknown): Promise<unknown>;
93
141
  newCDPSession(page: unknown): Promise<RecorderCdp>;
142
+ /**
143
+ * A `paste` step's clipboard access, when the driver grants it; absent,
144
+ * the paste is dispatched in the page and the recorder says so.
145
+ */
146
+ grantPermissions?(permissions: string[], opts?: {
147
+ origin?: string;
148
+ }): Promise<unknown>;
94
149
  close(): Promise<void>;
95
150
  }
96
151
  interface RecorderBrowser {
@@ -114,6 +169,33 @@ interface RecorderBrowser {
114
169
  interface FrameSink {
115
170
  frame(file: string, bytes: Uint8Array): Promise<void> | void;
116
171
  }
172
+ /**
173
+ * Where a `download` step's file goes: the CLI writes `outputs/<name>` in
174
+ * the take directory. A host that hands in none cannot keep a download,
175
+ * and the recorder refuses such a script before capturing anything.
176
+ */
177
+ interface OutputSink {
178
+ save(name: string, bytes: Uint8Array, contentType?: string): Promise<void> | void;
179
+ }
180
+ /**
181
+ * Where the sound a page played during the take is kept: the CLI writes
182
+ * `page-sound/<name>` in the take directory and answers that take-relative
183
+ * key, which the plan's audio clips name. A host that hands in none (the
184
+ * fleet) still records `meta.media`; the plan then places nothing.
185
+ */
186
+ interface SoundSink {
187
+ save(name: string, bytes: Uint8Array, contentType?: string): Promise<string> | string;
188
+ }
189
+ /**
190
+ * Where the files a script names beside itself come from: a drag's path
191
+ * file, a paste's file, by a plain name. The CLI reads the actions file's
192
+ * directory. A host that hands in none (a hosted recorder carries the
193
+ * script alone) cannot run such a step, and the recorder refuses it before
194
+ * capturing anything.
195
+ */
196
+ interface ScriptFiles {
197
+ read(name: string): Promise<Uint8Array>;
198
+ }
117
199
  /** The clock and the pauses, injectable so a rehearsal runs fast. */
118
200
  interface RecorderClock {
119
201
  now(): number;
@@ -152,6 +234,7 @@ declare const SETTLE_MS: {
152
234
  readonly click: 150;
153
235
  readonly type: 150;
154
236
  readonly press: 150;
237
+ readonly paste: 150;
155
238
  readonly scroll: 200;
156
239
  readonly drag: 80;
157
240
  };
@@ -332,6 +415,36 @@ declare function deadWarns(r: DeadReport): boolean;
332
415
  /** The dead time in words, naming the steps and what to cut. */
333
416
  declare function deadLine(r: DeadReport): string;
334
417
 
418
+ /**
419
+ * The page's own console errors, as the recorder reports them. A page
420
+ * under recording is someone else's, and its errors are context, never the
421
+ * story: a third party's Content-Security-Policy violation runs to a few
422
+ * kilobytes and repeats on every load, which buried the step lines. So the
423
+ * log gets one line per distinct error, cut short, and a repeat is counted
424
+ * instead of printed; the full text stays in the result (and a verbose
425
+ * host prints every one whole).
426
+ */
427
+ /** How much of an error a log line keeps. */
428
+ declare const PAGE_ERROR_LINE_CHARS = 160;
429
+ interface PageError {
430
+ /** the full text, whitespace as the page wrote it */
431
+ text: string;
432
+ count: number;
433
+ }
434
+ /** One line of an error: whitespace collapsed, cut to the line's length. */
435
+ declare function pageErrorLine(text: string): string;
436
+ declare class PageErrorLog {
437
+ private readonly verbose;
438
+ private readonly seen;
439
+ constructor(verbose?: boolean);
440
+ /** The line to log for this error now, or null when it repeats one already said. */
441
+ add(text: string): string | null;
442
+ /** Every distinct error with how often it came, in the order first seen. */
443
+ entries(): PageError[];
444
+ /** The closing lines: each error that repeated, with its count. */
445
+ repeats(): string[];
446
+ }
447
+
335
448
  /**
336
449
  * What a signed-in take SHOWS. Getting past the login is half the problem;
337
450
  * the other half is that the account's own data is now in the frame: an
@@ -359,6 +472,9 @@ declare function deadLine(r: DeadReport): string;
359
472
  type ExposureKind = 'email' | 'key' | 'card' | 'card-tail';
360
473
  type Exposure = NonNullable<RecordingMeta['exposures']>[number];
361
474
  type MaskReport = NonNullable<RecordingMeta['masks']>[number];
475
+ type Notice = NonNullable<RecordingMeta['notices']>[number];
476
+ /** A notice as one scan reports it. */
477
+ type ScannedNotice = Omit<Notice, 'step' | 'seen'>;
362
478
  interface MaskRule {
363
479
  selector: string;
364
480
  /** `blur` (default) blurs the element; `text` swaps its words for `text`. */
@@ -378,12 +494,28 @@ interface MaskRule {
378
494
  * - card-tail: a masked number's visible tail (•••• 4242).
379
495
  */
380
496
  declare const EXPOSURE_MATCHERS_SRC = "(() => {\n const EMAIL = /[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,})/g\n const SAFE_HOST = /(^|\\.)example\\.(com|org|net)$|\\.(test|example|invalid|localhost)$/i\n const KEY = new RegExp([\n '\\\\b[srp]k_(?:live|test)_[A-Za-z0-9]{8,}',\n '\\\\bgh[pousr]_[A-Za-z0-9]{20,}',\n '\\\\bgithub_pat_[A-Za-z0-9_]{20,}',\n '\\\\bxox[abprs]-[A-Za-z0-9-]{10,}',\n '\\\\bAKIA[0-9A-Z]{16}\\\\b',\n '\\\\bAIza[0-9A-Za-z_-]{30,}',\n '\\\\bvos_(?:sk|rg|ho)_[A-Za-z0-9]{8,}',\n '\\\\beyJ[A-Za-z0-9_-]{8,}\\\\.eyJ[A-Za-z0-9_-]{8,}\\\\.[A-Za-z0-9_-]{8,}',\n ].join('|'))\n const CARD = /(?:^|[^0-9])((?:[0-9][ -]?){12,18}[0-9])(?![0-9])/g\n const TAIL = /[\\u2022\\u00b7*xX]{2,}[ -]?[0-9]{4}(?![0-9])/\n const luhn = (digits) => {\n let sum = 0, alt = false\n for (let i = digits.length - 1; i >= 0; i--) {\n let d = digits.charCodeAt(i) - 48\n if (alt) { d *= 2; if (d > 9) d -= 9 }\n sum += d; alt = !alt\n }\n return sum % 10 === 0\n }\n const kindsIn = (text) => {\n const kinds = []\n if (!text || text.length < 6) return kinds\n EMAIL.lastIndex = 0\n for (let m; (m = EMAIL.exec(text)); ) {\n if (!SAFE_HOST.test(m[1])) { kinds.push('email'); break }\n }\n if (KEY.test(text)) kinds.push('key')\n CARD.lastIndex = 0\n for (let m; (m = CARD.exec(text)); ) {\n const digits = m[1].replace(/[ -]/g, '')\n if (digits.length >= 13 && digits.length <= 19 && !/^0+$/.test(digits) && luhn(digits)) { kinds.push('card'); break }\n }\n if (TAIL.test(text)) kinds.push('card-tail')\n return kinds\n }\n return { kindsIn }\n})()";
497
+ /**
498
+ * The INFORMATION the scan reports beside the exposures (never a failure):
499
+ * the signed-in account's own display name, wherever the frame shows it,
500
+ * and a list of the account's recent items. Runs inside EXPOSURE_PROBE,
501
+ * after `seen` and before any selector is made: it sets `acct`, which
502
+ * `quiet` then keeps out of every selector. Never `$` + `{` in here: it is
503
+ * spliced into a template.
504
+ *
505
+ * The name is read conservatively, from where apps say whose account it
506
+ * is: a line that greets or names the account ("Signed in as …",
507
+ * "Welcome back, …"), the label of an account menu ("Account menu for …"),
508
+ * or the words on the account menu's own trigger (the button that opens
509
+ * a menu and holds the avatar), which are SURE; or a name-like label on an avatar (an image's alt, a
510
+ * button holding the picture), which MAY be the name.
511
+ */
512
+ declare const NOTICE_SRC: string;
381
513
  /**
382
514
  * The scan: visible text in the viewport, plus the values of text inputs.
383
515
  * Skips anything inside a masked element, password fields, and text the eye
384
516
  * cannot see. Reports kind + a selector-ish name for the element + its rect.
385
517
  */
386
- declare const EXPOSURE_PROBE = "(() => {\n const { kindsIn } = (() => {\n const EMAIL = /[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,})/g\n const SAFE_HOST = /(^|\\.)example\\.(com|org|net)$|\\.(test|example|invalid|localhost)$/i\n const KEY = new RegExp([\n '\\\\b[srp]k_(?:live|test)_[A-Za-z0-9]{8,}',\n '\\\\bgh[pousr]_[A-Za-z0-9]{20,}',\n '\\\\bgithub_pat_[A-Za-z0-9_]{20,}',\n '\\\\bxox[abprs]-[A-Za-z0-9-]{10,}',\n '\\\\bAKIA[0-9A-Z]{16}\\\\b',\n '\\\\bAIza[0-9A-Za-z_-]{30,}',\n '\\\\bvos_(?:sk|rg|ho)_[A-Za-z0-9]{8,}',\n '\\\\beyJ[A-Za-z0-9_-]{8,}\\\\.eyJ[A-Za-z0-9_-]{8,}\\\\.[A-Za-z0-9_-]{8,}',\n ].join('|'))\n const CARD = /(?:^|[^0-9])((?:[0-9][ -]?){12,18}[0-9])(?![0-9])/g\n const TAIL = /[\\u2022\\u00b7*xX]{2,}[ -]?[0-9]{4}(?![0-9])/\n const luhn = (digits) => {\n let sum = 0, alt = false\n for (let i = digits.length - 1; i >= 0; i--) {\n let d = digits.charCodeAt(i) - 48\n if (alt) { d *= 2; if (d > 9) d -= 9 }\n sum += d; alt = !alt\n }\n return sum % 10 === 0\n }\n const kindsIn = (text) => {\n const kinds = []\n if (!text || text.length < 6) return kinds\n EMAIL.lastIndex = 0\n for (let m; (m = EMAIL.exec(text)); ) {\n if (!SAFE_HOST.test(m[1])) { kinds.push('email'); break }\n }\n if (KEY.test(text)) kinds.push('key')\n CARD.lastIndex = 0\n for (let m; (m = CARD.exec(text)); ) {\n const digits = m[1].replace(/[ -]/g, '')\n if (digits.length >= 13 && digits.length <= 19 && !/^0+$/.test(digits) && luhn(digits)) { kinds.push('card'); break }\n }\n if (TAIL.test(text)) kinds.push('card-tail')\n return kinds\n }\n return { kindsIn }\n})()\n const vw = innerWidth, vh = innerHeight\n // A selector that reaches THIS element and no other, so it can be pasted\n // into \"mask\" as it is: climb until the path is unique, and say which\n // sibling wherever a tag-and-class alone would match several.\n const segment = (el) => {\n const tid = el.getAttribute('data-testid')\n if (tid) return el.tagName.toLowerCase() + '[data-testid=\"' + tid + '\"]'\n if (el.id && /^[A-Za-z_][\\w-]*$/.test(el.id)) return el.tagName.toLowerCase() + '#' + el.id\n const cls = [...el.classList].filter((c) => /^[A-Za-z_-][\\w-]*$/.test(c)).slice(0, 2)\n let seg = el.tagName.toLowerCase() + (cls.length ? '.' + cls.join('.') : '')\n const p = el.parentElement\n if (p && [...p.children].filter((c) => c.matches(seg)).length > 1)\n seg += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')'\n return seg\n }\n const name = (el) => {\n const parts = []\n for (let cur = el; cur && cur !== document.body && cur !== document.documentElement; cur = cur.parentElement) {\n parts.unshift(segment(cur))\n let n = 2\n try { n = document.querySelectorAll(parts.join(' > ')).length } catch {}\n // a bare tag is unique HERE and nowhere else: give it its parent too\n const bare = parts.length === 1 && !/[#.\\[]/.test(parts[0])\n if ((n === 1 && !bare) || parts.length >= 5) break\n }\n return parts.join(' > ')\n }\n const seen = (el) => {\n if (el.closest('[data-vos-masked]')) return null\n const r = el.getBoundingClientRect()\n if (r.width < 2 || r.height < 2) return null\n if (r.bottom <= 0 || r.right <= 0 || r.top >= vh || r.left >= vw) return null\n const s = getComputedStyle(el)\n if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null\n return r\n }\n const out = [], keys = new Set()\n const add = (el, text) => {\n const kinds = kindsIn(text)\n if (!kinds.length) return\n const r = seen(el)\n if (!r) return\n const selector = name(el)\n for (const kind of kinds) {\n const k = kind + '|' + selector\n if (keys.has(k) || out.length >= 40) continue\n keys.add(k)\n out.push({ kind, selector, rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } })\n }\n }\n const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT)\n for (let n; (n = walker.nextNode()); ) {\n const el = n.parentElement\n if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(el.tagName)) continue\n add(el, n.nodeValue)\n }\n for (const el of document.querySelectorAll('input, textarea')) {\n if (/^(password|hidden|file|checkbox|radio)$/.test(el.type)) continue\n add(el, el.value)\n }\n const hits = {}\n for (const [sel, n] of Object.entries(window.__vosMaskHits || {})) hits[sel] = n\n return { exposures: out, maskHits: hits }\n})()";
518
+ declare const EXPOSURE_PROBE: string;
387
519
  /**
388
520
  * The mask, as an init script: it runs at document start on EVERY navigation,
389
521
  * so a masked value is hidden before the page's first paint, and a
@@ -410,6 +542,26 @@ declare class ExposureLog {
410
542
  /** One exposure, in words. `step` -1 is the page as it opened. */
411
543
  declare function exposureLine(e: Exposure): string;
412
544
  declare const EXPOSURE_ADVICE = "The recording shows these. Record from a demo account, or hide them before the camera rolls with \"mask\" in actions.json: { \"selector\": \"\u2026\", \"as\": \"text\", \"text\": \"demo@acme.test\" } swaps the words, \"as\": \"blur\" blurs. Then re-record.";
545
+ /**
546
+ * Accumulates the notices across a take: the account's name is ONE notice,
547
+ * its places gathered from every scan; a recent-items list is one notice
548
+ * per list. Each keeps the first step it was seen at.
549
+ */
550
+ declare class NoticeLog {
551
+ private byKey;
552
+ add(step: number, notices: ScannedNotice[] | undefined | null): void;
553
+ notices(): Notice[];
554
+ }
555
+ /**
556
+ * One notice, in words. The account's name is never printed: the line says
557
+ * where it showed, so each place can be masked.
558
+ */
559
+ declare function noticeLine(n: Notice): string;
560
+ /**
561
+ * What to do about the notices, in one line: the swap hint is about the
562
+ * account's name, so it is said only when the name was noticed.
563
+ */
564
+ declare function noticeAdvice(notices: readonly Pick<Notice, 'kind'>[]): string;
413
565
 
414
566
  /**
415
567
  * `setup`: the steps that run BEFORE the camera rolls. A sign-in form, a
@@ -489,6 +641,8 @@ declare function runSetup(page: RecorderPage, steps: SetupStep[], opts: {
489
641
  sleep: (ms: number) => Promise<void>;
490
642
  /** Absent means no env at all: a `{ env }` text then fails in words. */
491
643
  env?: Record<string, string | undefined>;
644
+ /** How a `goto` opens its page and waits for it (the take's ready mode); absent, network idle. */
645
+ goto?: (url: string) => Promise<unknown>;
492
646
  }): Promise<SetupResult>;
493
647
  /** A `{ env }` the shell did not provide: a usage error, not a page's. */
494
648
  declare class SetupEnvError extends Error {
@@ -502,6 +656,222 @@ declare class SetupError extends Error {
502
656
  /** `--header name=value`, repeatable: extra request headers on every request. */
503
657
  declare function parseHeaders(given: string[] | undefined): Record<string, string>;
504
658
 
659
+ /**
660
+ * When a page counts as ready for the take to begin. The default is the
661
+ * DOM: after `load`, the page must make no DOM change for a beat (counted
662
+ * from outside, the way an `await` reload's settle is), at most a few
663
+ * seconds. Network idle is the opt-in: plenty of real apps hold a socket
664
+ * open or keep an analytics beacon going and never go network-idle, so a
665
+ * take that waited for it started after its whole timeout, every time.
666
+ */
667
+
668
+ /** How a page counts as ready: its DOM settled (default), or its network went idle. */
669
+ type ReadyMode = 'networkidle' | 'dom';
670
+ declare const READY_MODES: readonly ReadyMode[];
671
+ declare const DEFAULT_READY: ReadyMode;
672
+ /** The longest a navigation waits for `load` or network idle. */
673
+ declare const NAV_TIMEOUT_MS = 45000;
674
+ /** The longest the settle waits on a page that never stops changing. */
675
+ declare const READY_SETTLE_CAP_MS = 8000;
676
+ /** After network idle, the beat a hydration takes (what the recorder always gave it). */
677
+ declare const NETWORKIDLE_HYDRATION_MS = 800;
678
+ /** How the first load settled: the done event's `ready`. */
679
+ interface ReadyReport {
680
+ mode: ReadyMode;
681
+ /** ms from the navigation's start until the page counted as ready */
682
+ ms: number;
683
+ /**
684
+ * The wait gave up instead of the page settling: the DOM never stopped
685
+ * changing within the cap (dom), or network idle never came within
686
+ * the navigation timeout (networkidle). Recorded anyway either way.
687
+ */
688
+ capped: boolean;
689
+ }
690
+ /** Why a ready mode is not one, or null. */
691
+ declare function readyProblem(v: unknown): string | null;
692
+ interface ReadyOpts {
693
+ sleep: (ms: number) => Promise<void>;
694
+ now?: () => number;
695
+ /** the settle's cap (default `READY_SETTLE_CAP_MS`) */
696
+ capMs?: number;
697
+ /** the navigation's own timeout (default `NAV_TIMEOUT_MS`) */
698
+ timeoutMs?: number;
699
+ }
700
+ /**
701
+ * Wait for a page that is navigating (or has just navigated) to be ready:
702
+ * `navigate` starts the navigation with the `waitUntil` the mode asks for;
703
+ * in `dom` mode the DOM settle follows `load`. A navigation that times
704
+ * out is not fatal: the take goes on from whatever the page shows, and the
705
+ * report says it was capped.
706
+ */
707
+ declare function untilReady(page: RecorderPage, mode: ReadyMode, navigate: (opts: {
708
+ waitUntil: 'networkidle' | 'load';
709
+ timeout: number;
710
+ }) => Promise<RecorderResponse | null>, opts: ReadyOpts): Promise<{
711
+ response: RecorderResponse | null;
712
+ ready: ReadyReport;
713
+ }>;
714
+ /**
715
+ * An `await`'s reload between polls, under the take's ready mode: the
716
+ * reload waits for `load` (dom) or network idle (networkidle), at most
717
+ * the await's own timeout, and then the DOM settles either way, because
718
+ * network quiet is not a drawn page (a list rendered from its data lands
719
+ * after it). A page that never goes network-idle no longer costs a whole
720
+ * navigation timeout per poll.
721
+ */
722
+ declare function reloadUntilReady(page: RecorderPage, mode: ReadyMode, timeoutMs: number, opts?: {
723
+ sleep?: (ms: number) => Promise<void>;
724
+ }): Promise<void>;
725
+ /** The ready report in words, for the take's log. */
726
+ declare function readyLine(r: ReadyReport): string;
727
+
728
+ /**
729
+ * The canvas gestures, the pure parts: key chords and sequences, the
730
+ * modifiers a drag holds, a drawn path and its timing, a click's count and
731
+ * button, and the overrun report. What a whiteboard, a design tool or a
732
+ * timeline is recorded with, said as data so a script can carry it and
733
+ * both hosts validate it in the same words.
734
+ *
735
+ * None of it reaches the cursor track as key identity: a key is a typing
736
+ * ping (when and where), a modifier is nothing at all, and a path is its
737
+ * points.
738
+ */
739
+ /** The modifiers a chord or a drag may hold. `Mod` is Meta on macOS, Control elsewhere. */
740
+ declare const MODIFIERS: readonly ["Shift", "Control", "Alt", "Meta", "Mod", "ControlOrMeta"];
741
+ type Modifier = (typeof MODIFIERS)[number];
742
+ /** The modifiers a drag names (the platform-neutral one is `Mod`). */
743
+ declare const DRAG_MODIFIERS: readonly ["Alt", "Shift", "Meta", "Control", "Mod"];
744
+ type DragModifier = (typeof DRAG_MODIFIERS)[number];
745
+ /** A key name the browser's keyboard answers: a character, a code, a named key. */
746
+ declare function isKeyName(k: string): boolean;
747
+ /**
748
+ * A chord split into its modifiers and its key: `Shift+D` is
749
+ * `{ mods: ['Shift'], key: 'D' }`, a bare `r` has no modifiers, and `+`
750
+ * names the plus key when it ends the chord (`Shift++`).
751
+ */
752
+ declare function splitChord(chord: string): {
753
+ mods: string[];
754
+ key: string;
755
+ };
756
+ /** Why a chord is not one the recorder can press, or null. */
757
+ declare function chordProblem(chord: unknown): string | null;
758
+ /**
759
+ * A chord as the browser's keyboard takes it: `Mod` becomes
760
+ * `ControlOrMeta`, which the browser resolves to Meta on macOS and to
761
+ * Control elsewhere, where the browser runs.
762
+ */
763
+ declare function resolveChord(chord: string): string;
764
+ /** A `press` step's keys as a list: one chord or a sequence. */
765
+ declare function pressSequence(step: {
766
+ key?: string;
767
+ keys?: string | string[];
768
+ }): string[];
769
+ /** The pause between two keys of a sequence, a person's hand: ms. */
770
+ declare const KEY_SEQUENCE_GAP_MS = 150;
771
+ /**
772
+ * What one key costs beyond the script's gaps: the browser's round trip for
773
+ * the press (a keydown and a keyup the page answers). Part of the plan, so a
774
+ * sequence on an idle page is never "slower than scripted".
775
+ */
776
+ declare const KEY_ROUNDTRIP_MS = 50;
777
+ /** A key sequence's plan, ms: its gaps, and each key's round trip. */
778
+ declare const keySequencePlanMs: (keys: number) => number;
779
+ /** The hold after a drag's release, ms: the recorder's, part of the gesture. */
780
+ declare const DRAG_HOLD_MS = 400;
781
+ /** The most keys one press step may name. */
782
+ declare const MAX_KEYS = 50;
783
+ /** A drag modifier as the keyboard holds it. */
784
+ declare function resolveModifier(m: DragModifier): string;
785
+ /**
786
+ * The modifier bits of a raw pointer event (the pen path): Alt 1, Control
787
+ * 2, Meta 4, Shift 8, with `Mod` read for the platform the browser runs on.
788
+ */
789
+ declare function modifierBits(mods: readonly DragModifier[], platform: 'mac' | 'linux' | 'windows' | undefined): number;
790
+ /** A click's button, and the pointer button number the cursor track records. */
791
+ declare const CLICK_BUTTONS: readonly ["left", "right", "middle"];
792
+ type ClickButton = (typeof CLICK_BUTTONS)[number];
793
+ declare const buttonNumber: (b: ClickButton | undefined) => 0 | 1 | 2;
794
+ /**
795
+ * The pause between the presses of a double or triple click, well inside
796
+ * every platform's double-click interval (500 ms on macOS and Windows by
797
+ * default), so the page reads one gesture.
798
+ */
799
+ declare const MULTI_CLICK_GAP_MS = 90;
800
+ /**
801
+ * One point of a drawn path: viewport CSS px, with an optional time in ms
802
+ * (from the path's start) and an optional pressure in [0, 1]. Written as a
803
+ * tuple `[x, y]`, `[x, y, t]`, `[x, y, t, pressure]`, or an object.
804
+ */
805
+ type PathPoint = [number, number] | [number, number, number] | [number, number, number, number] | {
806
+ x: number;
807
+ y: number;
808
+ t?: number;
809
+ pressure?: number;
810
+ };
811
+ /** A path point as the recorder replays it. */
812
+ interface PathSample {
813
+ x: number;
814
+ y: number;
815
+ /** ms from the path's first point */
816
+ at: number;
817
+ pressure?: number;
818
+ }
819
+ /** The most points one path may hold. */
820
+ declare const MAX_PATH_POINTS = 20000;
821
+ /** An untimed path's travel when the step names no ms. */
822
+ declare const PATH_DEFAULT_MS = 700;
823
+ /**
824
+ * A path's points read and checked, or the problems in words. The same
825
+ * reader takes an inline `path` and the contents of a path file (an array
826
+ * of points, or `{ "points": [...] }`).
827
+ */
828
+ declare function readPath(raw: unknown): {
829
+ points: PathSample[];
830
+ timed: boolean;
831
+ } | {
832
+ problems: string[];
833
+ };
834
+ /**
835
+ * The path's schedule: a timed path keeps its own times (from its first
836
+ * point), an untimed one spreads its points evenly over `ms` (default
837
+ * 700). The last point is at the path's whole duration.
838
+ */
839
+ declare function schedulePath(path: {
840
+ points: PathSample[];
841
+ timed: boolean;
842
+ }, ms: number | undefined): PathSample[];
843
+ /** How long a scheduled path takes, ms. */
844
+ declare const pathDurationMs: (s: readonly PathSample[]) => number;
845
+ /**
846
+ * A paced gesture that ran past its script: the wall time it took against
847
+ * the time the script planned for it (a drag's travel, a path's timing,
848
+ * typing's characters × delay, a key sequence's gaps). Reported when the
849
+ * gesture took more than a fifth longer, the sign of a page that could not
850
+ * keep up with what it was sent.
851
+ */
852
+ interface StepOverrun {
853
+ step: number;
854
+ id?: string;
855
+ do: string;
856
+ plannedMs: number;
857
+ tookMs: number;
858
+ overrunMs: number;
859
+ }
860
+ /** The share past the plan that counts as an overrun. */
861
+ declare const OVERRUN_SHARE = 0.2;
862
+ /** A plan shorter than this is not measured: its noise is the round trip. */
863
+ declare const OVERRUN_MIN_PLANNED_MS = 100;
864
+ /**
865
+ * The least a gesture must run over to count, ms: below it the difference
866
+ * is one slow sample or round trip on a busy machine, never a page that
867
+ * fell behind, however short the gesture.
868
+ */
869
+ declare const OVERRUN_MIN_MS = 100;
870
+ /** The overrun, ms, or null when the gesture kept to its plan. */
871
+ declare function overrunOf(plannedMs: number, tookMs: number): number | null;
872
+ /** The overruns in one line for the log and the done event, or null when none. */
873
+ declare function overrunLine(overruns: readonly StepOverrun[]): string | null;
874
+
505
875
  /**
506
876
  * The action script — the declarative recipe an agent (or human) writes to
507
877
  * drive a take. Small on purpose: selectors + a handful of verbs. The recorder
@@ -533,21 +903,51 @@ interface ActionsFile {
533
903
  * or stored.
534
904
  */
535
905
  setup?: SetupStep[];
906
+ /**
907
+ * When the first page counts as ready for step 1: `dom` (default),
908
+ * once its DOM has made no change for a beat after `load`, at most 8 s;
909
+ * `networkidle`, once the network has gone quiet, at most 45 s, which a
910
+ * page holding a socket open never does. `--ready` overrides.
911
+ */
912
+ ready?: ReadyMode;
913
+ /**
914
+ * Every gesture waits on the app after it (`settle` on a step overrides):
915
+ * a canvas that falls behind under load finishes the last stroke before
916
+ * the next one starts.
917
+ */
918
+ settle?: boolean;
536
919
  steps: ActionStep[];
537
920
  }
538
921
  type ActionStep = ({
539
922
  do: 'wait';
540
923
  ms: number;
541
- } | {
924
+ }
925
+ /**
926
+ * Rest the pointer on `selector`'s centre, or on the viewport point
927
+ * (`x`, `y`) when no selector names the spot (a place on a canvas).
928
+ */
929
+ | {
542
930
  do: 'hover';
543
- selector: string;
931
+ selector?: string;
932
+ x?: number;
933
+ y?: number;
544
934
  ms?: number;
545
935
  }
546
- /** `ms` is the settle after the press (default 150). */
936
+ /**
937
+ * Press `selector`'s centre, or the viewport point (`x`, `y`). `ms` is
938
+ * the settle after the press (default 150). `count` 2 or 3 is a real
939
+ * double or triple click; `button` presses the right or middle one;
940
+ * `modifiers` are held for the press (a Shift-click) and released after.
941
+ */
547
942
  | {
548
943
  do: 'click';
549
- selector: string;
944
+ selector?: string;
945
+ x?: number;
946
+ y?: number;
550
947
  ms?: number;
948
+ count?: 1 | 2 | 3;
949
+ button?: ClickButton;
950
+ modifiers?: DragModifier[];
551
951
  }
552
952
  /**
553
953
  * Type into `selector`. The recorder clicks the field first, which is what
@@ -577,14 +977,18 @@ type ActionStep = ({
577
977
  ms?: number;
578
978
  }
579
979
  /**
580
- * One named key, recorded as a typing ping where the pointer is (or on
581
- * `selector` when given, with no click): Escape closes what a click
582
- * opened, Enter submits what was typed, Tab moves on, an arrow nudges.
583
- * Never a character: words are typed with `type`.
980
+ * A key, a chord or a sequence, each recorded as a typing ping where the
981
+ * pointer is (or on `selector` when given, with no click), never as the
982
+ * key itself. `key` names one control key (Escape closes what a click
983
+ * opened, Enter submits what was typed); `keys` names a chord
984
+ * (`"Shift+D"`, `"Mod+Z"`, with `Mod` Meta on macOS and Control
985
+ * elsewhere), a plain key (`"r"`, a tool's shortcut) or a sequence
986
+ * (`["v", "Escape"]`). Words are typed with `type`.
584
987
  */
585
988
  | {
586
989
  do: 'press';
587
- key: PressKey;
990
+ key?: PressKey;
991
+ keys?: string | string[];
588
992
  selector?: string;
589
993
  ms?: number;
590
994
  }
@@ -602,16 +1006,87 @@ type ActionStep = ({
602
1006
  * Press-move-release — real edits (drag an element on the stage canvas,
603
1007
  * slide a range input, move a timeline clip). Start = the selector's center
604
1008
  * when given, else (x, y); end = (tx, ty); eased over ms (default 700).
1009
+ * `path` draws instead: viewport points (inline, or a file beside the
1010
+ * actions file), the press at the first and the release at the last,
1011
+ * replayed by the points' own times or evenly over ms. `modifiers` are
1012
+ * held for the whole drag.
605
1013
  */
606
1014
  | {
607
1015
  do: 'drag';
608
1016
  selector?: string;
609
1017
  x?: number;
610
1018
  y?: number;
611
- tx: number;
612
- ty: number;
1019
+ tx?: number;
1020
+ ty?: number;
1021
+ path?: PathPoint[] | string;
1022
+ modifiers?: DragModifier[];
613
1023
  ms?: number;
1024
+ }
1025
+ /**
1026
+ * Put words or a file (an image, an SVG) on the clipboard and paste it
1027
+ * the way a person's ⌘V does, into `selector` (clicked first) or where
1028
+ * the focus is. A file is read beside the actions file, by a plain name.
1029
+ */
1030
+ | {
1031
+ do: 'paste';
1032
+ selector?: string;
1033
+ text?: string;
1034
+ file?: string;
1035
+ ms?: number;
1036
+ }
1037
+ /**
1038
+ * Check that the page is as the flow expects, with no cursor move: an
1039
+ * element visible (`selector`; with `enabled`, enabled or disabled), some
1040
+ * visible text containing `text`, or `gone` no longer visible. Exactly
1041
+ * one of the three. Waits up to `timeout` seconds (default 5); a
1042
+ * condition that never holds is a skip with its reason.
1043
+ */
1044
+ | {
1045
+ do: 'expect';
1046
+ selector?: string;
1047
+ text?: string;
1048
+ gone?: string;
1049
+ enabled?: boolean;
1050
+ timeout?: number;
1051
+ }
1052
+ /**
1053
+ * Wait OFF CAMERA for something slow (a render, a generated result, a
1054
+ * deploy): the capture stops, the predicate is polled every `poll`
1055
+ * seconds (default 2) for up to `timeout` (default 600), reloading the
1056
+ * page between polls when `reload` is set, and the capture resumes when
1057
+ * it holds. The footage has no gap and no frozen stretch; the cut lands
1058
+ * in `meta.skips[]`. On timeout, `onTimeout: "fail"` (the default) stops
1059
+ * the take and `"continue"` resumes it.
1060
+ */
1061
+ | {
1062
+ do: 'await';
1063
+ until: AwaitUntil;
1064
+ timeout?: number;
1065
+ poll?: number;
1066
+ reload?: boolean;
1067
+ onTimeout?: 'fail' | 'continue';
1068
+ }
1069
+ /**
1070
+ * Keep a file the page makes: click `selector` and wait for the
1071
+ * browser's download, or fetch the `currentSrc` of the video, audio or
1072
+ * img element `src` names, with the page's own cookies. Saved under the
1073
+ * take's `outputs/` as `as`, else the name the page suggested, and listed
1074
+ * in `meta.outputs[]`.
1075
+ */
1076
+ | {
1077
+ do: 'download';
1078
+ selector?: string;
1079
+ src?: string;
1080
+ as?: string;
614
1081
  }) & {
1082
+ /**
1083
+ * `"spend"`: the step costs something real (credits, a purchase, a
1084
+ * message sent). Only a click, a press or a type carries it, and it
1085
+ * needs an `id`. A rehearsal hovers it and never presses it; a recording
1086
+ * presses it only when the command names it (`--confirm <id>`) and
1087
+ * refuses before capturing anything otherwise.
1088
+ */
1089
+ guard?: StepGuard;
615
1090
  /**
616
1091
  * Optional stable identity: anchors in doc.json name a step by this
617
1092
  * id (else by index), so a step can move or gain neighbours across script
@@ -633,12 +1108,79 @@ type ActionStep = ({
633
1108
  * what is read. Only what the flow names, never a secret it did not.
634
1109
  */
635
1110
  read?: string;
1111
+ /**
1112
+ * After the gesture, wait until the page has made no DOM change and
1113
+ * painted no new frame for a beat (at most a few seconds): an app that
1114
+ * falls behind catches up before the next step. The script's top-level
1115
+ * `settle` is the default.
1116
+ */
1117
+ settle?: boolean;
636
1118
  };
1119
+ /** What an `await` waits for: exactly one of the three. */
1120
+ interface AwaitUntil {
1121
+ selector?: string;
1122
+ text?: string;
1123
+ gone?: string;
1124
+ }
1125
+ /** The guards a step may carry: one today, a step that spends. */
1126
+ declare const STEP_GUARDS: readonly ["spend"];
1127
+ type StepGuard = (typeof STEP_GUARDS)[number];
1128
+ /** How long an `expect` waits by default, in seconds. */
1129
+ declare const EXPECT_TIMEOUT_S = 5;
1130
+ /** An `await`'s defaults, in seconds. */
1131
+ declare const AWAIT_TIMEOUT_S = 600;
1132
+ declare const AWAIT_POLL_S = 2;
1133
+ /** How long a `download` click waits for the browser's download, in seconds. */
1134
+ declare const DOWNLOAD_TIMEOUT_S = 30;
637
1135
  /** The keys a `press` step may name: navigation and control, never a character. */
638
1136
  declare const PRESS_KEYS: readonly ["Escape", "Enter", "Tab", "Backspace", "Delete", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown", "Space"];
639
1137
  type PressKey = (typeof PRESS_KEYS)[number];
640
1138
  /** Structural validation with actionable messages. Returns [] when valid. */
641
1139
  declare function validateActions(value: unknown): string[];
1140
+ /**
1141
+ * Why a name is not one a download may be saved under, or null. A plain
1142
+ * file name only: the file lands in the take's `outputs/` and nowhere
1143
+ * else, so no separator, no `..`, nothing hidden, nothing empty.
1144
+ */
1145
+ declare function fileNameProblem(name: string): string | null;
1146
+ /** A guarded step of a script: where it is, what it is, the id that confirms it. */
1147
+ interface GuardedStep {
1148
+ step: number;
1149
+ id: string;
1150
+ do: string;
1151
+ guard: StepGuard;
1152
+ }
1153
+ /** The guarded steps of a script, in order. */
1154
+ declare function guardedSteps(actions: Pick<ActionsFile, 'steps'>): GuardedStep[];
1155
+ /**
1156
+ * Whether a recording may run with the confirmations it was given: every
1157
+ * guarded step named by `--confirm`, and every name a guarded step. A
1158
+ * refusal is one sentence naming the steps and the flag, said before
1159
+ * anything is captured.
1160
+ */
1161
+ declare function guardVerdict(actions: Pick<ActionsFile, 'steps'>, confirm: readonly string[]): {
1162
+ ok: true;
1163
+ } | {
1164
+ ok: false;
1165
+ unconfirmed: string[];
1166
+ unknown: string[];
1167
+ message: string;
1168
+ };
1169
+ /**
1170
+ * What a hosted recorder cannot do with a script, in words, or []: a
1171
+ * guarded step has nobody to confirm it on a fleet, and a download has no
1172
+ * take directory to land in there.
1173
+ */
1174
+ declare function hostedRefusals(actions: Pick<ActionsFile, 'steps'>): string[];
1175
+ /**
1176
+ * The files a script names beside itself, in order: a drag's path file
1177
+ * and a paste's file. A host reads them before anything is captured.
1178
+ */
1179
+ declare function scriptFiles(actions: Pick<ActionsFile, 'steps'>): {
1180
+ step: number;
1181
+ name: string;
1182
+ as: 'path' | 'paste';
1183
+ }[];
642
1184
 
643
1185
  /**
644
1186
  * The wall check: did the recorder land where it was asked, or in front of
@@ -724,6 +1266,10 @@ declare const WALL_PROBE = "(() => {\n const shown = (el) => {\n const r = e
724
1266
  * same events, meta and frame index for the same script.
725
1267
  */
726
1268
 
1269
+ /** How often a long `await` says it is still waiting, in ms. */
1270
+ declare const AWAIT_HEARTBEAT_MS = 30000;
1271
+ /** An `await` that held sooner than this cut nothing: no skip, no segment split. */
1272
+ declare const MIN_SKIP_MS = 250;
727
1273
  /**
728
1274
  * The headers a request carries, or null to leave it alone: a job's
729
1275
  * headers (a preview's bypass token) go to the ORIGIN the take is of and
@@ -742,8 +1288,27 @@ interface FrameRec {
742
1288
  interface SkippedStep {
743
1289
  /** index into actions.steps */
744
1290
  step: number;
1291
+ id?: string;
745
1292
  do: string;
746
1293
  selector: string;
1294
+ /** why, when it is not a selector that never appeared: an expect that never held, a download that never came */
1295
+ reason?: string;
1296
+ }
1297
+ /** A click whose target was disabled when it was pressed: the press happened, and likely did nothing. */
1298
+ interface DisabledClick {
1299
+ step: number;
1300
+ id?: string;
1301
+ selector: string;
1302
+ }
1303
+ /** An `await` that timed out with `onTimeout: "fail"`: the take stopped there. */
1304
+ interface StoppedTake {
1305
+ step: number;
1306
+ id?: string;
1307
+ reason: string;
1308
+ }
1309
+ /** Thrown before anything is captured: a guarded step the command did not confirm, a download with nowhere to land. */
1310
+ declare class RecordRefusedError extends Error {
1311
+ constructor(message: string);
747
1312
  }
748
1313
  /** A stretch with no screencast frames — the page pixels did not change. */
749
1314
  interface FreezeSpan {
@@ -760,12 +1325,19 @@ interface RecordResult {
760
1325
  pace: PaceReport;
761
1326
  /** steps whose selector never became visible — the take continued without them. */
762
1327
  skipped: SkippedStep[];
763
- /** the initial goto never reached networkidle (recording proceeded anyway). */
1328
+ /** `ready: "networkidle"` was asked for and the first load never reached it (recording proceeded anyway). */
764
1329
  navTimeout: boolean;
1330
+ /** How the first load settled before step 1. */
1331
+ ready: ReadyReport;
765
1332
  /** A wall the caller let through; null when the recorder landed where it was asked. */
766
1333
  wall: WallVerdict | null;
767
1334
  /** Sensitive-looking text seen in the frame (kind + place, never the string). */
768
1335
  exposures: NonNullable<RecordingMeta['exposures']>;
1336
+ /**
1337
+ * INFORMATION, never a failure and never in `exposures`: the signed-in
1338
+ * account's own name and its recent-items lists (also `meta.notices`).
1339
+ */
1340
+ notices: NonNullable<RecordingMeta['notices']>;
769
1341
  /** The script's masks with how many elements each reached; hits 0 hid nothing. */
770
1342
  masks: NonNullable<RecordingMeta['masks']>;
771
1343
  /**
@@ -784,6 +1356,26 @@ interface RecordResult {
784
1356
  dead: DeadReport;
785
1357
  /** The take reached --max-duration and stopped there; later steps did not run. */
786
1358
  capped: boolean;
1359
+ /** Clicks pressed on a disabled target (reported; `--strict` fails on them). */
1360
+ disabled: DisabledClick[];
1361
+ /** The ids of the guarded steps this take pressed, because the command named them. */
1362
+ confirmed: string[];
1363
+ /** The cuts `await` steps made (also `meta.skips`). */
1364
+ skips: RecordingSkip[];
1365
+ /** The files `download` steps kept (also `meta.outputs`). */
1366
+ outputs: RecordingOutput[];
1367
+ /** The sound the page played, each heard stretch in footage seconds (also `meta.media`). */
1368
+ media: NonNullable<RecordingMeta['media']>;
1369
+ /** An `await` timed out and the take stopped there; null when it ran to the end. */
1370
+ stopped: StoppedTake | null;
1371
+ /** The page's own console errors, each distinct one with its full text and how often it came. */
1372
+ pageErrors: PageError[];
1373
+ /**
1374
+ * Paced gestures that ran more than a fifth past their script (a drag's
1375
+ * travel, a path's timing, typing, a key sequence): the page could not
1376
+ * keep up with what it was sent. Also on `meta.steps[].overrunMs`.
1377
+ */
1378
+ overruns: StepOverrun[];
787
1379
  /** The actions as recorded, `url` resolved: what a take directory writes back. */
788
1380
  actions: ActionsFile;
789
1381
  }
@@ -831,6 +1423,45 @@ interface RecordOpts {
831
1423
  env?: Record<string, string | undefined>;
832
1424
  /** The platform the take is recorded on, for `meta.platform`. */
833
1425
  platform?: RecordingMeta['platform'];
1426
+ /**
1427
+ * The ids of the guarded steps this recording may press (`--confirm`).
1428
+ * A guarded step not named here refuses the take before anything is
1429
+ * captured; a rehearsal hovers every guarded step and presses none.
1430
+ */
1431
+ confirm?: readonly string[];
1432
+ /**
1433
+ * Where a `download` step's file goes. A host that hands in none cannot
1434
+ * keep a download: such a script is refused before anything is captured.
1435
+ * A rehearsal fetches the file and keeps nothing.
1436
+ */
1437
+ outputs?: OutputSink;
1438
+ /**
1439
+ * Where the sound the page played is kept, fetched with the page's own
1440
+ * cookies at the end of the take. A host that hands in none still
1441
+ * records `meta.media`, with no file; a rehearsal fetches nothing.
1442
+ */
1443
+ sounds?: SoundSink;
1444
+ /**
1445
+ * The files the script names beside itself (a drag's path file, a
1446
+ * paste's file). A host that hands in none cannot run such a step: the
1447
+ * script is refused before anything is captured.
1448
+ */
1449
+ files?: ScriptFiles;
1450
+ /** Print every page error whole, each time it comes; by default one cut line per distinct error. */
1451
+ verbose?: boolean;
1452
+ /**
1453
+ * Structured progress for a host that reports it (the CLI's `--json`):
1454
+ * an `await`'s start, a heartbeat while it waits, and its end.
1455
+ */
1456
+ onEvent?: (event: Record<string, unknown>) => void;
1457
+ /**
1458
+ * A step the page stopped answering in (it ran into its bound) stops the
1459
+ * take there, as an `await` that timed out does (`--strict`). Without
1460
+ * it the step is a skip and the take goes on.
1461
+ */
1462
+ stopOnHang?: boolean;
1463
+ /** How often a long `await` reports it is still waiting (default 30 s). */
1464
+ awaitHeartbeatMs?: number;
834
1465
  }
835
1466
  /**
836
1467
  * The expression a scroll tick evaluates: move the first scrollable
@@ -847,6 +1478,13 @@ declare function jpegDims(buf: Uint8Array): {
847
1478
  /** Base64 to bytes without Node's Buffer, so the fleet's isolate runs it too. */
848
1479
  declare function base64Bytes(data: string): Uint8Array;
849
1480
  declare function recordTake(browser: RecorderBrowser, url: string, actions: ActionsFile, sink: FrameSink, log: (msg: string) => void, opts?: RecordOpts): Promise<RecordResult>;
1481
+ /** Why a step ended at its bound, with the real time it took. */
1482
+ declare function hangReason(verb: string, ms: number, boundMs: number): string;
1483
+ declare function extForType(type: string | undefined): string;
1484
+ /** A download's type from its name's extension, as `{ contentType }`, or `{}` when the extension says nothing. */
1485
+ declare function typeOfName(name: string): {
1486
+ contentType?: string;
1487
+ };
850
1488
 
851
1489
  /**
852
1490
  * The recording cap as the recorder applies it, pure. WHAT the cap is
@@ -898,5 +1536,513 @@ interface FreshPlanOptions {
898
1536
  activity?: readonly number[] | null;
899
1537
  }
900
1538
  declare function planFreshTake(artifact: RecordingArtifact, recordingName: string, opts?: FreshPlanOptions): ProjectDoc;
1539
+ /**
1540
+ * The export a FRESH document opens on, sized to what the footage holds:
1541
+ * the ingest's default resolution, or the largest the recording supports
1542
+ * without upscaling when that is less (a 1280×720 take exports 720p, not a
1543
+ * 1080p that `vos validate` then warns upscales). Never raised. In place,
1544
+ * and returned.
1545
+ */
1546
+ declare function withFittingExport(doc: ProjectDoc): ProjectDoc;
1547
+ /**
1548
+ * The sound the page played (`meta.media`) as the document's audio: one
1549
+ * clip per heard stretch, keyed to the fetched file, placed at the OUTPUT
1550
+ * time of the footage moment it was heard (through the segments, the speed
1551
+ * spans and the freezes), stamped `source: 'page'`. A re-plan replaces only
1552
+ * those clips, so a clip someone added stays. An auto speed span over a
1553
+ * heard stretch is dropped first, or the footage would race ahead of what
1554
+ * the page was saying. A stretch with no fetched file (a hosted take, a
1555
+ * fetch that failed) places nothing. In place, and returned.
1556
+ */
1557
+ declare function withPageSound(doc: ProjectDoc, media: readonly RecordingPageMedia[] | undefined): ProjectDoc;
1558
+ /**
1559
+ * The footage's segments split at every cut an `await` made
1560
+ * (`meta.skips[].at`, source seconds), so each cut is a real segment
1561
+ * boundary: the timeline shows it, a transition can sit on it, a trim can
1562
+ * take either side. A cut at an edge or outside a segment splits nothing,
1563
+ * and a segment of another media is left alone. A split segment keeps its
1564
+ * arrival on its first piece and its leaving on its last.
1565
+ */
1566
+ declare function splitSegmentsAtSkips(segments: ProjectDoc['segments'], skips: readonly {
1567
+ at: number;
1568
+ }[] | undefined): ProjectDoc['segments'];
1569
+
1570
+ /**
1571
+ * What an `expect` and an `await` wait for: a condition on the page, read
1572
+ * without touching it (no cursor, no input), polled until it holds or the
1573
+ * time runs out. `expect` polls on camera for a few seconds; `await` polls
1574
+ * off camera for as long as a slow result takes. One reader for both, so
1575
+ * "appeared" means the same thing in either.
1576
+ */
1577
+
1578
+ interface WaitCondition {
1579
+ /** an element matching this selector is visible */
1580
+ selector?: string;
1581
+ /** some visible text on the page contains this */
1582
+ text?: string;
1583
+ /** no element matching this selector is visible */
1584
+ gone?: string;
1585
+ /** with `selector`: the element is enabled (true) or disabled (false) */
1586
+ enabled?: boolean;
1587
+ }
1588
+ /** What the condition found the last time it looked: what a miss is said from. */
1589
+ interface ConditionState {
1590
+ holds: boolean;
1591
+ /** with `selector`: whether a match was visible */
1592
+ visible?: boolean;
1593
+ }
1594
+ /**
1595
+ * The visible text of the page contains `text`: `innerText` is what the
1596
+ * page renders, so a hidden element's words never count.
1597
+ */
1598
+ declare function visibleTextScript(text: string): string;
1599
+ /** Read the condition once. A page mid-navigation reads as "not yet". */
1600
+ declare function readCondition(page: RecorderPage, c: WaitCondition): Promise<ConditionState>;
1601
+ /** What a poll found: whether the condition held, after how long, and the last read. */
1602
+ interface PollResult {
1603
+ ok: boolean;
1604
+ ms: number;
1605
+ last: ConditionState;
1606
+ }
1607
+ /**
1608
+ * Poll the condition every `everyMs` until it holds or `timeoutMs` pass.
1609
+ * `between` runs before each look but the first (an `await`'s reload).
1610
+ * With `confirm`, a hold counts only once it has held on that many reads
1611
+ * in a row, `confirm.gapMs` apart: a page that renders after network quiet
1612
+ * reads half-built for a moment, and a `gone` condition holds on a list
1613
+ * that has not been drawn yet. The clock is the wall's: a rehearsal waits
1614
+ * as long as a take would, because how long a result takes is what a
1615
+ * rehearsal is for.
1616
+ */
1617
+ declare function pollCondition(page: RecorderPage, c: WaitCondition, opts: {
1618
+ timeoutMs: number;
1619
+ everyMs: number;
1620
+ sleep: (ms: number) => Promise<void>;
1621
+ now?: () => number;
1622
+ between?: () => Promise<void>;
1623
+ confirm?: {
1624
+ reads: number;
1625
+ gapMs: number;
1626
+ };
1627
+ }): Promise<PollResult>;
1628
+ /** The DOM counts as settled after this long with no change. */
1629
+ declare const DOM_QUIET_MS = 800;
1630
+ /** The longest a settle waits on a page that never stops changing. */
1631
+ declare const DOM_SETTLE_CAP_MS = 5000;
1632
+ /**
1633
+ * The page's own count of DOM changes: an observer installed once per
1634
+ * document, read back as a number. A string, the one evaluate shape every
1635
+ * driver shares.
1636
+ */
1637
+ declare const DOM_CHANGE_PROBE = "(() => {\n if (!window.__vosDomChanges) {\n window.__vosDomChanges = { n: 0 };\n new MutationObserver((list) => { window.__vosDomChanges.n += list.length })\n .observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n }\n return window.__vosDomChanges.n;\n})()";
1638
+ /**
1639
+ * Wait until the DOM has not changed for `quietMs`, at most `capMs`: what
1640
+ * a page does after its network went quiet (a list rendered from data, a
1641
+ * hydration). The count is polled from here rather than timed in the
1642
+ * page, because a background tab throttles its own timers.
1643
+ */
1644
+ declare function domSettled(page: RecorderPage, opts: {
1645
+ sleep: (ms: number) => Promise<void>;
1646
+ now?: () => number;
1647
+ quietMs?: number;
1648
+ capMs?: number;
1649
+ tickMs?: number;
1650
+ }): Promise<{
1651
+ settled: boolean;
1652
+ ms: number;
1653
+ }>;
1654
+ /** Seconds as the recorder says them: `5 s`, `2.5 s`, `3 min 20 s`. */
1655
+ declare function sayDuration(ms: number): string;
1656
+ /** What the condition waits for, in words: `'#result' to appear`. */
1657
+ declare function sayCondition(c: WaitCondition): string;
1658
+ /** Why the condition did not hold, in words: `'Generate' stayed disabled for 5 s`. */
1659
+ declare function conditionMiss(c: WaitCondition, last: ConditionState, ms: number): string;
1660
+
1661
+ /** The most a download keeps: a file the page made, never a disk's worth. */
1662
+ declare const OUTPUT_MAX_BYTES: number;
1663
+ /**
1664
+ * A name the page suggested, made plain: its last path segment, control
1665
+ * characters and leading dots removed, or `fallback` when nothing usable
1666
+ * is left. A page chooses this name, so it is cleaned, never trusted.
1667
+ */
1668
+ declare function plainName(suggested: string, fallback: string): string;
1669
+ /** The file name a URL's path ends in, decoded, or ''. */
1670
+ declare function nameFromUrl(url: string): string;
1671
+ /**
1672
+ * The name a download is saved under: the step's `as` when it gave one,
1673
+ * else the page's suggestion made plain, made unique among the names this
1674
+ * take already used (`clip.mp4`, `clip-2.mp4`).
1675
+ */
1676
+ declare function outputName(asked: string | undefined, suggested: string, fallback: string, used: ReadonlySet<string>): string;
1677
+ /** Read a stream of byte chunks whole, refusing past `max` bytes. */
1678
+ declare function readAllBytes(stream: AsyncIterable<unknown>, max?: number): Promise<Uint8Array>;
1679
+ /**
1680
+ * Read a media element's source, run in the page on the element the
1681
+ * step's selector matched through the locator engine: its `currentSrc`
1682
+ * (what it is actually playing), else its `src`, else the attribute; ''
1683
+ * while it has none yet. Self-contained, since it is serialized into the
1684
+ * page.
1685
+ */
1686
+ declare const readMediaSource: (el: RecorderMediaElement) => string;
1687
+ /**
1688
+ * The expression that fetches a URL inside the page, for what the page
1689
+ * alone can read (a `blob:` or a `data:` source): base64 and the type, or
1690
+ * an error in words. Same-origin requests carry the page's cookies.
1691
+ */
1692
+ declare function fetchInPageScript(url: string): string;
1693
+
1694
+ /**
1695
+ * The take's clock: ONE path from a wall-clock instant to footage time,
1696
+ * for the cursor events and for every screencast frame alike.
1697
+ *
1698
+ * Footage time is wall time since `t0` minus every off-camera stretch (an
1699
+ * `await`) that began before the instant; an instant inside a stretch reads
1700
+ * as the stretch's start, which is where the cut is. A frame is placed by
1701
+ * the instant it was CAPTURED, never by when its callback ran: a frame in
1702
+ * flight when the capture paused lands after the resume and must still sit
1703
+ * before the cut.
1704
+ *
1705
+ * Frame times are strictly increasing. The screencast does not promise it:
1706
+ * a frame can arrive after a later one, and a frame without a capture
1707
+ * timestamp is stamped by its arrival, a second clock. A frame whose time
1708
+ * is not later than the last one kept holds older pixels than what the
1709
+ * footage already shows, so it is DROPPED (never re-stamped forward, which
1710
+ * would put a stale picture after a newer one).
1711
+ */
1712
+ declare class TakeClock {
1713
+ readonly t0: number;
1714
+ private readonly pauses;
1715
+ private lastFrameMs;
1716
+ constructor(t0: number);
1717
+ /** Footage ms at a wall-clock instant (`Date.now()` space). */
1718
+ footageMs(wallMs: number): number;
1719
+ /** The camera stops at this instant; returns the cut's footage ms. */
1720
+ pause(wallMs: number): number;
1721
+ /** The camera rolls again; returns the wall ms the stretch lasted. */
1722
+ resume(wallMs: number): number;
1723
+ /**
1724
+ * The parts of a wall-clock stretch the camera saw: each sub-stretch
1725
+ * outside every off-camera wait, as its wall start (`wallMs`), its
1726
+ * footage start (`atMs`) and its length. A stretch an `await` cut in two
1727
+ * comes back as two; one wholly off camera as none.
1728
+ */
1729
+ onCamera(fromWall: number, toWall: number): {
1730
+ wallMs: number;
1731
+ atMs: number;
1732
+ ms: number;
1733
+ }[];
1734
+ /**
1735
+ * A captured frame's footage ms, or null when it is not later than the
1736
+ * last frame kept (the frame is dropped; see the module comment).
1737
+ */
1738
+ placeFrame(captureWallMs: number): number | null;
1739
+ }
1740
+
1741
+ interface SurfaceSize {
1742
+ width: number;
1743
+ height: number;
1744
+ }
1745
+ interface SurfaceFit {
1746
+ before: SurfaceSize;
1747
+ after: SurfaceSize;
1748
+ }
1749
+ /**
1750
+ * Make the surface the viewport. Returns what it measured, or null when
1751
+ * nothing could be measured; never throws, because a take made as before
1752
+ * is better than no take.
1753
+ */
1754
+ declare function fitSurfaceToViewport(cdp: RecorderCdp, width: number, height: number): Promise<SurfaceFit | null>;
1755
+
1756
+ /**
1757
+ * The sound a page plays during a take. The recorder captures frames, and
1758
+ * a product's sound (a voice preview, a video playing in the page) would
1759
+ * be lost with nothing to say it was there. Most of it is a media element,
1760
+ * so the recorder watches every `<video>` and `<audio>` the page plays, in
1761
+ * the page and in its same-origin frames, and keeps WHEN each was heard
1762
+ * and WHAT media time it played, so a clip of the same file can be placed
1763
+ * exactly under the footage (`meta.media`, then the plan's audio clips).
1764
+ *
1765
+ * Two pieces run IN the page, both plain JavaScript strings (a function
1766
+ * serialized out of a bundle picks up the bundler's helpers and dies in the
1767
+ * page): an init script that notes every play, pause, seek, end and volume
1768
+ * change into a log on the top window, and a drain that hands the log over
1769
+ * with the page's clock. The rest is here, in the recorder's clock: the
1770
+ * page's instants become wall instants, and the take clock turns those
1771
+ * into footage stretches, split wherever an `await` took the camera away.
1772
+ */
1773
+
1774
+ /**
1775
+ * The init script: runs at document start in every frame. A frame that
1776
+ * cannot reach the top window (another origin) notes nothing. A media
1777
+ * element out of the document (`new Audio(url).play()`) is reached by
1778
+ * the play it is started with.
1779
+ */
1780
+ declare const MEDIA_WATCH_SCRIPT = "(() => {\n let host\n try { host = window.top; void host.document.title } catch { return }\n if (window.__vosMediaWatch) return\n window.__vosMediaWatch = true\n if (!host.__vosMedia) {\n const M = { log: [], seq: 0, els: new Set(), doc: Math.random().toString(36).slice(2) }\n M.fields = (el, kind) => ({\n id: el.__vosMediaId,\n tag: el.tagName === 'VIDEO' ? 'video' : 'audio',\n src: el.currentSrc || el.src || '',\n kind,\n wall: Date.now(),\n t: el.currentTime,\n rate: el.playbackRate || 1,\n paused: el.paused || el.ended,\n audible: !el.muted && el.volume > 0,\n volume: el.volume,\n duration: isFinite(el.duration) ? el.duration : null,\n ab: typeof el.webkitAudioDecodedByteCount === 'number' ? el.webkitAudioDecodedByteCount : null,\n })\n host.__vosMedia = M\n }\n const M = host.__vosMedia\n const EVENTS = ['playing', 'pause', 'ended', 'seeking', 'seeked', 'volumechange', 'ratechange', 'emptied']\n const note = (e) => {\n const el = e.target\n if (!el || typeof el.currentTime !== 'number' || !/^(VIDEO|AUDIO)$/.test(el.tagName || '')) return\n if (e.__vosNoted) return\n e.__vosNoted = true\n if (!el.__vosMediaId) {\n if (M.els.size >= 50) return\n el.__vosMediaId = ++M.seq\n M.els.add(el)\n }\n if (M.log.length < 2000) M.log.push(M.fields(el, e.type))\n }\n for (const type of EVENTS) document.addEventListener(type, note, true)\n const proto = window.HTMLMediaElement && window.HTMLMediaElement.prototype\n if (proto && proto.play) {\n const play = proto.play\n proto.play = function () {\n if (!this.__vosListening && !this.isConnected) {\n this.__vosListening = true\n for (const type of EVENTS) this.addEventListener(type, note)\n }\n return play.apply(this, arguments)\n }\n }\n})()";
1781
+ /**
1782
+ * The drain: the notes since the last drain, a `tick` for every element
1783
+ * followed (what it is doing now, which catches a note that never came),
1784
+ * the page's clock, and which document the log belongs to (a navigation
1785
+ * starts a new one).
1786
+ */
1787
+ declare const MEDIA_DRAIN_PROBE = "(() => {\n const M = window.__vosMedia\n if (!M) return { now: Date.now(), doc: null, notes: [] }\n const notes = M.log.splice(0)\n M.els.forEach((el) => notes.push(M.fields(el, 'tick')))\n return { now: Date.now(), doc: M.doc, notes }\n})()";
1788
+ /** One note from the page. */
1789
+ interface MediaNote {
1790
+ id: number;
1791
+ tag: 'video' | 'audio';
1792
+ src: string;
1793
+ kind: string;
1794
+ wall: number;
1795
+ t: number;
1796
+ rate: number;
1797
+ paused: boolean;
1798
+ audible: boolean;
1799
+ volume: number;
1800
+ duration: number | null;
1801
+ ab: number | null;
1802
+ }
1803
+ interface MediaDrain {
1804
+ now: number;
1805
+ doc: string | null;
1806
+ notes: MediaNote[];
1807
+ }
1808
+ /** A page sound with its full source, for the fetch; `meta` is what the take keeps. */
1809
+ interface HeardSound {
1810
+ meta: RecordingPageMedia;
1811
+ /** the address as the page played it, query and all: fetched, never stored */
1812
+ src: string;
1813
+ }
1814
+ /** What `meta.media[].src` keeps of an address: origin and path; a `blob:` or `data:` by its scheme. */
1815
+ declare function soundSrcForMeta(src: string): string;
1816
+ /**
1817
+ * Accumulates the page's notes across a take, in the recorder's clock.
1818
+ * `add` takes each drain with the recorder's own `Date.now()` at the
1819
+ * moment it came back; `heard` closes what is still playing at the end
1820
+ * and maps every stretch to footage time.
1821
+ */
1822
+ declare class PageSoundLog {
1823
+ private byKey;
1824
+ private openKeyOf;
1825
+ private doc;
1826
+ private lastWall;
1827
+ add(drain: MediaDrain | null, nodeNow: number, step?: number): void;
1828
+ private note;
1829
+ private close;
1830
+ private closeAll;
1831
+ /**
1832
+ * Every element the page played, each heard stretch as footage seconds
1833
+ * `at` with media seconds `from`..`to`, none past `endMs` of footage.
1834
+ * A video whose decoder never decoded a byte of sound played none.
1835
+ */
1836
+ heard(clock: TakeClock, endWall: number, endMs: number): HeardSound[];
1837
+ }
1838
+
1839
+ /**
1840
+ * A step never runs past its own bound, however the page behaves. A page
1841
+ * that stops answering (a busy loop, a renderer stuck after a reload the
1842
+ * page made itself) leaves every driver call pending for good, and a poll
1843
+ * that reads it waits on the read, not on its clock: an `expect` that said
1844
+ * "for 5 s" ran for fifteen minutes. So the page the recorder drives is
1845
+ * BOUNDED: while a step runs, every call into it is raced against the
1846
+ * step's deadline, and once the deadline has passed a pending call answers
1847
+ * `undefined` (what an absent element answers) instead of waiting. The
1848
+ * step then ends as a skip with the real time it took; the call it left
1849
+ * pending is abandoned, never awaited.
1850
+ *
1851
+ * Between steps the same race holds with a looser bound, so the take's
1852
+ * own finish (its last reads, the page's title) cannot hang either.
1853
+ */
1854
+ /** A gesture's bound past what it planned to take. */
1855
+ declare const STEP_MARGIN_MS = 30000;
1856
+ /** A check's (`expect`, `await`) bound past its own timeout. */
1857
+ declare const WAIT_MARGIN_MS = 5000;
1858
+ /** One call's bound while no step runs (a navigation has its own 45 s timeout). */
1859
+ declare const IDLE_CALL_MS = 60000;
1860
+ /** The bound of one step, in ms: its own timeout for a check, its plan plus a margin for a gesture. */
1861
+ declare function stepBoundMs(step: {
1862
+ do: string;
1863
+ timeout?: number;
1864
+ until?: unknown;
1865
+ reload?: boolean;
1866
+ }, plannedMs: number, defaults: {
1867
+ expectS: number;
1868
+ awaitS: number;
1869
+ downloadS: number;
1870
+ }): number;
1871
+ declare class StepBound {
1872
+ private readonly now;
1873
+ private readonly idleMs;
1874
+ private deadline;
1875
+ private started;
1876
+ private hung;
1877
+ constructor(now?: () => number, idleMs?: number);
1878
+ /** A step begins: calls into the page answer within `boundMs` from now. */
1879
+ start(boundMs: number): void;
1880
+ /** The step ends: whether it ran into its bound, and how long it really took. */
1881
+ end(): {
1882
+ hung: boolean;
1883
+ ms: number;
1884
+ boundMs: number;
1885
+ };
1886
+ /** The pending call, or `undefined` once the bound has passed. */
1887
+ race<T>(p: Promise<T>): Promise<T>;
1888
+ }
1889
+ /**
1890
+ * `target` with every method that answers a promise raced through the
1891
+ * guard, and every object it hands back (`page.mouse`, `locator()`,
1892
+ * `.first()`) guarded the same way. A call is made on the real object, so
1893
+ * nothing private to the driver is touched through the proxy.
1894
+ */
1895
+ declare function bounded<T extends object>(target: T, bound: StepBound): T;
1896
+
1897
+ /**
1898
+ * The `paste` step, the pure parts: what a file is pasted as, and the two
1899
+ * page scripts that put an item where a person's ⌘V finds it.
1900
+ *
1901
+ * The first rung is the browser's own clipboard: the item is written with
1902
+ * `navigator.clipboard.write` (the context is granted clipboard access
1903
+ * first), and the recorder then presses the platform's paste chord, so the
1904
+ * page receives a TRUSTED keydown and a trusted `paste` event carrying the
1905
+ * item, and a page that reads `navigator.clipboard.read()` itself sees it
1906
+ * there too. A headless browser keeps its own clipboard, so nothing on the
1907
+ * machine's clipboard is touched.
1908
+ *
1909
+ * When the browser refuses the write (no clipboard access on this driver,
1910
+ * a type it will not hold), the second rung dispatches the paste in the
1911
+ * page: a `paste` event with a `DataTransfer` holding the item on the
1912
+ * focused element, and `navigator.clipboard.read`/`readText` answering the
1913
+ * item for the rest of the take. Those events are untrusted, which the
1914
+ * recorder says.
1915
+ *
1916
+ * Both scripts are strings, the one evaluate shape every driver shares,
1917
+ * and neither carries anything the cursor track keeps: a paste is a typing
1918
+ * ping, never its contents.
1919
+ */
1920
+ /** How long after a dispatched paste is first read its item stays on the async reads, ms. */
1921
+ declare const PASTE_READ_WINDOW_MS = 300;
1922
+ /** How long a dispatched paste's item stays on the async reads when nothing reads it, ms. */
1923
+ declare const PASTE_UNREAD_MS = 3000;
1924
+ /** The most bytes a pasted file may hold. */
1925
+ declare const PASTE_MAX_BYTES: number;
1926
+ /** The file types a paste takes, by extension. */
1927
+ declare const PASTE_TYPES: Record<string, string>;
1928
+ /** A pasted file's type from its name, or null when a paste does not take it. */
1929
+ declare function pasteTypeOf(name: string): string | null;
1930
+ /** What a paste puts on the clipboard: words, or a file's bytes (base64) with its type and name. */
1931
+ interface PasteItem {
1932
+ text?: string;
1933
+ b64?: string;
1934
+ type?: string;
1935
+ name?: string;
1936
+ }
1937
+ /**
1938
+ * Rung one: write the item to the browser's clipboard. Answers 'ok', or
1939
+ * 'refused: <why>'. An SVG the clipboard will not hold as an image is
1940
+ * written as its markup alone.
1941
+ */
1942
+ declare function stageClipboardScript(item: PasteItem): string;
1943
+ /**
1944
+ * Rung two: the paste dispatched in the page, on the focused element (or
1945
+ * the body), with the item in its `clipboardData`; and the async clipboard
1946
+ * reads answer the item from here on. A plain field the page did not take
1947
+ * the paste for gets the words inserted, as the browser's own paste does.
1948
+ * Answers 'ok', or 'failed: <why>'.
1949
+ */
1950
+ declare function syntheticPasteScript(item: PasteItem, opts?: {
1951
+ original?: boolean;
1952
+ }): string;
1953
+
1954
+ /**
1955
+ * A gesture that waits on the app (`settle`): after the gesture, the
1956
+ * recorder waits until the page has made no DOM change and painted no new
1957
+ * frame for a beat, at most a few seconds. A heavy canvas falls behind
1958
+ * under load, and a script paced by the wall clock then outruns it: the
1959
+ * next stroke is drawn before the app has processed the last one.
1960
+ *
1961
+ * The DOM half is the recorder's one DOM-settle reader (`domSettled`, the
1962
+ * same an `await`'s reload lets the page finish with). The paint half is
1963
+ * the screencast: it emits a frame only when the compositor painted
1964
+ * something new, a canvas redrawn by `requestAnimationFrame` included, and
1965
+ * it is read from here rather than by an animation frame in the page,
1966
+ * which a background tab never fires. The two are one count: the page's
1967
+ * DOM changes plus the frames captured so far. A rehearsal captures no
1968
+ * frames, so there it waits on the DOM alone.
1969
+ */
1970
+
1971
+ /** The quiet a settle waits for: no DOM change and no new frame, ms. */
1972
+ declare const APP_QUIET_MS = 300;
1973
+ /** The longest a settle waits on a page that never stops changing, ms. */
1974
+ declare const APP_SETTLE_CAP_MS = 4000;
1975
+ /** The verbs a settle follows: the ones that send the page input. */
1976
+ declare const SETTLE_VERBS: Set<string>;
1977
+ /**
1978
+ * How long ago the page last changed its DOM, in ms by the page's own clock,
1979
+ * or -1 when this is the first look at this document (the reader is
1980
+ * installed now and knows no history). A string, the one evaluate shape
1981
+ * every driver shares.
1982
+ */
1983
+ declare const CHANGE_AGE_PROBE = "(() => {\n const w = window;\n if (!w.__vosChangeAt) {\n w.__vosChangeAt = { at: Date.now() };\n new MutationObserver(() => { w.__vosChangeAt.at = Date.now() })\n .observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n return -1;\n }\n return Date.now() - w.__vosChangeAt.at;\n})()";
1984
+ /** Whether a step waits on the app after its gesture: its own `settle`, else the script's. */
1985
+ declare function settleOn(step: {
1986
+ do: string;
1987
+ settle?: boolean;
1988
+ }, scriptDefault: boolean | undefined): boolean;
1989
+ /**
1990
+ * Wait for the app: no DOM change and no new captured frame for `quietMs`,
1991
+ * at most `capMs`. `frames` reads how many frames the capture has kept.
1992
+ */
1993
+ declare function appSettled(page: Pick<RecorderPage, 'evaluate'>, frames: () => number, opts: {
1994
+ sleep: (ms: number) => Promise<void>;
1995
+ now?: () => number;
1996
+ quietMs?: number;
1997
+ capMs?: number;
1998
+ /** ms since the last captured frame, or null when none was captured */
1999
+ sinceFrame?: () => number | null;
2000
+ }): Promise<{
2001
+ settled: boolean;
2002
+ ms: number;
2003
+ }>;
2004
+
2005
+ /**
2006
+ * The files a script names beside itself, read before the take: a drag's
2007
+ * path file (points as JSON) and a paste's file (its bytes). The host
2008
+ * hands in where they come from (`ScriptFiles`); the names are plain file
2009
+ * names, checked by `validateActions`, so a script can only name what sits
2010
+ * next to it.
2011
+ */
2012
+
2013
+ interface LoadedFiles {
2014
+ /** a drag's path, read from its file, by step index */
2015
+ paths: Map<number, {
2016
+ points: PathSample[];
2017
+ timed: boolean;
2018
+ }>;
2019
+ /** a paste's file, by step index */
2020
+ pastes: Map<number, {
2021
+ bytes: Uint8Array;
2022
+ type: string;
2023
+ name: string;
2024
+ }>;
2025
+ }
2026
+ /**
2027
+ * Read every file the script names. Answers the files, or the problem in
2028
+ * words (the step, the file, what is wrong) for the caller to refuse the
2029
+ * take with.
2030
+ */
2031
+ declare function loadScriptFiles(actions: Pick<ActionsFile, 'steps'>, source: ScriptFiles | undefined): Promise<LoadedFiles | {
2032
+ problem: string;
2033
+ }>;
2034
+ /** A read path as inline points: its own times when it had them, its pressure when it carried one. */
2035
+ declare function inlinePoints(path: {
2036
+ points: PathSample[];
2037
+ timed: boolean;
2038
+ }): PathPoint[];
2039
+ /**
2040
+ * The script as it ran, self-contained: every path a file held is written
2041
+ * inline, so the script a take keeps (and the one a push embeds, a fetch
2042
+ * writes back and a hosted recorder runs) re-records the same strokes with
2043
+ * the file gone. A paste's file stays a name: it is bytes, kept beside the
2044
+ * script by the host.
2045
+ */
2046
+ declare function inlineScript<T extends Pick<ActionsFile, 'steps'>>(actions: T, files: LoadedFiles): T;
901
2047
 
902
- export { type ActionStep, type ActionsFile, type Arrival, DEAD_SHARE_WARN_PCT, DEAD_STRETCH_WARN_MS, type DeadReport, type DeadStep, EXPOSURE_ADVICE, EXPOSURE_MATCHERS_SRC, EXPOSURE_PROBE, type EncodePageOptions, type Exposure, type ExposureKind, ExposureLog, type FrameRec, type FrameSink, type FreezeSpan, type FreshPlanOptions, MOTION_TICK_MS, type MaskReport, type MaskRule, POINTER_MAX_MS, POINTER_MIN_MS, POINTER_MS_PER_PX, PRESS_HOLD_MS, PRESS_KEYS, PRESS_LEAD_MS, type PaceReport, type PressKey, QUIET_MS, READ_BEAT_MS, RESPONSE_MS, type RecordOpts, type RecordResult, type RecorderBrowser, type RecorderCdp, type RecorderClock, type RecorderConsoleMessage, type RecorderContext, type RecorderLocator, type RecorderLocatorRoot, type RecorderPage, type RecorderRect, type RecorderResponse, type RecorderRoute, SCROLL_MAX_MS, SCROLL_MIN_MS, SCROLL_MS_PER_PX, SCROLL_SETTLE_MS, SETTLE_CAP_MS, SETTLE_MS, SetupEnvError, SetupError, type SetupResult, type SetupStep, type SetupText, type SkippedStep, type StepPace, TRAILING_HOLD_MS, WALL_PROBE, WallError, type WallKind, type WallVerdict, askedMs, base64Bytes, buildEncodePage, capReached, cappedLine, clampWait, clockMotion, clockTyping, deadLine, deadTime, deadWarns, easeInOutCubic, exposureLine, formatDurationCap, headersFor, holdLeftMs, isIdpHost, isSignInPath, jpegDims, maskInitScript, paceLine, paceReport, parseHeaders, planFreshTake, pointerTravelMs, publicUrl, realSleep, recordTake, redirectedAway, resolveSetupText, runSetup, scrollByScript, scrollTravelMs, settleMs, settleVerdict, setupEnvNames, urlSchemeProblem, validateActions, validateSetup, wallVerdict };
2048
+ export { APP_QUIET_MS, APP_SETTLE_CAP_MS, AWAIT_HEARTBEAT_MS, AWAIT_POLL_S, AWAIT_TIMEOUT_S, type ActionStep, type ActionsFile, type Arrival, type AwaitUntil, CHANGE_AGE_PROBE, CLICK_BUTTONS, type ClickButton, type ConditionState, DEAD_SHARE_WARN_PCT, DEAD_STRETCH_WARN_MS, DEFAULT_READY, DOM_CHANGE_PROBE, DOM_QUIET_MS, DOM_SETTLE_CAP_MS, DOWNLOAD_TIMEOUT_S, DRAG_HOLD_MS, DRAG_MODIFIERS, type DeadReport, type DeadStep, type DisabledClick, type DragModifier, EXPECT_TIMEOUT_S, EXPOSURE_ADVICE, EXPOSURE_MATCHERS_SRC, EXPOSURE_PROBE, type EncodePageOptions, type Exposure, type ExposureKind, ExposureLog, type FrameRec, type FrameSink, type FreezeSpan, type FreshPlanOptions, type GuardedStep, type HeardSound, IDLE_CALL_MS, KEY_ROUNDTRIP_MS, KEY_SEQUENCE_GAP_MS, type LoadedFiles, MAX_KEYS, MAX_PATH_POINTS, MEDIA_DRAIN_PROBE, MEDIA_WATCH_SCRIPT, MIN_SKIP_MS, MODIFIERS, MOTION_TICK_MS, MULTI_CLICK_GAP_MS, type MaskReport, type MaskRule, type MediaDrain, type MediaNote, type Modifier, NAV_TIMEOUT_MS, NETWORKIDLE_HYDRATION_MS, NOTICE_SRC, type Notice, NoticeLog, OUTPUT_MAX_BYTES, OVERRUN_MIN_MS, OVERRUN_MIN_PLANNED_MS, OVERRUN_SHARE, type OutputSink, PAGE_ERROR_LINE_CHARS, PASTE_MAX_BYTES, PASTE_READ_WINDOW_MS, PASTE_TYPES, PASTE_UNREAD_MS, PATH_DEFAULT_MS, POINTER_MAX_MS, POINTER_MIN_MS, POINTER_MS_PER_PX, PRESS_HOLD_MS, PRESS_KEYS, PRESS_LEAD_MS, type PaceReport, type PageError, PageErrorLog, PageSoundLog, type PasteItem, type PathPoint, type PathSample, type PollResult, type PressKey, QUIET_MS, READY_MODES, READY_SETTLE_CAP_MS, READ_BEAT_MS, RESPONSE_MS, type ReadyMode, type ReadyOpts, type ReadyReport, type RecordOpts, RecordRefusedError, type RecordResult, type RecorderApiResponse, type RecorderBrowser, type RecorderButtonOpts, type RecorderCdp, type RecorderClock, type RecorderConsoleMessage, type RecorderContext, type RecorderDownload, type RecorderLocator, type RecorderLocatorRoot, type RecorderMediaElement, type RecorderPage, type RecorderRect, type RecorderResponse, type RecorderRoute, SCROLL_MAX_MS, SCROLL_MIN_MS, SCROLL_MS_PER_PX, SCROLL_SETTLE_MS, SETTLE_CAP_MS, SETTLE_MS, SETTLE_VERBS, STEP_GUARDS, STEP_MARGIN_MS, type ScannedNotice, type ScriptFiles, SetupEnvError, SetupError, type SetupResult, type SetupStep, type SetupText, type SkippedStep, type SoundSink, StepBound, type StepGuard, type StepOverrun, type StepPace, type StoppedTake, type SurfaceFit, type SurfaceSize, TRAILING_HOLD_MS, TakeClock, WAIT_MARGIN_MS, WALL_PROBE, type WaitCondition, WallError, type WallKind, type WallVerdict, appSettled, askedMs, base64Bytes, bounded, buildEncodePage, buttonNumber, capReached, cappedLine, chordProblem, clampWait, clockMotion, clockTyping, conditionMiss, deadLine, deadTime, deadWarns, domSettled, easeInOutCubic, exposureLine, extForType, fetchInPageScript, fileNameProblem, fitSurfaceToViewport, formatDurationCap, guardVerdict, guardedSteps, hangReason, headersFor, holdLeftMs, hostedRefusals, inlinePoints, inlineScript, isIdpHost, isKeyName, isSignInPath, jpegDims, keySequencePlanMs, loadScriptFiles, maskInitScript, modifierBits, nameFromUrl, noticeAdvice, noticeLine, outputName, overrunLine, overrunOf, paceLine, paceReport, pageErrorLine, parseHeaders, pasteTypeOf, pathDurationMs, plainName, planFreshTake, pointerTravelMs, pollCondition, pressSequence, publicUrl, readAllBytes, readCondition, readMediaSource, readPath, readyLine, readyProblem, realSleep, recordTake, redirectedAway, reloadUntilReady, resolveChord, resolveModifier, resolveSetupText, runSetup, sayCondition, sayDuration, schedulePath, scriptFiles, scrollByScript, scrollTravelMs, settleMs, settleOn, settleVerdict, setupEnvNames, soundSrcForMeta, splitChord, splitSegmentsAtSkips, stageClipboardScript, stepBoundMs, syntheticPasteScript, typeOfName, untilReady, urlSchemeProblem, validateActions, validateSetup, visibleTextScript, wallVerdict, withFittingExport, withPageSound };