nothing-browser 0.0.19 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,507 +1,518 @@
1
- // piggy/register/index.ts
2
- import { PiggyClient } from "../client";
3
- import logger from "../logger";
4
- import { routeRegistry, keepAliveSites, type RouteHandler, type BeforeMiddleware, type RouteDetail } from "../server";
5
- import { randomDelay, humanTypeSequence } from "../human";
6
- import { buildRespondScript, buildModifyResponseScript } from "../intercept/scripts";
7
- import { storeRecord } from "../store";
8
- import { TabPool } from "../pool";
9
-
10
- let globalClient: PiggyClient | null = null;
11
- export let humanMode = false;
12
-
13
- export function setClient(c: PiggyClient | null) { globalClient = c; }
14
- export function setHumanMode(v: boolean) { humanMode = v; }
15
-
16
- async function retry<T>(label: string, fn: () => Promise<T>, retries = 2, backoff = 150): Promise<T> {
17
- let last!: Error;
18
- for (let i = 0; i <= retries; i++) {
19
- try { return await fn(); } catch (e: any) {
20
- last = e;
21
- if (i < retries) {
22
- logger.warn(`[${label}] retry ${i + 1}/${retries}: ${e.message}`);
23
- await new Promise(r => setTimeout(r, backoff * (i + 1)));
24
- }
25
- }
26
- }
27
- throw last;
28
- }
29
-
30
- export function createSiteObject(
31
- name: string,
32
- registeredUrl: string,
33
- client: PiggyClient,
34
- tabId: string,
35
- pool?: TabPool
36
- ) {
37
- let _currentUrl: string = registeredUrl;
38
- let _modifyRuleCounter = 0;
39
-
40
- // ── helpers ────────────────────────────────────────────────────────────────
41
- // If pool exists, run fn with a pool tab. Otherwise use the fixed tabId.
42
- function withTab<T>(fn: (t: string) => Promise<T>): Promise<T> {
43
- return pool ? pool.withTab(fn) : fn(tabId);
44
- }
45
-
46
- const _eventListeners = new Map<string, Set<(data: any) => void>>();
47
-
48
- const _unsubNavigate = client.onEvent("navigate", tabId, (url: string) => {
49
- _currentUrl = url;
50
- const handlers = _eventListeners.get("navigate");
51
- if (handlers) {
52
- for (const h of handlers) {
53
- try { h(url); } catch (e) { logger.error(`[${name}] navigate handler error: ${e}`); }
54
- }
55
- }
56
- });
57
-
58
- const withErrScreen = async <T>(fn: () => Promise<T>, label: string): Promise<T> => {
59
- try { return await fn(); } catch (err: any) {
60
- const p = `./error-${name}-${Date.now()}.png`;
61
- try { await client.screenshot(p, tabId); logger.error(`[${name}] ${label} failed → ${p}`); }
62
- catch { logger.error(`[${name}] ${label} failed (no screenshot)`); }
63
- throw err;
64
- }
65
- };
66
-
67
- const site: any = {
68
- _name: name,
69
- _tabId: tabId,
70
- _pool: pool ?? null,
71
-
72
- // ── Pool stats ────────────────────────────────────────────────────────────
73
- poolStats: () => pool?.stats ?? null,
74
-
75
- // ── Navigation ────────────────────────────────────────────────────────────
76
- navigate: (url?: string, opts?: { retries?: number }) => {
77
- const target = url ?? registeredUrl;
78
- return withTab(t =>
79
- retry(name, async () => {
80
- logger.network(`[${name}] navigating → ${target}`);
81
- await client.navigate(target, t);
82
- _currentUrl = target;
83
- }, opts?.retries ?? 2)
84
- );
85
- },
86
-
87
- reload: () => withTab(t => client.reload(t)),
88
- goBack: () => withTab(t => client.goBack(t)),
89
- goForward: () => withTab(t => client.goForward(t)),
90
- waitForNavigation: () => withTab(t => client.waitForNavigation(t)),
91
-
92
- title: () => withTab(async t => {
93
- const title = await client.getTitle(t);
94
- logger.info(`[${name}] title: ${title}`);
95
- return title;
96
- }),
97
-
98
- url: () => _currentUrl,
99
- content: () => withTab(t => client.content(t)),
100
-
101
- wait: (ms: number) => {
102
- const actual = humanMode ? ms + Math.floor(Math.random() * 600) - 300 : ms;
103
- return new Promise<void>(r => setTimeout(r, Math.max(0, actual)));
104
- },
105
-
106
- waitForSelector: (selector: string, timeout = 30000) =>
107
- withTab(t => {
108
- logger.debug(`[${name}] waitForSelector: ${selector}`);
109
- return client.waitForSelector(selector, timeout, t);
110
- }),
111
-
112
- waitForVisible: (selector: string, timeout = 30000) =>
113
- withTab(t => client.waitForSelector(selector, timeout, t)),
114
-
115
- waitForResponse: (pattern: string, timeout = 30000) =>
116
- withTab(t => client.waitForResponse(pattern, timeout, t)),
117
-
118
- // ── Init Script ───────────────────────────────────────────────────────────
119
- addInitScript: async (js: string | (() => void)) => {
120
- const code = typeof js === "function" ? `(${js.toString()})();` : js;
121
- await withTab(t => client.addInitScript(code, t));
122
- logger.success(`[${name}] init script added`);
123
- return site;
124
- },
125
-
126
- // ── Events ────────────────────────────────────────────────────────────────
127
- on: (event: string, handler: (data: any) => void): (() => void) => {
128
- if (!_eventListeners.has(event)) _eventListeners.set(event, new Set());
129
- _eventListeners.get(event)!.add(handler);
130
- logger.debug(`[${name}] on('${event}') registered`);
131
- return () => {
132
- _eventListeners.get(event)?.delete(handler);
133
- logger.debug(`[${name}] on('${event}') unsubscribed`);
134
- };
135
- },
136
-
137
- off: (event: string, handler: (data: any) => void) => {
138
- _eventListeners.get(event)?.delete(handler);
139
- },
140
-
141
- // ── Interactions ──────────────────────────────────────────────────────────
142
- click: (selector: string, opts?: { retries?: number; timeout?: number }) =>
143
- withErrScreen(() =>
144
- withTab(t =>
145
- retry(name, async () => {
146
- if (humanMode) await randomDelay(80, 220);
147
- await client.waitForSelector(selector, opts?.timeout ?? 15000, t);
148
- const ok = await client.click(selector, t);
149
- if (!ok) throw new Error(`click failed: ${selector}`);
150
- logger.success(`[${name}] clicked: ${selector}`);
151
- return ok;
152
- }, opts?.retries ?? 2)
153
- ),
154
- `click(${selector})`
155
- ),
156
-
157
- doubleClick: (selector: string) =>
158
- withErrScreen(() =>
159
- withTab(async t => {
160
- if (humanMode) await randomDelay(80, 200);
161
- return client.doubleClick(selector, t);
162
- }),
163
- `dblclick(${selector})`
164
- ),
165
-
166
- hover: (selector: string) =>
167
- withErrScreen(() =>
168
- withTab(async t => {
169
- if (humanMode) await randomDelay(50, 150);
170
- return client.hover(selector, t);
171
- }),
172
- `hover(${selector})`
173
- ),
174
-
175
- type: (selector: string, text: string, opts?: { delay?: number; retries?: number; fact?: boolean; wpm?: number }) =>
176
- withErrScreen(() =>
177
- withTab(async t => {
178
- await client.waitForSelector(selector, 15000, t);
179
- if (humanMode && !opts?.fact) {
180
- const seq = humanTypeSequence(text);
181
- let current = "";
182
- for (const action of seq) {
183
- if (action === "BACKSPACE") current = current.slice(0, -1);
184
- else current += action;
185
- await client.type(selector, current, t);
186
- const wpm = opts?.wpm ?? 120;
187
- const msPerChar = Math.round(60000 / (wpm * 5));
188
- await randomDelay(msPerChar * 0.5, msPerChar * 1.8);
189
- }
190
- } else if (opts?.delay) {
191
- for (const ch of text) {
192
- await client.type(selector, ch, t);
193
- await new Promise(r => setTimeout(r, opts.delay));
194
- }
195
- } else {
196
- await client.type(selector, text, t);
197
- }
198
- logger.success(`[${name}] typed into: ${selector}`);
199
- return true;
200
- }),
201
- `type(${selector})`
202
- ),
203
-
204
- select: (selector: string, value: string) => withTab(t => client.select(selector, value, t)),
205
-
206
- evaluate: (js: string | (() => any), ...args: any[]) => {
207
- const code = typeof js === "function"
208
- ? `(${js.toString()})(${args.map(a => JSON.stringify(a)).join(",")})`
209
- : js;
210
- return withTab(t => client.evaluate(code, t));
211
- },
212
-
213
- keyboard: {
214
- press: (key: string) => withTab(t => client.keyPress(key, t)),
215
- combo: (combo: string) => withTab(t => client.keyCombo(combo, t)),
216
- },
217
-
218
- mouse: {
219
- move: (x: number, y: number) => withTab(t => client.mouseMove(x, y, t)),
220
- drag: (from: { x: number; y: number }, to: { x: number; y: number }) =>
221
- withTab(t => client.mouseDrag(from, to, t)),
222
- },
223
-
224
- scroll: {
225
- to: (selector: string) => withTab(t => client.scrollTo(selector, t)),
226
- by: (px: number) => withTab(async t => {
227
- if (humanMode) {
228
- const steps = Math.ceil(Math.abs(px) / 120);
229
- const chunk = px / steps;
230
- for (let i = 0; i < steps; i++) {
231
- await client.scrollBy(chunk, t);
232
- await randomDelay(30, 80);
233
- }
234
- } else {
235
- await client.scrollBy(px, t);
236
- }
237
- }) as Promise<void>,
238
- },
239
-
240
- // ── Fetch ─────────────────────────────────────────────────────────────────
241
- fetchText: (selector: string) => withTab(t => client.fetchText(selector, t)),
242
-
243
- fetchLinks: async (selector: string) => {
244
- const links = await withTab(t => client.fetchLinks(selector, t));
245
- logger.info(`[${name}] fetchLinks(${selector}): ${links.length}`);
246
- return links;
247
- },
248
-
249
- fetchImages: async (selector: string) => {
250
- const imgs = await withTab(t => client.fetchImages(selector, t));
251
- logger.info(`[${name}] fetchImages(${selector}): ${imgs.length}`);
252
- return imgs;
253
- },
254
-
255
- search: {
256
- css: (query: string) => withTab(t => client.searchCss(query, t)),
257
- id: (query: string) => withTab(t => client.searchId(query, t)),
258
- },
259
-
260
- // ── Screenshot / PDF ──────────────────────────────────────────────────────
261
- screenshot: async (filePath?: string) => {
262
- const r = await withTab(t => client.screenshot(filePath, t));
263
- logger.success(`[${name}] screenshot → ${filePath ?? "base64"}`);
264
- return r;
265
- },
266
-
267
- pdf: async (filePath?: string) => {
268
- const r = await withTab(t => client.pdf(filePath, t));
269
- logger.success(`[${name}] pdf → ${filePath ?? "base64"}`);
270
- return r;
271
- },
272
-
273
- blockImages: () => withTab(async t => { await client.blockImages(t); logger.info(`[${name}] images blocked`); }),
274
- unblockImages: () => withTab(async t => { await client.unblockImages(t); logger.info(`[${name}] images unblocked`); }),
275
-
276
- // ── Cookies ───────────────────────────────────────────────────────────────
277
- cookies: {
278
- set: async (cookieName: string, value: string, domain: string, path = "/") => {
279
- await withTab(t => client.setCookie(cookieName, value, domain, path, t));
280
- logger.info(`[${name}] cookie set: ${cookieName} @ ${domain}`);
281
- },
282
- get: (cookieName: string) => withTab(t => client.getCookie(cookieName, t)),
283
- delete: async (cookieName: string) => {
284
- await withTab(t => client.deleteCookie(cookieName, t));
285
- logger.info(`[${name}] cookie deleted: ${cookieName}`);
286
- },
287
- list: () => withTab(t => client.listCookies(t)),
288
- },
289
-
290
- // ── Interception ──────────────────────────────────────────────────────────
291
- intercept: {
292
- block: async (pattern: string) => {
293
- await withTab(t => client.addInterceptRule("block", pattern, {}, t));
294
- logger.info(`[${name}] intercept block: ${pattern}`);
295
- },
296
-
297
- redirect: async (pattern: string, redirectUrl: string) => {
298
- await withTab(t => client.addInterceptRule("redirect", pattern, { redirectUrl }, t));
299
- logger.info(`[${name}] intercept redirect: ${pattern} → ${redirectUrl}`);
300
- },
301
-
302
- headers: async (pattern: string, headers: Record<string, string>) => {
303
- await withTab(t => client.addInterceptRule("modifyHeaders", pattern, { headers }, t));
304
- logger.info(`[${name}] intercept modifyHeaders: ${pattern}`);
305
- },
306
-
307
- respond: async (
308
- pattern: string,
309
- handlerOrResponse:
310
- | { status?: number; contentType?: string; body: string }
311
- | ((req: { url: string; method: string }) => { status?: number; contentType?: string; body: string })
312
- ) => {
313
- const isStatic = typeof handlerOrResponse === "object";
314
-
315
- if (!isStatic) {
316
- const fnName = `__piggy_respond_${name}_${++_modifyRuleCounter}__`;
317
- await client.exposeFunction(fnName, async (req: { url: string; method: string }) => {
318
- try {
319
- const result = (handlerOrResponse as Function)(req);
320
- return { success: true, result: { status: result.status ?? 200, contentType: result.contentType ?? "application/json", body: result.body ?? "" } };
321
- } catch (e: any) {
322
- return { success: false, error: e.message };
323
- }
324
- }, tabId);
325
-
326
- const dynamicScript = `
327
- (function() {
328
- 'use strict';
329
- if (!window.__PIGGY_DYNAMIC_RESPOND__) window.__PIGGY_DYNAMIC_RESPOND__ = [];
330
- window.__PIGGY_DYNAMIC_RESPOND__.push({ pattern: ${JSON.stringify(pattern)}, fn: ${JSON.stringify(fnName)} });
331
- function matchUrl(url, pattern) { try { return url.includes(pattern) || new RegExp(pattern).test(url); } catch { return url.includes(pattern); } }
332
- if (window.__PIGGY_DYN_INSTALLED__) return;
333
- window.__PIGGY_DYN_INSTALLED__ = true;
334
- const _origFetch = window.fetch;
335
- window.fetch = async function(input, init) {
336
- const url = typeof input === 'string' ? input : (input?.url ?? String(input));
337
- const method = (init?.method ?? 'GET').toUpperCase();
338
- const rules = window.__PIGGY_DYNAMIC_RESPOND__ || [];
339
- for (const rule of rules) {
340
- if (matchUrl(url, rule.pattern) && typeof window[rule.fn] === 'function') {
341
- try { const r = await window[rule.fn]({ url, method }); return new Response(r.body ?? '', { status: r.status ?? 200, headers: { 'Content-Type': r.contentType ?? 'application/json' } }); } catch { break; }
342
- }
343
- }
344
- return _origFetch.apply(this, arguments);
345
- };
346
- })();`;
347
- await withTab(async t => {
348
- await client.addInitScript(dynamicScript, t);
349
- await client.evaluate(dynamicScript, t);
350
- });
351
- logger.success(`[${name}] intercept.respond (dynamic): ${pattern}`);
352
- return site;
353
- }
354
-
355
- const response = handlerOrResponse;
356
- const script = buildRespondScript(pattern, response.status ?? 200, response.contentType ?? "application/json", response.body);
357
- await withTab(async t => {
358
- await client.addInitScript(script, t);
359
- await client.evaluate(script, t);
360
- });
361
- logger.success(`[${name}] intercept.respond (static): ${pattern} → ${response.status ?? 200}`);
362
- return site;
363
- },
364
-
365
- modifyResponse: async (
366
- pattern: string,
367
- handler: (response: { body: string; status: number; headers: Record<string, string> }) =>
368
- Promise<{ body?: string; status?: number; headers?: Record<string, string> } | void> | void
369
- ) => {
370
- const fnName = `__piggy_modres_${name}_${++_modifyRuleCounter}__`;
371
- await client.exposeFunction(fnName, async (response: any) => {
372
- try { const mod = await handler(response); return { success: true, result: mod ?? {} }; }
373
- catch (e: any) { return { success: false, error: e.message }; }
374
- }, tabId);
375
-
376
- const script = buildModifyResponseScript(pattern, fnName);
377
- await withTab(async t => {
378
- await client.addInitScript(script, t);
379
- await client.evaluate(script, t);
380
- });
381
- logger.success(`[${name}] intercept.modifyResponse: ${pattern}`);
382
- return site;
383
- },
384
-
385
- clear: async () => {
386
- await withTab(t => client.clearInterceptRules(t));
387
- logger.info(`[${name}] intercept rules cleared`);
388
- },
389
- },
390
-
391
- // ── Network capture ───────────────────────────────────────────────────────
392
- capture: {
393
- start: () => withTab(async t => { await client.captureStart(t); logger.info(`[${name}] capture started`); }),
394
- stop: () => withTab(async t => { await client.captureStop(t); logger.info(`[${name}] capture stopped`); }),
395
- requests: () => withTab(t => client.captureRequests(t)),
396
- ws: () => withTab(t => client.captureWs(t)),
397
- cookies: () => withTab(t => client.captureCookies(t)),
398
- storage: () => withTab(t => client.captureStorage(t)),
399
- clear: () => withTab(async t => { await client.captureClear(t); logger.info(`[${name}] capture cleared`); }),
400
- },
401
-
402
- // ── Session ───────────────────────────────────────────────────────────────
403
- session: {
404
- export: async () => {
405
- const data = await withTab(t => client.sessionExport(t));
406
- logger.success(`[${name}] session exported`);
407
- return data;
408
- },
409
- import: async (data: any) => {
410
- await withTab(t => client.sessionImport(data, t));
411
- logger.success(`[${name}] session imported`);
412
- },
413
- },
414
-
415
- // ── Expose Function ───────────────────────────────────────────────────────
416
- exposeFunction: async (fnName: string, handler: (data: any) => Promise<any> | any) => {
417
- await client.exposeFunction(fnName, handler, tabId);
418
- logger.success(`[${name}] exposed function: ${fnName}`);
419
- return site;
420
- },
421
- unexposeFunction: async (fnName: string) => {
422
- await client.unexposeFunction(fnName, tabId);
423
- logger.info(`[${name}] unexposed function: ${fnName}`);
424
- return site;
425
- },
426
- clearExposedFunctions: async () => {
427
- await client.clearExposedFunctions(tabId);
428
- logger.info(`[${name}] cleared all exposed functions`);
429
- return site;
430
- },
431
- exposeAndInject: async (fnName: string, handler: (data: any) => Promise<any> | any, injectionJs: string | ((fnName: string) => string)) => {
432
- await client.exposeFunction(fnName, handler, tabId);
433
- const js = typeof injectionJs === "function" ? injectionJs(fnName) : injectionJs;
434
- await withTab(t => client.evaluate(js, t));
435
- logger.success(`[${name}] exposed and injected: ${fnName}`);
436
- return site;
437
- },
438
-
439
- // ── Store ─────────────────────────────────────────────────────────────────
440
- store: async (
441
- data: Record<string, any> | Record<string, any>[],
442
- schemaName?: string
443
- ) => {
444
- const target = schemaName ?? name;
445
- const result = await storeRecord(target, data);
446
- logger.info(`[${name}] store stored: ${result.stored}, skipped: ${result.skipped}`);
447
- return result;
448
- },
449
-
450
- // ── Elysia API ────────────────────────────────────────────────────────────
451
- api: (
452
- path: string,
453
- handler: RouteHandler,
454
- opts?: {
455
- ttl?: number;
456
- before?: BeforeMiddleware[];
457
- method?: "GET" | "POST" | "PUT" | "DELETE";
458
- detail?: RouteDetail;
459
- }
460
- ) => {
461
- const key = `${name}:${path}`;
462
- if (routeRegistry.has(key)) { logger.warn(`[${name}] route ${path} already registered`); return site; }
463
- routeRegistry.set(key, {
464
- path,
465
- method: opts?.method ?? "GET",
466
- handler,
467
- ttl: opts?.ttl ?? 360_000,
468
- before: opts?.before ?? [],
469
- detail: opts?.detail,
470
- });
471
- logger.info(`[${name}] api route: ${opts?.method ?? "GET"} /${name}${path}`);
472
- return site;
473
- },
474
-
475
- noclose: () => { keepAliveSites.add(name); logger.info(`[${name}] keep-alive`); return site; },
476
-
477
- close: async () => {
478
- _unsubNavigate();
479
- keepAliveSites.delete(name);
480
- if (pool) {
481
- await pool.close();
482
- } else if (tabId !== "default") {
483
- await client.closeTab(tabId);
484
- logger.info(`[${name}] tab closed`);
485
- }
486
- },
487
- };
488
-
489
- return site;
490
- }
491
-
492
- export type SiteObject = ReturnType<typeof createSiteObject>;
493
-
494
- export function createExposedAPI<T extends Record<string, (data: any) => any>>(
495
- site: any,
496
- apiName: string,
497
- handlers: T
498
- ): Promise<void> {
499
- const wrappedHandler = async (call: any) => {
500
- const { method, args } = call;
501
- const handler = handlers[method as keyof T];
502
- if (!handler) throw new Error(`Unknown method: ${method}`);
503
- try { return await handler(args); }
504
- catch (err) { logger.error(`[${site._name}] API error in ${method}: ${err}`); throw err; }
505
- };
506
- return site.exposeFunction(apiName, wrappedHandler);
1
+ // piggy/register/index.ts
2
+ import { PiggyClient } from "../client";
3
+ import logger from "../logger";
4
+ import { routeRegistry, keepAliveSites, type RouteHandler, type BeforeMiddleware, type RouteDetail } from "../server";
5
+ import { randomDelay, humanTypeSequence } from "../human";
6
+ import { buildRespondScript, buildModifyResponseScript } from "../intercept/scripts";
7
+ import { storeRecord } from "../store";
8
+ import { TabPool } from "../pool";
9
+
10
+ let globalClient: PiggyClient | null = null;
11
+ export let humanMode = false;
12
+
13
+ export function setClient(c: PiggyClient | null) { globalClient = c; }
14
+ export function setHumanMode(v: boolean) { humanMode = v; }
15
+
16
+ async function retry<T>(label: string, fn: () => Promise<T>, retries = 2, backoff = 150): Promise<T> {
17
+ let last!: Error;
18
+ for (let i = 0; i <= retries; i++) {
19
+ try { return await fn(); } catch (e: any) {
20
+ last = e;
21
+ if (i < retries) {
22
+ logger.warn(`[${label}] retry ${i + 1}/${retries}: ${e.message}`);
23
+ await new Promise(r => setTimeout(r, backoff * (i + 1)));
24
+ }
25
+ }
26
+ }
27
+ throw last;
28
+ }
29
+
30
+ export function createSiteObject(
31
+ name: string,
32
+ registeredUrl: string,
33
+ client: PiggyClient,
34
+ tabId: string,
35
+ pool?: TabPool
36
+ ) {
37
+ let _currentUrl: string = registeredUrl;
38
+ let _modifyRuleCounter = 0;
39
+
40
+ // ── helpers ────────────────────────────────────────────────────────────────
41
+ // If pool exists, run fn with a pool tab. Otherwise use the fixed tabId.
42
+ function withTab<T>(fn: (t: string) => Promise<T>): Promise<T> {
43
+ return pool ? pool.withTab(fn) : fn(tabId);
44
+ }
45
+
46
+ const _eventListeners = new Map<string, Set<(data: any) => void>>();
47
+
48
+ const _unsubNavigate = client.onEvent("navigate", tabId, (url: string) => {
49
+ _currentUrl = url;
50
+ const handlers = _eventListeners.get("navigate");
51
+ if (handlers) {
52
+ for (const h of handlers) {
53
+ try { h(url); } catch (e) { logger.error(`[${name}] navigate handler error: ${e}`); }
54
+ }
55
+ }
56
+ });
57
+
58
+ const withErrScreen = async <T>(fn: () => Promise<T>, label: string): Promise<T> => {
59
+ try { return await fn(); } catch (err: any) {
60
+ const p = `./error-${name}-${Date.now()}.png`;
61
+ try { await client.screenshot(p, tabId); logger.error(`[${name}] ${label} failed → ${p}`); }
62
+ catch { logger.error(`[${name}] ${label} failed (no screenshot)`); }
63
+ throw err;
64
+ }
65
+ };
66
+
67
+ const site: any = {
68
+ _name: name,
69
+ _tabId: tabId,
70
+ _pool: pool ?? null,
71
+
72
+ // ── Pool stats ────────────────────────────────────────────────────────────
73
+ poolStats: () => pool?.stats ?? null,
74
+
75
+ // ── Navigation ────────────────────────────────────────────────────────────
76
+ navigate: (url?: string, opts?: { retries?: number }) => {
77
+ const target = url ?? registeredUrl;
78
+ return withTab(t =>
79
+ retry(name, async () => {
80
+ logger.network(`[${name}] navigating → ${target}`);
81
+ await client.navigate(target, t);
82
+ _currentUrl = target;
83
+ }, opts?.retries ?? 2)
84
+ );
85
+ },
86
+
87
+ reload: () => withTab(t => client.reload(t)),
88
+ goBack: () => withTab(t => client.goBack(t)),
89
+ goForward: () => withTab(t => client.goForward(t)),
90
+ waitForNavigation: () => withTab(t => client.waitForNavigation(t)),
91
+
92
+ title: () => withTab(async t => {
93
+ const title = await client.getTitle(t);
94
+ logger.info(`[${name}] title: ${title}`);
95
+ return title;
96
+ }),
97
+
98
+ url: () => _currentUrl,
99
+ content: () => withTab(t => client.content(t)),
100
+
101
+ wait: (ms: number) => {
102
+ const actual = humanMode ? ms + Math.floor(Math.random() * 600) - 300 : ms;
103
+ return new Promise<void>(r => setTimeout(r, Math.max(0, actual)));
104
+ },
105
+
106
+ waitForSelector: (selector: string, timeout = 30000) =>
107
+ withTab(t => {
108
+ logger.debug(`[${name}] waitForSelector: ${selector}`);
109
+ return client.waitForSelector(selector, timeout, t);
110
+ }),
111
+
112
+ waitForVisible: (selector: string, timeout = 30000) =>
113
+ withTab(t => client.waitForSelector(selector, timeout, t)),
114
+
115
+ waitForResponse: (pattern: string, timeout = 30000) =>
116
+ withTab(t => client.waitForResponse(pattern, timeout, t)),
117
+
118
+ // ── Init Script ───────────────────────────────────────────────────────────
119
+ addInitScript: async (js: string | (() => void)) => {
120
+ const code = typeof js === "function" ? `(${js.toString()})();` : js;
121
+ await withTab(t => client.addInitScript(code, t));
122
+ logger.success(`[${name}] init script added`);
123
+ return site;
124
+ },
125
+
126
+ // ── Events ────────────────────────────────────────────────────────────────
127
+ on: (event: string, handler: (data: any) => void): (() => void) => {
128
+ if (!_eventListeners.has(event)) _eventListeners.set(event, new Set());
129
+ _eventListeners.get(event)!.add(handler);
130
+ logger.debug(`[${name}] on('${event}') registered`);
131
+ return () => {
132
+ _eventListeners.get(event)?.delete(handler);
133
+ logger.debug(`[${name}] on('${event}') unsubscribed`);
134
+ };
135
+ },
136
+
137
+ off: (event: string, handler: (data: any) => void) => {
138
+ _eventListeners.get(event)?.delete(handler);
139
+ },
140
+
141
+ // ── Interactions ──────────────────────────────────────────────────────────
142
+ click: (selector: string, opts?: { retries?: number; timeout?: number }) =>
143
+ withErrScreen(() =>
144
+ withTab(t =>
145
+ retry(name, async () => {
146
+ if (humanMode) await randomDelay(80, 220);
147
+ await client.waitForSelector(selector, opts?.timeout ?? 15000, t);
148
+ const ok = await client.click(selector, t);
149
+ if (!ok) throw new Error(`click failed: ${selector}`);
150
+ logger.success(`[${name}] clicked: ${selector}`);
151
+ return ok;
152
+ }, opts?.retries ?? 2)
153
+ ),
154
+ `click(${selector})`
155
+ ),
156
+
157
+ doubleClick: (selector: string) =>
158
+ withErrScreen(() =>
159
+ withTab(async t => {
160
+ if (humanMode) await randomDelay(80, 200);
161
+ return client.doubleClick(selector, t);
162
+ }),
163
+ `dblclick(${selector})`
164
+ ),
165
+
166
+ hover: (selector: string) =>
167
+ withErrScreen(() =>
168
+ withTab(async t => {
169
+ if (humanMode) await randomDelay(50, 150);
170
+ return client.hover(selector, t);
171
+ }),
172
+ `hover(${selector})`
173
+ ),
174
+
175
+ type: (selector: string, text: string, opts?: { delay?: number; retries?: number; fact?: boolean; wpm?: number }) =>
176
+ withErrScreen(() =>
177
+ withTab(async t => {
178
+ await client.waitForSelector(selector, 30000, t);
179
+ if (humanMode && !opts?.fact) {
180
+ const seq = humanTypeSequence(text);
181
+ let current = "";
182
+ for (const action of seq) {
183
+ if (action === "BACKSPACE") current = current.slice(0, -1);
184
+ else current += action;
185
+ await client.evaluate(`
186
+ (function() {
187
+ const el = document.querySelector('${selector}');
188
+ const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
189
+ nativeSetter.call(el, '${current.replace(/'/g, "\\'")}');
190
+ el.dispatchEvent(new Event('input', { bubbles: true }));
191
+ el.dispatchEvent(new Event('change', { bubbles: true }));
192
+ })()
193
+ `, t);
194
+ const wpm = opts?.wpm ?? 120;
195
+ const msPerChar = Math.round(60000 / (wpm * 5));
196
+ await randomDelay(msPerChar * 0.5, msPerChar * 1.8);
197
+ }
198
+ } else if (opts?.delay) {
199
+ for (const ch of text) {
200
+ await client.type(selector, ch, t);
201
+ await new Promise(r => setTimeout(r, opts.delay));
202
+ }
203
+ } else {
204
+ await client.type(selector, text, t);
205
+ }
206
+ // fire blur so Phoenix/Angular/React pick up the final value
207
+ await client.evaluate(`
208
+ document.querySelector('${selector}').dispatchEvent(new Event('blur', { bubbles: true }))
209
+ `, t);
210
+ logger.success(`[${name}] typed into: ${selector}`);
211
+ return true;
212
+ }),
213
+ `type(${selector})`
214
+ ),
215
+ select: (selector: string, value: string) => withTab(t => client.select(selector, value, t)),
216
+
217
+ evaluate: (js: string | (() => any), ...args: any[]) => {
218
+ const code = typeof js === "function"
219
+ ? `(${js.toString()})(${args.map(a => JSON.stringify(a)).join(",")})`
220
+ : js;
221
+ return withTab(t => client.evaluate(code, t));
222
+ },
223
+
224
+ keyboard: {
225
+ press: (key: string) => withTab(t => client.keyPress(key, t)),
226
+ combo: (combo: string) => withTab(t => client.keyCombo(combo, t)),
227
+ },
228
+
229
+ mouse: {
230
+ move: (x: number, y: number) => withTab(t => client.mouseMove(x, y, t)),
231
+ drag: (from: { x: number; y: number }, to: { x: number; y: number }) =>
232
+ withTab(t => client.mouseDrag(from, to, t)),
233
+ },
234
+
235
+ scroll: {
236
+ to: (selector: string) => withTab(t => client.scrollTo(selector, t)),
237
+ by: (px: number) => withTab(async t => {
238
+ if (humanMode) {
239
+ const steps = Math.ceil(Math.abs(px) / 120);
240
+ const chunk = px / steps;
241
+ for (let i = 0; i < steps; i++) {
242
+ await client.scrollBy(chunk, t);
243
+ await randomDelay(30, 80);
244
+ }
245
+ } else {
246
+ await client.scrollBy(px, t);
247
+ }
248
+ }) as Promise<void>,
249
+ },
250
+
251
+ // ── Fetch ─────────────────────────────────────────────────────────────────
252
+ fetchText: (selector: string) => withTab(t => client.fetchText(selector, t)),
253
+
254
+ fetchLinks: async (selector: string) => {
255
+ const links = await withTab(t => client.fetchLinks(selector, t));
256
+ logger.info(`[${name}] fetchLinks(${selector}): ${links.length}`);
257
+ return links;
258
+ },
259
+
260
+ fetchImages: async (selector: string) => {
261
+ const imgs = await withTab(t => client.fetchImages(selector, t));
262
+ logger.info(`[${name}] fetchImages(${selector}): ${imgs.length}`);
263
+ return imgs;
264
+ },
265
+
266
+ search: {
267
+ css: (query: string) => withTab(t => client.searchCss(query, t)),
268
+ id: (query: string) => withTab(t => client.searchId(query, t)),
269
+ },
270
+
271
+ // ── Screenshot / PDF ──────────────────────────────────────────────────────
272
+ screenshot: async (filePath?: string) => {
273
+ const r = await withTab(t => client.screenshot(filePath, t));
274
+ logger.success(`[${name}] screenshot → ${filePath ?? "base64"}`);
275
+ return r;
276
+ },
277
+
278
+ pdf: async (filePath?: string) => {
279
+ const r = await withTab(t => client.pdf(filePath, t));
280
+ logger.success(`[${name}] pdf ${filePath ?? "base64"}`);
281
+ return r;
282
+ },
283
+
284
+ blockImages: () => withTab(async t => { await client.blockImages(t); logger.info(`[${name}] images blocked`); }),
285
+ unblockImages: () => withTab(async t => { await client.unblockImages(t); logger.info(`[${name}] images unblocked`); }),
286
+
287
+ // ── Cookies ───────────────────────────────────────────────────────────────
288
+ cookies: {
289
+ set: async (cookieName: string, value: string, domain: string, path = "/") => {
290
+ await withTab(t => client.setCookie(cookieName, value, domain, path, t));
291
+ logger.info(`[${name}] cookie set: ${cookieName} @ ${domain}`);
292
+ },
293
+ get: (cookieName: string) => withTab(t => client.getCookie(cookieName, t)),
294
+ delete: async (cookieName: string) => {
295
+ await withTab(t => client.deleteCookie(cookieName, t));
296
+ logger.info(`[${name}] cookie deleted: ${cookieName}`);
297
+ },
298
+ list: () => withTab(t => client.listCookies(t)),
299
+ },
300
+
301
+ // ── Interception ──────────────────────────────────────────────────────────
302
+ intercept: {
303
+ block: async (pattern: string) => {
304
+ await withTab(t => client.addInterceptRule("block", pattern, {}, t));
305
+ logger.info(`[${name}] intercept block: ${pattern}`);
306
+ },
307
+
308
+ redirect: async (pattern: string, redirectUrl: string) => {
309
+ await withTab(t => client.addInterceptRule("redirect", pattern, { redirectUrl }, t));
310
+ logger.info(`[${name}] intercept redirect: ${pattern} ${redirectUrl}`);
311
+ },
312
+
313
+ headers: async (pattern: string, headers: Record<string, string>) => {
314
+ await withTab(t => client.addInterceptRule("modifyHeaders", pattern, { headers }, t));
315
+ logger.info(`[${name}] intercept modifyHeaders: ${pattern}`);
316
+ },
317
+
318
+ respond: async (
319
+ pattern: string,
320
+ handlerOrResponse:
321
+ | { status?: number; contentType?: string; body: string }
322
+ | ((req: { url: string; method: string }) => { status?: number; contentType?: string; body: string })
323
+ ) => {
324
+ const isStatic = typeof handlerOrResponse === "object";
325
+
326
+ if (!isStatic) {
327
+ const fnName = `__piggy_respond_${name}_${++_modifyRuleCounter}__`;
328
+ await client.exposeFunction(fnName, async (req: { url: string; method: string }) => {
329
+ try {
330
+ const result = (handlerOrResponse as Function)(req);
331
+ return { success: true, result: { status: result.status ?? 200, contentType: result.contentType ?? "application/json", body: result.body ?? "" } };
332
+ } catch (e: any) {
333
+ return { success: false, error: e.message };
334
+ }
335
+ }, tabId);
336
+
337
+ const dynamicScript = `
338
+ (function() {
339
+ 'use strict';
340
+ if (!window.__PIGGY_DYNAMIC_RESPOND__) window.__PIGGY_DYNAMIC_RESPOND__ = [];
341
+ window.__PIGGY_DYNAMIC_RESPOND__.push({ pattern: ${JSON.stringify(pattern)}, fn: ${JSON.stringify(fnName)} });
342
+ function matchUrl(url, pattern) { try { return url.includes(pattern) || new RegExp(pattern).test(url); } catch { return url.includes(pattern); } }
343
+ if (window.__PIGGY_DYN_INSTALLED__) return;
344
+ window.__PIGGY_DYN_INSTALLED__ = true;
345
+ const _origFetch = window.fetch;
346
+ window.fetch = async function(input, init) {
347
+ const url = typeof input === 'string' ? input : (input?.url ?? String(input));
348
+ const method = (init?.method ?? 'GET').toUpperCase();
349
+ const rules = window.__PIGGY_DYNAMIC_RESPOND__ || [];
350
+ for (const rule of rules) {
351
+ if (matchUrl(url, rule.pattern) && typeof window[rule.fn] === 'function') {
352
+ try { const r = await window[rule.fn]({ url, method }); return new Response(r.body ?? '', { status: r.status ?? 200, headers: { 'Content-Type': r.contentType ?? 'application/json' } }); } catch { break; }
353
+ }
354
+ }
355
+ return _origFetch.apply(this, arguments);
356
+ };
357
+ })();`;
358
+ await withTab(async t => {
359
+ await client.addInitScript(dynamicScript, t);
360
+ await client.evaluate(dynamicScript, t);
361
+ });
362
+ logger.success(`[${name}] intercept.respond (dynamic): ${pattern}`);
363
+ return site;
364
+ }
365
+
366
+ const response = handlerOrResponse;
367
+ const script = buildRespondScript(pattern, response.status ?? 200, response.contentType ?? "application/json", response.body);
368
+ await withTab(async t => {
369
+ await client.addInitScript(script, t);
370
+ await client.evaluate(script, t);
371
+ });
372
+ logger.success(`[${name}] intercept.respond (static): ${pattern} ${response.status ?? 200}`);
373
+ return site;
374
+ },
375
+
376
+ modifyResponse: async (
377
+ pattern: string,
378
+ handler: (response: { body: string; status: number; headers: Record<string, string> }) =>
379
+ Promise<{ body?: string; status?: number; headers?: Record<string, string> } | void> | void
380
+ ) => {
381
+ const fnName = `__piggy_modres_${name}_${++_modifyRuleCounter}__`;
382
+ await client.exposeFunction(fnName, async (response: any) => {
383
+ try { const mod = await handler(response); return { success: true, result: mod ?? {} }; }
384
+ catch (e: any) { return { success: false, error: e.message }; }
385
+ }, tabId);
386
+
387
+ const script = buildModifyResponseScript(pattern, fnName);
388
+ await withTab(async t => {
389
+ await client.addInitScript(script, t);
390
+ await client.evaluate(script, t);
391
+ });
392
+ logger.success(`[${name}] intercept.modifyResponse: ${pattern}`);
393
+ return site;
394
+ },
395
+
396
+ clear: async () => {
397
+ await withTab(t => client.clearInterceptRules(t));
398
+ logger.info(`[${name}] intercept rules cleared`);
399
+ },
400
+ },
401
+
402
+ // ── Network capture ───────────────────────────────────────────────────────
403
+ capture: {
404
+ start: () => withTab(async t => { await client.captureStart(t); logger.info(`[${name}] capture started`); }),
405
+ stop: () => withTab(async t => { await client.captureStop(t); logger.info(`[${name}] capture stopped`); }),
406
+ requests: () => withTab(t => client.captureRequests(t)),
407
+ ws: () => withTab(t => client.captureWs(t)),
408
+ cookies: () => withTab(t => client.captureCookies(t)),
409
+ storage: () => withTab(t => client.captureStorage(t)),
410
+ clear: () => withTab(async t => { await client.captureClear(t); logger.info(`[${name}] capture cleared`); }),
411
+ },
412
+
413
+ // ── Session ───────────────────────────────────────────────────────────────
414
+ session: {
415
+ export: async () => {
416
+ const data = await withTab(t => client.sessionExport(t));
417
+ logger.success(`[${name}] session exported`);
418
+ return data;
419
+ },
420
+ import: async (data: any) => {
421
+ await withTab(t => client.sessionImport(data, t));
422
+ logger.success(`[${name}] session imported`);
423
+ },
424
+ },
425
+
426
+ // ── Expose Function ───────────────────────────────────────────────────────
427
+ exposeFunction: async (fnName: string, handler: (data: any) => Promise<any> | any) => {
428
+ await client.exposeFunction(fnName, handler, tabId);
429
+ logger.success(`[${name}] exposed function: ${fnName}`);
430
+ return site;
431
+ },
432
+ unexposeFunction: async (fnName: string) => {
433
+ await client.unexposeFunction(fnName, tabId);
434
+ logger.info(`[${name}] unexposed function: ${fnName}`);
435
+ return site;
436
+ },
437
+ clearExposedFunctions: async () => {
438
+ await client.clearExposedFunctions(tabId);
439
+ logger.info(`[${name}] cleared all exposed functions`);
440
+ return site;
441
+ },
442
+ exposeAndInject: async (fnName: string, handler: (data: any) => Promise<any> | any, injectionJs: string | ((fnName: string) => string)) => {
443
+ await client.exposeFunction(fnName, handler, tabId);
444
+ const js = typeof injectionJs === "function" ? injectionJs(fnName) : injectionJs;
445
+ await withTab(t => client.evaluate(js, t));
446
+ logger.success(`[${name}] exposed and injected: ${fnName}`);
447
+ return site;
448
+ },
449
+
450
+ // ── Store ─────────────────────────────────────────────────────────────────
451
+ store: async (
452
+ data: Record<string, any> | Record<string, any>[],
453
+ schemaName?: string
454
+ ) => {
455
+ const target = schemaName ?? name;
456
+ const result = await storeRecord(target, data);
457
+ logger.info(`[${name}] store stored: ${result.stored}, skipped: ${result.skipped}`);
458
+ return result;
459
+ },
460
+
461
+ // ── Elysia API ────────────────────────────────────────────────────────────
462
+ api: (
463
+ path: string,
464
+ handler: RouteHandler,
465
+ opts?: {
466
+ ttl?: number;
467
+ before?: BeforeMiddleware[];
468
+ method?: "GET" | "POST" | "PUT" | "DELETE";
469
+ detail?: RouteDetail;
470
+ }
471
+ ) => {
472
+ const key = `${name}:${path}`;
473
+ if (routeRegistry.has(key)) { logger.warn(`[${name}] route ${path} already registered`); return site; }
474
+ routeRegistry.set(key, {
475
+ path,
476
+ method: opts?.method ?? "GET",
477
+ handler,
478
+ ttl: opts?.ttl ?? 360_000,
479
+ before: opts?.before ?? [],
480
+ detail: opts?.detail,
481
+ });
482
+ logger.info(`[${name}] api route: ${opts?.method ?? "GET"} /${name}${path}`);
483
+ return site;
484
+ },
485
+
486
+ noclose: () => { keepAliveSites.add(name); logger.info(`[${name}] keep-alive`); return site; },
487
+
488
+ close: async () => {
489
+ _unsubNavigate();
490
+ keepAliveSites.delete(name);
491
+ if (pool) {
492
+ await pool.close();
493
+ } else if (tabId !== "default") {
494
+ await client.closeTab(tabId);
495
+ logger.info(`[${name}] tab closed`);
496
+ }
497
+ },
498
+ };
499
+
500
+ return site;
501
+ }
502
+
503
+ export type SiteObject = ReturnType<typeof createSiteObject>;
504
+
505
+ export function createExposedAPI<T extends Record<string, (data: any) => any>>(
506
+ site: any,
507
+ apiName: string,
508
+ handlers: T
509
+ ): Promise<void> {
510
+ const wrappedHandler = async (call: any) => {
511
+ const { method, args } = call;
512
+ const handler = handlers[method as keyof T];
513
+ if (!handler) throw new Error(`Unknown method: ${method}`);
514
+ try { return await handler(args); }
515
+ catch (err) { logger.error(`[${site._name}] API error in ${method}: ${err}`); throw err; }
516
+ };
517
+ return site.exposeFunction(apiName, wrappedHandler);
507
518
  }