@vosjs/cli 0.48.1 → 0.49.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.
- package/README.md +9 -1
- package/dist/chunk-72ADTMWW.js +40 -0
- package/dist/chunk-72ADTMWW.js.map +1 -0
- package/dist/{chunk-RHPRZQRM.js → chunk-TAWRT7UM.js} +300 -1092
- package/dist/chunk-TAWRT7UM.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +9 -346
- package/dist/index.js +2 -2
- package/dist/{run-DV73Z3MQ.js → run-EQZXEPXZ.js} +3 -3
- package/dist/{shotList-7F2FE3RU.js → shotList-IYFDHSIK.js} +2 -2
- package/package.json +3 -3
- package/dist/chunk-AUPGJJTL.js +0 -184
- package/dist/chunk-AUPGJJTL.js.map +0 -1
- package/dist/chunk-RHPRZQRM.js.map +0 -1
- /package/dist/{run-DV73Z3MQ.js.map → run-EQZXEPXZ.js.map} +0 -0
- /package/dist/{shotList-7F2FE3RU.js.map → shotList-IYFDHSIK.js.map} +0 -0
|
@@ -7,23 +7,9 @@ import {
|
|
|
7
7
|
trackFlags
|
|
8
8
|
} from "./chunk-FACYY7VV.js";
|
|
9
9
|
import {
|
|
10
|
-
PRESS_HOLD_MS,
|
|
11
|
-
PRESS_LEAD_MS,
|
|
12
|
-
SCROLL_SETTLE_MS,
|
|
13
|
-
TRAILING_HOLD_MS,
|
|
14
|
-
askedMs,
|
|
15
|
-
clockMotion,
|
|
16
|
-
clockTyping,
|
|
17
10
|
deadLine,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
holdLeftMs,
|
|
21
|
-
paceLine,
|
|
22
|
-
paceReport,
|
|
23
|
-
pointerTravelMs,
|
|
24
|
-
settleMs,
|
|
25
|
-
settleVerdict
|
|
26
|
-
} from "./chunk-AUPGJJTL.js";
|
|
11
|
+
deadWarns
|
|
12
|
+
} from "./chunk-72ADTMWW.js";
|
|
27
13
|
|
|
28
14
|
// src/plugin/run.ts
|
|
29
15
|
import { mkdir as mkdir10, readFile as readFile15, rm as rm5 } from "fs/promises";
|
|
@@ -119,238 +105,8 @@ function createReporter(json) {
|
|
|
119
105
|
};
|
|
120
106
|
}
|
|
121
107
|
|
|
122
|
-
// src/plugin/setup.ts
|
|
123
|
-
var VERBS = /* @__PURE__ */ new Set(["wait", "click", "type", "press", "goto"]);
|
|
124
|
-
function validateSetup(value) {
|
|
125
|
-
const errors = [];
|
|
126
|
-
if (!Array.isArray(value)) return ["setup must be an array of steps"];
|
|
127
|
-
value.forEach((raw, i) => {
|
|
128
|
-
const at2 = `setup[${i}]`;
|
|
129
|
-
if (typeof raw !== "object" || raw === null) {
|
|
130
|
-
errors.push(`${at2}: must be an object`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const s = raw;
|
|
134
|
-
if (typeof s.do !== "string" || !VERBS.has(s.do)) {
|
|
135
|
-
errors.push(`${at2}: "do" must be one of ${[...VERBS].join(", ")}`);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if ((s.do === "click" || s.do === "type") && typeof s.selector !== "string")
|
|
139
|
-
errors.push(`${at2}: ${s.do} needs a selector`);
|
|
140
|
-
if (s.do === "wait" && typeof s.ms !== "number")
|
|
141
|
-
errors.push(`${at2}: wait needs ms`);
|
|
142
|
-
if (s.do === "press" && typeof s.key !== "string")
|
|
143
|
-
errors.push(`${at2}: press needs a key`);
|
|
144
|
-
if (s.do === "goto" && typeof s.url !== "string")
|
|
145
|
-
errors.push(`${at2}: goto needs a url`);
|
|
146
|
-
if (s.do === "type") {
|
|
147
|
-
const t = s.text;
|
|
148
|
-
const isEnv = typeof t === "object" && t !== null && typeof t.env === "string" && Object.keys(t).length === 1;
|
|
149
|
-
if (typeof t !== "string" && !isEnv)
|
|
150
|
-
errors.push(
|
|
151
|
-
`${at2}: type needs text, a string or { "env": "NAME" } read at run time`
|
|
152
|
-
);
|
|
153
|
-
if (typeof t === "string" && /passw|secret|token/i.test(String(s.selector)) && t.length > 0)
|
|
154
|
-
errors.push(
|
|
155
|
-
`${at2}: a literal value typed into ${String(s.selector)} would live in actions.json, which is committed and pushed. Use { "env": "NAME" }`
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
return errors;
|
|
160
|
-
}
|
|
161
|
-
function resolveSetupText(t, env = process.env) {
|
|
162
|
-
if (typeof t === "string") return { value: t, secret: false };
|
|
163
|
-
const v = env[t.env];
|
|
164
|
-
if (v === void 0 || v === "")
|
|
165
|
-
throw new SetupEnvError(
|
|
166
|
-
`setup: the environment variable ${t.env} is not set, and a type step reads it. Export it in the shell that runs vos record; it is never written to a file`
|
|
167
|
-
);
|
|
168
|
-
return { value: v, secret: true };
|
|
169
|
-
}
|
|
170
|
-
async function runSetup(page, steps, opts) {
|
|
171
|
-
const lines = [];
|
|
172
|
-
for (const [i, step] of steps.entries()) {
|
|
173
|
-
const settle = "ms" in step && typeof step.ms === "number" ? step.ms : 150;
|
|
174
|
-
try {
|
|
175
|
-
switch (step.do) {
|
|
176
|
-
case "wait":
|
|
177
|
-
await opts.sleep(step.ms);
|
|
178
|
-
lines.push(`setup #${i} wait ${step.ms}ms`);
|
|
179
|
-
break;
|
|
180
|
-
case "goto":
|
|
181
|
-
await page.goto(step.url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
|
|
182
|
-
});
|
|
183
|
-
lines.push(`setup #${i} goto ${step.url}`);
|
|
184
|
-
break;
|
|
185
|
-
case "press":
|
|
186
|
-
await page.keyboard.press(step.key);
|
|
187
|
-
await opts.sleep(settle);
|
|
188
|
-
lines.push(`setup #${i} press ${step.key}`);
|
|
189
|
-
break;
|
|
190
|
-
case "click": {
|
|
191
|
-
const loc = page.locator(step.selector).first();
|
|
192
|
-
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
193
|
-
await loc.click();
|
|
194
|
-
await opts.sleep(settle);
|
|
195
|
-
lines.push(`setup #${i} click ${step.selector}`);
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
case "type": {
|
|
199
|
-
const loc = page.locator(step.selector).first();
|
|
200
|
-
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
201
|
-
const { value, secret } = resolveSetupText(step.text, opts.env);
|
|
202
|
-
await loc.fill(value);
|
|
203
|
-
await opts.sleep(settle);
|
|
204
|
-
lines.push(
|
|
205
|
-
`setup #${i} type ${secret ? `\${${step.text.env}}` : `${value.length} chars`} into ${step.selector}`
|
|
206
|
-
);
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
} catch (e) {
|
|
211
|
-
if (e instanceof SetupEnvError) throw e;
|
|
212
|
-
return {
|
|
213
|
-
ran: i,
|
|
214
|
-
lines,
|
|
215
|
-
failed: {
|
|
216
|
-
step: i,
|
|
217
|
-
do: step.do,
|
|
218
|
-
..."selector" in step ? { selector: step.selector } : {}
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return { ran: steps.length, lines };
|
|
224
|
-
}
|
|
225
|
-
var SetupEnvError = class extends Error {
|
|
226
|
-
constructor(message) {
|
|
227
|
-
super(message);
|
|
228
|
-
this.name = "SetupEnvError";
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
var SetupError = class extends Error {
|
|
232
|
-
constructor(failed2) {
|
|
233
|
-
super(
|
|
234
|
-
`setup #${failed2.step} ${failed2.do}${failed2.selector ? ` ${failed2.selector}` : ""}: the selector never appeared, so the take would begin at a half-finished setup. Nothing was recorded. Fix the setup step (rehearse with --dry-run), then record.`
|
|
235
|
-
);
|
|
236
|
-
this.failed = failed2;
|
|
237
|
-
this.name = "SetupError";
|
|
238
|
-
}
|
|
239
|
-
failed;
|
|
240
|
-
};
|
|
241
|
-
function parseHeaders(given) {
|
|
242
|
-
const out = {};
|
|
243
|
-
for (const h of given ?? []) {
|
|
244
|
-
const i = h.indexOf("=");
|
|
245
|
-
if (i <= 0) throw new Error(`--header wants name=value, got "${h}"`);
|
|
246
|
-
out[h.slice(0, i).trim()] = h.slice(i + 1);
|
|
247
|
-
}
|
|
248
|
-
return out;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
108
|
// src/plugin/actions.ts
|
|
252
|
-
|
|
253
|
-
"wait",
|
|
254
|
-
"hover",
|
|
255
|
-
"click",
|
|
256
|
-
"type",
|
|
257
|
-
"scroll",
|
|
258
|
-
"move",
|
|
259
|
-
"drag"
|
|
260
|
-
]);
|
|
261
|
-
function validateActions(value) {
|
|
262
|
-
const errors = [];
|
|
263
|
-
if (typeof value !== "object" || value === null)
|
|
264
|
-
return ["actions file must be a JSON object"];
|
|
265
|
-
const obj = value;
|
|
266
|
-
if (obj.url !== void 0 && typeof obj.url !== "string")
|
|
267
|
-
errors.push("url must be a string");
|
|
268
|
-
if (obj.viewport !== void 0) {
|
|
269
|
-
const v = obj.viewport;
|
|
270
|
-
if (typeof v !== "object" || v === null) {
|
|
271
|
-
errors.push("viewport must be { width, height }");
|
|
272
|
-
} else {
|
|
273
|
-
const vp = v;
|
|
274
|
-
if (typeof vp.width !== "number" || typeof vp.height !== "number") {
|
|
275
|
-
errors.push("viewport must be { width, height }");
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (obj.setup !== void 0) errors.push(...validateSetup(obj.setup));
|
|
280
|
-
if (obj.mask !== void 0) {
|
|
281
|
-
if (!Array.isArray(obj.mask)) {
|
|
282
|
-
errors.push("mask must be an array of { selector, as?, text? }");
|
|
283
|
-
} else {
|
|
284
|
-
obj.mask.forEach((raw, i) => {
|
|
285
|
-
const at2 = `mask[${i}]`;
|
|
286
|
-
const m = raw ?? {};
|
|
287
|
-
if (typeof m.selector !== "string" || !m.selector.trim())
|
|
288
|
-
errors.push(`${at2}: needs a selector`);
|
|
289
|
-
if (m.as !== void 0 && m.as !== "blur" && m.as !== "text")
|
|
290
|
-
errors.push(`${at2}: as must be "blur" or "text"`);
|
|
291
|
-
if (m.as === "text" && (typeof m.text !== "string" || !m.text))
|
|
292
|
-
errors.push(`${at2}: as "text" needs the text to show instead`);
|
|
293
|
-
if (m.as !== "text" && m.text !== void 0)
|
|
294
|
-
errors.push(`${at2}: text is only read with as "text"`);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (!Array.isArray(obj.steps) || obj.steps.length === 0) {
|
|
299
|
-
errors.push("steps must be a non-empty array");
|
|
300
|
-
return errors;
|
|
301
|
-
}
|
|
302
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
303
|
-
obj.steps.forEach((raw, i) => {
|
|
304
|
-
const at2 = `steps[${i}]`;
|
|
305
|
-
if (typeof raw !== "object" || raw === null) {
|
|
306
|
-
errors.push(`${at2}: must be an object`);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
const s = raw;
|
|
310
|
-
if (typeof s.do !== "string" || !VERBS2.has(s.do)) {
|
|
311
|
-
errors.push(`${at2}: "do" must be one of ${[...VERBS2].join(", ")}`);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
if (s.id !== void 0) {
|
|
315
|
-
if (typeof s.id !== "string" || !s.id.trim()) {
|
|
316
|
-
errors.push(`${at2}: id must be a non-empty string`);
|
|
317
|
-
} else if (seenIds.has(s.id)) {
|
|
318
|
-
errors.push(
|
|
319
|
-
`${at2}: duplicate id "${s.id}" \u2014 an anchor could not tell the two steps apart`
|
|
320
|
-
);
|
|
321
|
-
} else {
|
|
322
|
-
seenIds.add(s.id);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
const needSelector = s.do === "hover" || s.do === "click" || s.do === "type";
|
|
326
|
-
if (needSelector && typeof s.selector !== "string")
|
|
327
|
-
errors.push(`${at2}: ${s.do} needs a selector`);
|
|
328
|
-
if (s.do === "wait" && typeof s.ms !== "number")
|
|
329
|
-
errors.push(`${at2}: wait needs ms`);
|
|
330
|
-
if (s.ms !== void 0 && (typeof s.ms !== "number" || !Number.isFinite(s.ms) || s.ms < 0))
|
|
331
|
-
errors.push(`${at2}: ms must be a number of milliseconds \u2265 0`);
|
|
332
|
-
if (s.do === "type" && typeof s.text !== "string")
|
|
333
|
-
errors.push(`${at2}: type needs text`);
|
|
334
|
-
if (s.do === "type" && s.focus !== void 0 && typeof s.focus !== "boolean")
|
|
335
|
-
errors.push(`${at2}: type focus must be true or false`);
|
|
336
|
-
if (s.do === "scroll" && typeof s.dy !== "number")
|
|
337
|
-
errors.push(`${at2}: scroll needs dy`);
|
|
338
|
-
if (s.do === "move" && (typeof s.x !== "number" || typeof s.y !== "number")) {
|
|
339
|
-
errors.push(`${at2}: move needs x and y (CSS px)`);
|
|
340
|
-
}
|
|
341
|
-
if (s.do === "drag") {
|
|
342
|
-
if (typeof s.tx !== "number" || typeof s.ty !== "number") {
|
|
343
|
-
errors.push(`${at2}: drag needs tx and ty (CSS px)`);
|
|
344
|
-
}
|
|
345
|
-
const hasStart = typeof s.selector === "string" || typeof s.x === "number" && typeof s.y === "number";
|
|
346
|
-
if (!hasStart)
|
|
347
|
-
errors.push(
|
|
348
|
-
`${at2}: drag needs a selector OR x and y as the start point`
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
return errors;
|
|
353
|
-
}
|
|
109
|
+
import { validateActions } from "@vosjs/render-core/record";
|
|
354
110
|
|
|
355
111
|
// src/plugin/validateDoc.ts
|
|
356
112
|
import {
|
|
@@ -6674,805 +6430,67 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6674
6430
|
}
|
|
6675
6431
|
|
|
6676
6432
|
// src/plugin/recorder.ts
|
|
6677
|
-
import {
|
|
6433
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
6678
6434
|
import { join as join13 } from "path";
|
|
6679
|
-
|
|
6680
|
-
// src/plugin/recordingCap.ts
|
|
6681
|
-
var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
|
|
6682
|
-
function parseHostedCap(body) {
|
|
6683
|
-
const limits = body && typeof body === "object" ? body.limits : void 0;
|
|
6684
|
-
const cap = limits?.recordingMaxSeconds;
|
|
6685
|
-
return typeof cap === "number" && Number.isInteger(cap) && cap > 0 ? cap : null;
|
|
6686
|
-
}
|
|
6687
|
-
async function hostedRecordingCap(origin, key, fetchImpl = fetch) {
|
|
6688
|
-
const ctl = new AbortController();
|
|
6689
|
-
const timer = setTimeout(() => ctl.abort(), 2e3);
|
|
6690
|
-
try {
|
|
6691
|
-
const res = await fetchImpl(`${origin.replace(/\/+$/, "")}/api/limits`, {
|
|
6692
|
-
signal: ctl.signal,
|
|
6693
|
-
...key ? { headers: { authorization: `Bearer ${key}` } } : {}
|
|
6694
|
-
});
|
|
6695
|
-
if (!res.ok) return null;
|
|
6696
|
-
return parseHostedCap(await res.json());
|
|
6697
|
-
} catch {
|
|
6698
|
-
return null;
|
|
6699
|
-
} finally {
|
|
6700
|
-
clearTimeout(timer);
|
|
6701
|
-
}
|
|
6702
|
-
}
|
|
6703
|
-
function defaultMaxDurationSeconds() {
|
|
6704
|
-
return HOSTED_RECORDING_CAP_SECONDS;
|
|
6705
|
-
}
|
|
6706
|
-
function capReached(elapsedMs, capSeconds) {
|
|
6707
|
-
return elapsedMs >= capSeconds * 1e3;
|
|
6708
|
-
}
|
|
6709
|
-
function clampWait(waitMs, elapsedMs, capSeconds) {
|
|
6710
|
-
return Math.max(0, Math.min(waitMs, capSeconds * 1e3 - elapsedMs));
|
|
6711
|
-
}
|
|
6712
|
-
function formatDurationCap(seconds) {
|
|
6713
|
-
if (seconds < 60) return `${seconds} s`;
|
|
6714
|
-
const m = Math.round(seconds / 60);
|
|
6715
|
-
if (m < 60) return `${m} min`;
|
|
6716
|
-
const h = Math.floor(m / 60);
|
|
6717
|
-
const rest = m % 60;
|
|
6718
|
-
return rest ? `${h} h ${rest} min` : `${h} h`;
|
|
6719
|
-
}
|
|
6720
|
-
function cappedLine(capSeconds) {
|
|
6721
|
-
return `stopped at ${formatDurationCap(capSeconds)} (--max-duration ${capSeconds}); the remaining steps did not run`;
|
|
6722
|
-
}
|
|
6723
|
-
|
|
6724
|
-
// src/plugin/wall.ts
|
|
6725
|
-
var IDP_HOSTS = [
|
|
6726
|
-
"accounts.google.com",
|
|
6727
|
-
"login.microsoftonline.com",
|
|
6728
|
-
"login.live.com",
|
|
6729
|
-
"appleid.apple.com",
|
|
6730
|
-
"auth0.com",
|
|
6731
|
-
"okta.com",
|
|
6732
|
-
"onelogin.com",
|
|
6733
|
-
"clerk.accounts.dev",
|
|
6734
|
-
"accounts.dev",
|
|
6735
|
-
"b2clogin.com",
|
|
6736
|
-
"amazoncognito.com",
|
|
6737
|
-
"workos.com",
|
|
6738
|
-
"stytch.com"
|
|
6739
|
-
];
|
|
6740
|
-
var SIGNIN_SEGMENTS = /* @__PURE__ */ new Set([
|
|
6741
|
-
"login",
|
|
6742
|
-
"log-in",
|
|
6743
|
-
"signin",
|
|
6744
|
-
"sign-in",
|
|
6745
|
-
"sign_in",
|
|
6746
|
-
"sso",
|
|
6747
|
-
"auth",
|
|
6748
|
-
"authenticate",
|
|
6749
|
-
"authorize",
|
|
6750
|
-
"oauth",
|
|
6751
|
-
"oauth2",
|
|
6752
|
-
"saml"
|
|
6753
|
-
]);
|
|
6754
|
-
function parse(url) {
|
|
6755
|
-
try {
|
|
6756
|
-
return new URL(url);
|
|
6757
|
-
} catch {
|
|
6758
|
-
return null;
|
|
6759
|
-
}
|
|
6760
|
-
}
|
|
6761
|
-
var bareHost = (h) => h.replace(/^www\./, "").toLowerCase();
|
|
6762
|
-
var trimSlash = (p) => p.length > 1 ? p.replace(/\/+$/, "") : p;
|
|
6763
|
-
var place = (u) => `${u.origin}${trimSlash(u.pathname)}`;
|
|
6764
|
-
function isIdpHost(host) {
|
|
6765
|
-
const h = host.toLowerCase();
|
|
6766
|
-
if (h.startsWith("clerk.")) return true;
|
|
6767
|
-
return IDP_HOSTS.some((idp) => h === idp || h.endsWith(`.${idp}`));
|
|
6768
|
-
}
|
|
6769
|
-
function isSignInPath(pathname) {
|
|
6770
|
-
const segs = pathname.toLowerCase().split("/").filter(Boolean);
|
|
6771
|
-
return segs.some((s) => SIGNIN_SEGMENTS.has(s));
|
|
6772
|
-
}
|
|
6773
|
-
function redirectedAway(asked, landed) {
|
|
6774
|
-
if (bareHost(asked.hostname) !== bareHost(landed.hostname)) return true;
|
|
6775
|
-
const a = trimSlash(asked.pathname).toLowerCase();
|
|
6776
|
-
const l = trimSlash(landed.pathname).toLowerCase();
|
|
6777
|
-
if (a === l || a === "/") return false;
|
|
6778
|
-
if (l.startsWith(`${a}/`)) return false;
|
|
6779
|
-
if (l.endsWith(a)) return false;
|
|
6780
|
-
return true;
|
|
6781
|
-
}
|
|
6782
|
-
var LADDER = 'This is a missing or expired session, not a script bug. Ways past, cheapest first: mint a session from the test auth the project already has; script the sign-in off camera and save the storage state; or have a person sign in once (npx playwright open --channel chrome --save-storage=<file> <url>). Then pass --storage-state <file>. The ladder: https://vos.so/llms-full.txt, "Sessions". A take OF this page is --allow-wall.';
|
|
6783
|
-
function wallVerdict(a) {
|
|
6784
|
-
const asked = parse(a.askedUrl);
|
|
6785
|
-
const landed = parse(a.landedUrl);
|
|
6786
|
-
if (!asked || !landed) return null;
|
|
6787
|
-
if (!/^https?:$/.test(asked.protocol)) return null;
|
|
6788
|
-
const away = redirectedAway(asked, landed);
|
|
6789
|
-
const where = { asked: place(asked), landed: place(landed) };
|
|
6790
|
-
const host = bareHost(asked.hostname);
|
|
6791
|
-
if (a.status === 401 || a.status === 403) {
|
|
6792
|
-
return {
|
|
6793
|
-
kind: "status",
|
|
6794
|
-
level: "hard",
|
|
6795
|
-
...where,
|
|
6796
|
-
message: `asked for ${asked.pathname}, answered ${a.status}: no session for ${host}. ${LADDER}`
|
|
6797
|
-
};
|
|
6798
|
-
}
|
|
6799
|
-
const askedForSignIn = isSignInPath(asked.pathname) || isIdpHost(asked.hostname);
|
|
6800
|
-
if (askedForSignIn && !away) return null;
|
|
6801
|
-
if (away && isIdpHost(landed.hostname) && !isIdpHost(asked.hostname)) {
|
|
6802
|
-
return {
|
|
6803
|
-
kind: "idp",
|
|
6804
|
-
level: "hard",
|
|
6805
|
-
...where,
|
|
6806
|
-
message: `asked for ${asked.pathname}, landed on ${landed.hostname}: no session for ${host}. ${LADDER}`
|
|
6807
|
-
};
|
|
6808
|
-
}
|
|
6809
|
-
const signInForm = a.passwordFields === 1 && !a.newPasswordField || a.oneTimeCodeField;
|
|
6810
|
-
if (away && isSignInPath(landed.pathname) || signInForm && !askedForSignIn) {
|
|
6811
|
-
const seen = away ? `landed on ${landed.pathname}` : "the page is a sign-in form";
|
|
6812
|
-
return {
|
|
6813
|
-
kind: "signin",
|
|
6814
|
-
level: "hard",
|
|
6815
|
-
...where,
|
|
6816
|
-
message: `asked for ${asked.pathname}, ${seen}: no session for ${host}. ${LADDER}`
|
|
6817
|
-
};
|
|
6818
|
-
}
|
|
6819
|
-
if (away) {
|
|
6820
|
-
return {
|
|
6821
|
-
kind: "redirect",
|
|
6822
|
-
level: "soft",
|
|
6823
|
-
...where,
|
|
6824
|
-
message: `asked for ${asked.pathname}, landed on ${landed.pathname}: the site sent the recorder elsewhere, which is what a page behind a login does to a stranger. If that is the page you meant, record it by its own URL. ${LADDER}`
|
|
6825
|
-
};
|
|
6826
|
-
}
|
|
6827
|
-
return null;
|
|
6828
|
-
}
|
|
6829
|
-
var WallError = class extends Error {
|
|
6830
|
-
constructor(verdict) {
|
|
6831
|
-
super(verdict.message);
|
|
6832
|
-
this.verdict = verdict;
|
|
6833
|
-
this.name = "WallError";
|
|
6834
|
-
}
|
|
6835
|
-
verdict;
|
|
6836
|
-
};
|
|
6837
|
-
var WALL_PROBE = `(() => {
|
|
6838
|
-
const shown = (el) => {
|
|
6839
|
-
const r = el.getBoundingClientRect()
|
|
6840
|
-
if (r.width < 2 || r.height < 2) return false
|
|
6841
|
-
const s = getComputedStyle(el)
|
|
6842
|
-
return s.visibility !== 'hidden' && s.display !== 'none'
|
|
6843
|
-
}
|
|
6844
|
-
const pw = [...document.querySelectorAll('input[type="password"]')].filter(shown)
|
|
6845
|
-
const otp = [...document.querySelectorAll('input[autocomplete="one-time-code"]')].filter(shown)
|
|
6846
|
-
return {
|
|
6847
|
-
passwordFields: pw.length,
|
|
6848
|
-
newPasswordField: pw.some((el) => (el.getAttribute('autocomplete') || '').includes('new-password')),
|
|
6849
|
-
oneTimeCodeField: otp.length > 0,
|
|
6850
|
-
}
|
|
6851
|
-
})()`;
|
|
6852
|
-
|
|
6853
|
-
// src/plugin/exposure.ts
|
|
6854
|
-
var EXPOSURE_MATCHERS_SRC = `(() => {
|
|
6855
|
-
const EMAIL = /[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,})/g
|
|
6856
|
-
const SAFE_HOST = /(^|\\.)example\\.(com|org|net)$|\\.(test|example|invalid|localhost)$/i
|
|
6857
|
-
const KEY = new RegExp([
|
|
6858
|
-
'\\\\b[srp]k_(?:live|test)_[A-Za-z0-9]{8,}',
|
|
6859
|
-
'\\\\bgh[pousr]_[A-Za-z0-9]{20,}',
|
|
6860
|
-
'\\\\bgithub_pat_[A-Za-z0-9_]{20,}',
|
|
6861
|
-
'\\\\bxox[abprs]-[A-Za-z0-9-]{10,}',
|
|
6862
|
-
'\\\\bAKIA[0-9A-Z]{16}\\\\b',
|
|
6863
|
-
'\\\\bAIza[0-9A-Za-z_-]{30,}',
|
|
6864
|
-
'\\\\bvos_(?:sk|rg|ho)_[A-Za-z0-9]{8,}',
|
|
6865
|
-
'\\\\beyJ[A-Za-z0-9_-]{8,}\\\\.eyJ[A-Za-z0-9_-]{8,}\\\\.[A-Za-z0-9_-]{8,}',
|
|
6866
|
-
].join('|'))
|
|
6867
|
-
const CARD = /(?:^|[^0-9])((?:[0-9][ -]?){12,18}[0-9])(?![0-9])/g
|
|
6868
|
-
const TAIL = /[\\u2022\\u00b7*xX]{2,}[ -]?[0-9]{4}(?![0-9])/
|
|
6869
|
-
const luhn = (digits) => {
|
|
6870
|
-
let sum = 0, alt = false
|
|
6871
|
-
for (let i = digits.length - 1; i >= 0; i--) {
|
|
6872
|
-
let d = digits.charCodeAt(i) - 48
|
|
6873
|
-
if (alt) { d *= 2; if (d > 9) d -= 9 }
|
|
6874
|
-
sum += d; alt = !alt
|
|
6875
|
-
}
|
|
6876
|
-
return sum % 10 === 0
|
|
6877
|
-
}
|
|
6878
|
-
const kindsIn = (text) => {
|
|
6879
|
-
const kinds = []
|
|
6880
|
-
if (!text || text.length < 6) return kinds
|
|
6881
|
-
EMAIL.lastIndex = 0
|
|
6882
|
-
for (let m; (m = EMAIL.exec(text)); ) {
|
|
6883
|
-
if (!SAFE_HOST.test(m[1])) { kinds.push('email'); break }
|
|
6884
|
-
}
|
|
6885
|
-
if (KEY.test(text)) kinds.push('key')
|
|
6886
|
-
CARD.lastIndex = 0
|
|
6887
|
-
for (let m; (m = CARD.exec(text)); ) {
|
|
6888
|
-
const digits = m[1].replace(/[ -]/g, '')
|
|
6889
|
-
if (digits.length >= 13 && digits.length <= 19 && !/^0+$/.test(digits) && luhn(digits)) { kinds.push('card'); break }
|
|
6890
|
-
}
|
|
6891
|
-
if (TAIL.test(text)) kinds.push('card-tail')
|
|
6892
|
-
return kinds
|
|
6893
|
-
}
|
|
6894
|
-
return { kindsIn }
|
|
6895
|
-
})()`;
|
|
6896
|
-
var EXPOSURE_PROBE = `(() => {
|
|
6897
|
-
const { kindsIn } = ${EXPOSURE_MATCHERS_SRC}
|
|
6898
|
-
const vw = innerWidth, vh = innerHeight
|
|
6899
|
-
// A selector that reaches THIS element and no other, so it can be pasted
|
|
6900
|
-
// into "mask" as it is: climb until the path is unique, and say which
|
|
6901
|
-
// sibling wherever a tag-and-class alone would match several.
|
|
6902
|
-
const segment = (el) => {
|
|
6903
|
-
const tid = el.getAttribute('data-testid')
|
|
6904
|
-
if (tid) return el.tagName.toLowerCase() + '[data-testid="' + tid + '"]'
|
|
6905
|
-
if (el.id && /^[A-Za-z_][\\w-]*$/.test(el.id)) return el.tagName.toLowerCase() + '#' + el.id
|
|
6906
|
-
const cls = [...el.classList].filter((c) => /^[A-Za-z_-][\\w-]*$/.test(c)).slice(0, 2)
|
|
6907
|
-
let seg = el.tagName.toLowerCase() + (cls.length ? '.' + cls.join('.') : '')
|
|
6908
|
-
const p = el.parentElement
|
|
6909
|
-
if (p && [...p.children].filter((c) => c.matches(seg)).length > 1)
|
|
6910
|
-
seg += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')'
|
|
6911
|
-
return seg
|
|
6912
|
-
}
|
|
6913
|
-
const name = (el) => {
|
|
6914
|
-
const parts = []
|
|
6915
|
-
for (let cur = el; cur && cur !== document.body && cur !== document.documentElement; cur = cur.parentElement) {
|
|
6916
|
-
parts.unshift(segment(cur))
|
|
6917
|
-
let n = 2
|
|
6918
|
-
try { n = document.querySelectorAll(parts.join(' > ')).length } catch {}
|
|
6919
|
-
// a bare tag is unique HERE and nowhere else: give it its parent too
|
|
6920
|
-
const bare = parts.length === 1 && !/[#.\\[]/.test(parts[0])
|
|
6921
|
-
if ((n === 1 && !bare) || parts.length >= 5) break
|
|
6922
|
-
}
|
|
6923
|
-
return parts.join(' > ')
|
|
6924
|
-
}
|
|
6925
|
-
const seen = (el) => {
|
|
6926
|
-
if (el.closest('[data-vos-masked]')) return null
|
|
6927
|
-
const r = el.getBoundingClientRect()
|
|
6928
|
-
if (r.width < 2 || r.height < 2) return null
|
|
6929
|
-
if (r.bottom <= 0 || r.right <= 0 || r.top >= vh || r.left >= vw) return null
|
|
6930
|
-
const s = getComputedStyle(el)
|
|
6931
|
-
if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null
|
|
6932
|
-
return r
|
|
6933
|
-
}
|
|
6934
|
-
const out = [], keys = new Set()
|
|
6935
|
-
const add = (el, text) => {
|
|
6936
|
-
const kinds = kindsIn(text)
|
|
6937
|
-
if (!kinds.length) return
|
|
6938
|
-
const r = seen(el)
|
|
6939
|
-
if (!r) return
|
|
6940
|
-
const selector = name(el)
|
|
6941
|
-
for (const kind of kinds) {
|
|
6942
|
-
const k = kind + '|' + selector
|
|
6943
|
-
if (keys.has(k) || out.length >= 40) continue
|
|
6944
|
-
keys.add(k)
|
|
6945
|
-
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) } })
|
|
6946
|
-
}
|
|
6947
|
-
}
|
|
6948
|
-
const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT)
|
|
6949
|
-
for (let n; (n = walker.nextNode()); ) {
|
|
6950
|
-
const el = n.parentElement
|
|
6951
|
-
if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(el.tagName)) continue
|
|
6952
|
-
add(el, n.nodeValue)
|
|
6953
|
-
}
|
|
6954
|
-
for (const el of document.querySelectorAll('input, textarea')) {
|
|
6955
|
-
if (/^(password|hidden|file|checkbox|radio)$/.test(el.type)) continue
|
|
6956
|
-
add(el, el.value)
|
|
6957
|
-
}
|
|
6958
|
-
const hits = {}
|
|
6959
|
-
for (const [sel, n] of Object.entries(window.__vosMaskHits || {})) hits[sel] = n
|
|
6960
|
-
return { exposures: out, maskHits: hits }
|
|
6961
|
-
})()`;
|
|
6962
|
-
function maskInitScript(masks) {
|
|
6963
|
-
const rules = masks.map((m) => ({
|
|
6964
|
-
selector: m.selector,
|
|
6965
|
-
as: m.as === "text" ? "text" : "blur",
|
|
6966
|
-
text: m.text ?? ""
|
|
6967
|
-
}));
|
|
6968
|
-
return `(() => {
|
|
6969
|
-
const RULES = ${JSON.stringify(rules)}
|
|
6970
|
-
const hits = (window.__vosMaskHits = window.__vosMaskHits || {})
|
|
6971
|
-
for (const r of RULES) hits[r.selector] = hits[r.selector] || 0
|
|
6972
|
-
const css = RULES.map((r) => r.selector + '[data-vos-masked="blur"]{filter:blur(7px) !important;user-select:none !important}').join('\\n')
|
|
6973
|
-
const paint = () => {
|
|
6974
|
-
const root = document.head || document.documentElement
|
|
6975
|
-
if (!root || document.getElementById('__vos-mask-style')) return
|
|
6976
|
-
const style = document.createElement('style')
|
|
6977
|
-
style.id = '__vos-mask-style'
|
|
6978
|
-
style.textContent = css
|
|
6979
|
-
root.appendChild(style)
|
|
6980
|
-
}
|
|
6981
|
-
const apply = () => {
|
|
6982
|
-
paint()
|
|
6983
|
-
for (const r of RULES) {
|
|
6984
|
-
let found
|
|
6985
|
-
try { found = document.querySelectorAll(r.selector) } catch { continue }
|
|
6986
|
-
if (found.length > hits[r.selector]) hits[r.selector] = found.length
|
|
6987
|
-
for (const el of found) {
|
|
6988
|
-
const form = /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)
|
|
6989
|
-
const mode = r.as === 'text' && !form ? 'text' : 'blur'
|
|
6990
|
-
if (el.getAttribute('data-vos-masked') !== mode) el.setAttribute('data-vos-masked', mode)
|
|
6991
|
-
if (mode !== 'text') continue
|
|
6992
|
-
const texts = []
|
|
6993
|
-
const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
|
|
6994
|
-
for (let n; (n = w.nextNode()); ) texts.push(n)
|
|
6995
|
-
if (!texts.length) { if (el.textContent !== r.text) el.textContent = r.text; continue }
|
|
6996
|
-
texts.forEach((n, i) => { const want = i === 0 ? r.text : ''; if (n.nodeValue !== want) n.nodeValue = want })
|
|
6997
|
-
}
|
|
6998
|
-
}
|
|
6999
|
-
}
|
|
7000
|
-
new MutationObserver(apply).observe(document, { childList: true, subtree: true, characterData: true })
|
|
7001
|
-
document.addEventListener('DOMContentLoaded', apply)
|
|
7002
|
-
apply()
|
|
7003
|
-
})()`;
|
|
7004
|
-
}
|
|
7005
|
-
var ExposureLog = class {
|
|
7006
|
-
byKey = /* @__PURE__ */ new Map();
|
|
7007
|
-
maskHits = /* @__PURE__ */ new Map();
|
|
7008
|
-
constructor(masks = []) {
|
|
7009
|
-
for (const m of masks) this.maskHits.set(m.selector, 0);
|
|
7010
|
-
}
|
|
7011
|
-
add(step, scan) {
|
|
7012
|
-
if (!scan) return;
|
|
7013
|
-
for (const e of scan.exposures ?? []) {
|
|
7014
|
-
const key = `${e.kind}|${e.selector}`;
|
|
7015
|
-
const had = this.byKey.get(key);
|
|
7016
|
-
if (had) had.seen += 1;
|
|
7017
|
-
else this.byKey.set(key, { step, ...e, seen: 1 });
|
|
7018
|
-
}
|
|
7019
|
-
for (const [sel, n] of Object.entries(scan.maskHits ?? {})) {
|
|
7020
|
-
if (n > (this.maskHits.get(sel) ?? 0)) this.maskHits.set(sel, n);
|
|
7021
|
-
}
|
|
7022
|
-
}
|
|
7023
|
-
exposures() {
|
|
7024
|
-
return [...this.byKey.values()];
|
|
7025
|
-
}
|
|
7026
|
-
masks(rules) {
|
|
7027
|
-
return rules.map((m) => ({
|
|
7028
|
-
selector: m.selector,
|
|
7029
|
-
as: m.as === "text" ? "text" : "blur",
|
|
7030
|
-
hits: this.maskHits.get(m.selector) ?? 0
|
|
7031
|
-
}));
|
|
7032
|
-
}
|
|
7033
|
-
};
|
|
7034
|
-
var KIND_WORDS = {
|
|
7035
|
-
email: "an email address",
|
|
7036
|
-
key: "something shaped like an API key or token",
|
|
7037
|
-
card: "a card number",
|
|
7038
|
-
"card-tail": "a card's last digits"
|
|
7039
|
-
};
|
|
7040
|
-
function exposureLine(e) {
|
|
7041
|
-
const when = e.step < 0 ? "as the page opened" : `at step #${e.step}`;
|
|
7042
|
-
return `${KIND_WORDS[e.kind]} in ${e.selector} (${when}, ${e.rect.x},${e.rect.y} ${e.rect.w}\xD7${e.rect.h})`;
|
|
7043
|
-
}
|
|
7044
|
-
var 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.';
|
|
7045
|
-
|
|
7046
|
-
// src/plugin/recorder.ts
|
|
7047
|
-
var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
7048
|
-
var DRY_PAUSE_MS = 120;
|
|
7049
|
-
function jpegDims(buf) {
|
|
7050
|
-
let i = 2;
|
|
7051
|
-
while (i < buf.length - 9) {
|
|
7052
|
-
if (buf[i] !== 255) return null;
|
|
7053
|
-
const marker = buf[i + 1];
|
|
7054
|
-
const len = buf.readUInt16BE(i + 2);
|
|
7055
|
-
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
7056
|
-
return { h: buf.readUInt16BE(i + 5), w: buf.readUInt16BE(i + 7) };
|
|
7057
|
-
}
|
|
7058
|
-
i += 2 + len;
|
|
7059
|
-
}
|
|
7060
|
-
return null;
|
|
7061
|
-
}
|
|
6435
|
+
import { recordTake as recordTakeCore } from "@vosjs/render-core/record";
|
|
7062
6436
|
async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
7063
6437
|
const dry = opts.dryRun === true;
|
|
7064
|
-
const
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
...opts.storageState ? { storageState: opts.storageState } : {},
|
|
7072
|
-
...opts.headers && Object.keys(opts.headers).length ? { extraHTTPHeaders: opts.headers } : {}
|
|
7073
|
-
});
|
|
7074
|
-
if (opts.context && opts.headers && Object.keys(opts.headers).length)
|
|
7075
|
-
await context.setExtraHTTPHeaders(opts.headers);
|
|
7076
|
-
const masks = actions.mask ?? [];
|
|
7077
|
-
if (masks.length) await context.addInitScript(maskInitScript(masks));
|
|
7078
|
-
const exposureLog = new ExposureLog(masks);
|
|
7079
|
-
const page = await context.newPage();
|
|
7080
|
-
const scanExposures = async (step) => {
|
|
7081
|
-
const scan = await page.evaluate(EXPOSURE_PROBE).catch(() => null);
|
|
7082
|
-
exposureLog.add(step, scan);
|
|
7083
|
-
};
|
|
7084
|
-
page.on("console", (m) => {
|
|
7085
|
-
if (m.type() === "error") log(` [page error] ${m.text()}`);
|
|
7086
|
-
});
|
|
7087
|
-
log(`goto ${url}`);
|
|
7088
|
-
let navTimeout = false;
|
|
7089
|
-
let response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
|
|
7090
|
-
navTimeout = true;
|
|
7091
|
-
log(" (networkidle timeout \u2014 continuing)");
|
|
7092
|
-
return null;
|
|
7093
|
-
});
|
|
7094
|
-
await sleep(800);
|
|
7095
|
-
const setup = actions.setup ?? [];
|
|
7096
|
-
if (setup.length) {
|
|
7097
|
-
log(`setup: ${setup.length} step(s), off camera`);
|
|
7098
|
-
const result = await runSetup(page, setup, { sleep: realSleep });
|
|
7099
|
-
for (const line of result.lines) log(` ${line}`);
|
|
7100
|
-
if (result.failed) {
|
|
7101
|
-
await context.close().catch(() => {
|
|
7102
|
-
});
|
|
7103
|
-
throw new SetupError(result.failed);
|
|
7104
|
-
}
|
|
7105
|
-
log(`goto ${url} (after setup)`);
|
|
7106
|
-
response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => null);
|
|
7107
|
-
await sleep(800);
|
|
7108
|
-
}
|
|
7109
|
-
let wall = null;
|
|
7110
|
-
if (opts.onArrival) {
|
|
7111
|
-
const seen = await page.evaluate(WALL_PROBE).catch(() => null);
|
|
7112
|
-
try {
|
|
7113
|
-
wall = await opts.onArrival({
|
|
7114
|
-
askedUrl: url,
|
|
7115
|
-
landedUrl: page.url(),
|
|
7116
|
-
...response ? { status: response.status() } : {},
|
|
7117
|
-
passwordFields: seen?.passwordFields ?? 0,
|
|
7118
|
-
newPasswordField: seen?.newPasswordField ?? false,
|
|
7119
|
-
oneTimeCodeField: seen?.oneTimeCodeField ?? false
|
|
7120
|
-
}) ?? null;
|
|
7121
|
-
} catch (e) {
|
|
7122
|
-
await context.close().catch(() => {
|
|
7123
|
-
});
|
|
7124
|
-
throw e;
|
|
7125
|
-
}
|
|
7126
|
-
}
|
|
7127
|
-
await scanExposures(-1);
|
|
7128
|
-
const cdp = await context.newCDPSession(page);
|
|
7129
|
-
const frames = [];
|
|
7130
|
-
let frameIdx = 0;
|
|
7131
|
-
let t0 = 0;
|
|
7132
|
-
if (!dry)
|
|
7133
|
-
cdp.on("Page.screencastFrame", (ev) => {
|
|
7134
|
-
const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
|
|
7135
|
-
const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
|
|
7136
|
-
writeFileSync2(join13(paths.framesDir, file), Buffer.from(ev.data, "base64"));
|
|
7137
|
-
frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
|
|
7138
|
-
cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
|
|
7139
|
-
});
|
|
7140
|
-
});
|
|
7141
|
-
t0 = Date.now();
|
|
7142
|
-
if (!dry)
|
|
7143
|
-
await cdp.send("Page.startScreencast", {
|
|
7144
|
-
format: "jpeg",
|
|
7145
|
-
quality: 90,
|
|
7146
|
-
maxWidth: vw,
|
|
7147
|
-
maxHeight: vh,
|
|
7148
|
-
everyNthFrame: 1
|
|
7149
|
-
});
|
|
7150
|
-
const events = [];
|
|
7151
|
-
const cur = { x: 48, y: 48 };
|
|
7152
|
-
const now = () => Date.now() - t0;
|
|
7153
|
-
const emit = (e) => events.push({ t: now(), ...e });
|
|
7154
|
-
await page.mouse.move(cur.x, cur.y);
|
|
7155
|
-
emit({ x: cur.x, y: cur.y, type: "move" });
|
|
7156
|
-
const clock = { now: () => Date.now(), sleep };
|
|
7157
|
-
const moveTo = async (tx, ty) => {
|
|
7158
|
-
const dist = Math.hypot(tx - cur.x, ty - cur.y);
|
|
7159
|
-
const from = { ...cur };
|
|
7160
|
-
await clockMotion(
|
|
7161
|
-
dry ? 0 : pointerTravelMs(dist),
|
|
7162
|
-
async (u) => {
|
|
7163
|
-
cur.x = from.x + (tx - from.x) * u;
|
|
7164
|
-
cur.y = from.y + (ty - from.y) * u;
|
|
7165
|
-
await page.mouse.move(cur.x, cur.y);
|
|
7166
|
-
emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
|
|
7167
|
-
},
|
|
7168
|
-
clock
|
|
7169
|
-
);
|
|
7170
|
-
};
|
|
7171
|
-
const boxOf = async (selector) => {
|
|
7172
|
-
const loc = page.locator(selector).first();
|
|
7173
|
-
try {
|
|
7174
|
-
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
7175
|
-
const before = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => [0, 0]);
|
|
7176
|
-
await loc.scrollIntoViewIfNeeded();
|
|
7177
|
-
const after = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => before);
|
|
7178
|
-
if (before[0] !== after[0] || before[1] !== after[1])
|
|
7179
|
-
await sleep(SCROLL_SETTLE_MS);
|
|
7180
|
-
const bb = await loc.boundingBox();
|
|
7181
|
-
if (!bb) return null;
|
|
7182
|
-
return { x: bb.x, y: bb.y, w: bb.width, h: bb.height };
|
|
7183
|
-
} catch {
|
|
7184
|
-
log(` (selector not found, skipping: ${selector})`);
|
|
7185
|
-
return null;
|
|
7186
|
-
}
|
|
7187
|
-
};
|
|
7188
|
-
const clickAt = async (rect) => {
|
|
7189
|
-
await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
|
|
7190
|
-
await sleep(PRESS_LEAD_MS);
|
|
7191
|
-
emit({
|
|
7192
|
-
x: Math.round(cur.x),
|
|
7193
|
-
y: Math.round(cur.y),
|
|
7194
|
-
type: "down",
|
|
7195
|
-
button: 0,
|
|
7196
|
-
rect
|
|
7197
|
-
});
|
|
7198
|
-
await page.mouse.down();
|
|
7199
|
-
await sleep(PRESS_HOLD_MS);
|
|
7200
|
-
await page.mouse.up();
|
|
7201
|
-
emit({
|
|
7202
|
-
x: Math.round(cur.x),
|
|
7203
|
-
y: Math.round(cur.y),
|
|
7204
|
-
type: "up",
|
|
7205
|
-
button: 0,
|
|
7206
|
-
rect
|
|
7207
|
-
});
|
|
7208
|
-
};
|
|
7209
|
-
const waitSettled = async () => {
|
|
7210
|
-
if (dry) return null;
|
|
7211
|
-
const pressAt = now();
|
|
7212
|
-
const before = frames.length;
|
|
7213
|
-
for (; ; ) {
|
|
7214
|
-
const t = now();
|
|
7215
|
-
const last = frames.length > before ? frames[frames.length - 1] : null;
|
|
7216
|
-
const sinceChange = last ? t - last.tMs : null;
|
|
7217
|
-
if (settleVerdict({ sincePress: t - pressAt, sinceChange }) === "settled")
|
|
7218
|
-
return sinceChange;
|
|
7219
|
-
await sleep(40);
|
|
7220
|
-
}
|
|
7221
|
-
};
|
|
7222
|
-
const skipped = [];
|
|
7223
|
-
const stepSpans = [];
|
|
7224
|
-
const paces = [];
|
|
7225
|
-
let capped = false;
|
|
7226
|
-
for (const [stepIdx, step] of actions.steps.entries()) {
|
|
7227
|
-
if (capReached(now(), maxSeconds)) {
|
|
7228
|
-
capped = true;
|
|
7229
|
-
break;
|
|
7230
|
-
}
|
|
7231
|
-
const stepStart = now();
|
|
7232
|
-
let gestureMs = 0;
|
|
7233
|
-
const gesture = async (fn) => {
|
|
7234
|
-
const t = now();
|
|
7235
|
-
const out = await fn();
|
|
7236
|
-
gestureMs += now() - t;
|
|
7237
|
-
return out;
|
|
7238
|
-
};
|
|
7239
|
-
const skippedBefore = skipped.length;
|
|
7240
|
-
const urlBefore = page.url();
|
|
7241
|
-
let stepRect = null;
|
|
7242
|
-
switch (step.do) {
|
|
7243
|
-
case "wait":
|
|
7244
|
-
await sleep(clampWait(step.ms, now(), maxSeconds));
|
|
7245
|
-
break;
|
|
7246
|
-
case "move":
|
|
7247
|
-
await moveTo(step.x, step.y);
|
|
7248
|
-
break;
|
|
7249
|
-
case "hover": {
|
|
7250
|
-
const rect = await boxOf(step.selector);
|
|
7251
|
-
stepRect = rect;
|
|
7252
|
-
if (rect) {
|
|
7253
|
-
await gesture(() => moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2));
|
|
7254
|
-
log(`hover ${step.selector}`);
|
|
7255
|
-
await sleep(step.ms ?? 700);
|
|
7256
|
-
} else
|
|
7257
|
-
skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
|
|
7258
|
-
break;
|
|
7259
|
-
}
|
|
7260
|
-
case "click": {
|
|
7261
|
-
const rect = await boxOf(step.selector);
|
|
7262
|
-
stepRect = rect;
|
|
7263
|
-
if (rect) {
|
|
7264
|
-
await gesture(() => clickAt(rect));
|
|
7265
|
-
log(`click ${step.selector}`);
|
|
7266
|
-
const sinceChange = await gesture(() => waitSettled());
|
|
7267
|
-
await sleep(holdLeftMs(settleMs(step), sinceChange));
|
|
7268
|
-
} else
|
|
7269
|
-
skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
|
|
7270
|
-
break;
|
|
7271
|
-
}
|
|
7272
|
-
case "type": {
|
|
7273
|
-
const rect = await boxOf(step.selector);
|
|
7274
|
-
stepRect = rect;
|
|
7275
|
-
if (rect) {
|
|
7276
|
-
if (step.focus !== false) await gesture(() => clickAt(rect));
|
|
7277
|
-
const delay = step.delayMs ?? 40;
|
|
7278
|
-
const ping = () => emit({
|
|
7279
|
-
x: Math.round(rect.x + rect.w / 2),
|
|
7280
|
-
y: Math.round(rect.y + rect.h / 2),
|
|
7281
|
-
type: "key",
|
|
7282
|
-
rect
|
|
7283
|
-
});
|
|
7284
|
-
let lastPing = -Infinity;
|
|
7285
|
-
await clockTyping(
|
|
7286
|
-
[...step.text],
|
|
7287
|
-
delay,
|
|
7288
|
-
async (ch) => {
|
|
7289
|
-
if (now() - lastPing >= 350) {
|
|
7290
|
-
lastPing = now();
|
|
7291
|
-
ping();
|
|
7292
|
-
}
|
|
7293
|
-
await page.keyboard.type(ch);
|
|
7294
|
-
},
|
|
7295
|
-
clock
|
|
7296
|
-
);
|
|
7297
|
-
ping();
|
|
7298
|
-
log(`type "${step.text}" into ${step.selector}`);
|
|
7299
|
-
await gesture(() => sleep(settleMs(step)));
|
|
7300
|
-
} else
|
|
7301
|
-
skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
|
|
7302
|
-
break;
|
|
7303
|
-
}
|
|
7304
|
-
case "scroll": {
|
|
7305
|
-
const total = Math.abs(step.dy);
|
|
7306
|
-
const dir = Math.sign(step.dy);
|
|
7307
|
-
for (let done = 0; done < total; done += 120) {
|
|
7308
|
-
await page.mouse.wheel(0, dir * Math.min(120, total - done));
|
|
7309
|
-
emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "scroll" });
|
|
7310
|
-
await sleep(40);
|
|
7311
|
-
}
|
|
7312
|
-
await gesture(() => sleep(settleMs(step)));
|
|
7313
|
-
break;
|
|
7314
|
-
}
|
|
7315
|
-
case "drag": {
|
|
7316
|
-
let start = null;
|
|
7317
|
-
let rect;
|
|
7318
|
-
if (step.selector) {
|
|
7319
|
-
const box = await boxOf(step.selector);
|
|
7320
|
-
stepRect = box;
|
|
7321
|
-
if (box) {
|
|
7322
|
-
rect = box;
|
|
7323
|
-
start = { x: box.x + box.w / 2, y: box.y + box.h / 2 };
|
|
7324
|
-
} else {
|
|
7325
|
-
skipped.push({
|
|
7326
|
-
step: stepIdx,
|
|
7327
|
-
do: step.do,
|
|
7328
|
-
selector: step.selector
|
|
7329
|
-
});
|
|
7330
|
-
break;
|
|
7331
|
-
}
|
|
7332
|
-
} else if (typeof step.x === "number" && typeof step.y === "number") {
|
|
7333
|
-
start = { x: step.x, y: step.y };
|
|
7334
|
-
}
|
|
7335
|
-
if (!start) break;
|
|
7336
|
-
await gesture(() => moveTo(start.x, start.y));
|
|
7337
|
-
await gesture(() => sleep(PRESS_LEAD_MS));
|
|
7338
|
-
emit({
|
|
7339
|
-
x: Math.round(cur.x),
|
|
7340
|
-
y: Math.round(cur.y),
|
|
7341
|
-
type: "down",
|
|
7342
|
-
button: 0,
|
|
7343
|
-
rect
|
|
7344
|
-
});
|
|
7345
|
-
await page.mouse.down();
|
|
7346
|
-
await gesture(() => sleep(PRESS_HOLD_MS));
|
|
7347
|
-
const dur = step.ms ?? 700;
|
|
7348
|
-
const from = { ...cur };
|
|
7349
|
-
await clockMotion(
|
|
7350
|
-
dur,
|
|
7351
|
-
async (u) => {
|
|
7352
|
-
cur.x = from.x + (step.tx - from.x) * u;
|
|
7353
|
-
cur.y = from.y + (step.ty - from.y) * u;
|
|
7354
|
-
await page.mouse.move(cur.x, cur.y);
|
|
7355
|
-
emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
|
|
7356
|
-
},
|
|
7357
|
-
clock
|
|
7358
|
-
);
|
|
7359
|
-
await gesture(() => sleep(settleMs({ do: "drag" })));
|
|
7360
|
-
await page.mouse.up();
|
|
7361
|
-
emit({
|
|
7362
|
-
x: Math.round(cur.x),
|
|
7363
|
-
y: Math.round(cur.y),
|
|
7364
|
-
type: "up",
|
|
7365
|
-
button: 0,
|
|
7366
|
-
rect
|
|
7367
|
-
});
|
|
7368
|
-
log(
|
|
7369
|
-
`drag ${step.selector ?? `${start.x},${start.y}`} \u2192 ${step.tx},${step.ty}`
|
|
7370
|
-
);
|
|
7371
|
-
await sleep(400);
|
|
7372
|
-
break;
|
|
6438
|
+
const result = await recordTakeCore(
|
|
6439
|
+
browser,
|
|
6440
|
+
url,
|
|
6441
|
+
actions,
|
|
6442
|
+
{
|
|
6443
|
+
frame: (file, bytes) => {
|
|
6444
|
+
writeFileSync2(join13(paths.framesDir, file), bytes);
|
|
7373
6445
|
}
|
|
6446
|
+
},
|
|
6447
|
+
log,
|
|
6448
|
+
{
|
|
6449
|
+
...opts,
|
|
6450
|
+
env: process.env,
|
|
6451
|
+
platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux"
|
|
7374
6452
|
}
|
|
7375
|
-
|
|
7376
|
-
stepSpans.push({
|
|
7377
|
-
step: stepIdx,
|
|
7378
|
-
...stepInfo.id ? { id: stepInfo.id } : {},
|
|
7379
|
-
do: step.do,
|
|
7380
|
-
...stepInfo.selector ? { selector: stepInfo.selector } : {},
|
|
7381
|
-
tStart: +(stepStart / 1e3).toFixed(3),
|
|
7382
|
-
tEnd: +(now() / 1e3).toFixed(3),
|
|
7383
|
-
...skipped.length > skippedBefore ? { skipped: true } : {},
|
|
7384
|
-
...page.url() !== urlBefore ? { navigated: true } : {},
|
|
7385
|
-
...stepRect ? {
|
|
7386
|
-
rect: {
|
|
7387
|
-
x: +stepRect.x.toFixed(1),
|
|
7388
|
-
y: +stepRect.y.toFixed(1),
|
|
7389
|
-
w: +stepRect.w.toFixed(1),
|
|
7390
|
-
h: +stepRect.h.toFixed(1)
|
|
7391
|
-
}
|
|
7392
|
-
} : {}
|
|
7393
|
-
});
|
|
7394
|
-
paces.push({
|
|
7395
|
-
step: stepIdx,
|
|
7396
|
-
do: step.do,
|
|
7397
|
-
askedMs: askedMs(step),
|
|
7398
|
-
gestureMs,
|
|
7399
|
-
wallMs: now() - stepStart
|
|
7400
|
-
});
|
|
7401
|
-
await scanExposures(stepIdx);
|
|
7402
|
-
}
|
|
7403
|
-
if (!capped) await sleep(TRAILING_HOLD_MS);
|
|
7404
|
-
if (capped || capReached(now(), maxSeconds)) {
|
|
7405
|
-
capped = true;
|
|
7406
|
-
log(` ${cappedLine(maxSeconds)}`);
|
|
7407
|
-
}
|
|
7408
|
-
const durationMs = capped ? Math.min(now(), maxSeconds * 1e3) : now();
|
|
7409
|
-
await cdp.send("Page.stopScreencast").catch(() => {
|
|
7410
|
-
});
|
|
7411
|
-
await sleep(200);
|
|
7412
|
-
const pageTitle = await page.title().catch(() => "");
|
|
7413
|
-
await context.close();
|
|
7414
|
-
const firstFrame = frames[0] ? jpegDims(readFileSync3(join13(paths.framesDir, frames[0].file))) : null;
|
|
7415
|
-
const meta = {
|
|
7416
|
-
dpr: 1,
|
|
7417
|
-
zoom: 1,
|
|
7418
|
-
t0,
|
|
7419
|
-
durationMs,
|
|
7420
|
-
width: vw,
|
|
7421
|
-
height: vh,
|
|
7422
|
-
fps: 30,
|
|
7423
|
-
hasAudio: false,
|
|
7424
|
-
captureWidth: firstFrame?.w ?? vw,
|
|
7425
|
-
captureHeight: firstFrame?.h ?? vh,
|
|
7426
|
-
captureSurface: "tab",
|
|
7427
|
-
pageUrl: url,
|
|
7428
|
-
pageTitle,
|
|
7429
|
-
platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux",
|
|
7430
|
-
producer: "cli",
|
|
7431
|
-
steps: stepSpans,
|
|
7432
|
-
...wall ? { wall: { kind: wall.kind, asked: wall.asked, landed: wall.landed } } : {},
|
|
7433
|
-
...exposureLog.exposures().length ? { exposures: exposureLog.exposures() } : {},
|
|
7434
|
-
...masks.length ? { masks: exposureLog.masks(masks) } : {}
|
|
7435
|
-
};
|
|
7436
|
-
const freezes = [];
|
|
7437
|
-
for (let i = 1; i < frames.length; i++) {
|
|
7438
|
-
const gap = frames[i].tMs - frames[i - 1].tMs;
|
|
7439
|
-
if (gap >= 400) {
|
|
7440
|
-
freezes.push({
|
|
7441
|
-
from: +(frames[i - 1].tMs / 1e3).toFixed(2),
|
|
7442
|
-
to: +(frames[i].tMs / 1e3).toFixed(2),
|
|
7443
|
-
ms: gap
|
|
7444
|
-
});
|
|
7445
|
-
}
|
|
7446
|
-
}
|
|
7447
|
-
const frozenMs = freezes.reduce((sum, f) => sum + f.ms, 0);
|
|
7448
|
-
const freezePct = durationMs > 0 ? Math.round(frozenMs / durationMs * 100) : 0;
|
|
7449
|
-
const dead = deadTime(stepSpans, frames, events, durationMs);
|
|
7450
|
-
if (deadWarns(dead)) log(` WARNING: ${deadLine(dead)}`);
|
|
7451
|
-
const pace = paceReport(paces);
|
|
6453
|
+
);
|
|
7452
6454
|
if (!dry) {
|
|
7453
|
-
await writeJson(paths.cursor, events);
|
|
7454
|
-
await writeJson(paths.meta, meta, true);
|
|
7455
|
-
await writeJson(paths.framesIndex, frames);
|
|
7456
|
-
await writeJson(paths.actions,
|
|
7457
|
-
log(` ${paceLine(pace)}`);
|
|
6455
|
+
await writeJson(paths.cursor, result.events);
|
|
6456
|
+
await writeJson(paths.meta, result.meta, true);
|
|
6457
|
+
await writeJson(paths.framesIndex, result.frames);
|
|
6458
|
+
await writeJson(paths.actions, result.actions, true);
|
|
7458
6459
|
}
|
|
7459
|
-
return
|
|
7460
|
-
events,
|
|
7461
|
-
frames,
|
|
7462
|
-
meta,
|
|
7463
|
-
pace,
|
|
7464
|
-
skipped,
|
|
7465
|
-
navTimeout,
|
|
7466
|
-
wall,
|
|
7467
|
-
exposures: exposureLog.exposures(),
|
|
7468
|
-
masks: exposureLog.masks(masks),
|
|
7469
|
-
freezes,
|
|
7470
|
-
freezePct,
|
|
7471
|
-
dead,
|
|
7472
|
-
capped
|
|
7473
|
-
};
|
|
6460
|
+
return result;
|
|
7474
6461
|
}
|
|
7475
6462
|
|
|
6463
|
+
// src/plugin/wall.ts
|
|
6464
|
+
import {
|
|
6465
|
+
WALL_PROBE,
|
|
6466
|
+
WallError,
|
|
6467
|
+
isIdpHost,
|
|
6468
|
+
isSignInPath,
|
|
6469
|
+
redirectedAway,
|
|
6470
|
+
wallVerdict
|
|
6471
|
+
} from "@vosjs/render-core/record";
|
|
6472
|
+
|
|
6473
|
+
// src/plugin/exposure.ts
|
|
6474
|
+
import {
|
|
6475
|
+
EXPOSURE_ADVICE,
|
|
6476
|
+
EXPOSURE_MATCHERS_SRC,
|
|
6477
|
+
EXPOSURE_PROBE,
|
|
6478
|
+
ExposureLog,
|
|
6479
|
+
exposureLine,
|
|
6480
|
+
maskInitScript
|
|
6481
|
+
} from "@vosjs/render-core/record";
|
|
6482
|
+
|
|
6483
|
+
// src/plugin/setup.ts
|
|
6484
|
+
import {
|
|
6485
|
+
SetupEnvError,
|
|
6486
|
+
SetupError,
|
|
6487
|
+
parseHeaders,
|
|
6488
|
+
resolveSetupText,
|
|
6489
|
+
runSetup,
|
|
6490
|
+
setupEnvNames,
|
|
6491
|
+
validateSetup
|
|
6492
|
+
} from "@vosjs/render-core/record";
|
|
6493
|
+
|
|
7476
6494
|
// src/plugin/session.ts
|
|
7477
6495
|
import { spawn } from "child_process";
|
|
7478
6496
|
import { existsSync as existsSync12, mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync, statSync as statSync3 } from "fs";
|
|
@@ -7616,47 +6634,47 @@ function looksLikeStorageState(value) {
|
|
|
7616
6634
|
return Array.isArray(v.cookies) && Array.isArray(v.origins);
|
|
7617
6635
|
}
|
|
7618
6636
|
|
|
6637
|
+
// src/plugin/recordingCap.ts
|
|
6638
|
+
import {
|
|
6639
|
+
capReached,
|
|
6640
|
+
cappedLine,
|
|
6641
|
+
clampWait,
|
|
6642
|
+
formatDurationCap
|
|
6643
|
+
} from "@vosjs/render-core/record";
|
|
6644
|
+
var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
|
|
6645
|
+
function parseHostedCap(body) {
|
|
6646
|
+
const limits = body && typeof body === "object" ? body.limits : void 0;
|
|
6647
|
+
const cap = limits?.recordingMaxSeconds;
|
|
6648
|
+
return typeof cap === "number" && Number.isInteger(cap) && cap > 0 ? cap : null;
|
|
6649
|
+
}
|
|
6650
|
+
async function hostedRecordingCap(origin, key, fetchImpl = fetch) {
|
|
6651
|
+
const ctl = new AbortController();
|
|
6652
|
+
const timer = setTimeout(() => ctl.abort(), 2e3);
|
|
6653
|
+
try {
|
|
6654
|
+
const res = await fetchImpl(`${origin.replace(/\/+$/, "")}/api/limits`, {
|
|
6655
|
+
signal: ctl.signal,
|
|
6656
|
+
...key ? { headers: { authorization: `Bearer ${key}` } } : {}
|
|
6657
|
+
});
|
|
6658
|
+
if (!res.ok) return null;
|
|
6659
|
+
return parseHostedCap(await res.json());
|
|
6660
|
+
} catch {
|
|
6661
|
+
return null;
|
|
6662
|
+
} finally {
|
|
6663
|
+
clearTimeout(timer);
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
function defaultMaxDurationSeconds() {
|
|
6667
|
+
return HOSTED_RECORDING_CAP_SECONDS;
|
|
6668
|
+
}
|
|
6669
|
+
|
|
7619
6670
|
// src/plugin/encode.ts
|
|
7620
|
-
|
|
7621
|
-
var ENCODE_HTML =
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
const W = meta.captureWidth ?? meta.width, H = meta.captureHeight ?? meta.height
|
|
7628
|
-
const FPS = 30
|
|
7629
|
-
const durSec = meta.durationMs / 1000
|
|
7630
|
-
const cv = document.createElement('canvas')
|
|
7631
|
-
cv.width = W; cv.height = H
|
|
7632
|
-
const c2 = cv.getContext('2d')
|
|
7633
|
-
const output = new MB.Output({ format: new MB.WebMOutputFormat(), target: new MB.BufferTarget() })
|
|
7634
|
-
const src = new MB.CanvasSource(cv, { codec: 'vp9', bitrate: 8_000_000 })
|
|
7635
|
-
output.addVideoTrack(src, { frameRate: FPS })
|
|
7636
|
-
await output.start()
|
|
7637
|
-
let fi = -1
|
|
7638
|
-
const total = Math.ceil(durSec * FPS)
|
|
7639
|
-
for (let i = 0; i < total; i++) {
|
|
7640
|
-
const t = i / FPS
|
|
7641
|
-
let next = fi
|
|
7642
|
-
while (next + 1 < frames.length && frames[next + 1].tMs <= t * 1000) next++
|
|
7643
|
-
if (next < 0) next = 0
|
|
7644
|
-
if (next !== fi) {
|
|
7645
|
-
fi = next
|
|
7646
|
-
const blob = await (await fetch('/frames/' + frames[fi].file)).blob()
|
|
7647
|
-
const bmp = await createImageBitmap(blob)
|
|
7648
|
-
c2.drawImage(bmp, 0, 0, W, H)
|
|
7649
|
-
bmp.close()
|
|
7650
|
-
}
|
|
7651
|
-
await src.add(t, 1 / FPS)
|
|
7652
|
-
window.__progress = i / total
|
|
7653
|
-
}
|
|
7654
|
-
await output.finalize()
|
|
7655
|
-
const buf = output.target.buffer
|
|
7656
|
-
await fetch('/save?name=${RECORDING_NAME}', { method: 'POST', body: buf })
|
|
7657
|
-
window.__done = { ok: true, bytes: buf.byteLength }
|
|
7658
|
-
} catch (e) { window.__error = String(e && e.stack || e) }
|
|
7659
|
-
</script></body></html>`;
|
|
6671
|
+
import { buildEncodePage } from "@vosjs/render-core/record";
|
|
6672
|
+
var ENCODE_HTML = buildEncodePage({
|
|
6673
|
+
framesIndexUrl: "/frames.json",
|
|
6674
|
+
metaUrl: "/meta.json",
|
|
6675
|
+
framesBaseUrl: "/frames/",
|
|
6676
|
+
save: { method: "POST", url: `/save?name=${RECORDING_NAME}` }
|
|
6677
|
+
});
|
|
7660
6678
|
async function encodeRecording(browser, takeDir, onProgress) {
|
|
7661
6679
|
const server = await startTakeServer(takeDir, { "/encode.html": ENCODE_HTML });
|
|
7662
6680
|
const page = await browser.newPage();
|
|
@@ -8112,7 +7130,7 @@ async function planTake(dir, opts = {}) {
|
|
|
8112
7130
|
|
|
8113
7131
|
// src/plugin/sync.ts
|
|
8114
7132
|
import { createHash } from "crypto";
|
|
8115
|
-
import { existsSync as existsSync14, readFileSync as
|
|
7133
|
+
import { existsSync as existsSync14, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "fs";
|
|
8116
7134
|
import { readFile as readFile13 } from "fs/promises";
|
|
8117
7135
|
import { createInterface } from "readline/promises";
|
|
8118
7136
|
import { basename as basename3, join as join17 } from "path";
|
|
@@ -8616,7 +7634,7 @@ function storageStateInDir(dir) {
|
|
|
8616
7634
|
if (!f.endsWith(".json") || f === "doc.json" || f === "meta.json" || f === "actions.json" || f === "cursor.json" || f === "vos.json" || f === "doc.prev.json")
|
|
8617
7635
|
continue;
|
|
8618
7636
|
try {
|
|
8619
|
-
const v = JSON.parse(
|
|
7637
|
+
const v = JSON.parse(readFileSync3(join17(dir, f), "utf8"));
|
|
8620
7638
|
if (looksLikeStorageState(v)) return join17(dir, f);
|
|
8621
7639
|
} catch {
|
|
8622
7640
|
}
|
|
@@ -9723,7 +8741,7 @@ async function writeProgramDoc(dir, hosted) {
|
|
|
9723
8741
|
}
|
|
9724
8742
|
|
|
9725
8743
|
// src/plugin/asset.ts
|
|
9726
|
-
import { readFileSync as
|
|
8744
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
9727
8745
|
import { basename as basename5, extname as extname2 } from "path";
|
|
9728
8746
|
var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
9729
8747
|
var MIME_BY_EXT = {
|
|
@@ -9777,7 +8795,7 @@ async function cmdPush(argv) {
|
|
|
9777
8795
|
for (const file of positionals) {
|
|
9778
8796
|
const name = basename5(file);
|
|
9779
8797
|
const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
|
|
9780
|
-
const body =
|
|
8798
|
+
const body = readFileSync4(file);
|
|
9781
8799
|
const form = new FormData();
|
|
9782
8800
|
form.set("file", new Blob([body], { type: mime }), name);
|
|
9783
8801
|
if (folder) form.set("folderId", folder.id);
|
|
@@ -9831,7 +8849,7 @@ async function cmdAsset(argv) {
|
|
|
9831
8849
|
}
|
|
9832
8850
|
|
|
9833
8851
|
// src/plugin/recipe.ts
|
|
9834
|
-
import { readFileSync as
|
|
8852
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
9835
8853
|
import { basename as basename6 } from "path";
|
|
9836
8854
|
var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
9837
8855
|
var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
|
|
@@ -9851,7 +8869,7 @@ async function cmdPush2(argv) {
|
|
|
9851
8869
|
throw new UsageError(USAGE);
|
|
9852
8870
|
}
|
|
9853
8871
|
const filename = recipeFilename(file);
|
|
9854
|
-
const body =
|
|
8872
|
+
const body = readFileSync5(file);
|
|
9855
8873
|
const r = createReporter(flags.json === true);
|
|
9856
8874
|
const origin = platformOrigin({
|
|
9857
8875
|
origin: strFlag(flags, "origin"),
|
|
@@ -10294,6 +9312,103 @@ async function cmdBrand(argv) {
|
|
|
10294
9312
|
return 0;
|
|
10295
9313
|
}
|
|
10296
9314
|
|
|
9315
|
+
// src/plugin/hostedRecord.ts
|
|
9316
|
+
import { setupEnvNames as setupEnvNames2 } from "@vosjs/render-core/record";
|
|
9317
|
+
var POLL_MS = 3e3;
|
|
9318
|
+
var DEFAULT_TIMEOUT_MS = 20 * 60 * 1e3;
|
|
9319
|
+
function hostedEnv(actions, shell) {
|
|
9320
|
+
const env = {};
|
|
9321
|
+
const missing = [];
|
|
9322
|
+
for (const name of setupEnvNames2(actions.setup)) {
|
|
9323
|
+
const v = shell[name];
|
|
9324
|
+
if (v === void 0 || v === "") missing.push(name);
|
|
9325
|
+
else env[name] = v;
|
|
9326
|
+
}
|
|
9327
|
+
return { env, missing };
|
|
9328
|
+
}
|
|
9329
|
+
function isWallError(error) {
|
|
9330
|
+
return !!error && /^(\[\w+\] )?The page is behind a sign-in/.test(error);
|
|
9331
|
+
}
|
|
9332
|
+
async function recordHosted(input) {
|
|
9333
|
+
const sleep = input.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
9334
|
+
const posted = await apiJson(input.origin, "/api/record/jobs", {
|
|
9335
|
+
method: "POST",
|
|
9336
|
+
key: input.key,
|
|
9337
|
+
body: {
|
|
9338
|
+
url: input.url,
|
|
9339
|
+
actions: input.actions,
|
|
9340
|
+
viewport: input.actions.viewport,
|
|
9341
|
+
...Object.keys(input.headers).length ? { headers: input.headers } : {},
|
|
9342
|
+
...Object.keys(input.env).length ? { env: input.env } : {},
|
|
9343
|
+
...input.title ? { title: input.title } : {},
|
|
9344
|
+
...input.folderId ? { folderId: input.folderId } : {}
|
|
9345
|
+
}
|
|
9346
|
+
});
|
|
9347
|
+
if (posted.status !== 202) {
|
|
9348
|
+
return {
|
|
9349
|
+
ok: false,
|
|
9350
|
+
wall: false,
|
|
9351
|
+
error: apiError("record on vos.so", posted),
|
|
9352
|
+
vosId: null,
|
|
9353
|
+
jobId: null
|
|
9354
|
+
};
|
|
9355
|
+
}
|
|
9356
|
+
const job = posted.body.job;
|
|
9357
|
+
const vos = posted.body.vos;
|
|
9358
|
+
const cap = posted.body.cap;
|
|
9359
|
+
input.log(
|
|
9360
|
+
`queued on vos.so as job ${job.id.slice(0, 8)} (vos ${job.vosId}${cap?.seconds ? `, cap ${cap.seconds}s` : ""})`
|
|
9361
|
+
);
|
|
9362
|
+
const deadline = Date.now() + (input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
9363
|
+
let lastStatus = "queued";
|
|
9364
|
+
while (Date.now() < deadline) {
|
|
9365
|
+
await sleep(POLL_MS);
|
|
9366
|
+
const polled = await apiJson(input.origin, `/api/record/jobs/${job.id}`, {
|
|
9367
|
+
key: input.key
|
|
9368
|
+
});
|
|
9369
|
+
if (polled.status !== 200) {
|
|
9370
|
+
return {
|
|
9371
|
+
ok: false,
|
|
9372
|
+
wall: false,
|
|
9373
|
+
error: apiError(`poll record job ${job.id}`, polled),
|
|
9374
|
+
vosId: job.vosId,
|
|
9375
|
+
jobId: job.id
|
|
9376
|
+
};
|
|
9377
|
+
}
|
|
9378
|
+
const state = polled.body.job;
|
|
9379
|
+
if (state.status !== lastStatus) {
|
|
9380
|
+
input.log(`${state.status}\u2026`);
|
|
9381
|
+
lastStatus = state.status;
|
|
9382
|
+
}
|
|
9383
|
+
if (state.status === "completed") {
|
|
9384
|
+
return {
|
|
9385
|
+
ok: true,
|
|
9386
|
+
vosId: state.vosId,
|
|
9387
|
+
jobId: state.id,
|
|
9388
|
+
job: state,
|
|
9389
|
+
studio: vos?.studio ?? `${input.origin}/studio?vos=${state.vosId}`
|
|
9390
|
+
};
|
|
9391
|
+
}
|
|
9392
|
+
if (state.status === "failed" || state.status === "canceled") {
|
|
9393
|
+
const error = state.error ?? (state.status === "canceled" ? "the record job was canceled" : "the record job failed");
|
|
9394
|
+
return {
|
|
9395
|
+
ok: false,
|
|
9396
|
+
wall: isWallError(error),
|
|
9397
|
+
error,
|
|
9398
|
+
vosId: state.vosId,
|
|
9399
|
+
jobId: state.id
|
|
9400
|
+
};
|
|
9401
|
+
}
|
|
9402
|
+
}
|
|
9403
|
+
return {
|
|
9404
|
+
ok: false,
|
|
9405
|
+
wall: false,
|
|
9406
|
+
error: `the record job ${job.id} did not finish within ${Math.round((input.timeoutMs ?? DEFAULT_TIMEOUT_MS) / 6e4)} min; poll ${input.origin}/api/record/jobs/${job.id}`,
|
|
9407
|
+
vosId: job.vosId,
|
|
9408
|
+
jobId: job.id
|
|
9409
|
+
};
|
|
9410
|
+
}
|
|
9411
|
+
|
|
10297
9412
|
// src/plugin/agentBrowser.ts
|
|
10298
9413
|
import { writeFile as writeFile13 } from "fs/promises";
|
|
10299
9414
|
import { resolve as resolve8 } from "path";
|
|
@@ -10814,7 +9929,7 @@ function summary(result) {
|
|
|
10814
9929
|
async function cmdActionsScript(input, flags) {
|
|
10815
9930
|
const r = createReporter(flags.json === true);
|
|
10816
9931
|
const { readFile: readFile16 } = await import("fs/promises");
|
|
10817
|
-
const { shotList, formatShotList } = await import("./shotList-
|
|
9932
|
+
const { shotList, formatShotList } = await import("./shotList-IYFDHSIK.js");
|
|
10818
9933
|
const raw = JSON.parse(await readFile16(resolve8(input), "utf8"));
|
|
10819
9934
|
const errors = validateActions(raw);
|
|
10820
9935
|
if (errors.length) throw new UsageError(`${input}:
|
|
@@ -10830,7 +9945,7 @@ import {
|
|
|
10830
9945
|
cpSync,
|
|
10831
9946
|
existsSync as existsSync17,
|
|
10832
9947
|
mkdirSync as mkdirSync3,
|
|
10833
|
-
readFileSync as
|
|
9948
|
+
readFileSync as readFileSync7,
|
|
10834
9949
|
rmSync as rmSync2,
|
|
10835
9950
|
writeFileSync as writeFileSync3
|
|
10836
9951
|
} from "fs";
|
|
@@ -10839,7 +9954,7 @@ import { join as join20 } from "path";
|
|
|
10839
9954
|
import { chromium as chromium2 } from "playwright";
|
|
10840
9955
|
|
|
10841
9956
|
// src/plugin/machine.ts
|
|
10842
|
-
import { existsSync as existsSync16, readFileSync as
|
|
9957
|
+
import { existsSync as existsSync16, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
10843
9958
|
import { dirname as dirname5, join as join19 } from "path";
|
|
10844
9959
|
var AGENT_NAMES = [
|
|
10845
9960
|
"claude",
|
|
@@ -11056,7 +10171,7 @@ function bundledSkillsDir(from) {
|
|
|
11056
10171
|
const pkg = join19(dir, "package.json");
|
|
11057
10172
|
if (existsSync16(pkg)) {
|
|
11058
10173
|
try {
|
|
11059
|
-
const name = JSON.parse(
|
|
10174
|
+
const name = JSON.parse(readFileSync6(pkg, "utf8")).name;
|
|
11060
10175
|
if (name === "@vosjs/cli") {
|
|
11061
10176
|
const skills = join19(dir, "skills");
|
|
11062
10177
|
return existsSync16(skills) && statSync4(skills).isDirectory() ? skills : null;
|
|
@@ -11238,7 +10353,7 @@ async function cmdSetup(argv) {
|
|
|
11238
10353
|
let rules = null;
|
|
11239
10354
|
if (!noRules) {
|
|
11240
10355
|
const file = rulesFileFor(root);
|
|
11241
|
-
const existing = existsSync17(file) ?
|
|
10356
|
+
const existing = existsSync17(file) ? readFileSync7(file, "utf8") : null;
|
|
11242
10357
|
const { text, changed } = applyRulesBlock(existing);
|
|
11243
10358
|
if (changed) writeFileSync3(file, text);
|
|
11244
10359
|
rules = { file, changed };
|
|
@@ -11389,7 +10504,8 @@ var BOOLEAN_FLAGS6 = /* @__PURE__ */ new Set([
|
|
|
11389
10504
|
"leader",
|
|
11390
10505
|
"keep-frames",
|
|
11391
10506
|
"dry-run",
|
|
11392
|
-
"allow-wall"
|
|
10507
|
+
"allow-wall",
|
|
10508
|
+
"hosted"
|
|
11393
10509
|
]);
|
|
11394
10510
|
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
|
|
11395
10511
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
@@ -11397,6 +10513,8 @@ var HELP = `vos \u2014 record a browser flow, plan effects, render a product vid
|
|
|
11397
10513
|
Take pipeline
|
|
11398
10514
|
vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
|
|
11399
10515
|
vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
|
|
10516
|
+
vos record --hosted --actions actions.json [--url <url>] [--out take] [--title "\u2026"] [--folder <id>] [--header name=value]... [--json]
|
|
10517
|
+
the take recorded on vos.so's fleet (no browser here): lands as a private vos with its doc and digest, then fetched into --out with its media
|
|
11400
10518
|
vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
|
|
11401
10519
|
vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
|
|
11402
10520
|
vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
|
|
@@ -11811,12 +10929,102 @@ async function cmdRecord(argv) {
|
|
|
11811
10929
|
if (!url)
|
|
11812
10930
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
11813
10931
|
const outDir = resolve9(strFlag(flags, "out") ?? "take");
|
|
11814
|
-
const backdrop = await takeBackdrop(flags, r);
|
|
11815
|
-
const maxDurationSeconds = await maxDuration(flags, r);
|
|
11816
10932
|
const storageState = takeStorageState(flags);
|
|
11817
10933
|
const sessionName = takeSessionName(flags, storageState);
|
|
11818
10934
|
const browserArgs = takeBrowserArgs(multi);
|
|
11819
10935
|
const headers = takeHeaders(multi);
|
|
10936
|
+
if (flags.hosted === true) {
|
|
10937
|
+
if (flags["dry-run"] === true)
|
|
10938
|
+
throw new UsageError(
|
|
10939
|
+
"--dry-run rehearses on this machine; drop --hosted to rehearse, or --dry-run to record on vos.so"
|
|
10940
|
+
);
|
|
10941
|
+
if (storageState || sessionName)
|
|
10942
|
+
throw new UsageError(
|
|
10943
|
+
'a session never reaches vos.so: --hosted takes no --session or --storage-state. Put the sign-in in the setup steps with { "env": "NAME" }, or record here with vos record.'
|
|
10944
|
+
);
|
|
10945
|
+
const origin = platformOrigin({
|
|
10946
|
+
origin: strFlag(flags, "origin"),
|
|
10947
|
+
api: strFlag(flags, "api")
|
|
10948
|
+
});
|
|
10949
|
+
const key = resolveCredential(strFlag(flags, "key"));
|
|
10950
|
+
if (!key)
|
|
10951
|
+
throw new UsageError(
|
|
10952
|
+
"recording on vos.so needs a credential: vos login, or VOS_API_KEY (mint a key at https://vos.so/app/api)"
|
|
10953
|
+
);
|
|
10954
|
+
const { env, missing } = hostedEnv(actions, process.env);
|
|
10955
|
+
if (missing.length)
|
|
10956
|
+
throw new UsageError(
|
|
10957
|
+
`the setup reads ${missing.map((n) => `{ "env": "${n}" }`).join(", ")} and this shell has no ${missing.length === 1 ? "value for it" : "values for them"}; export ${missing.join(", ")} and run again (the value rides the job, and is never stored)`
|
|
10958
|
+
);
|
|
10959
|
+
r.log("recording on vos.so\u2026");
|
|
10960
|
+
r.event({ event: "phase", phase: "record" });
|
|
10961
|
+
const started = Date.now();
|
|
10962
|
+
const outcome = await recordHosted({
|
|
10963
|
+
origin,
|
|
10964
|
+
key,
|
|
10965
|
+
url,
|
|
10966
|
+
actions: { ...actions, url },
|
|
10967
|
+
headers,
|
|
10968
|
+
title: strFlag(flags, "title"),
|
|
10969
|
+
folderId: strFlag(flags, "folder"),
|
|
10970
|
+
env,
|
|
10971
|
+
log: r.log
|
|
10972
|
+
});
|
|
10973
|
+
if (!outcome.ok) {
|
|
10974
|
+
if (outcome.vosId) {
|
|
10975
|
+
const gone = await apiJson(origin, `/api/vos/${outcome.vosId}`, {
|
|
10976
|
+
method: "DELETE",
|
|
10977
|
+
key
|
|
10978
|
+
});
|
|
10979
|
+
if (gone.status === 200)
|
|
10980
|
+
r.log("the empty vos it would have landed on was removed");
|
|
10981
|
+
}
|
|
10982
|
+
r.done(
|
|
10983
|
+
{
|
|
10984
|
+
hosted: true,
|
|
10985
|
+
ok: false,
|
|
10986
|
+
wall: outcome.wall,
|
|
10987
|
+
error: outcome.error,
|
|
10988
|
+
vosId: outcome.vosId,
|
|
10989
|
+
jobId: outcome.jobId
|
|
10990
|
+
},
|
|
10991
|
+
`${outcome.wall ? "REFUSED" : "FAILED"} on vos.so: ${outcome.error}`
|
|
10992
|
+
);
|
|
10993
|
+
return outcome.wall ? EXIT_WALL : EXIT_ERROR;
|
|
10994
|
+
}
|
|
10995
|
+
r.event({ event: "phase", phase: "fetch" });
|
|
10996
|
+
r.log(`landed as ${outcome.studio}; fetching the take into ${outDir}\u2026`);
|
|
10997
|
+
const fetched = await cmdFetch([
|
|
10998
|
+
outcome.vosId,
|
|
10999
|
+
"--out",
|
|
11000
|
+
outDir,
|
|
11001
|
+
"--media",
|
|
11002
|
+
...strFlag(flags, "origin") ? ["--origin", strFlag(flags, "origin")] : [],
|
|
11003
|
+
...strFlag(flags, "key") ? ["--key", strFlag(flags, "key")] : [],
|
|
11004
|
+
...flags.json === true ? ["--json"] : []
|
|
11005
|
+
]);
|
|
11006
|
+
if (fetched !== EXIT_OK) return fetched;
|
|
11007
|
+
const t = outcome.job.telemetry ?? {};
|
|
11008
|
+
r.done(
|
|
11009
|
+
{
|
|
11010
|
+
hosted: true,
|
|
11011
|
+
ok: true,
|
|
11012
|
+
vosId: outcome.vosId,
|
|
11013
|
+
jobId: outcome.jobId,
|
|
11014
|
+
out: outDir,
|
|
11015
|
+
seconds: +((Date.now() - started) / 1e3).toFixed(1),
|
|
11016
|
+
telemetry: t
|
|
11017
|
+
},
|
|
11018
|
+
`Recorded on vos.so in ${((Date.now() - started) / 1e3).toFixed(1)}s: ${outcome.studio}
|
|
11019
|
+
${outDir}/ holds doc.json, the recording and vos.json${typeof t.durationMs === "number" ? ` (${(t.durationMs / 1e3).toFixed(1)}s of footage)` : ""}${Array.isArray(t.dead) && t.dead.length ? `
|
|
11020
|
+
DEAD TIME: ${t.dead.length} stretch(es) under a parked cursor; give those steps their ms` : ""}${Array.isArray(t.exposures) && t.exposures.length ? `
|
|
11021
|
+
EXPOSURES: ${t.exposures.length} sensitive-looking text(s) in frame; mask them in actions.json` : ""}
|
|
11022
|
+
Next: vos digest ${outDir} \u2014 look before you cut; edit doc.json; vos push ${outDir}`
|
|
11023
|
+
);
|
|
11024
|
+
return EXIT_OK;
|
|
11025
|
+
}
|
|
11026
|
+
const backdrop = await takeBackdrop(flags, r);
|
|
11027
|
+
const maxDurationSeconds = await maxDuration(flags, r);
|
|
11820
11028
|
if (flags["dry-run"] === true) {
|
|
11821
11029
|
void flags.strict;
|
|
11822
11030
|
void flags["keep-frames"];
|
|
@@ -13326,4 +12534,4 @@ export {
|
|
|
13326
12534
|
verbHelp,
|
|
13327
12535
|
run
|
|
13328
12536
|
};
|
|
13329
|
-
//# sourceMappingURL=chunk-
|
|
12537
|
+
//# sourceMappingURL=chunk-TAWRT7UM.js.map
|