nothing-browser 0.1.1 → 0.1.3

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,4 +1,4 @@
1
- // piggy/client/index.ts
1
+ //piggy/client/index.ts
2
2
  import { connect, type Socket } from "net";
3
3
  import { writeFileSync, mkdirSync } from "fs";
4
4
  import { dirname } from "path";
@@ -280,6 +280,23 @@ export class PiggyClient {
280
280
  }
281
281
  }
282
282
  }
283
+
284
+ if (event.event === "dialog") {
285
+ const key = `dialog:${event.tabId ?? "default"}`;
286
+ const handlers = this.globalEventHandlers.get(key);
287
+ if (handlers) {
288
+ for (const h of handlers) {
289
+ try {
290
+ h({
291
+ dialogType: event.dialogType,
292
+ message: event.message,
293
+ defaultValue: event.defaultValue,
294
+ tabId: event.tabId,
295
+ });
296
+ } catch (e) { logger.error(`dialog handler error: ${e}`); }
297
+ }
298
+ }
299
+ }
283
300
  }
284
301
 
285
302
  onEvent(eventName: string, tabId: string, handler: (data: any) => void): () => void {
@@ -384,9 +401,9 @@ export class PiggyClient {
384
401
 
385
402
  // ── Cookies ───────────────────────────────────────────────────────────────
386
403
  async setCookie(name: string, value: string, domain: string, path = "/", tabId = "default"): Promise<void> { await this.send("cookie.set", { name, value, domain, path, tabId }); }
387
- async getCookie(name: string, tabId = "default"): Promise<any> { return this.send("cookie.get", { name, tabId }); }
388
- async deleteCookie(name: string, tabId = "default"): Promise<void> { await this.send("cookie.delete", { name, tabId }); }
389
- async listCookies(tabId = "default"): Promise<any[]> { return this.send<any[]>("cookie.list", { tabId }); }
404
+ async getCookie(name: string, domain = "", tabId = "default"): Promise<any> { return this.send("cookie.get", { name, domain, tabId }); }
405
+ async deleteCookie(name: string, domain: string, tabId = "default"): Promise<void> { await this.send("cookie.delete", { name, domain, tabId }); }
406
+ async listCookies(domain = "", tabId = "default"): Promise<any[]> { return this.send<any[]>("cookie.list", { domain, tabId }); }
390
407
 
391
408
  // ── Interception ──────────────────────────────────────────────────────────
392
409
  async addInterceptRule(action: "block" | "redirect" | "modifyHeaders", pattern: string, options: { redirectUrl?: string; headers?: Record<string, string> } = {}, tabId = "default"): Promise<void> {
@@ -407,55 +424,14 @@ export class PiggyClient {
407
424
  async sessionExport(tabId = "default"): Promise<any> { return this.send("session.export", { tabId }); }
408
425
  async sessionImport(data: any, tabId = "default"): Promise<void> { await this.send("session.import", { data, tabId }); }
409
426
 
410
- // ── Session persistence (opt-in) ──────────────────────────────────────────
411
- // WS frames and pings are NOT saved by default you must opt in.
412
- // Files are written to cwd (same folder as cookies.json / profile.json).
413
-
414
- /** Enable or disable saving WebSocket frames to ws.json in cwd */
415
- async sessionWsSave(enabled = true): Promise<void> {
416
- await this.send("session.ws.save", { enabled });
417
- }
418
-
419
- /** Enable or disable saving ping log to pings.json in cwd */
420
- async sessionPingsSave(enabled = true): Promise<void> {
421
- await this.send("session.pings.save", { enabled });
422
- }
423
-
424
- /** Get all data file paths for the current session */
425
- async sessionPaths(): Promise<{
426
- workDir: string;
427
- cookies: string;
428
- profile: string;
429
- ws: string;
430
- pings: string;
431
- }> {
432
- return this.send("session.paths", {});
433
- }
434
-
435
- /** Get path to cookies.json */
436
- async sessionCookiesPath(): Promise<string> {
437
- return this.send("session.cookies.path", {});
438
- }
439
-
440
- /** Get path to profile.json */
441
- async sessionProfilePath(): Promise<string> {
442
- return this.send("session.profile.path", {});
443
- }
444
-
445
- /** Get path to ws.json */
446
- async sessionWsPath(): Promise<string> {
447
- return this.send("session.ws.path", {});
448
- }
449
-
450
- /** Get path to pings.json */
451
- async sessionPingsPath(): Promise<string> {
452
- return this.send("session.pings.path", {});
453
- }
454
-
455
- /** Reload cookies.json and profile.json from disk without restarting */
456
- async sessionReload(): Promise<void> {
457
- await this.send("session.reload", {});
458
- }
427
+ async sessionWsSave(enabled = true): Promise<void> { await this.send("session.ws.save", { enabled }); }
428
+ async sessionPingsSave(enabled = true): Promise<void> { await this.send("session.pings.save", { enabled }); }
429
+ async sessionPaths(): Promise<{ workDir: string; cookies: string; profile: string; ws: string; pings: string; }> { return this.send("session.paths", {}); }
430
+ async sessionCookiesPath(): Promise<string> { return this.send("session.cookies.path", {}); }
431
+ async sessionProfilePath(): Promise<string> { return this.send("session.profile.path", {}); }
432
+ async sessionWsPath(): Promise<string> { return this.send("session.ws.path", {}); }
433
+ async sessionPingsPath(): Promise<string> { return this.send("session.pings.path", {}); }
434
+ async sessionReload(): Promise<void> { await this.send("session.reload", {}); }
459
435
 
460
436
  // ── Expose Function ───────────────────────────────────────────────────────
461
437
  async exposeFunction(name: string, handler: (data: any) => Promise<any> | any, tabId = "default"): Promise<void> {
@@ -485,82 +461,28 @@ export class PiggyClient {
485
461
  }
486
462
 
487
463
  // ── Proxy ─────────────────────────────────────────────────────────────────
488
-
489
- async proxyLoad(path: string): Promise<void> {
490
- await this.send("proxy.load", { path });
491
- }
492
-
493
- async proxyFetch(url: string): Promise<void> {
494
- await this.send("proxy.fetch", { url });
495
- }
496
-
497
- async proxyOvpn(path: string): Promise<void> {
498
- await this.send("proxy.ovpn", { path });
499
- }
464
+ async proxyLoad(path: string): Promise<void> { await this.send("proxy.load", { path }); }
465
+ async proxyFetch(url: string): Promise<void> { await this.send("proxy.fetch", { url }); }
466
+ async proxyOvpn(path: string): Promise<void> { await this.send("proxy.ovpn", { path }); }
500
467
 
501
468
  async proxySet(opts: {
502
- host?: string;
503
- port?: number;
469
+ host?: string; port?: number;
504
470
  type?: "http" | "https" | "socks5" | "socks4";
505
- user?: string;
506
- pass?: string;
507
- proxy?: string;
508
- }): Promise<void> {
509
- await this.send("proxy.set", opts as Record<string, any>);
510
- }
511
-
512
- async proxyTest(): Promise<void> {
513
- await this.send("proxy.test", {});
514
- }
515
-
516
- async proxyTestStop(): Promise<void> {
517
- await this.send("proxy.test.stop", {});
518
- }
519
-
520
- async proxyNext(): Promise<void> {
521
- await this.send("proxy.next", {});
522
- }
523
-
524
- async proxyDisable(): Promise<void> {
525
- await this.send("proxy.disable", {});
526
- }
527
-
528
- async proxyEnable(): Promise<void> {
529
- await this.send("proxy.enable", {});
530
- }
531
-
532
- async proxyCurrent(): Promise<{
533
- host: string; port: number; type: string;
534
- user?: string; alive: boolean; latencyMs?: number;
535
- }> {
536
- return this.send("proxy.current", {});
537
- }
538
-
539
- async proxyStats(): Promise<{
540
- total: number; alive: number; dead: number;
541
- index: number; checking: boolean;
542
- }> {
543
- return this.send("proxy.stats", {});
544
- }
545
-
546
- async proxyList(limit?: number): Promise<{
547
- host: string; port: number; type: string;
548
- alive: boolean; latencyMs?: number;
549
- }[]> {
550
- return this.send("proxy.list", limit !== undefined ? { limit } : {});
551
- }
552
-
553
- async proxyRotation(mode: "none" | "timed" | "perrequest", interval?: number): Promise<void> {
554
- await this.send("proxy.rotation", { mode, ...(interval !== undefined ? { interval } : {}) });
555
- }
556
-
557
- async proxyConfig(opts: { skipDead?: boolean; autoCheck?: boolean }): Promise<void> {
558
- await this.send("proxy.config", opts as Record<string, any>);
559
- }
560
-
561
- async proxySave(path: string, filter: "alive" | "dead" | "all" = "all"): Promise<void> {
562
- await this.send("proxy.save", { path, filter });
563
- }
471
+ user?: string; pass?: string; proxy?: string;
472
+ }): Promise<void> { await this.send("proxy.set", opts as Record<string, any>); }
473
+
474
+ async proxyTest(): Promise<void> { await this.send("proxy.test", {}); }
475
+ async proxyTestStop(): Promise<void> { await this.send("proxy.test.stop", {}); }
476
+ async proxyNext(): Promise<void> { await this.send("proxy.next", {}); }
477
+ async proxyDisable(): Promise<void> { await this.send("proxy.disable", {}); }
478
+ async proxyEnable(): Promise<void> { await this.send("proxy.enable", {}); }
479
+
480
+ async proxyCurrent(): Promise<{ host: string; port: number; type: string; user?: string; alive: boolean; latencyMs?: number; }> { return this.send("proxy.current", {}); }
481
+ async proxyStats(): Promise<{ total: number; alive: number; dead: number; index: number; checking: boolean; }> { return this.send("proxy.stats", {}); }
482
+ async proxyList(limit?: number): Promise<{ host: string; port: number; type: string; alive: boolean; latencyMs?: number; }[]> { return this.send("proxy.list", limit !== undefined ? { limit } : {}); }
483
+ async proxyRotation(mode: "none" | "timed" | "perrequest", interval?: number): Promise<void> { await this.send("proxy.rotation", { mode, ...(interval !== undefined ? { interval } : {}) }); }
484
+ async proxyConfig(opts: { skipDead?: boolean; autoCheck?: boolean }): Promise<void> { await this.send("proxy.config", opts as Record<string, any>); }
485
+ async proxySave(path: string, filter: "alive" | "dead" | "all" = "all"): Promise<void> { await this.send("proxy.save", { path, filter }); }
564
486
 
565
487
  onProxyEvent(event: string, handler: (data: any) => void): () => void {
566
488
  return this.onEvent(event, "*", handler);
@@ -0,0 +1,42 @@
1
+ import { PiggyClient } from "../client";
2
+ import logger from "../logger";
3
+
4
+ export async function exposeFunction(
5
+ client: PiggyClient,
6
+ fnName: string,
7
+ handler: (data: any) => Promise<any> | any,
8
+ tabId: string
9
+ ): Promise<void> {
10
+ await client.exposeFunction(fnName, handler, tabId);
11
+ logger.success(`[${tabId}] exposed function: ${fnName}`);
12
+ }
13
+
14
+ export async function unexposeFunction(
15
+ client: PiggyClient,
16
+ fnName: string,
17
+ tabId: string
18
+ ): Promise<void> {
19
+ await client.unexposeFunction(fnName, tabId);
20
+ logger.info(`[${tabId}] unexposed function: ${fnName}`);
21
+ }
22
+
23
+ export async function clearExposedFunctions(
24
+ client: PiggyClient,
25
+ tabId: string
26
+ ): Promise<void> {
27
+ await client.clearExposedFunctions(tabId);
28
+ logger.info(`[${tabId}] cleared all exposed functions`);
29
+ }
30
+
31
+ export async function exposeAndInject(
32
+ client: PiggyClient,
33
+ fnName: string,
34
+ handler: (data: any) => Promise<any> | any,
35
+ injectionJs: string | ((fnName: string) => string),
36
+ tabId: string
37
+ ): Promise<void> {
38
+ await client.exposeFunction(fnName, handler, tabId);
39
+ const js = typeof injectionJs === "function" ? injectionJs(fnName) : injectionJs;
40
+ await client.evaluate(js, tabId);
41
+ logger.success(`[${tabId}] exposed and injected: ${fnName}`);
42
+ }
@@ -6,7 +6,14 @@ import { routeRegistry, keepAliveSites, type RouteHandler, type BeforeMiddleware
6
6
  import { buildRespondScript, buildModifyResponseScript } from "../intercept/scripts";
7
7
  import { storeRecord } from "../store";
8
8
  import { TabPool } from "../pool";
9
-
9
+ import { createFindAPI } from "../find";
10
+ import { createProvideAPI } from "../provide";
11
+ import { createHumanAPI } from "../human";
12
+ import { createSessionAPI } from "../session";
13
+ import { DialogClient } from "../dialog";
14
+ import { createIframeAPI } from "../iframe";
15
+ import { exposeFunction, unexposeFunction, clearExposedFunctions, exposeAndInject } from "../expose";
16
+ import { createCaptchaAPI } from "../captcha";
10
17
  let globalClient: PiggyClient | null = null;
11
18
  export let humanMode = false;
12
19
 
@@ -105,6 +112,18 @@ export function createSiteObject(
105
112
  return client.waitForSelector(selector, timeout, t);
106
113
  }),
107
114
 
115
+ exposeFunction: (fnName: string, handler: (data: any) => Promise<any> | any) =>
116
+ exposeFunction(client, fnName, handler, tabId).then(() => site),
117
+
118
+ unexposeFunction: (fnName: string) =>
119
+ unexposeFunction(client, fnName, tabId).then(() => site),
120
+
121
+ clearExposedFunctions: () =>
122
+ clearExposedFunctions(client, tabId).then(() => site),
123
+
124
+ exposeAndInject: (fnName: string, handler: (data: any) => Promise<any> | any, injectionJs: string | ((fnName: string) => string)) =>
125
+ exposeAndInject(client, fnName, handler, injectionJs, tabId).then(() => site),
126
+
108
127
  waitForVisible: (selector: string, timeout = 30000) =>
109
128
  withTab(t => client.waitForSelector(selector, timeout, t)),
110
129
 
@@ -257,6 +276,69 @@ export function createSiteObject(
257
276
  css: (query: string) => withTab(t => client.searchCss(query, t)),
258
277
  id: (query: string) => withTab(t => client.searchId(query, t)),
259
278
  },
279
+ captcha: {
280
+ status: () => withTab(t => createCaptchaAPI(client).status(t)),
281
+ resolve: () => withTab(t => createCaptchaAPI(client).resolve(t)),
282
+ pause: () => withTab(t => createCaptchaAPI(client).pause(t)),
283
+ check: () => withTab(t => createCaptchaAPI(client).check(t)),
284
+ autoRetry: (opts: { enabled: boolean }) => withTab(t => createCaptchaAPI(client).setAutoRetry(opts.enabled)),
285
+ onCaptcha: (handler: any) => createCaptchaAPI(client).onCaptcha(tabId, handler),
286
+ onResolved:(handler: any) => createCaptchaAPI(client).onCaptchaResolved(tabId, handler),
287
+ },
288
+ block: {
289
+ status: () => withTab(t => createCaptchaAPI(client).blockStatus(t)),
290
+ retry: () => withTab(t => createCaptchaAPI(client).blockRetry(t)),
291
+ onBlocked: (handler: any) => createCaptchaAPI(client).onBlocked(tabId, handler),
292
+ onRetry: (handler: any) => createCaptchaAPI(client).onBlockRetry(tabId, handler),
293
+ },
294
+ find: {
295
+ css: (selector: string) => withTab(t => {
296
+ console.log("[DEBUG] find.css tabId:", t);
297
+ return createFindAPI(client).css(selector, t);
298
+ }),
299
+ all: (selector: string) => withTab(t => createFindAPI(client).all(selector, t)),
300
+ first: (selector: string) => withTab(t => createFindAPI(client).first(selector, t)),
301
+ byText: (text: string) => withTab(t => createFindAPI(client).byText(text, t)),
302
+ byAttr: (attr: string, value?: string) => withTab(t => createFindAPI(client).byAttr(attr, value, t)),
303
+ byTag: (tag: string) => withTab(t => createFindAPI(client).byTag(tag, t)),
304
+ byPlaceholder: (text: string) => withTab(t => createFindAPI(client).byPlaceholder(text, t)),
305
+ byRole: (role: string, name?: string) => withTab(t => createFindAPI(client).byRole(role, name, t)),
306
+ children: (selector: string) => withTab(t => createFindAPI(client).children(selector, t)),
307
+ parent: (selector: string) => withTab(t => createFindAPI(client).parent(selector, t)),
308
+ closest: (selector: string, ancestor: string) => withTab(t => createFindAPI(client).closest(selector, ancestor, t)),
309
+ count: (selector: string) => withTab(t => createFindAPI(client).count(selector, t)),
310
+ exists: (selector: string) => withTab(t => createFindAPI(client).exists(selector, t)),
311
+ visible: (selector: string) => withTab(t => createFindAPI(client).visible(selector, t)),
312
+ enabled: (selector: string) => withTab(t => createFindAPI(client).enabled(selector, t)),
313
+ checked: (selector: string) => withTab(t => createFindAPI(client).checked(selector, t)),
314
+ },
315
+
316
+ provide: {
317
+ text: (opts: any) => withTab(t => createProvideAPI(client).text(opts, t)),
318
+ textAll: (opts: any) => withTab(t => createProvideAPI(client).textAll(opts, t)),
319
+ attr: (opts: any) => withTab(t => createProvideAPI(client).attr(opts, t)),
320
+ attrAll: (opts: any) => withTab(t => createProvideAPI(client).attrAll(opts, t)),
321
+ html: (opts: any) => withTab(t => createProvideAPI(client).html(opts, t)),
322
+ table: (opts: any) => withTab(t => createProvideAPI(client).table(opts, t)),
323
+ list: (opts: any) => withTab(t => createProvideAPI(client).list(opts, t)),
324
+ links: (opts: any) => withTab(t => createProvideAPI(client).links(opts, t)),
325
+ images: (opts: any) => withTab(t => createProvideAPI(client).images(opts, t)),
326
+ form: (opts: any) => withTab(t => createProvideAPI(client).form(opts, t)),
327
+ page: () => withTab(t => createProvideAPI(client).page(t)),
328
+ div: (opts: any) => withTab(t => createProvideAPI(client).div(opts, t)),
329
+ meta: () => withTab(t => createProvideAPI(client).meta(t)),
330
+ select: (opts: any) => withTab(t => createProvideAPI(client).select(opts, t)),
331
+ json: (opts?: any) => withTab(t => createProvideAPI(client).json(opts, t)),
332
+ },
333
+ iframe: {
334
+ list: () => withTab(t => createIframeAPI(client).list(t)),
335
+ evaluate:(opts: any) => withTab(t => createIframeAPI(client).evaluate(opts, t)),
336
+ click: (opts: any) => withTab(t => createIframeAPI(client).click(opts, t)),
337
+ type: (opts: any) => withTab(t => createIframeAPI(client).type(opts, t)),
338
+ text: (opts: any) => withTab(t => createIframeAPI(client).text(opts, t)),
339
+ html: (opts: any) => withTab(t => createIframeAPI(client).html(opts, t)),
340
+ waitSel: (opts: any) => withTab(t => createIframeAPI(client).waitSel(opts, t)),
341
+ },
260
342
 
261
343
  screenshot: async (filePath?: string) => {
262
344
  const r = await withTab(t => client.screenshot(filePath, t));
@@ -269,6 +351,13 @@ export function createSiteObject(
269
351
  logger.success(`[${name}] pdf → ${filePath ?? "base64"}`);
270
352
  return r;
271
353
  },
354
+ human: {
355
+ set: (opts: any) => withTab(t => createHumanAPI(client).set(opts, t)),
356
+ get: () => withTab(t => createHumanAPI(client).get(t)),
357
+ type: (opts: any) => withTab(t => createHumanAPI(client).type(opts, t)),
358
+ click: (opts: any) => withTab(t => createHumanAPI(client).click(opts, t)),
359
+ },
360
+
272
361
 
273
362
  blockImages: () => withTab(async t => { await client.blockImages(t); logger.info(`[${name}] images blocked`); }),
274
363
  unblockImages: () => withTab(async t => { await client.unblockImages(t); logger.info(`[${name}] images unblocked`); }),
@@ -278,14 +367,14 @@ export function createSiteObject(
278
367
  await withTab(t => client.setCookie(cookieName, value, domain, path, t));
279
368
  logger.info(`[${name}] cookie set: ${cookieName} @ ${domain}`);
280
369
  },
281
- get: (cookieName: string) => withTab(t => client.getCookie(cookieName, t)),
282
- delete: async (cookieName: string) => {
283
- await withTab(t => client.deleteCookie(cookieName, t));
370
+ get: (cookieName: string, domain = "") => withTab(t => client.getCookie(cookieName, domain, t)),
371
+ delete: async (cookieName: string, domain?: string) => {
372
+ const d = domain ?? new URL(registeredUrl).hostname;
373
+ await withTab(t => client.deleteCookie(cookieName, d, t));
284
374
  logger.info(`[${name}] cookie deleted: ${cookieName}`);
285
375
  },
286
- list: () => withTab(t => client.listCookies(t)),
376
+ list: (domain = "") => withTab(t => client.listCookies(domain, t)),
287
377
  },
288
-
289
378
  intercept: {
290
379
  block: async (pattern: string) => {
291
380
  await withTab(t => client.addInterceptRule("block", pattern, {}, t));
@@ -385,6 +474,16 @@ export function createSiteObject(
385
474
  logger.info(`[${name}] intercept rules cleared`);
386
475
  },
387
476
  },
477
+ dialog: {
478
+ accept: (tabId = "default", text?: string) => new DialogClient(client).accept(tabId, text),
479
+ dismiss: (tabId = "default") => new DialogClient(client).dismiss(tabId),
480
+ status: (tabId = "default") => new DialogClient(client).status(tabId),
481
+ setAutoAction: (tabId = "default", action: "accept" | "dismiss" | "") => new DialogClient(client).setAutoAction(tabId, action),
482
+ upload: (selector: string, filePath: string, tabId = "default") => new DialogClient(client).upload(selector, filePath, tabId),
483
+ onDialog: (tabId: string, handler: (data: any) => void) => new DialogClient(client).onDialog(tabId, handler),
484
+ waitAndAccept: (tabId = "default", text?: string, timeoutMs = 30000) => new DialogClient(client).waitAndAccept(tabId, text, timeoutMs),
485
+ waitAndDismiss:(tabId = "default", timeoutMs = 30000) => new DialogClient(client).waitAndDismiss(tabId, timeoutMs),
486
+ },
388
487
 
389
488
  capture: {
390
489
  start: () => withTab(async t => { await client.captureStart(t); logger.info(`[${name}] capture started`); }),
@@ -396,40 +495,16 @@ export function createSiteObject(
396
495
  clear: () => withTab(async t => { await client.captureClear(t); logger.info(`[${name}] capture cleared`); }),
397
496
  },
398
497
 
399
- session: {
400
- export: async () => {
401
- const data = await withTab(t => client.sessionExport(t));
402
- logger.success(`[${name}] session exported`);
403
- return data;
404
- },
405
- import: async (data: any) => {
406
- await withTab(t => client.sessionImport(data, t));
407
- logger.success(`[${name}] session imported`);
408
- },
409
- },
410
-
411
- exposeFunction: async (fnName: string, handler: (data: any) => Promise<any> | any) => {
412
- await client.exposeFunction(fnName, handler, tabId);
413
- logger.success(`[${name}] exposed function: ${fnName}`);
414
- return site;
415
- },
416
- unexposeFunction: async (fnName: string) => {
417
- await client.unexposeFunction(fnName, tabId);
418
- logger.info(`[${name}] unexposed function: ${fnName}`);
419
- return site;
420
- },
421
- clearExposedFunctions: async () => {
422
- await client.clearExposedFunctions(tabId);
423
- logger.info(`[${name}] cleared all exposed functions`);
424
- return site;
425
- },
426
- exposeAndInject: async (fnName: string, handler: (data: any) => Promise<any> | any, injectionJs: string | ((fnName: string) => string)) => {
427
- await client.exposeFunction(fnName, handler, tabId);
428
- const js = typeof injectionJs === "function" ? injectionJs(fnName) : injectionJs;
429
- await withTab(t => client.evaluate(js, t));
430
- logger.success(`[${name}] exposed and injected: ${fnName}`);
431
- return site;
432
- },
498
+ session: {
499
+ export: () => withTab(t => createSessionAPI(client).export(t)),
500
+ import: (data: any) => withTab(t => createSessionAPI(client).import(data, t)),
501
+ reload: () => withTab(t => createSessionAPI(client).reload(t)),
502
+ paths: () => createSessionAPI(client).paths(),
503
+ cookies: { path: () => createSessionAPI(client).cookiesPath() },
504
+ profile: { path: () => createSessionAPI(client).profilePath() },
505
+ ws: { save: (opts: any) => createSessionAPI(client).setWsSave(opts.enabled) },
506
+ pings: { save: (opts: any) => createSessionAPI(client).setPingsSave(opts.enabled) },
507
+ },
433
508
 
434
509
  store: async (
435
510
  data: Record<string, any> | Record<string, any>[],
@@ -8,7 +8,8 @@ export class TabsClient {
8
8
  return this.client.send("tab.new", {});
9
9
  }
10
10
 
11
- close(tabId: string): Promise<void> {
11
+ close(opts: string | { tabId: string }): Promise<void> {
12
+ const tabId = typeof opts === "string" ? opts : opts.tabId;
12
13
  return this.client.send("tab.close", { tabId });
13
14
  }
14
15
 
package/piggy.ts CHANGED
@@ -1,4 +1,4 @@
1
- // piggy.ts
1
+ // piggy.ts — patched: adds single:true + piggy.extend()
2
2
  import { detectBinary, type BinaryMode } from "./piggy/launch/detect";
3
3
  import { spawnBrowser, killBrowser, spawnBrowserOnSocket } from "./piggy/launch/spawn";
4
4
  import { PiggyClient } from "./piggy/client";
@@ -26,12 +26,19 @@ import logger from "./piggy/logger";
26
26
 
27
27
  type TabMode = "tab" | "process";
28
28
 
29
+ // A plugin installer is an async function that receives a site and enriches it.
30
+ type PluginInstaller = (site: SiteObject) => Promise<any>;
31
+
29
32
  let _client: PiggyClient | null = null;
30
33
  let _router: PiggyRouter | null = null;
31
34
  let _tabMode: TabMode = "tab";
32
35
  const _extraProcs: { socket: string; client: PiggyClient }[] = [];
33
36
  const _sites: Record<string, SiteObject> = {};
34
37
 
38
+ // ── Single-site tracking for extend() ────────────────────────────────────────
39
+ // Only one site may be registered with { single: true } at a time.
40
+ let _singleSiteName: string | null = null;
41
+
35
42
  // ── Internal guard ────────────────────────────────────────────────────────────
36
43
 
37
44
  function guardClient(): PiggyClient {
@@ -73,37 +80,57 @@ const piggy: any = {
73
80
  },
74
81
 
75
82
  // ── HTTP client (port 2005 direct) ────────────────────────────────────────
76
- // Use when you want to talk to the browser over HTTP without a socket client.
77
83
  http: (opts: HttpClientOptions) => createHttpClient(opts),
78
84
 
79
85
  // ── Register ──────────────────────────────────────────────────────────────
80
86
  register: async (
81
87
  name: string,
82
88
  url: string,
83
- opts?: { binary?: BinaryMode; pool?: number }
89
+ opts?: { binary?: BinaryMode; pool?: number; single?: boolean }
84
90
  ) => {
85
91
  if (!url?.trim()) throw new Error(`No URL for site "${name}"`);
92
+
86
93
  const binaryMode: BinaryMode = opts?.binary ?? "headless";
87
- const poolSize = opts?.pool ?? 0;
94
+ const poolSize = opts?.pool ?? 0;
95
+ const isSingle = opts?.single === true;
96
+
97
+ // ── single: true enforcement ───────────────────────────────────────────
98
+ // A single-site registration uses the default tab and blocks tab.new so
99
+ // the binary stays strictly single-tab. Only one site may be single.
100
+ if (isSingle && _singleSiteName && _singleSiteName !== name) {
101
+ throw new Error(
102
+ `piggy: site "${_singleSiteName}" is already registered as single. ` +
103
+ `Only one site may use { single: true } at a time.`
104
+ );
105
+ }
88
106
 
89
107
  if (_tabMode === "tab") {
90
108
  const client = guardClient();
91
109
 
92
110
  if (poolSize > 1) {
111
+ if (isSingle) throw new Error('piggy: { single: true } is incompatible with pool > 1');
93
112
  const pool = new TabPool(client, poolSize, url, name);
94
113
  await pool.init();
95
114
  const siteObj = createSiteObject(name, url, client, "default", pool);
96
115
  _sites[name] = siteObj;
97
- piggy[name] = siteObj;
116
+ piggy[name] = siteObj;
98
117
  logger.success(`[${name}] registered with pool of ${poolSize} tabs`);
99
118
  } else {
100
- const tabId = await client.newTab();
119
+ // single: true → reuse the default tab, never call newTab()
120
+ const tabId = isSingle ? "default" : await client.newTab();
101
121
  const siteObj = createSiteObject(name, url, client, tabId);
102
122
  _sites[name] = siteObj;
103
- piggy[name] = siteObj;
104
- logger.success(`[${name}] registered as tab ${tabId}`);
123
+ piggy[name] = siteObj;
124
+
125
+ if (isSingle) {
126
+ _singleSiteName = name;
127
+ logger.success(`[${name}] registered as single-tab site (default tab)`);
128
+ } else {
129
+ logger.success(`[${name}] registered as tab ${tabId}`);
130
+ }
105
131
  }
106
132
  } else {
133
+ if (isSingle) throw new Error('piggy: { single: true } is only supported in tab mode');
107
134
  const socketName = `piggy_${name}`;
108
135
  await spawnBrowserOnSocket(socketName, binaryMode);
109
136
  await new Promise(r => setTimeout(r, 500));
@@ -112,16 +139,56 @@ const piggy: any = {
112
139
  _extraProcs.push({ socket: socketName, client: c });
113
140
  const siteObj = createSiteObject(name, url, c, "default");
114
141
  _sites[name] = siteObj;
115
- piggy[name] = siteObj;
142
+ piggy[name] = siteObj;
116
143
  logger.success(`[${name}] registered as process on "${socketName}"`);
117
144
  }
118
145
 
119
146
  return piggy;
120
147
  },
121
148
 
149
+ // ── extend() — installs plugins onto the single-flagged site ─────────────
150
+ //
151
+ // Each installer is an async function returned by a plugin factory, e.g.:
152
+ // innerstorage({ path: './wa-storage.json' }) → installer fn
153
+ //
154
+ // Usage:
155
+ // await piggy.extend(
156
+ // innerstorage({ path: './wa-storage.json' }),
157
+ // cookiesinject({ cookieFile: './wa-cookies.json' }),
158
+ // mediacapture({ downloadDir: './wa-media/' })
159
+ // );
160
+ extend: async (...installers: PluginInstaller[]) => {
161
+ if (!_singleSiteName) {
162
+ throw new Error(
163
+ 'piggy.extend() requires a site registered with { single: true }.\n' +
164
+ 'Example: await piggy.register("mysite", url, { single: true })'
165
+ );
166
+ }
167
+ if (installers.length === 0) {
168
+ logger.warn('[piggy] extend() called with no plugins — nothing to do');
169
+ return piggy;
170
+ }
171
+
172
+ const site = _sites[_singleSiteName];
173
+ if (!site) {
174
+ throw new Error(`piggy.extend(): site "${_singleSiteName}" not found — register it first`);
175
+ }
176
+
177
+ for (const installer of installers) {
178
+ if (typeof installer !== 'function') {
179
+ throw new Error('piggy.extend(): each argument must be a plugin installer function');
180
+ }
181
+ await installer(site);
182
+ }
183
+
184
+ logger.success(`[piggy] ${installers.length} plugin(s) installed on "${_singleSiteName}"`);
185
+ return piggy;
186
+ },
187
+
122
188
  // ── Sub-APIs (1:1 with C++ files, available after launch/connect) ─────────
123
189
 
124
190
  get tabs() { return _router?.tabs ?? createTabsAPI(guardClient()); },
191
+ get tab() { return _router?.tabs ?? createTabsAPI(guardClient()); },
125
192
  get navigation() { return _router?.navigation ?? createNavigationAPI(guardClient()); },
126
193
  get interactions() { return _router?.interactions ?? createInteractionsAPI(guardClient()); },
127
194
  get media() { return _router?.media ?? createMediaAPI(guardClient()); },
@@ -144,7 +211,7 @@ const piggy: any = {
144
211
  return {
145
212
  load: (path: string) => api.load(path),
146
213
  fetch: (url: string) => api.fetch(url),
147
- ovpn: (path: string) => api.ovpn(path),
214
+ ovpn: (path: string) => api.ovpn(path),
148
215
  set: (opts: Parameters<typeof api.set>[0]) => api.set(opts),
149
216
  test: () => api.test(),
150
217
  testStop: () => api.testStop(),
@@ -158,7 +225,7 @@ const piggy: any = {
158
225
  rotation: (mode: "none" | "timed" | "perrequest", interval?: number) => api.rotation(mode, interval),
159
226
  config: (opts: { skipDead?: boolean; autoCheck?: boolean }) => api.config(opts),
160
227
  save: (path: string, filter?: "alive" | "dead" | "all") => api.save(path, filter),
161
- on: (event: string, handler: (data: any) => void) => guardClient().onProxyEvent(event, handler),
228
+ on: (event: string, handler: (data: any) => void) => guardClient().onProxyEvent(event, handler),
162
229
  };
163
230
  },
164
231
 
@@ -230,6 +297,7 @@ const piggy: any = {
230
297
  // ── Shutdown ──────────────────────────────────────────────────────────────
231
298
  close: async (opts?: { force?: boolean }) => {
232
299
  stopServer();
300
+ _singleSiteName = null;
233
301
  if (opts?.force) {
234
302
  for (const { client: c } of _extraProcs) c.disconnect();
235
303
  _client?.disconnect();
@@ -1,7 +0,0 @@
1
- export declare function get(key: string): any | null;
2
- export declare function set(key: string, data: any, ttlMs: number): void;
3
- export declare function del(key: string): void;
4
- export declare function clear(): void;
5
- export declare function size(): number;
6
- export declare function keys(): string[];
7
- //# sourceMappingURL=memory.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["memory.ts"],"names":[],"mappings":"AASA,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAQ3C;AAED,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,QAExD;AAED,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,QAE9B;AAED,wBAAgB,KAAK,SAEpB;AAED,wBAAgB,IAAI,WAEnB;AAED,wBAAgB,IAAI,aAEnB"}