nothing-browser 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.js +39 -19
- package/dist/piggy.js +289 -23
- package/dist/register/index.js +242 -0
- package/package.json +1 -1
- package/piggy/client/index.ts +117 -247
- package/piggy/intercept/scripts.ts +153 -0
- package/piggy/register/index.ts +182 -36
- package/piggy.ts +11 -5
package/piggy/register/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { PiggyClient } from "../client";
|
|
|
3
3
|
import logger from "../logger";
|
|
4
4
|
import { routeRegistry, keepAliveSites, type RouteHandler, type BeforeMiddleware } from "../server";
|
|
5
5
|
import { randomDelay, humanTypeSequence } from "../human";
|
|
6
|
+
import { buildRespondScript, buildModifyResponseScript } from "../intercept/scripts";
|
|
6
7
|
|
|
7
8
|
let globalClient: PiggyClient | null = null;
|
|
8
9
|
export let humanMode = false;
|
|
@@ -27,6 +28,20 @@ async function retry<T>(label: string, fn: () => Promise<T>, retries = 2, backof
|
|
|
27
28
|
export function createSiteObject(name: string, registeredUrl: string, client: PiggyClient, tabId: string) {
|
|
28
29
|
let _currentUrl: string = registeredUrl;
|
|
29
30
|
|
|
31
|
+
// ── Event listeners store ──────────────────────────────────────────────────
|
|
32
|
+
const _eventListeners = new Map<string, Set<(data: any) => void>>();
|
|
33
|
+
|
|
34
|
+
// Wire the client-level navigate event into site-level listeners
|
|
35
|
+
const _unsubNavigate = client.onEvent("navigate", tabId, (url: string) => {
|
|
36
|
+
_currentUrl = url;
|
|
37
|
+
const handlers = _eventListeners.get("navigate");
|
|
38
|
+
if (handlers) {
|
|
39
|
+
for (const h of handlers) {
|
|
40
|
+
try { h(url); } catch (e) { logger.error(`[${name}] navigate handler error: ${e}`); }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
30
45
|
const withErrScreen = async <T>(fn: () => Promise<T>, label: string): Promise<T> => {
|
|
31
46
|
try { return await fn(); } catch (err: any) {
|
|
32
47
|
const p = `./error-${name}-${Date.now()}.png`;
|
|
@@ -36,6 +51,9 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
36
51
|
}
|
|
37
52
|
};
|
|
38
53
|
|
|
54
|
+
// ── Intercept helper: unique fn name per pattern ───────────────────────────
|
|
55
|
+
let _modifyRuleCounter = 0;
|
|
56
|
+
|
|
39
57
|
const site: any = {
|
|
40
58
|
_name: name,
|
|
41
59
|
_tabId: tabId,
|
|
@@ -75,8 +93,7 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
75
93
|
waitForVisible: (selector: string, timeout = 30000) => client.waitForSelector(selector, timeout, tabId),
|
|
76
94
|
waitForResponse: (pattern: string, timeout = 30000) => client.waitForResponse(pattern, timeout, tabId),
|
|
77
95
|
|
|
78
|
-
// ── Init Script
|
|
79
|
-
// HERE IT IS - ADD THIS METHOD TO THE SITE OBJECT
|
|
96
|
+
// ── Init Script ────────────────────────────────────────────────────────────
|
|
80
97
|
addInitScript: async (js: string | (() => void)) => {
|
|
81
98
|
const code = typeof js === 'function' ? `(${js.toString()})();` : js;
|
|
82
99
|
await client.addInitScript(code, tabId);
|
|
@@ -84,6 +101,23 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
84
101
|
return site;
|
|
85
102
|
},
|
|
86
103
|
|
|
104
|
+
// ── Event emitter ──────────────────────────────────────────────────────────
|
|
105
|
+
// Usage: site.on('navigate', url => console.log('went to', url))
|
|
106
|
+
// Returns unsubscribe function
|
|
107
|
+
on: (event: string, handler: (data: any) => void): (() => void) => {
|
|
108
|
+
if (!_eventListeners.has(event)) _eventListeners.set(event, new Set());
|
|
109
|
+
_eventListeners.get(event)!.add(handler);
|
|
110
|
+
logger.debug(`[${name}] on('${event}') registered`);
|
|
111
|
+
return () => {
|
|
112
|
+
_eventListeners.get(event)?.delete(handler);
|
|
113
|
+
logger.debug(`[${name}] on('${event}') unsubscribed`);
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
off: (event: string, handler: (data: any) => void) => {
|
|
118
|
+
_eventListeners.get(event)?.delete(handler);
|
|
119
|
+
},
|
|
120
|
+
|
|
87
121
|
// ── Interactions ───────────────────────────────────────────────────────────
|
|
88
122
|
click: (selector: string, opts?: { retries?: number; timeout?: number }) =>
|
|
89
123
|
withErrScreen(() =>
|
|
@@ -222,14 +256,151 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
222
256
|
await client.addInterceptRule("block", pattern, {}, tabId);
|
|
223
257
|
logger.info(`[${name}] intercept block: ${pattern}`);
|
|
224
258
|
},
|
|
259
|
+
|
|
225
260
|
redirect: async (pattern: string, redirectUrl: string) => {
|
|
226
261
|
await client.addInterceptRule("redirect", pattern, { redirectUrl }, tabId);
|
|
227
262
|
logger.info(`[${name}] intercept redirect: ${pattern} → ${redirectUrl}`);
|
|
228
263
|
},
|
|
264
|
+
|
|
229
265
|
headers: async (pattern: string, headers: Record<string, string>) => {
|
|
230
266
|
await client.addInterceptRule("modifyHeaders", pattern, { headers }, tabId);
|
|
231
267
|
logger.info(`[${name}] intercept modifyHeaders: ${pattern}`);
|
|
232
268
|
},
|
|
269
|
+
|
|
270
|
+
// ── NEW: intercept.respond ──────────────────────────────────────────────
|
|
271
|
+
// Intercepts matching requests and returns a fake response — request never
|
|
272
|
+
// leaves the browser. Works for both fetch and XHR via JS injection.
|
|
273
|
+
//
|
|
274
|
+
// Usage:
|
|
275
|
+
// await site.intercept.respond('/api/prices', (req) => ({
|
|
276
|
+
// status: 200,
|
|
277
|
+
// contentType: 'application/json',
|
|
278
|
+
// body: JSON.stringify({ price: 99 })
|
|
279
|
+
// }))
|
|
280
|
+
//
|
|
281
|
+
// // Static shorthand:
|
|
282
|
+
// await site.intercept.respond('/api/prices', {
|
|
283
|
+
// status: 200, contentType: 'application/json', body: '{"price":99}'
|
|
284
|
+
// })
|
|
285
|
+
respond: async (
|
|
286
|
+
pattern: string,
|
|
287
|
+
handlerOrResponse:
|
|
288
|
+
| { status?: number; contentType?: string; body: string }
|
|
289
|
+
| ((req: { url: string; method: string }) => { status?: number; contentType?: string; body: string })
|
|
290
|
+
) => {
|
|
291
|
+
// Static response — just inject the JS rule directly
|
|
292
|
+
const isStatic = typeof handlerOrResponse === "object";
|
|
293
|
+
const response = isStatic
|
|
294
|
+
? handlerOrResponse
|
|
295
|
+
: { status: 200, contentType: "application/json", body: "" };
|
|
296
|
+
|
|
297
|
+
if (!isStatic) {
|
|
298
|
+
// Dynamic: expose a function, call it from the injected script
|
|
299
|
+
const fnName = `__piggy_respond_${name}_${++_modifyRuleCounter}__`;
|
|
300
|
+
|
|
301
|
+
await client.exposeFunction(fnName, async (req: { url: string; method: string }) => {
|
|
302
|
+
try {
|
|
303
|
+
const result = (handlerOrResponse as Function)(req);
|
|
304
|
+
return {
|
|
305
|
+
success: true,
|
|
306
|
+
result: {
|
|
307
|
+
status: result.status ?? 200,
|
|
308
|
+
contentType: result.contentType ?? "application/json",
|
|
309
|
+
body: result.body ?? "",
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
} catch (e: any) {
|
|
313
|
+
return { success: false, error: e.message };
|
|
314
|
+
}
|
|
315
|
+
}, tabId);
|
|
316
|
+
|
|
317
|
+
// Inject a script that calls the exposed function instead of static body
|
|
318
|
+
const dynamicScript = `
|
|
319
|
+
(function() {
|
|
320
|
+
'use strict';
|
|
321
|
+
if (!window.__PIGGY_DYNAMIC_RESPOND__) window.__PIGGY_DYNAMIC_RESPOND__ = [];
|
|
322
|
+
window.__PIGGY_DYNAMIC_RESPOND__.push({ pattern: ${JSON.stringify(pattern)}, fn: ${JSON.stringify(fnName)} });
|
|
323
|
+
|
|
324
|
+
function matchUrl(url, pattern) {
|
|
325
|
+
try { return url.includes(pattern) || new RegExp(pattern).test(url); }
|
|
326
|
+
catch { return url.includes(pattern); }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (window.__PIGGY_DYN_INSTALLED__) return;
|
|
330
|
+
window.__PIGGY_DYN_INSTALLED__ = true;
|
|
331
|
+
|
|
332
|
+
const _origFetch = window.fetch;
|
|
333
|
+
window.fetch = async function(input, init) {
|
|
334
|
+
const url = typeof input === 'string' ? input : (input?.url ?? String(input));
|
|
335
|
+
const method = (init?.method ?? 'GET').toUpperCase();
|
|
336
|
+
const rules = window.__PIGGY_DYNAMIC_RESPOND__ || [];
|
|
337
|
+
for (const rule of rules) {
|
|
338
|
+
if (matchUrl(url, rule.pattern) && typeof window[rule.fn] === 'function') {
|
|
339
|
+
try {
|
|
340
|
+
const r = await window[rule.fn]({ url, method });
|
|
341
|
+
return new Response(r.body ?? '', {
|
|
342
|
+
status: r.status ?? 200,
|
|
343
|
+
headers: { 'Content-Type': r.contentType ?? 'application/json' }
|
|
344
|
+
});
|
|
345
|
+
} catch { break; }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return _origFetch.apply(this, arguments);
|
|
349
|
+
};
|
|
350
|
+
})();`;
|
|
351
|
+
await client.addInitScript(dynamicScript, tabId);
|
|
352
|
+
await client.evaluate(dynamicScript, tabId);
|
|
353
|
+
logger.success(`[${name}] intercept.respond (dynamic): ${pattern}`);
|
|
354
|
+
return site;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Static path: inject the JS intercept rule
|
|
358
|
+
const script = buildRespondScript(
|
|
359
|
+
pattern,
|
|
360
|
+
response.status ?? 200,
|
|
361
|
+
response.contentType ?? "application/json",
|
|
362
|
+
response.body
|
|
363
|
+
);
|
|
364
|
+
await client.addInitScript(script, tabId);
|
|
365
|
+
await client.evaluate(script, tabId);
|
|
366
|
+
logger.success(`[${name}] intercept.respond (static): ${pattern} → ${response.status ?? 200}`);
|
|
367
|
+
return site;
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
// ── NEW: intercept.modifyResponse ───────────────────────────────────────
|
|
371
|
+
// Lets the request hit the network, then calls your handler with the
|
|
372
|
+
// response. Return { body?, status?, headers? } to modify, or {} to
|
|
373
|
+
// pass through unchanged.
|
|
374
|
+
//
|
|
375
|
+
// Usage:
|
|
376
|
+
// await site.intercept.modifyResponse('/api/feed', async ({ body, status }) => {
|
|
377
|
+
// const data = JSON.parse(body)
|
|
378
|
+
// data.items = data.items.slice(0, 5)
|
|
379
|
+
// return { body: JSON.stringify(data) }
|
|
380
|
+
// })
|
|
381
|
+
modifyResponse: async (
|
|
382
|
+
pattern: string,
|
|
383
|
+
handler: (response: { body: string; status: number; headers: Record<string, string> }) =>
|
|
384
|
+
Promise<{ body?: string; status?: number; headers?: Record<string, string> } | void> | void
|
|
385
|
+
) => {
|
|
386
|
+
const fnName = `__piggy_modres_${name}_${++_modifyRuleCounter}__`;
|
|
387
|
+
|
|
388
|
+
await client.exposeFunction(fnName, async (response: { body: string; status: number; headers: Record<string, string> }) => {
|
|
389
|
+
try {
|
|
390
|
+
const mod = await handler(response);
|
|
391
|
+
return { success: true, result: mod ?? {} };
|
|
392
|
+
} catch (e: any) {
|
|
393
|
+
return { success: false, error: e.message };
|
|
394
|
+
}
|
|
395
|
+
}, tabId);
|
|
396
|
+
|
|
397
|
+
const script = buildModifyResponseScript(pattern, fnName);
|
|
398
|
+
await client.addInitScript(script, tabId);
|
|
399
|
+
await client.evaluate(script, tabId);
|
|
400
|
+
logger.success(`[${name}] intercept.modifyResponse: ${pattern}`);
|
|
401
|
+
return site;
|
|
402
|
+
},
|
|
403
|
+
|
|
233
404
|
clear: async () => {
|
|
234
405
|
await client.clearInterceptRules(tabId);
|
|
235
406
|
logger.info(`[${name}] intercept rules cleared`);
|
|
@@ -238,35 +409,19 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
238
409
|
|
|
239
410
|
// ── Network capture ────────────────────────────────────────────────────────
|
|
240
411
|
capture: {
|
|
241
|
-
start: async () => {
|
|
242
|
-
|
|
243
|
-
logger.info(`[${name}] capture started`);
|
|
244
|
-
},
|
|
245
|
-
stop: async () => {
|
|
246
|
-
await client.captureStop(tabId);
|
|
247
|
-
logger.info(`[${name}] capture stopped`);
|
|
248
|
-
},
|
|
412
|
+
start: async () => { await client.captureStart(tabId); logger.info(`[${name}] capture started`); },
|
|
413
|
+
stop: async () => { await client.captureStop(tabId); logger.info(`[${name}] capture stopped`); },
|
|
249
414
|
requests: () => client.captureRequests(tabId),
|
|
250
415
|
ws: () => client.captureWs(tabId),
|
|
251
416
|
cookies: () => client.captureCookies(tabId),
|
|
252
417
|
storage: () => client.captureStorage(tabId),
|
|
253
|
-
clear: async () => {
|
|
254
|
-
await client.captureClear(tabId);
|
|
255
|
-
logger.info(`[${name}] capture cleared`);
|
|
256
|
-
},
|
|
418
|
+
clear: async () => { await client.captureClear(tabId); logger.info(`[${name}] capture cleared`); },
|
|
257
419
|
},
|
|
258
420
|
|
|
259
421
|
// ── Session ────────────────────────────────────────────────────────────────
|
|
260
422
|
session: {
|
|
261
|
-
export: async () => {
|
|
262
|
-
|
|
263
|
-
logger.success(`[${name}] session exported`);
|
|
264
|
-
return data;
|
|
265
|
-
},
|
|
266
|
-
import: async (data: any) => {
|
|
267
|
-
await client.sessionImport(data, tabId);
|
|
268
|
-
logger.success(`[${name}] session imported`);
|
|
269
|
-
},
|
|
423
|
+
export: async () => { const data = await client.sessionExport(tabId); logger.success(`[${name}] session exported`); return data; },
|
|
424
|
+
import: async (data: any) => { await client.sessionImport(data, tabId); logger.success(`[${name}] session imported`); },
|
|
270
425
|
},
|
|
271
426
|
|
|
272
427
|
// ── Expose Function ─────────────────────────────────────────────────────────
|
|
@@ -275,19 +430,16 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
275
430
|
logger.success(`[${name}] exposed function: ${fnName}`);
|
|
276
431
|
return site;
|
|
277
432
|
},
|
|
278
|
-
|
|
279
433
|
unexposeFunction: async (fnName: string) => {
|
|
280
434
|
await client.unexposeFunction(fnName, tabId);
|
|
281
435
|
logger.info(`[${name}] unexposed function: ${fnName}`);
|
|
282
436
|
return site;
|
|
283
437
|
},
|
|
284
|
-
|
|
285
438
|
clearExposedFunctions: async () => {
|
|
286
439
|
await client.clearExposedFunctions(tabId);
|
|
287
440
|
logger.info(`[${name}] cleared all exposed functions`);
|
|
288
441
|
return site;
|
|
289
442
|
},
|
|
290
|
-
|
|
291
443
|
exposeAndInject: async (fnName: string, handler: (data: any) => Promise<any> | any, injectionJs: string | ((fnName: string) => string)) => {
|
|
292
444
|
await client.exposeFunction(fnName, handler, tabId);
|
|
293
445
|
const js = typeof injectionJs === "function" ? injectionJs(fnName) : injectionJs;
|
|
@@ -299,10 +451,7 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
299
451
|
// ── Elysia API ─────────────────────────────────────────────────────────────
|
|
300
452
|
api: (path: string, handler: RouteHandler, opts?: { ttl?: number; before?: BeforeMiddleware[]; method?: "GET" | "POST" | "PUT" | "DELETE" }) => {
|
|
301
453
|
const key = `${name}:${path}`;
|
|
302
|
-
if (routeRegistry.has(key)) {
|
|
303
|
-
logger.warn(`[${name}] route ${path} already registered`);
|
|
304
|
-
return site;
|
|
305
|
-
}
|
|
454
|
+
if (routeRegistry.has(key)) { logger.warn(`[${name}] route ${path} already registered`); return site; }
|
|
306
455
|
routeRegistry.set(key, {
|
|
307
456
|
path,
|
|
308
457
|
method: opts?.method ?? "GET",
|
|
@@ -317,6 +466,7 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
317
466
|
noclose: () => { keepAliveSites.add(name); logger.info(`[${name}] keep-alive`); return site; },
|
|
318
467
|
|
|
319
468
|
close: async () => {
|
|
469
|
+
_unsubNavigate(); // Clean up navigate listener
|
|
320
470
|
keepAliveSites.delete(name);
|
|
321
471
|
if (tabId !== "default") {
|
|
322
472
|
await client.closeTab(tabId);
|
|
@@ -333,12 +483,8 @@ export function createExposedAPI<T extends Record<string, (data: any) => any>>(s
|
|
|
333
483
|
const { method, args } = call;
|
|
334
484
|
const handler = handlers[method as keyof T];
|
|
335
485
|
if (!handler) throw new Error(`Unknown method: ${method}`);
|
|
336
|
-
try {
|
|
337
|
-
|
|
338
|
-
} catch (err) {
|
|
339
|
-
logger.error(`[${site._name}] API error in ${method}:`, err);
|
|
340
|
-
throw err;
|
|
341
|
-
}
|
|
486
|
+
try { return await handler(args); }
|
|
487
|
+
catch (err) { logger.error(`[${site._name}] API error in ${method}:`, err); throw err; }
|
|
342
488
|
};
|
|
343
489
|
return site.exposeFunction(apiName, wrappedHandler);
|
|
344
490
|
}
|
package/piggy.ts
CHANGED
|
@@ -14,6 +14,7 @@ let _tabMode: TabMode = "tab";
|
|
|
14
14
|
const _extraProcs: { socket: string; client: PiggyClient }[] = [];
|
|
15
15
|
const _sites: Record<string, SiteObject> = {};
|
|
16
16
|
|
|
17
|
+
// CREATE THE PIGGY OBJECT AS A PLAIN OBJECT - NOT A PROXY
|
|
17
18
|
const piggy: any = {
|
|
18
19
|
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -35,9 +36,12 @@ const piggy: any = {
|
|
|
35
36
|
|
|
36
37
|
let tabId = "default";
|
|
37
38
|
if (_tabMode === "tab") {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
if (!_client) throw new Error("No client. Call piggy.launch() first.");
|
|
40
|
+
tabId = await _client.newTab();
|
|
41
|
+
// HERE IT IS - CREATE SITE OBJECT AND ASSIGN DIRECTLY
|
|
42
|
+
const siteObj = createSiteObject(name, url, _client, tabId);
|
|
43
|
+
_sites[name] = siteObj;
|
|
44
|
+
piggy[name] = siteObj; // DIRECT ASSIGNMENT - NO PROXY
|
|
41
45
|
logger.success(`[${name}] registered as tab ${tabId}`);
|
|
42
46
|
} else {
|
|
43
47
|
const socketName = `piggy_${name}`;
|
|
@@ -46,8 +50,9 @@ const piggy: any = {
|
|
|
46
50
|
const c = new PiggyClient(socketName);
|
|
47
51
|
await c.connect();
|
|
48
52
|
_extraProcs.push({ socket: socketName, client: c });
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
const siteObj = createSiteObject(name, url, c, "default");
|
|
54
|
+
_sites[name] = siteObj;
|
|
55
|
+
piggy[name] = siteObj; // DIRECT ASSIGNMENT - NO PROXY
|
|
51
56
|
logger.success(`[${name}] registered as process on "${socketName}"`);
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -148,5 +153,6 @@ const piggy: any = {
|
|
|
148
153
|
logger,
|
|
149
154
|
};
|
|
150
155
|
|
|
156
|
+
// NO PROXY WRAPPER - EXPORT THE PLAIN OBJECT DIRECTLY
|
|
151
157
|
export default piggy;
|
|
152
158
|
export { piggy };
|