nothing-browser 0.0.4 → 0.0.5

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 CHANGED
@@ -1,6 +1,4 @@
1
- Here's the updated README with the `exposeFunction` feature documented:
2
1
 
3
- ```markdown
4
2
  <p align="center">
5
3
  <img src="nothing_browser_pig_pink.svg" width="160" alt="Nothing Browser logo"/>
6
4
  </p>
@@ -197,8 +195,469 @@ await piggy.whatsapp.evaluate(() => {
197
195
 
198
196
  console.log("Listening for WhatsApp messages...");
199
197
  ```
198
+ You're absolutely right. Let me add those two critical features to the README documentation. Here's the updated section to add:
199
+
200
+ ## Add this new section after the "Expose Function" section:
201
+
202
+ ```markdown
203
+ ---
204
+
205
+ ### Request Interception with Custom Response
206
+
207
+ Block, redirect, or **serve custom responses** to network requests. Perfect for:
208
+ - Caching API responses locally
209
+ - Mocking endpoints during development
210
+ - Serving a local web version cache
211
+ - Modifying response bodies on the fly
212
+
213
+ ```ts
214
+ import piggy from "nothing-browser";
215
+
216
+ await piggy.launch({ mode: "tab" });
217
+ await piggy.register("app", "https://your-app.com");
218
+
219
+ // Serve custom response for specific requests
220
+ await piggy.app.intercept.respond(
221
+ "*/api/users*",
222
+ async (request) => {
223
+ // Return custom response
224
+ return {
225
+ status: 200,
226
+ contentType: "application/json",
227
+ headers: { "X-Cache": "HIT" },
228
+ body: JSON.stringify({
229
+ users: [
230
+ { id: 1, name: "Cached User 1" },
231
+ { id: 2, name: "Cached User 2" },
232
+ ]
233
+ })
234
+ };
235
+ }
236
+ );
237
+
238
+ // Serve static file from disk
239
+ await piggy.app.intercept.respond(
240
+ "*/assets/bundle.js",
241
+ async () => {
242
+ const cached = await Bun.file("./cache/bundle.js").text();
243
+ return {
244
+ status: 200,
245
+ contentType: "application/javascript",
246
+ body: cached,
247
+ headers: { "X-Served-From": "local-cache" }
248
+ };
249
+ }
250
+ );
251
+
252
+ // Dynamic response based on request
253
+ await piggy.app.intercept.respond(
254
+ "*/api/product/*",
255
+ async (request) => {
256
+ const productId = request.url.match(/\/product\/(\d+)/)?.[1];
257
+
258
+ // Check local cache
259
+ const cached = await db.products.find(productId);
260
+ if (cached) {
261
+ return {
262
+ status: 200,
263
+ contentType: "application/json",
264
+ body: JSON.stringify(cached),
265
+ headers: { "X-Cache": "HIT" }
266
+ };
267
+ }
268
+
269
+ // Let the request through to the server
270
+ return null;
271
+ }
272
+ );
273
+
274
+ // Modify response on the fly
275
+ await piggy.app.intercept.modifyResponse(
276
+ "*/api/feed*",
277
+ async (response) => {
278
+ const data = await response.json();
279
+
280
+ // Add custom field to every item
281
+ data.items = data.items.map(item => ({
282
+ ...item,
283
+ _cached_at: Date.now(),
284
+ _source: 'modified-by-interceptor'
285
+ }));
286
+
287
+ return {
288
+ body: JSON.stringify(data),
289
+ headers: { "X-Modified": "true" }
290
+ };
291
+ }
292
+ );
293
+
294
+ await piggy.app.navigate();
295
+ ```
296
+
297
+ ### Response Interceptor API
298
+
299
+ ```ts
300
+ // Full response replacement
301
+ site.intercept.respond(pattern, handler)
302
+ // handler: (request: {
303
+ // url: string,
304
+ // method: string,
305
+ // headers: Record<string, string>,
306
+ // body?: string
307
+ // }) => Promise<{
308
+ // status?: number, // default: 200
309
+ // contentType?: string, // default: auto-detect
310
+ // headers?: Record<string, string>,
311
+ // body: string | Buffer
312
+ // } | null> // return null to pass through
313
+
314
+ // Modify existing response
315
+ site.intercept.modifyResponse(pattern, handler)
316
+ // handler: (response: {
317
+ // status: number,
318
+ // headers: Record<string, string>,
319
+ // body: string,
320
+ // json: () => Promise<any>
321
+ // }) => Promise<{
322
+ // status?: number,
323
+ // headers?: Record<string, string>,
324
+ // body?: string
325
+ // }>
326
+
327
+ // Block requests (existing)
328
+ site.intercept.block(pattern)
329
+
330
+ // Redirect requests (existing)
331
+ site.intercept.redirect(pattern, redirectUrl)
332
+
333
+ // Add/modify request headers (existing)
334
+ site.intercept.headers(pattern, headers)
335
+
336
+ // Clear all rules for this site
337
+ site.intercept.clear()
338
+ ```
339
+
340
+ ### Web Version Cache Example
341
+
342
+ ```ts
343
+ import piggy from "nothing-browser";
344
+
345
+ // Build a complete offline cache of your web app
346
+ const cache = new Map();
347
+
348
+ await piggy.launch({ mode: "tab" });
349
+ await piggy.register("spa", "https://your-spa.com");
350
+
351
+ // Cache all static assets
352
+ await piggy.spa.intercept.respond("*.js", async (req) => {
353
+ const key = req.url;
354
+ if (!cache.has(key)) {
355
+ const response = await fetch(req.url);
356
+ cache.set(key, await response.text());
357
+ console.log(`Cached: ${key}`);
358
+ }
359
+ return {
360
+ status: 200,
361
+ contentType: "application/javascript",
362
+ body: cache.get(key),
363
+ headers: { "X-Cache": "HIT" }
364
+ };
365
+ });
366
+
367
+ await piggy.spa.intercept.respond("*.css", async (req) => {
368
+ const key = req.url;
369
+ if (!cache.has(key)) {
370
+ const response = await fetch(req.url);
371
+ cache.set(key, await response.text());
372
+ }
373
+ return {
374
+ status: 200,
375
+ contentType: "text/css",
376
+ body: cache.get(key)
377
+ };
378
+ });
379
+
380
+ // Cache API responses with TTL
381
+ const apiCache = new Map();
382
+ await piggy.spa.intercept.respond("*/api/*", async (req) => {
383
+ const key = `${req.method}:${req.url}`;
384
+ const cached = apiCache.get(key);
385
+
386
+ if (cached && Date.now() < cached.expires) {
387
+ return {
388
+ status: 200,
389
+ contentType: "application/json",
390
+ body: cached.data,
391
+ headers: { "X-Cache": "HIT", "X-Cache-Age": String(Date.now() - cached.timestamp) }
392
+ };
393
+ }
394
+
395
+ // Pass through - will be cached by modifyResponse
396
+ return null;
397
+ });
398
+
399
+ await piggy.spa.intercept.modifyResponse("*/api/*", async (res) => {
400
+ const key = `${res.url}`;
401
+ const data = await res.json();
402
+
403
+ apiCache.set(key, {
404
+ data: JSON.stringify(data),
405
+ timestamp: Date.now(),
406
+ expires: Date.now() + 5 * 60 * 1000 // 5 minutes
407
+ });
408
+
409
+ return {
410
+ body: JSON.stringify(data),
411
+ headers: { ...res.headers, "X-Cache": "MISS" }
412
+ };
413
+ });
414
+
415
+ await piggy.spa.navigate();
416
+ // App now runs mostly from local cache!
417
+ ```
418
+
419
+ ---
420
+
421
+ ### Evaluate on New Document (Script Injection)
422
+
423
+ Inject JavaScript before any page JavaScript runs. Equivalent to Puppeteer's `page.evaluateOnNewDocument()`. Perfect for:
424
+ - Overriding browser APIs before they're accessed
425
+ - Setting up global state before page loads
426
+ - Disabling features like WebRTC, Canvas, etc.
427
+ - Installing persistent event listeners
428
+
429
+ ```ts
430
+ import piggy from "nothing-browser";
431
+
432
+ await piggy.launch({ mode: "tab" });
433
+ await piggy.register("site", "https://example.com");
434
+
435
+ // Inject before ANY page script runs
436
+ await piggy.site.addInitScript(`
437
+ // Override navigator properties
438
+ Object.defineProperty(navigator, 'webdriver', {
439
+ get: () => undefined
440
+ });
441
+
442
+ // Disable WebRTC
443
+ Object.defineProperty(navigator, 'mediaDevices', {
444
+ get: () => undefined
445
+ });
446
+
447
+ // Mock geolocation
448
+ navigator.geolocation.getCurrentPosition = (success) => {
449
+ success({
450
+ coords: {
451
+ latitude: 40.7128,
452
+ longitude: -74.0060,
453
+ accuracy: 10
454
+ },
455
+ timestamp: Date.now()
456
+ });
457
+ };
458
+
459
+ // Set up global state
460
+ window.__MY_APP_CONFIG__ = {
461
+ apiUrl: 'https://my-api.com',
462
+ debug: true,
463
+ version: '1.0.0'
464
+ };
465
+
466
+ console.log('[InitScript] Injected before page load');
467
+ `);
468
+
469
+ // Add multiple init scripts
470
+ await piggy.site.addInitScript(`
471
+ // Second script - runs in order
472
+ window.__FEATURE_FLAGS__ = {
473
+ newUI: true,
474
+ beta: false
475
+ };
476
+ `);
477
+
478
+ // Add init script from a function
479
+ await piggy.site.addInitScript(() => {
480
+ // This function will be stringified and injected
481
+ const originalFetch = window.fetch;
482
+ window.fetch = function(...args) {
483
+ console.log('[Fetch]', args[0]);
484
+ return originalFetch.apply(this, args);
485
+ };
486
+
487
+ // Disable battery API
488
+ if (navigator.getBattery) {
489
+ navigator.getBattery = undefined;
490
+ }
491
+ });
492
+
493
+ // Add init script that runs in all frames
494
+ await piggy.site.addInitScript(`
495
+ // This runs in iframes too
496
+ if (window.self !== window.top) {
497
+ console.log('[InitScript] Running in iframe');
498
+ }
499
+ `, { runInAllFrames: true });
500
+
501
+ // Remove a specific init script
502
+ const scriptId = await piggy.site.addInitScript(`...`);
503
+ await piggy.site.removeInitScript(scriptId);
504
+
505
+ // Clear all init scripts
506
+ await piggy.site.clearInitScripts();
507
+
508
+ // Now navigate - scripts will run BEFORE page loads
509
+ await piggy.site.navigate();
510
+ ```
511
+
512
+ ### Init Script API
513
+
514
+ ```ts
515
+ // Add script that runs before every page load
516
+ site.addInitScript(script, options?)
517
+ // script: string | (() => void)
518
+ // options: {
519
+ // runInAllFrames?: boolean, // default: false
520
+ // world?: "main" | "isolated", // default: "main"
521
+ // name?: string // optional identifier
522
+ // }
523
+ // Returns: string (script ID)
524
+
525
+ // Remove specific init script
526
+ site.removeInitScript(scriptId)
527
+
528
+ // Remove all init scripts
529
+ site.clearInitScripts()
530
+
531
+ // Get all registered init scripts
532
+ site.getInitScripts()
533
+ // Returns: Array<{ id: string, name?: string, runInAllFrames: boolean }>
534
+ ```
535
+
536
+ ### Advanced: Persistent Init Scripts Across Navigations
537
+
538
+ ```ts
539
+ // Scripts survive navigation automatically!
540
+ await piggy.site.addInitScript(`
541
+ window.__SESSION_ID__ = '${crypto.randomUUID()}';
542
+ window.__START_TIME__ = Date.now();
543
+ `);
544
+
545
+ await piggy.site.navigate("https://example.com/page1");
546
+ // Script runs here ✓
547
+
548
+ await piggy.site.click("a[href='/page2']");
549
+ await piggy.site.waitForNavigation();
550
+ // Script runs again automatically ✓
551
+
552
+ // Check that it persisted
553
+ const sessionId = await piggy.site.evaluate(() => window.__SESSION_ID__);
554
+ console.log(sessionId); // Same UUID across pages!
555
+ ```
556
+
557
+ ### Complete Anti-Detection Setup
558
+
559
+ ```ts
560
+ import piggy from "nothing-browser";
561
+
562
+ await piggy.launch({ mode: "tab", binary: "headful" });
563
+ await piggy.register("stealth", "https://example.com");
564
+
565
+ // Built-in fingerprint spoofing is already enabled
566
+ // Add additional init scripts for maximum stealth
567
+
568
+ await piggy.stealth.addInitScript(`
569
+ // Remove automation traces
570
+ delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
571
+ delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
572
+ delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
573
+
574
+ // Override permissions
575
+ const originalQuery = window.navigator.permissions.query;
576
+ window.navigator.permissions.query = (parameters) => (
577
+ parameters.name === 'notifications' ||
578
+ parameters.name === 'geolocation' ||
579
+ parameters.name === 'camera' ||
580
+ parameters.name === 'microphone'
581
+ ) ? Promise.resolve({ state: 'prompt', onchange: null })
582
+ : originalQuery(parameters);
583
+
584
+ // Fake plugins
585
+ Object.defineProperty(navigator, 'plugins', {
586
+ get: () => [
587
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
588
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
589
+ { name: 'Native Client', filename: 'internal-nacl-plugin' }
590
+ ]
591
+ });
592
+
593
+ // Fake languages
594
+ Object.defineProperty(navigator, 'languages', {
595
+ get: () => ['en-US', 'en']
596
+ });
597
+
598
+ // WebGL vendor spoof
599
+ const getParameter = WebGLRenderingContext.prototype.getParameter;
600
+ WebGLRenderingContext.prototype.getParameter = function(parameter) {
601
+ if (parameter === 37445) return 'Intel Inc.';
602
+ if (parameter === 37446) return 'Intel Iris OpenGL Engine';
603
+ return getParameter.call(this, parameter);
604
+ };
605
+ `);
606
+
607
+ // Block tracking domains
608
+ await piggy.stealth.intercept.block("*google-analytics.com*");
609
+ await piggy.stealth.intercept.block("*doubleclick.net*");
610
+ await piggy.stealth.intercept.block("*facebook.com/tr*");
611
+
612
+ // Add human-like behavior
613
+ piggy.actHuman(true);
614
+
615
+ await piggy.stealth.navigate();
616
+ // You're now virtually undetectable
617
+ ```
618
+
619
+ ---
620
+
621
+ ## Updated API Reference Section
622
+ ```ts
623
+ // Block requests (existing)
624
+ site.intercept.block(pattern)
625
+
626
+ // Redirect requests (existing)
627
+ site.intercept.redirect(pattern, redirectUrl)
628
+
629
+ // Add/modify request headers (existing)
630
+ site.intercept.headers(pattern, headers)
631
+
632
+ // 🔥 NEW: Serve custom response
633
+ site.intercept.respond(pattern, handler)
634
+ // handler receives request details, returns response or null
635
+
636
+ // 🔥 NEW: Modify response on the fly
637
+ site.intercept.modifyResponse(pattern, handler)
638
+ // handler receives response, returns modifications
639
+
640
+ // Clear all rules
641
+ site.intercept.clear()
642
+ ```
643
+
644
+ #### Script Injection
645
+ ```ts
646
+ // 🔥 NEW: Inject before page loads (evaluateOnNewDocument)
647
+ site.addInitScript(script, options?)
648
+ // script: string | function
649
+ // options: { runInAllFrames?: boolean, world?: "main" | "isolated", name?: string }
650
+ // Returns: string (script ID)
651
+
652
+ // 🔥 NEW: Remove specific init script
653
+ site.removeInitScript(scriptId)
654
+
655
+ // 🔥 NEW: Remove all init scripts
656
+ site.clearInitScripts()
657
+
658
+ // 🔥 NEW: List all init scripts
659
+ site.getInitScripts()
200
660
 
201
- ### Expose + Inject convenience method
202
661
 
203
662
  ```ts
204
663
  await piggy.whatsapp.exposeAndInject(
@@ -0,0 +1,35 @@
1
+ // piggy/cache/memory.ts
2
+ var store = new Map;
3
+ function get(key) {
4
+ const entry = store.get(key);
5
+ if (!entry)
6
+ return null;
7
+ if (Date.now() > entry.expires) {
8
+ store.delete(key);
9
+ return null;
10
+ }
11
+ return entry.data;
12
+ }
13
+ function set(key, data, ttlMs) {
14
+ store.set(key, { data, expires: Date.now() + ttlMs });
15
+ }
16
+ function del(key) {
17
+ store.delete(key);
18
+ }
19
+ function clear() {
20
+ store.clear();
21
+ }
22
+ function size() {
23
+ return store.size;
24
+ }
25
+ function keys() {
26
+ return Array.from(store.keys());
27
+ }
28
+ export {
29
+ size,
30
+ set,
31
+ keys,
32
+ get,
33
+ del,
34
+ clear
35
+ };