@vosjs/render-core 0.8.0 → 0.9.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.
@@ -1274,8 +1274,79 @@ async function fitSurfaceToViewport(cdp, width, height) {
1274
1274
  function visibleTextScript(text) {
1275
1275
  return `(() => { const b = document.body; return !!b && (b.innerText || '').includes(${JSON.stringify(text)}) })()`;
1276
1276
  }
1277
+ var JS_SAID_MAX = 120;
1278
+ var JS_VALUE_MAX = 2048;
1279
+ function jsConditionScript(expr) {
1280
+ return `(async () => {
1281
+ let v;
1282
+ try { v = await (${expr}
1283
+ ); } catch (e) { return JSON.stringify({ threw: String(e && e.message ? e.message : e) }) }
1284
+ const out = { truthy: !!v };
1285
+ try { const j = JSON.stringify(v); if (j !== undefined) out.json = j } catch (e) {}
1286
+ try { out.said = typeof v === 'string' ? JSON.stringify(v) : String(v) } catch (e) { out.said = typeof v }
1287
+ if (out.json !== undefined && typeof v !== 'string') out.said = out.json
1288
+ return JSON.stringify(out);
1289
+ })()`;
1290
+ }
1291
+ function sameJson(a, b) {
1292
+ if (a === b) return true;
1293
+ if (typeof a !== typeof b || a === null || b === null) return false;
1294
+ if (typeof a !== "object") return false;
1295
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
1296
+ if (Array.isArray(a)) {
1297
+ const bb = b;
1298
+ return a.length === bb.length && a.every((v, i) => sameJson(v, bb[i]));
1299
+ }
1300
+ const ao = a;
1301
+ const bo = b;
1302
+ const keys = Object.keys(ao);
1303
+ if (keys.length !== Object.keys(bo).length) return false;
1304
+ return keys.every(
1305
+ (k) => Object.prototype.hasOwnProperty.call(bo, k) && sameJson(ao[k], bo[k])
1306
+ );
1307
+ }
1308
+ async function readJs(page, c) {
1309
+ let raw;
1310
+ try {
1311
+ raw = await page.evaluate(jsConditionScript(c.js));
1312
+ } catch (e) {
1313
+ const why = e instanceof Error ? e.message : String(e);
1314
+ if (/SyntaxError/.test(why) || e instanceof Error && e.name === "SyntaxError")
1315
+ return { holds: false, threw: `it does not parse: ${why.split("\n")[0]}` };
1316
+ throw e;
1317
+ }
1318
+ let got;
1319
+ try {
1320
+ got = JSON.parse(raw);
1321
+ } catch {
1322
+ return { holds: false };
1323
+ }
1324
+ if (got.threw !== void 0) return { holds: false, threw: got.threw };
1325
+ let value;
1326
+ let hasValue = false;
1327
+ if (got.json !== void 0) {
1328
+ try {
1329
+ value = JSON.parse(got.json);
1330
+ hasValue = true;
1331
+ } catch {
1332
+ }
1333
+ }
1334
+ const holds = c.equals !== void 0 ? hasValue && sameJson(value, c.equals) : got.truthy === true;
1335
+ return {
1336
+ holds,
1337
+ ...hasValue ? { value } : {},
1338
+ said: clip(got.said ?? "undefined", JS_SAID_MAX)
1339
+ };
1340
+ }
1341
+ var clip = (s, max) => s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
1342
+ function keptValue(value) {
1343
+ const json = JSON.stringify(value);
1344
+ if (json === void 0) return void 0;
1345
+ return json.length <= JS_VALUE_MAX ? value : clip(json, JS_VALUE_MAX);
1346
+ }
1277
1347
  async function readCondition(page, c) {
1278
1348
  try {
1349
+ if (c.js !== void 0) return await readJs(page, { ...c, js: c.js });
1279
1350
  if (c.text !== void 0)
1280
1351
  return { holds: await page.evaluate(visibleTextScript(c.text)) };
1281
1352
  if (c.gone !== void 0)
@@ -1347,6 +1418,8 @@ function sayDuration(ms) {
1347
1418
  return rest ? `${m} min ${rest} s` : `${m} min`;
1348
1419
  }
1349
1420
  function sayCondition(c) {
1421
+ if (c.js !== void 0)
1422
+ return c.equals !== void 0 ? `${sayJs(c.js)} to equal ${clip(JSON.stringify(c.equals) ?? "undefined", JS_SAID_MAX)}` : `${sayJs(c.js)} to hold`;
1350
1423
  if (c.text !== void 0) return `text "${c.text}" to show`;
1351
1424
  if (c.gone !== void 0) return `'${c.gone}' to go`;
1352
1425
  if (c.enabled === true) return `'${c.selector}' to be enabled`;
@@ -1355,6 +1428,12 @@ function sayCondition(c) {
1355
1428
  }
1356
1429
  function conditionMiss(c, last, ms) {
1357
1430
  const t = sayDuration(ms);
1431
+ if (c.js !== void 0) {
1432
+ if (last.threw !== void 0)
1433
+ return `${sayJs(c.js)} threw for ${t} (${clip(last.threw, JS_SAID_MAX)})`;
1434
+ const was = last.said ?? "never answered";
1435
+ return c.equals !== void 0 ? `${sayJs(c.js)} was ${was}, not ${clip(JSON.stringify(c.equals) ?? "undefined", JS_SAID_MAX)}, for ${t}` : `${sayJs(c.js)} was ${was} for ${t}`;
1436
+ }
1358
1437
  if (c.text !== void 0)
1359
1438
  return `no visible text contained "${c.text}" for ${t}`;
1360
1439
  if (c.gone !== void 0) return `'${c.gone}' was still there after ${t}`;
@@ -1364,6 +1443,12 @@ function conditionMiss(c, last, ms) {
1364
1443
  return `'${c.selector}' stayed enabled for ${t}`;
1365
1444
  return `'${c.selector}' never appeared in ${t}`;
1366
1445
  }
1446
+ function sayJs(expr) {
1447
+ return `js \`${clip(expr.replace(/\s+/g, " ").trim(), 80)}\``;
1448
+ }
1449
+ function sayValue(value) {
1450
+ return clip(JSON.stringify(value) ?? "undefined", JS_SAID_MAX);
1451
+ }
1367
1452
 
1368
1453
  // src/record/ready.ts
1369
1454
  var READY_MODES = ["dom", "networkidle"];
@@ -1814,10 +1899,455 @@ async function appSettled(page, frames, opts) {
1814
1899
  });
1815
1900
  }
1816
1901
 
1902
+ // src/record/peers.ts
1903
+ var PEER_VERBS = [
1904
+ "wait",
1905
+ "move",
1906
+ "hover",
1907
+ "click",
1908
+ "type",
1909
+ "press",
1910
+ "scroll",
1911
+ "drag",
1912
+ "expect"
1913
+ ];
1914
+ var MAX_PEERS = 4;
1915
+ var MAX_PEER_STEPS = 200;
1916
+ var TAKE_ONLY = {
1917
+ guard: "a guard is pressed only on the filmed page, under --confirm",
1918
+ read: "read keeps words from the filmed page",
1919
+ caption: "a caption is a beat of the film, which is the take\u2019s",
1920
+ settle: "settle reads the filmed page\u2019s frames",
1921
+ zoom: "a zoom mark picks a moment on the cursor track, and a peer is never on it"
1922
+ };
1923
+ var PEER_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
1924
+ function validatePeers(value, takeSteps, validate) {
1925
+ if (!Array.isArray(value))
1926
+ return [
1927
+ "peers must be an array of { id, url?, viewport?, storageState?, steps }"
1928
+ ];
1929
+ const errors = [];
1930
+ if (value.length > MAX_PEERS)
1931
+ errors.push(
1932
+ `peers names ${value.length} peers; a take holds at most ${MAX_PEERS} (each is a whole browser)`
1933
+ );
1934
+ const takeIds = new Set(
1935
+ (Array.isArray(takeSteps) ? takeSteps : []).map((s) => s?.id).filter((id) => typeof id === "string")
1936
+ );
1937
+ const seen = /* @__PURE__ */ new Set();
1938
+ value.forEach((raw, i) => {
1939
+ const at = `peers[${i}]`;
1940
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1941
+ errors.push(`${at}: must be an object`);
1942
+ return;
1943
+ }
1944
+ const p = raw;
1945
+ if (typeof p.id !== "string" || !PEER_ID.test(p.id))
1946
+ errors.push(
1947
+ `${at}: id must be a short word (letters, digits, - and _, at most 32), which lines and meta.peers name the peer by`
1948
+ );
1949
+ else if (seen.has(p.id)) errors.push(`${at}: duplicate peer id "${p.id}"`);
1950
+ else seen.add(p.id);
1951
+ if (p.url !== void 0) {
1952
+ if (typeof p.url !== "string") errors.push(`${at}: url must be a string`);
1953
+ else {
1954
+ const problem = urlSchemeProblem(p.url);
1955
+ if (problem) errors.push(`${at}: url ${problem}`);
1956
+ }
1957
+ }
1958
+ if (p.viewport !== void 0) {
1959
+ const v = p.viewport;
1960
+ if (typeof v !== "object" || v === null || typeof v.width !== "number" || typeof v.height !== "number")
1961
+ errors.push(`${at}: viewport must be { width, height }`);
1962
+ }
1963
+ if (p.storageState !== void 0 && (typeof p.storageState !== "string" || !p.storageState.trim()))
1964
+ errors.push(
1965
+ `${at}: storageState must name a Playwright storage state file (the second person's sign-in)`
1966
+ );
1967
+ for (const k of Object.keys(p))
1968
+ if (!["id", "url", "viewport", "storageState", "steps"].includes(k))
1969
+ errors.push(
1970
+ `${at}: a peer takes id, url, viewport, storageState and steps, not ${k}`
1971
+ );
1972
+ if (!Array.isArray(p.steps) || p.steps.length === 0) {
1973
+ errors.push(`${at}: steps must be a non-empty array`);
1974
+ return;
1975
+ }
1976
+ if (p.steps.length > MAX_PEER_STEPS)
1977
+ errors.push(
1978
+ `${at}: steps holds ${p.steps.length} steps; a peer takes at most ${MAX_PEER_STEPS}`
1979
+ );
1980
+ const steps = p.steps;
1981
+ steps.forEach((rawStep, j) => {
1982
+ const sat = `${at}.steps[${j}]`;
1983
+ if (typeof rawStep !== "object" || rawStep === null) return;
1984
+ const s = rawStep;
1985
+ if (typeof s.do === "string" && !PEER_VERBS.includes(s.do))
1986
+ errors.push(
1987
+ `${sat}: a peer step is one of ${PEER_VERBS.join(", ")}; ${/^[aeiou]/.test(s.do) ? "an" : "a"} ${s.do} belongs to the filmed page${s.do === "await" ? " (wait for what the peer did with the take\u2019s own await or expect)" : ""}`
1988
+ );
1989
+ for (const [k, why] of Object.entries(TAKE_ONLY))
1990
+ if (s[k] !== void 0)
1991
+ errors.push(`${sat}: no ${k} on a peer step: ${why}`);
1992
+ if (s.do === "drag" && typeof s.path === "string")
1993
+ errors.push(
1994
+ `${sat}: a peer draws its path inline (an array of points), never from a file`
1995
+ );
1996
+ if (s.with !== void 0 && s.after !== void 0)
1997
+ errors.push(
1998
+ `${sat}: with or after, not both (with starts as the take's step starts, after once it ends)`
1999
+ );
2000
+ for (const k of ["with", "after"]) {
2001
+ const v = s[k];
2002
+ if (v === void 0) continue;
2003
+ if (typeof v !== "string" || !v.trim())
2004
+ errors.push(`${sat}: ${k} must name a take step by its id`);
2005
+ else if (!takeIds.has(v))
2006
+ errors.push(
2007
+ `${sat}: ${k} "${v}" names no take step (the take's steps with ids: ${takeIds.size ? [...takeIds].join(", ") : "none"}); give the take step an id`
2008
+ );
2009
+ }
2010
+ });
2011
+ for (const e of validate(steps))
2012
+ errors.push(e.replace(/^steps\[/, `${at}.steps[`));
2013
+ });
2014
+ return errors;
2015
+ }
2016
+ function peersHostedRefusal(peers) {
2017
+ if (!Array.isArray(peers) || !peers.length) return null;
2018
+ return "the script has peers (more browsers on the page, off camera), and a hosted take runs one browser: record it on this machine";
2019
+ }
2020
+ var StepSignals = class {
2021
+ started = /* @__PURE__ */ new Map();
2022
+ ended = /* @__PURE__ */ new Map();
2023
+ began = deferred();
2024
+ closed = false;
2025
+ constructor(ids) {
2026
+ for (const id of ids) {
2027
+ this.started.set(id, deferred());
2028
+ this.ended.set(id, deferred());
2029
+ }
2030
+ }
2031
+ begin() {
2032
+ this.began.resolve(true);
2033
+ }
2034
+ start(id) {
2035
+ if (id) this.started.get(id)?.resolve(true);
2036
+ }
2037
+ end(id) {
2038
+ if (id) this.ended.get(id)?.resolve(true);
2039
+ }
2040
+ close() {
2041
+ this.closed = true;
2042
+ this.began.resolve(false);
2043
+ for (const d of [...this.started.values(), ...this.ended.values()])
2044
+ d.resolve(false);
2045
+ }
2046
+ get isClosed() {
2047
+ return this.closed;
2048
+ }
2049
+ /** When a step with this trigger may start: true, or false for never. */
2050
+ when(trigger) {
2051
+ const d = trigger.with ? this.started.get(trigger.with) : trigger.after ? this.ended.get(trigger.after) : this.began;
2052
+ if (!d) return { ready: Promise.resolve(false), already: false };
2053
+ return { ready: d.promise, already: d.settled };
2054
+ }
2055
+ };
2056
+ function deferred() {
2057
+ let resolve = () => {
2058
+ };
2059
+ const d = {
2060
+ promise: new Promise((r) => {
2061
+ resolve = r;
2062
+ }),
2063
+ resolve: (v) => {
2064
+ if (d.settled) return;
2065
+ d.settled = true;
2066
+ resolve(v);
2067
+ },
2068
+ settled: false
2069
+ };
2070
+ return d;
2071
+ }
2072
+ var PEER_VALUE_MAX = 2048;
2073
+ function peerValue(last) {
2074
+ if (!("value" in last)) return void 0;
2075
+ const value = last.value;
2076
+ const json = JSON.stringify(value);
2077
+ if (json === void 0) return void 0;
2078
+ return json.length <= PEER_VALUE_MAX ? value : `${json.slice(0, PEER_VALUE_MAX - 1)}\u2026`;
2079
+ }
2080
+ var EXPECT_TIMEOUT_S = 5;
2081
+ var EXPECT_EVERY_MS = 150;
2082
+ async function runPeer(peer, rawPage, signals, opts) {
2083
+ const bound = new StepBound();
2084
+ const page = bounded(rawPage, bound);
2085
+ const { clock, log } = opts;
2086
+ const run = { id: peer.id, steps: [], notRun: [] };
2087
+ const cur = { x: 24, y: 24 };
2088
+ const moveTo = async (tx, ty) => {
2089
+ const from = { ...cur };
2090
+ const dist = Math.hypot(tx - from.x, ty - from.y);
2091
+ await clockMotion(
2092
+ opts.dry ? 0 : pointerTravelMs(dist),
2093
+ async (u) => {
2094
+ cur.x = from.x + (tx - from.x) * u;
2095
+ cur.y = from.y + (ty - from.y) * u;
2096
+ await page.mouse.move(cur.x, cur.y);
2097
+ },
2098
+ clock
2099
+ );
2100
+ };
2101
+ const spotOf = async (s) => {
2102
+ if (s.selector === void 0) return { x: s.x ?? cur.x, y: s.y ?? cur.y };
2103
+ const loc = page.locator(s.selector).first();
2104
+ try {
2105
+ await loc.waitFor({ state: "visible", timeout: 8e3 });
2106
+ await loc.scrollIntoViewIfNeeded();
2107
+ const bb = await loc.boundingBox();
2108
+ return bb ? { x: bb.x + bb.width / 2, y: bb.y + bb.height / 2 } : null;
2109
+ } catch {
2110
+ return null;
2111
+ }
2112
+ };
2113
+ const press = async (how) => {
2114
+ const held = [];
2115
+ try {
2116
+ for (const m of how.modifiers ?? []) {
2117
+ const key = resolveModifier(m);
2118
+ await page.keyboard.down(key);
2119
+ held.push(key);
2120
+ }
2121
+ await clock.sleep(PRESS_LEAD_MS);
2122
+ const count = how.count ?? 1;
2123
+ for (let n = 1; n <= count; n++) {
2124
+ if (n > 1) await clock.sleep(MULTI_CLICK_GAP_MS);
2125
+ const o = {
2126
+ ...how.button && how.button !== "left" ? { button: how.button } : {},
2127
+ ...n > 1 ? { clickCount: n } : {}
2128
+ };
2129
+ const has = Object.keys(o).length > 0;
2130
+ await (has ? page.mouse.down(o) : page.mouse.down());
2131
+ await clock.sleep(PRESS_HOLD_MS);
2132
+ await (has ? page.mouse.up(o) : page.mouse.up());
2133
+ }
2134
+ } finally {
2135
+ for (const key of held.reverse())
2136
+ await page.keyboard.up(key).catch(() => void 0);
2137
+ }
2138
+ };
2139
+ for (const [i, step] of peer.steps.entries()) {
2140
+ const tag = `peer ${peer.id} #${i}${step.id ? ` ${step.id}` : ""}`;
2141
+ const trigger = signals.when(step);
2142
+ const go = await trigger.ready;
2143
+ if (!go) {
2144
+ run.notRun = peer.steps.slice(i).map((_, k) => i + k);
2145
+ log(
2146
+ ` (${tag} ${step.do}: not run: ${step.with ? `the take's step ${step.with} never started` : step.after ? `the take's step ${step.after} never ended` : "the take ended first"})`
2147
+ );
2148
+ break;
2149
+ }
2150
+ const late = (step.with !== void 0 || step.after !== void 0) && trigger.already;
2151
+ const tStart = opts.footageMs();
2152
+ let selector;
2153
+ let reason = null;
2154
+ let value;
2155
+ bound.start(
2156
+ stepBoundMs(
2157
+ step,
2158
+ askedMs(step),
2159
+ { expectS: EXPECT_TIMEOUT_S, awaitS: 0, downloadS: 0 }
2160
+ )
2161
+ );
2162
+ try {
2163
+ switch (step.do) {
2164
+ case "wait":
2165
+ await clock.sleep(step.ms);
2166
+ break;
2167
+ case "move":
2168
+ await moveTo(step.x, step.y);
2169
+ break;
2170
+ case "hover": {
2171
+ selector = step.selector;
2172
+ const spot = await spotOf(step);
2173
+ if (!spot) reason = `'${step.selector}' never appeared`;
2174
+ else {
2175
+ await moveTo(spot.x, spot.y);
2176
+ await clock.sleep(step.ms ?? 700);
2177
+ }
2178
+ break;
2179
+ }
2180
+ case "click": {
2181
+ selector = step.selector;
2182
+ const spot = await spotOf(step);
2183
+ if (!spot) {
2184
+ reason = `'${step.selector}' never appeared`;
2185
+ break;
2186
+ }
2187
+ await moveTo(spot.x, spot.y);
2188
+ await press(step);
2189
+ await clock.sleep(step.ms ?? SETTLE_MS.click);
2190
+ break;
2191
+ }
2192
+ case "type": {
2193
+ selector = step.selector;
2194
+ if (step.focus !== false) {
2195
+ const spot = await spotOf(step);
2196
+ if (!spot) {
2197
+ reason = `'${step.selector}' never appeared`;
2198
+ break;
2199
+ }
2200
+ await moveTo(spot.x, spot.y);
2201
+ await press({});
2202
+ }
2203
+ if (step.at === "end") await page.keyboard.press("End");
2204
+ else if (step.at === "start") await page.keyboard.press("Home");
2205
+ if (step.replace) await page.keyboard.press("ControlOrMeta+a");
2206
+ await clockTyping(
2207
+ [...step.text],
2208
+ opts.dry ? 0 : step.delayMs ?? 40,
2209
+ (ch) => page.keyboard.type(ch),
2210
+ clock
2211
+ );
2212
+ await clock.sleep(step.ms ?? SETTLE_MS.type);
2213
+ break;
2214
+ }
2215
+ case "press": {
2216
+ selector = step.selector;
2217
+ if (step.selector !== void 0) {
2218
+ const spot = await spotOf(step);
2219
+ if (!spot) {
2220
+ reason = `'${step.selector}' never appeared`;
2221
+ break;
2222
+ }
2223
+ }
2224
+ const keys = pressSequence(step);
2225
+ for (const [k, key] of keys.entries()) {
2226
+ if (k > 0) await clock.sleep(KEY_SEQUENCE_GAP_MS);
2227
+ await page.keyboard.press(resolveChord(key));
2228
+ }
2229
+ await clock.sleep(step.ms ?? SETTLE_MS.press);
2230
+ break;
2231
+ }
2232
+ case "scroll":
2233
+ await page.mouse.wheel(0, step.dy);
2234
+ await clock.sleep(step.ms ?? 200);
2235
+ break;
2236
+ case "drag": {
2237
+ selector = step.selector;
2238
+ const read = step.path !== void 0 ? readPath(step.path) : null;
2239
+ if (read && "problems" in read) {
2240
+ reason = `its path ${read.problems.join("; ")}`;
2241
+ break;
2242
+ }
2243
+ const held = [];
2244
+ try {
2245
+ if (read) {
2246
+ const sched = schedulePath(read, step.ms);
2247
+ await moveTo(sched[0].x, sched[0].y);
2248
+ for (const m of step.modifiers ?? []) {
2249
+ const key = resolveModifier(m);
2250
+ await page.keyboard.down(key);
2251
+ held.push(key);
2252
+ }
2253
+ await page.mouse.down();
2254
+ const t0 = clock.now();
2255
+ for (const p of sched.slice(1)) {
2256
+ const wait = t0 + (opts.dry ? 0 : p.at) - clock.now();
2257
+ if (wait > 0) await clock.sleep(wait);
2258
+ cur.x = p.x;
2259
+ cur.y = p.y;
2260
+ await page.mouse.move(p.x, p.y);
2261
+ }
2262
+ } else {
2263
+ const start = await spotOf(step);
2264
+ if (!start) {
2265
+ reason = `'${step.selector}' never appeared`;
2266
+ break;
2267
+ }
2268
+ await moveTo(start.x, start.y);
2269
+ for (const m of step.modifiers ?? []) {
2270
+ const key = resolveModifier(m);
2271
+ await page.keyboard.down(key);
2272
+ held.push(key);
2273
+ }
2274
+ await page.mouse.down();
2275
+ const from = { ...cur };
2276
+ const tx = step.tx;
2277
+ const ty = step.ty;
2278
+ await clockMotion(
2279
+ opts.dry ? 0 : step.ms ?? 700,
2280
+ async (u) => {
2281
+ cur.x = from.x + (tx - from.x) * u;
2282
+ cur.y = from.y + (ty - from.y) * u;
2283
+ await page.mouse.move(cur.x, cur.y);
2284
+ },
2285
+ clock
2286
+ );
2287
+ }
2288
+ await page.mouse.up();
2289
+ } finally {
2290
+ for (const key of held.reverse())
2291
+ await page.keyboard.up(key).catch(() => void 0);
2292
+ }
2293
+ await clock.sleep(DRAG_HOLD_MS);
2294
+ break;
2295
+ }
2296
+ case "expect": {
2297
+ const cond = step;
2298
+ selector = step.selector ?? step.gone;
2299
+ const timeoutMs = (step.timeout ?? EXPECT_TIMEOUT_S) * 1e3;
2300
+ const res = await pollCondition(page, cond, {
2301
+ timeoutMs,
2302
+ everyMs: EXPECT_EVERY_MS,
2303
+ sleep: clock.sleep
2304
+ });
2305
+ value = peerValue(res.last);
2306
+ if (!res.ok) reason = conditionMiss(cond, res.last, timeoutMs);
2307
+ else log(` ${tag} expect ${sayCondition(cond)}: holds`);
2308
+ break;
2309
+ }
2310
+ default:
2311
+ reason = `a peer does not ${step.do}`;
2312
+ }
2313
+ } catch (e) {
2314
+ reason = e instanceof Error ? e.message.split("\n")[0] : String(e);
2315
+ }
2316
+ const hang = bound.end();
2317
+ if (hang.hung)
2318
+ reason = `the page stopped answering: this ${step.do} ran past its bound`;
2319
+ if (reason) log(` (${tag} ${step.do}: ${reason})`);
2320
+ else if (step.do !== "expect")
2321
+ log(` ${tag} ${step.do}${selector ? ` ${selector}` : ""}`);
2322
+ run.steps.push({
2323
+ step: i,
2324
+ ...step.id ? { id: step.id } : {},
2325
+ do: step.do,
2326
+ ...step.with ? { with: step.with } : {},
2327
+ ...step.after ? { after: step.after } : {},
2328
+ ...selector ? { selector } : {},
2329
+ tStart: +(tStart / 1e3).toFixed(3),
2330
+ tEnd: +(opts.footageMs() / 1e3).toFixed(3),
2331
+ ...reason ? { skipped: true, reason } : {},
2332
+ ...late ? { late: true } : {},
2333
+ ...value !== void 0 ? { value } : {}
2334
+ });
2335
+ }
2336
+ return run;
2337
+ }
2338
+
1817
2339
  // src/record/actions.ts
1818
2340
  var STEP_GUARDS = ["spend"];
2341
+ var ZOOM_MARK_VERBS = [
2342
+ "click",
2343
+ "drag",
2344
+ "type",
2345
+ "paste",
2346
+ "press",
2347
+ "hover"
2348
+ ];
1819
2349
  var GUARDABLE = /* @__PURE__ */ new Set(["click", "press", "type"]);
1820
- var EXPECT_TIMEOUT_S = 5;
2350
+ var EXPECT_TIMEOUT_S2 = 5;
1821
2351
  var AWAIT_TIMEOUT_S = 600;
1822
2352
  var AWAIT_POLL_S = 2;
1823
2353
  var DOWNLOAD_TIMEOUT_S = 30;
@@ -1901,6 +2431,14 @@ function validateActions(value) {
1901
2431
  errors.push("steps must be a non-empty array");
1902
2432
  return errors;
1903
2433
  }
2434
+ if (obj.peers !== void 0)
2435
+ errors.push(
2436
+ ...validatePeers(
2437
+ obj.peers,
2438
+ obj.steps,
2439
+ (steps) => validateActions({ steps })
2440
+ )
2441
+ );
1904
2442
  const seenIds = /* @__PURE__ */ new Set();
1905
2443
  obj.steps.forEach((raw, i) => {
1906
2444
  const at = `steps[${i}]`;
@@ -2020,6 +2558,14 @@ function validateActions(value) {
2020
2558
  if (s.do === "move" && (typeof s.x !== "number" || typeof s.y !== "number")) {
2021
2559
  errors.push(`${at}: move needs x and y (CSS px)`);
2022
2560
  }
2561
+ if (s.zoom !== void 0) {
2562
+ if (typeof s.zoom !== "boolean")
2563
+ errors.push(`${at}: zoom must be true or false`);
2564
+ else if (!ZOOM_MARK_VERBS.includes(s.do))
2565
+ errors.push(
2566
+ `${at}: zoom marks a gesture (${ZOOM_MARK_VERBS.join(", ")}); a ${s.do} shows nothing to zoom on`
2567
+ );
2568
+ }
2023
2569
  if (s.guard !== void 0) {
2024
2570
  if (!STEP_GUARDS.includes(s.guard))
2025
2571
  errors.push(
@@ -2035,16 +2581,17 @@ function validateActions(value) {
2035
2581
  );
2036
2582
  }
2037
2583
  if (s.do === "expect") {
2038
- const given = ["selector", "text", "gone"].filter(
2584
+ const given = ["selector", "text", "gone", "js"].filter(
2039
2585
  (k) => s[k] !== void 0
2040
2586
  );
2041
2587
  if (given.length !== 1)
2042
2588
  errors.push(
2043
- `${at}: expect needs exactly one of selector, text or gone${given.length ? ` (it has ${given.join(" and ")})` : ""}`
2589
+ `${at}: expect needs exactly one of selector, text, gone or js${given.length ? ` (it has ${given.join(" and ")})` : ""}`
2044
2590
  );
2045
2591
  for (const k of given)
2046
2592
  if (!isFilled(s[k]))
2047
2593
  errors.push(`${at}: expect ${k} must be a non-empty string`);
2594
+ errors.push(...equalsProblem(`${at}: expect`, s));
2048
2595
  if (s.enabled !== void 0) {
2049
2596
  if (typeof s.enabled !== "boolean")
2050
2597
  errors.push(`${at}: expect enabled must be true or false`);
@@ -2059,28 +2606,29 @@ function validateActions(value) {
2059
2606
  const u = s.until;
2060
2607
  if (typeof u !== "object" || u === null || Array.isArray(u)) {
2061
2608
  errors.push(
2062
- `${at}: await needs until, one of { "selector" }, { "text" } or { "gone" }`
2609
+ `${at}: await needs until, one of { "selector" }, { "text" }, { "gone" } or { "js" }`
2063
2610
  );
2064
2611
  } else {
2065
2612
  const until = u;
2066
2613
  const keys = Object.keys(until);
2067
2614
  const known = keys.filter(
2068
- (k) => k === "selector" || k === "text" || k === "gone"
2615
+ (k) => k === "selector" || k === "text" || k === "gone" || k === "js"
2069
2616
  );
2070
- const unknown = keys.filter((k) => !known.includes(k));
2617
+ const unknown = keys.filter((k) => !known.includes(k) && k !== "equals");
2071
2618
  if (unknown.length)
2072
2619
  errors.push(
2073
- `${at}: await until takes selector, text or gone, not ${unknown.join(", ")}`
2620
+ `${at}: await until takes selector, text, gone or js (with equals), not ${unknown.join(", ")}`
2074
2621
  );
2075
2622
  if (known.length !== 1)
2076
2623
  errors.push(
2077
- `${at}: await until needs exactly one of selector, text or gone${known.length ? ` (it has ${known.join(" and ")})` : ""}`
2624
+ `${at}: await until needs exactly one of selector, text, gone or js${known.length ? ` (it has ${known.join(" and ")})` : ""}`
2078
2625
  );
2079
2626
  for (const k of known) {
2080
2627
  const v = until[k];
2081
2628
  if (typeof v !== "string" || !v.trim())
2082
2629
  errors.push(`${at}: await until.${k} must be a non-empty string`);
2083
2630
  }
2631
+ errors.push(...equalsProblem(`${at}: await until`, until));
2084
2632
  }
2085
2633
  errors.push(...secondsProblem(at, "await timeout", s.timeout));
2086
2634
  errors.push(...secondsProblem(at, "await poll", s.poll));
@@ -2151,6 +2699,29 @@ function modifiersProblem(at, verb, v) {
2151
2699
  return [`${at}: ${verb} modifiers names a modifier twice`];
2152
2700
  return [];
2153
2701
  }
2702
+ function equalsProblem(at, s) {
2703
+ if (s.equals === void 0) return [];
2704
+ if (s.js === void 0)
2705
+ return [
2706
+ `${at} equals is the value a js expression must answer, so it needs js`
2707
+ ];
2708
+ if (!isJsonValue(s.equals, 0))
2709
+ return [
2710
+ `${at} equals must be a JSON value (a number, a string, true, false, null, a list or an object of them)`
2711
+ ];
2712
+ return [];
2713
+ }
2714
+ function isJsonValue(v, depth) {
2715
+ if (depth > 32) return false;
2716
+ if (v === null || typeof v === "string" || typeof v === "boolean") return true;
2717
+ if (typeof v === "number") return Number.isFinite(v);
2718
+ if (Array.isArray(v)) return v.every((x) => isJsonValue(x, depth + 1));
2719
+ if (typeof v === "object")
2720
+ return Object.values(v).every(
2721
+ (x) => isJsonValue(x, depth + 1)
2722
+ );
2723
+ return false;
2724
+ }
2154
2725
  var isFilled = (v) => typeof v === "string" && v.trim().length > 0;
2155
2726
  function secondsProblem(at, what, v) {
2156
2727
  if (v === void 0) return [];
@@ -2203,6 +2774,8 @@ function guardVerdict(actions, confirm) {
2203
2774
  }
2204
2775
  function hostedRefusals(actions) {
2205
2776
  const out = [];
2777
+ const peers = peersHostedRefusal(actions.peers);
2778
+ if (peers) out.push(peers);
2206
2779
  for (const g of guardedSteps(actions))
2207
2780
  out.push(
2208
2781
  `steps[${g.step}] (${g.id}) is guarded (${g.guard}), and a hosted take has no way to confirm it: record it on this machine with --confirm ${g.id}`
@@ -2216,6 +2789,10 @@ function hostedRefusals(actions) {
2216
2789
  out.push(
2217
2790
  `steps[${i}] is a paste, and a hosted browser has no clipboard to paste from: record it on this machine`
2218
2791
  );
2792
+ if (s.do === "expect" && s.js !== void 0 || s.do === "await" && s.until.js !== void 0)
2793
+ out.push(
2794
+ `steps[${i}] ${s.do === "expect" ? "checks" : "waits on"} a js expression, which runs as code in the page, and a hosted browser runs no script it was handed: record it on this machine, or check what the page shows (selector, text or gone)`
2795
+ );
2219
2796
  if (s.do === "drag" && typeof s.path === "string")
2220
2797
  out.push(
2221
2798
  `steps[${i}] draws a path from the file "${s.path}", and a hosted take carries the script alone: write the points inline as path`
@@ -2255,7 +2832,7 @@ async function loadScriptFiles(actions, source) {
2255
2832
  } catch (e) {
2256
2833
  const why = e instanceof Error ? e.message : String(e);
2257
2834
  return {
2258
- problem: f.as === "paste" ? `${at} pastes the file "${f.name}", which could not be read beside the actions file (${why}): a file paste needs its file beside the script, so put ${f.name} next to it (a take fetched from vos.so carries its paths and its words, never a pasted file). Nothing was recorded.` : `${at} names "${f.name}", which could not be read beside the actions file (${why}). Nothing was recorded.`
2835
+ problem: f.as === "paste" ? `${at} pastes the file "${f.name}", which could not be read beside the actions file (${why}): a file paste needs its file beside the script, so put ${f.name} next to it (a take fetched with its media brings its pasted files home beside its script; one pushed before they travelled with it does not). Nothing was recorded.` : `${at} names "${f.name}", which could not be read beside the actions file (${why}). Nothing was recorded.`
2259
2836
  };
2260
2837
  }
2261
2838
  if (f.as === "path") {
@@ -2416,7 +2993,7 @@ function fetchInPageScript(url) {
2416
2993
  }
2417
2994
 
2418
2995
  // src/record/recorder.ts
2419
- var EXPECT_EVERY_MS = 100;
2996
+ var EXPECT_EVERY_MS2 = 100;
2420
2997
  var SRC_WAIT_MS = 8e3;
2421
2998
  var AWAIT_HEARTBEAT_MS = 3e4;
2422
2999
  var MIN_SKIP_MS = 250;
@@ -2509,6 +3086,22 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
2509
3086
  const read = await loadScriptFiles(actions, opts.files);
2510
3087
  if ("problem" in read) throw new RecordRefusedError(read.problem);
2511
3088
  const files = read;
3089
+ const peers = actions.peers ?? [];
3090
+ const peerStates = /* @__PURE__ */ new Map();
3091
+ for (const p of peers) {
3092
+ if (p.storageState === void 0) continue;
3093
+ if (!opts.peerStorageState)
3094
+ throw new RecordRefusedError(
3095
+ `peer ${p.id} signs in from the storage state "${p.storageState}", and this recorder has nowhere to read one from. Nothing was recorded.`
3096
+ );
3097
+ try {
3098
+ peerStates.set(p.id, await opts.peerStorageState(p.storageState));
3099
+ } catch (e) {
3100
+ throw new RecordRefusedError(
3101
+ `peer ${p.id}'s storage state "${p.storageState}" could not be read (${e instanceof Error ? e.message : String(e)}). Nothing was recorded.`
3102
+ );
3103
+ }
3104
+ }
2512
3105
  const vw = actions.viewport?.width ?? 1280;
2513
3106
  const vh = actions.viewport?.height ?? 720;
2514
3107
  const context = opts.context ?? await browser.newContext({
@@ -2587,6 +3180,59 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
2587
3180
  await page.evaluate(MEDIA_DRAIN_PROBE).catch(() => null);
2588
3181
  if (actions.steps.some((s) => settleOn(s, actions.settle)))
2589
3182
  await page.evaluate(CHANGE_AGE_PROBE).catch(() => void 0);
3183
+ const peerPages = [];
3184
+ const closePeers = async () => {
3185
+ const bound = new StepBound();
3186
+ bound.start(CLOSE_BOUND_MS);
3187
+ await Promise.all(
3188
+ peerPages.map((p) => bound.race(p.context.close().catch(() => {
3189
+ })))
3190
+ );
3191
+ bound.end();
3192
+ };
3193
+ for (const peer of peers) {
3194
+ const peerUrl = peer.url ?? url;
3195
+ const pctx = await browser.newContext({
3196
+ viewport: peer.viewport ?? { width: vw, height: vh },
3197
+ deviceScaleFactor: 1,
3198
+ ...peerStates.has(peer.id) ? { storageState: peerStates.get(peer.id) } : {}
3199
+ });
3200
+ if (opts.headers && Object.keys(opts.headers).length)
3201
+ await applyHeaders(pctx, opts.headers, peerUrl, log);
3202
+ const ppage = await pctx.newPage();
3203
+ peerPages.push({ peer, context: pctx, page: ppage });
3204
+ const peerErrors = new PageErrorLog(opts.verbose === true);
3205
+ ppage.on("console", (m) => {
3206
+ if (m.type() !== "error") return;
3207
+ const line = peerErrors.add(m.text());
3208
+ if (line) log(` peer ${peer.id}: ${line.trim()}`);
3209
+ });
3210
+ log(`peer ${peer.id}: goto ${peerUrl} (off camera)`);
3211
+ const popened = await untilReady(
3212
+ ppage,
3213
+ readyMode,
3214
+ (o) => ppage.goto(peerUrl, o),
3215
+ { sleep: realSleep }
3216
+ );
3217
+ const seen = await ppage.evaluate(WALL_PROBE).catch(() => null);
3218
+ const peerWall = wallVerdict({
3219
+ askedUrl: peerUrl,
3220
+ landedUrl: ppage.url(),
3221
+ ...popened.response ? { status: popened.response.status() } : {},
3222
+ passwordFields: seen?.passwordFields ?? 0,
3223
+ newPasswordField: seen?.newPasswordField ?? false,
3224
+ oneTimeCodeField: seen?.oneTimeCodeField ?? false
3225
+ });
3226
+ if (peerWall?.level === "hard") {
3227
+ await closePeers();
3228
+ await context.close().catch(() => {
3229
+ });
3230
+ throw new RecordRefusedError(
3231
+ `peer ${peer.id} landed on ${peerWall.landed}, not ${peerWall.asked} (${peerWall.kind}): a peer acts as someone signed in, so give it a storageState of the second person's sign-in (beside actions.json). Nothing was recorded.`
3232
+ );
3233
+ }
3234
+ log(` peer ${peer.id}: ${readyLine(popened.ready)}`);
3235
+ }
2590
3236
  const cdp = await context.newCDPSession(rawPage);
2591
3237
  if (!dry) await fitSurfaceToViewport(cdp, vw, vh);
2592
3238
  const frames = [];
@@ -2805,6 +3451,23 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
2805
3451
  let stopped = null;
2806
3452
  let unpressedGuard = null;
2807
3453
  let capped = false;
3454
+ const signals = new StepSignals(
3455
+ actions.steps.map((s) => s.id).filter((id) => !!id)
3456
+ );
3457
+ const lanes = peerPages.map(
3458
+ ({ peer, page: ppage }) => runPeer(peer, ppage, signals, {
3459
+ clock: { now: () => Date.now(), sleep },
3460
+ footageMs: () => now(),
3461
+ log,
3462
+ dry
3463
+ }).catch((e) => {
3464
+ log(
3465
+ ` (peer ${peer.id}: stopped: ${e instanceof Error ? e.message : String(e)})`
3466
+ );
3467
+ return { id: peer.id, steps: [], notRun: [] };
3468
+ })
3469
+ );
3470
+ signals.begin();
2808
3471
  for (const [stepIdx, step] of actions.steps.entries()) {
2809
3472
  if (capReached(now(), maxSeconds)) {
2810
3473
  capped = true;
@@ -2844,6 +3507,12 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
2844
3507
  if (reason) extra.reason = reason;
2845
3508
  };
2846
3509
  if (step.guard) extra.guard = step.guard;
3510
+ if (typeof step.zoom === "boolean") extra.zoom = step.zoom;
3511
+ const keepValue = (last) => {
3512
+ if (!("value" in last)) return;
3513
+ const kept = keptValue(last.value);
3514
+ if (kept !== void 0) extra.value = kept;
3515
+ };
2847
3516
  const rehearseGuard = async (sel, point) => {
2848
3517
  if (!step.guard || !dry) return false;
2849
3518
  const rect = sel ? await boxOf(sel) : null;
@@ -2872,7 +3541,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
2872
3541
  step,
2873
3542
  askedMs(step),
2874
3543
  {
2875
- expectS: EXPECT_TIMEOUT_S,
3544
+ expectS: EXPECT_TIMEOUT_S2,
2876
3545
  awaitS: AWAIT_TIMEOUT_S,
2877
3546
  downloadS: DOWNLOAD_TIMEOUT_S
2878
3547
  }
@@ -2884,6 +3553,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
2884
3553
  ...step.id ? { id: step.id } : {},
2885
3554
  do: step.do
2886
3555
  });
3556
+ signals.start(step.id);
2887
3557
  const stepWall = Date.now();
2888
3558
  pageBound.start(boundMs);
2889
3559
  switch (step.do) {
@@ -3225,12 +3895,13 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
3225
3895
  case "expect": {
3226
3896
  if (notWaited()) break;
3227
3897
  const cond = step;
3228
- const timeoutMs = (step.timeout ?? EXPECT_TIMEOUT_S) * 1e3;
3898
+ const timeoutMs = (step.timeout ?? EXPECT_TIMEOUT_S2) * 1e3;
3229
3899
  const res = await pollCondition(page, cond, {
3230
3900
  timeoutMs,
3231
- everyMs: EXPECT_EVERY_MS,
3901
+ everyMs: EXPECT_EVERY_MS2,
3232
3902
  sleep: realSleep
3233
3903
  });
3904
+ keepValue(res.last);
3234
3905
  if (res.ok) log(`expect ${sayCondition(cond)}: holds`);
3235
3906
  else {
3236
3907
  const reason = conditionMiss(cond, res.last, timeoutMs);
@@ -3275,6 +3946,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
3275
3946
  clearInterval(beat);
3276
3947
  }
3277
3948
  const { out, at, ms } = waited;
3949
+ keepValue(out.last);
3278
3950
  opts.onEvent?.({ ...awaitEvent, phase: "end", ms, held: out.ok });
3279
3951
  extra.offCameraMs = ms;
3280
3952
  if (!out.ok) extra.timedOut = true;
@@ -3459,6 +4131,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
3459
4131
  await drainSound(stepIdx);
3460
4132
  pageBound.end();
3461
4133
  const stepSpan = stepSpans[stepSpans.length - 1];
4134
+ signals.end(step.id);
3462
4135
  opts.onEvent?.({
3463
4136
  event: "step",
3464
4137
  phase: "end",
@@ -3471,6 +4144,19 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
3471
4144
  });
3472
4145
  if (stopped) break;
3473
4146
  }
4147
+ signals.close();
4148
+ const peerRuns = await Promise.all(lanes);
4149
+ for (const pr of peerRuns)
4150
+ for (const ps of pr.steps)
4151
+ if (ps.skipped)
4152
+ skipped.push({
4153
+ step: ps.step,
4154
+ ...ps.id ? { id: ps.id } : {},
4155
+ do: ps.do,
4156
+ selector: ps.selector ?? "",
4157
+ ...ps.reason ? { reason: ps.reason } : {},
4158
+ peer: pr.id
4159
+ });
3474
4160
  if (!capped && !stopped) await sleep(TRAILING_HOLD_MS);
3475
4161
  if (capped || capReached(now(), maxSeconds)) {
3476
4162
  capped = true;
@@ -3497,6 +4183,7 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
3497
4183
  await pageBound.race(context.close().catch(() => {
3498
4184
  }));
3499
4185
  pageBound.end();
4186
+ await closePeers();
3500
4187
  await sinkChain;
3501
4188
  const meta = {
3502
4189
  dpr: 1,
@@ -3523,7 +4210,14 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
3523
4210
  ...masks.length ? { masks: exposureLog.masks(masks) } : {},
3524
4211
  ...skips.length ? { skips } : {},
3525
4212
  ...outputs.length ? { outputs } : {},
3526
- ...media.length ? { media } : {}
4213
+ ...media.length ? { media } : {},
4214
+ ...peerRuns.length ? {
4215
+ peers: peerRuns.map((pr) => ({
4216
+ id: pr.id,
4217
+ steps: pr.steps,
4218
+ ...pr.notRun.length ? { notRun: pr.notRun } : {}
4219
+ }))
4220
+ } : {}
3527
4221
  };
3528
4222
  const freezes = [];
3529
4223
  for (let i = 1; i < frames.length; i++) {
@@ -3570,7 +4264,8 @@ async function recordTake(browser, url, actions, sink, log, opts = {}) {
3570
4264
  overruns,
3571
4265
  // Self-contained: a path a file held is inline, so the take re-records
3572
4266
  // from its own script with the file gone.
3573
- actions: { ...inlineScript(actions, files), url }
4267
+ actions: { ...inlineScript(actions, files), url },
4268
+ peers: peerRuns
3574
4269
  };
3575
4270
  }
3576
4271
  async function keepPageSound(heard, sink, fetchSource, log) {
@@ -3834,16 +4529,20 @@ export {
3834
4529
  DOWNLOAD_TIMEOUT_S,
3835
4530
  DRAG_HOLD_MS,
3836
4531
  DRAG_MODIFIERS,
3837
- EXPECT_TIMEOUT_S,
4532
+ EXPECT_TIMEOUT_S2 as EXPECT_TIMEOUT_S,
3838
4533
  EXPOSURE_ADVICE,
3839
4534
  EXPOSURE_MATCHERS_SRC,
3840
4535
  EXPOSURE_PROBE,
3841
4536
  ExposureLog,
3842
4537
  IDLE_CALL_MS,
4538
+ JS_SAID_MAX,
4539
+ JS_VALUE_MAX,
3843
4540
  KEY_ROUNDTRIP_MS,
3844
4541
  KEY_SEQUENCE_GAP_MS,
3845
4542
  MAX_KEYS,
3846
4543
  MAX_PATH_POINTS,
4544
+ MAX_PEERS,
4545
+ MAX_PEER_STEPS,
3847
4546
  MEDIA_DRAIN_PROBE,
3848
4547
  MEDIA_WATCH_SCRIPT,
3849
4548
  MIN_SKIP_MS,
@@ -3864,6 +4563,8 @@ export {
3864
4563
  PASTE_TYPES,
3865
4564
  PASTE_UNREAD_MS,
3866
4565
  PATH_DEFAULT_MS,
4566
+ PEER_VALUE_MAX,
4567
+ PEER_VERBS,
3867
4568
  POINTER_MAX_MS,
3868
4569
  POINTER_MIN_MS,
3869
4570
  POINTER_MS_PER_PX,
@@ -3890,11 +4591,13 @@ export {
3890
4591
  SetupEnvError,
3891
4592
  SetupError,
3892
4593
  StepBound,
4594
+ StepSignals,
3893
4595
  TRAILING_HOLD_MS,
3894
4596
  TakeClock,
3895
4597
  WAIT_MARGIN_MS,
3896
4598
  WALL_PROBE,
3897
4599
  WallError,
4600
+ ZOOM_MARK_VERBS,
3898
4601
  appSettled,
3899
4602
  askedMs,
3900
4603
  base64Bytes,
@@ -3931,6 +4634,8 @@ export {
3931
4634
  isKeyName,
3932
4635
  isSignInPath,
3933
4636
  jpegDims,
4637
+ jsConditionScript,
4638
+ keptValue,
3934
4639
  keySequencePlanMs,
3935
4640
  loadScriptFiles,
3936
4641
  maskInitScript,
@@ -3947,6 +4652,8 @@ export {
3947
4652
  parseHeaders,
3948
4653
  pasteTypeOf,
3949
4654
  pathDurationMs,
4655
+ peerValue,
4656
+ peersHostedRefusal,
3950
4657
  plainName,
3951
4658
  planFreshTake,
3952
4659
  pointerTravelMs,
@@ -3966,9 +4673,13 @@ export {
3966
4673
  resolveChord,
3967
4674
  resolveModifier,
3968
4675
  resolveSetupText,
4676
+ runPeer,
3969
4677
  runSetup,
4678
+ sameJson,
3970
4679
  sayCondition,
3971
4680
  sayDuration,
4681
+ sayJs,
4682
+ sayValue,
3972
4683
  schedulePath,
3973
4684
  scriptFiles,
3974
4685
  scrollByScript,
@@ -3987,6 +4698,7 @@ export {
3987
4698
  untilReady,
3988
4699
  urlSchemeProblem,
3989
4700
  validateActions,
4701
+ validatePeers,
3990
4702
  validateSetup,
3991
4703
  visibleTextScript,
3992
4704
  wallVerdict,