nothing-browser 0.0.10 → 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.
@@ -746,6 +746,7 @@ class PiggyClient {
746
746
  buf = "";
747
747
  eventBuffer = "";
748
748
  eventHandlers = new Map;
749
+ globalEventHandlers = new Map;
749
750
  constructor(socketPath = SOCKET_PATH) {
750
751
  this.socketPath = socketPath;
751
752
  this.eventHandlers.set("default", new Map);
@@ -806,21 +807,12 @@ class PiggyClient {
806
807
  if (handler) {
807
808
  Promise.resolve(handler(JSON.parse(data || "null"))).then((response) => {
808
809
  if (response && typeof response === "object" && "success" in response) {
809
- if (response.success) {
810
- this.send("exposed.result", {
811
- tabId: effectiveTabId,
812
- callId,
813
- result: JSON.stringify(response.result),
814
- isError: false
815
- }).catch((e) => logger_default.error(`Failed to send exposed result: ${e}`));
816
- } else {
817
- this.send("exposed.result", {
818
- tabId: effectiveTabId,
819
- callId,
820
- result: response.error || "Unknown error",
821
- isError: true
822
- }).catch((e) => logger_default.error(`Failed to send exposed error: ${e}`));
823
- }
810
+ this.send("exposed.result", {
811
+ tabId: effectiveTabId,
812
+ callId,
813
+ result: response.success ? JSON.stringify(response.result) : response.error || "Unknown error",
814
+ isError: !response.success
815
+ }).catch((e) => logger_default.error(`Failed to send exposed result: ${e}`));
824
816
  } else {
825
817
  this.send("exposed.result", {
826
818
  tabId: effectiveTabId,
@@ -840,8 +832,38 @@ class PiggyClient {
840
832
  } else {
841
833
  logger_default.warn(`No handler for exposed function: ${name} in tab ${effectiveTabId}`);
842
834
  }
835
+ return;
836
+ }
837
+ if (event.event === "navigate") {
838
+ const handlers = this.globalEventHandlers.get(`navigate:${event.tabId}`);
839
+ if (handlers) {
840
+ for (const h of handlers) {
841
+ try {
842
+ h(event.url);
843
+ } catch (e) {
844
+ logger_default.error(`navigate handler error: ${e}`);
845
+ }
846
+ }
847
+ }
848
+ const wildcard = this.globalEventHandlers.get("navigate:*");
849
+ if (wildcard) {
850
+ for (const h of wildcard) {
851
+ try {
852
+ h({ url: event.url, tabId: event.tabId });
853
+ } catch {}
854
+ }
855
+ }
856
+ return;
843
857
  }
844
858
  }
859
+ onEvent(eventName, tabId, handler) {
860
+ const key = `${eventName}:${tabId}`;
861
+ if (!this.globalEventHandlers.has(key)) {
862
+ this.globalEventHandlers.set(key, new Set);
863
+ }
864
+ this.globalEventHandlers.get(key).add(handler);
865
+ return () => this.globalEventHandlers.get(key)?.delete(handler);
866
+ }
845
867
  disconnect() {
846
868
  this.socket?.destroy();
847
869
  this.socket = null;
@@ -1023,15 +1045,13 @@ class PiggyClient {
1023
1045
  await this.send("session.import", { data, tabId });
1024
1046
  }
1025
1047
  async exposeFunction(name, handler, tabId = "default") {
1026
- if (!this.eventHandlers.has(tabId)) {
1048
+ if (!this.eventHandlers.has(tabId))
1027
1049
  this.eventHandlers.set(tabId, new Map);
1028
- }
1029
1050
  this.eventHandlers.get(tabId).set(name, async (data) => {
1030
1051
  try {
1031
1052
  const result = await handler(data);
1032
- if (result && typeof result === "object" && (("success" in result) || ("error" in result))) {
1053
+ if (result && typeof result === "object" && (("success" in result) || ("error" in result)))
1033
1054
  return result;
1034
- }
1035
1055
  return { success: true, result };
1036
1056
  } catch (err) {
1037
1057
  return { success: false, error: err.message || String(err) };
package/dist/piggy.js CHANGED
@@ -6274,6 +6274,7 @@ class PiggyClient {
6274
6274
  buf = "";
6275
6275
  eventBuffer = "";
6276
6276
  eventHandlers = new Map;
6277
+ globalEventHandlers = new Map;
6277
6278
  constructor(socketPath = SOCKET_PATH) {
6278
6279
  this.socketPath = socketPath;
6279
6280
  this.eventHandlers.set("default", new Map);
@@ -6334,21 +6335,12 @@ class PiggyClient {
6334
6335
  if (handler) {
6335
6336
  Promise.resolve(handler(JSON.parse(data || "null"))).then((response) => {
6336
6337
  if (response && typeof response === "object" && "success" in response) {
6337
- if (response.success) {
6338
- this.send("exposed.result", {
6339
- tabId: effectiveTabId,
6340
- callId,
6341
- result: JSON.stringify(response.result),
6342
- isError: false
6343
- }).catch((e) => logger_default.error(`Failed to send exposed result: ${e}`));
6344
- } else {
6345
- this.send("exposed.result", {
6346
- tabId: effectiveTabId,
6347
- callId,
6348
- result: response.error || "Unknown error",
6349
- isError: true
6350
- }).catch((e) => logger_default.error(`Failed to send exposed error: ${e}`));
6351
- }
6338
+ this.send("exposed.result", {
6339
+ tabId: effectiveTabId,
6340
+ callId,
6341
+ result: response.success ? JSON.stringify(response.result) : response.error || "Unknown error",
6342
+ isError: !response.success
6343
+ }).catch((e) => logger_default.error(`Failed to send exposed result: ${e}`));
6352
6344
  } else {
6353
6345
  this.send("exposed.result", {
6354
6346
  tabId: effectiveTabId,
@@ -6368,8 +6360,38 @@ class PiggyClient {
6368
6360
  } else {
6369
6361
  logger_default.warn(`No handler for exposed function: ${name} in tab ${effectiveTabId}`);
6370
6362
  }
6363
+ return;
6364
+ }
6365
+ if (event.event === "navigate") {
6366
+ const handlers = this.globalEventHandlers.get(`navigate:${event.tabId}`);
6367
+ if (handlers) {
6368
+ for (const h of handlers) {
6369
+ try {
6370
+ h(event.url);
6371
+ } catch (e) {
6372
+ logger_default.error(`navigate handler error: ${e}`);
6373
+ }
6374
+ }
6375
+ }
6376
+ const wildcard = this.globalEventHandlers.get("navigate:*");
6377
+ if (wildcard) {
6378
+ for (const h of wildcard) {
6379
+ try {
6380
+ h({ url: event.url, tabId: event.tabId });
6381
+ } catch {}
6382
+ }
6383
+ }
6384
+ return;
6371
6385
  }
6372
6386
  }
6387
+ onEvent(eventName, tabId, handler) {
6388
+ const key = `${eventName}:${tabId}`;
6389
+ if (!this.globalEventHandlers.has(key)) {
6390
+ this.globalEventHandlers.set(key, new Set);
6391
+ }
6392
+ this.globalEventHandlers.get(key).add(handler);
6393
+ return () => this.globalEventHandlers.get(key)?.delete(handler);
6394
+ }
6373
6395
  disconnect() {
6374
6396
  this.socket?.destroy();
6375
6397
  this.socket = null;
@@ -6551,15 +6573,13 @@ class PiggyClient {
6551
6573
  await this.send("session.import", { data, tabId });
6552
6574
  }
6553
6575
  async exposeFunction(name, handler, tabId = "default") {
6554
- if (!this.eventHandlers.has(tabId)) {
6576
+ if (!this.eventHandlers.has(tabId))
6555
6577
  this.eventHandlers.set(tabId, new Map);
6556
- }
6557
6578
  this.eventHandlers.get(tabId).set(name, async (data) => {
6558
6579
  try {
6559
6580
  const result = await handler(data);
6560
- if (result && typeof result === "object" && (("success" in result) || ("error" in result))) {
6581
+ if (result && typeof result === "object" && (("success" in result) || ("error" in result)))
6561
6582
  return result;
6562
- }
6563
6583
  return { success: true, result };
6564
6584
  } catch (err) {
6565
6585
  return { success: false, error: err.message || String(err) };
@@ -21191,6 +21211,139 @@ function humanTypeSequence(text) {
21191
21211
  return actions;
21192
21212
  }
21193
21213
 
21214
+ // piggy/intercept/scripts.ts
21215
+ function buildRespondScript(pattern, status2, contentType, body) {
21216
+ const safePattern = pattern.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
21217
+ const safeBody = body.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
21218
+ const safeContentType = contentType.replace(/'/g, "\\'");
21219
+ return `
21220
+ (function() {
21221
+ 'use strict';
21222
+ if (!window.__PIGGY_RESPOND_RULES__) window.__PIGGY_RESPOND_RULES__ = [];
21223
+ window.__PIGGY_RESPOND_RULES__.push({
21224
+ pattern: '${safePattern}',
21225
+ status: ${status2},
21226
+ contentType: '${safeContentType}',
21227
+ body: \`${safeBody}\`
21228
+ });
21229
+
21230
+ function _piggyMatchUrl(url, pattern) {
21231
+ try { return url.includes(pattern) || new RegExp(pattern).test(url); }
21232
+ catch { return url.includes(pattern); }
21233
+ }
21234
+
21235
+ // Only install wrappers once per page
21236
+ if (window.__PIGGY_RESPOND_INSTALLED__) return;
21237
+ window.__PIGGY_RESPOND_INSTALLED__ = true;
21238
+
21239
+ // ── fetch wrapper ──────────────────────────────────────────────────────────
21240
+ const _origFetch = window.fetch;
21241
+ window.fetch = function(input, init) {
21242
+ const url = typeof input === 'string' ? input : (input?.url ?? String(input));
21243
+ const rules = window.__PIGGY_RESPOND_RULES__ || [];
21244
+ for (const rule of rules) {
21245
+ if (_piggyMatchUrl(url, rule.pattern)) {
21246
+ return Promise.resolve(new Response(rule.body, {
21247
+ status: rule.status,
21248
+ headers: { 'Content-Type': rule.contentType }
21249
+ }));
21250
+ }
21251
+ }
21252
+ return _origFetch.apply(this, arguments);
21253
+ };
21254
+
21255
+ // ── XHR wrapper ────────────────────────────────────────────────────────────
21256
+ const _origOpen = XMLHttpRequest.prototype.open;
21257
+ const _origSend = XMLHttpRequest.prototype.send;
21258
+
21259
+ XMLHttpRequest.prototype.open = function(method, url) {
21260
+ this.__piggy_url__ = String(url);
21261
+ return _origOpen.apply(this, arguments);
21262
+ };
21263
+
21264
+ XMLHttpRequest.prototype.send = function() {
21265
+ const url = this.__piggy_url__ || '';
21266
+ const rules = window.__PIGGY_RESPOND_RULES__ || [];
21267
+ for (const rule of rules) {
21268
+ if (_piggyMatchUrl(url, rule.pattern)) {
21269
+ const self = this;
21270
+ Object.defineProperty(self, 'readyState', { get: () => 4, configurable: true });
21271
+ Object.defineProperty(self, 'status', { get: () => rule.status, configurable: true });
21272
+ Object.defineProperty(self, 'responseText', { get: () => rule.body, configurable: true });
21273
+ Object.defineProperty(self, 'response', { get: () => rule.body, configurable: true });
21274
+ setTimeout(() => {
21275
+ if (typeof self.onreadystatechange === 'function') self.onreadystatechange();
21276
+ self.dispatchEvent(new Event('readystatechange'));
21277
+ self.dispatchEvent(new Event('load'));
21278
+ self.dispatchEvent(new Event('loadend'));
21279
+ }, 0);
21280
+ return;
21281
+ }
21282
+ }
21283
+ return _origSend.apply(this, arguments);
21284
+ };
21285
+ })();
21286
+ `;
21287
+ }
21288
+ function buildModifyResponseScript(pattern, exposedFnName) {
21289
+ const safePattern = pattern.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
21290
+ const safeFnName = exposedFnName.replace(/'/g, "\\'");
21291
+ return `
21292
+ (function() {
21293
+ 'use strict';
21294
+ if (!window.__PIGGY_MODIFY_RULES__) window.__PIGGY_MODIFY_RULES__ = [];
21295
+ window.__PIGGY_MODIFY_RULES__.push({ pattern: '${safePattern}', fn: '${safeFnName}' });
21296
+
21297
+ function _piggyMatchUrl(url, pattern) {
21298
+ try { return url.includes(pattern) || new RegExp(pattern).test(url); }
21299
+ catch { return url.includes(pattern); }
21300
+ }
21301
+
21302
+ // Only install wrappers once per page
21303
+ if (window.__PIGGY_MODIFY_INSTALLED__) return;
21304
+ window.__PIGGY_MODIFY_INSTALLED__ = true;
21305
+
21306
+ const _origFetch = window.fetch;
21307
+ window.fetch = async function(input, init) {
21308
+ const url = typeof input === 'string' ? input : (input?.url ?? String(input));
21309
+ const rules = window.__PIGGY_MODIFY_RULES__ || [];
21310
+
21311
+ let matchedFn = null;
21312
+ for (const rule of rules) {
21313
+ if (_piggyMatchUrl(url, rule.pattern)) { matchedFn = rule.fn; break; }
21314
+ }
21315
+
21316
+ // No match — pass through untouched
21317
+ const resp = await _origFetch.apply(this, arguments);
21318
+ if (!matchedFn) return resp;
21319
+
21320
+ try {
21321
+ const bodyText = await resp.clone().text();
21322
+ const headers = {};
21323
+ resp.headers.forEach((v, k) => { headers[k] = v; });
21324
+
21325
+ const handlerFn = window[matchedFn];
21326
+ if (typeof handlerFn !== 'function') return resp;
21327
+
21328
+ // Call Node.js handler via exposeFunction bridge
21329
+ const mod = await handlerFn({ body: bodyText, status: resp.status, headers });
21330
+ if (!mod || typeof mod !== 'object' || Object.keys(mod).length === 0) return resp;
21331
+
21332
+ return new Response(
21333
+ mod.body !== undefined ? mod.body : bodyText,
21334
+ {
21335
+ status: mod.status !== undefined ? mod.status : resp.status,
21336
+ headers: mod.headers !== undefined ? mod.headers : headers,
21337
+ }
21338
+ );
21339
+ } catch {
21340
+ return resp; // On any error, pass through original response
21341
+ }
21342
+ };
21343
+ })();
21344
+ `;
21345
+ }
21346
+
21194
21347
  // piggy/register/index.ts
21195
21348
  var globalClient = null;
21196
21349
  var humanMode = false;
@@ -21217,6 +21370,20 @@ async function retry(label, fn, retries = 2, backoff = 150) {
21217
21370
  }
21218
21371
  function createSiteObject(name, registeredUrl, client, tabId) {
21219
21372
  let _currentUrl = registeredUrl;
21373
+ const _eventListeners = new Map;
21374
+ const _unsubNavigate = client.onEvent("navigate", tabId, (url) => {
21375
+ _currentUrl = url;
21376
+ const handlers = _eventListeners.get("navigate");
21377
+ if (handlers) {
21378
+ for (const h of handlers) {
21379
+ try {
21380
+ h(url);
21381
+ } catch (e) {
21382
+ logger_default.error(`[${name}] navigate handler error: ${e}`);
21383
+ }
21384
+ }
21385
+ }
21386
+ });
21220
21387
  const withErrScreen = async (fn, label) => {
21221
21388
  try {
21222
21389
  return await fn();
@@ -21231,6 +21398,7 @@ function createSiteObject(name, registeredUrl, client, tabId) {
21231
21398
  throw err;
21232
21399
  }
21233
21400
  };
21401
+ let _modifyRuleCounter = 0;
21234
21402
  const site = {
21235
21403
  _name: name,
21236
21404
  _tabId: tabId,
@@ -21269,6 +21437,19 @@ function createSiteObject(name, registeredUrl, client, tabId) {
21269
21437
  logger_default.success(`[${name}] init script added`);
21270
21438
  return site;
21271
21439
  },
21440
+ on: (event, handler) => {
21441
+ if (!_eventListeners.has(event))
21442
+ _eventListeners.set(event, new Set);
21443
+ _eventListeners.get(event).add(handler);
21444
+ logger_default.debug(`[${name}] on('${event}') registered`);
21445
+ return () => {
21446
+ _eventListeners.get(event)?.delete(handler);
21447
+ logger_default.debug(`[${name}] on('${event}') unsubscribed`);
21448
+ };
21449
+ },
21450
+ off: (event, handler) => {
21451
+ _eventListeners.get(event)?.delete(handler);
21452
+ },
21272
21453
  click: (selector, opts) => withErrScreen(() => retry(name, async () => {
21273
21454
  if (humanMode)
21274
21455
  await randomDelay(80, 220);
@@ -21402,6 +21583,86 @@ function createSiteObject(name, registeredUrl, client, tabId) {
21402
21583
  await client.addInterceptRule("modifyHeaders", pattern, { headers }, tabId);
21403
21584
  logger_default.info(`[${name}] intercept modifyHeaders: ${pattern}`);
21404
21585
  },
21586
+ respond: async (pattern, handlerOrResponse) => {
21587
+ const isStatic = typeof handlerOrResponse === "object";
21588
+ const response = isStatic ? handlerOrResponse : { status: 200, contentType: "application/json", body: "" };
21589
+ if (!isStatic) {
21590
+ const fnName = `__piggy_respond_${name}_${++_modifyRuleCounter}__`;
21591
+ await client.exposeFunction(fnName, async (req) => {
21592
+ try {
21593
+ const result = handlerOrResponse(req);
21594
+ return {
21595
+ success: true,
21596
+ result: {
21597
+ status: result.status ?? 200,
21598
+ contentType: result.contentType ?? "application/json",
21599
+ body: result.body ?? ""
21600
+ }
21601
+ };
21602
+ } catch (e) {
21603
+ return { success: false, error: e.message };
21604
+ }
21605
+ }, tabId);
21606
+ const dynamicScript = `
21607
+ (function() {
21608
+ 'use strict';
21609
+ if (!window.__PIGGY_DYNAMIC_RESPOND__) window.__PIGGY_DYNAMIC_RESPOND__ = [];
21610
+ window.__PIGGY_DYNAMIC_RESPOND__.push({ pattern: ${JSON.stringify(pattern)}, fn: ${JSON.stringify(fnName)} });
21611
+
21612
+ function matchUrl(url, pattern) {
21613
+ try { return url.includes(pattern) || new RegExp(pattern).test(url); }
21614
+ catch { return url.includes(pattern); }
21615
+ }
21616
+
21617
+ if (window.__PIGGY_DYN_INSTALLED__) return;
21618
+ window.__PIGGY_DYN_INSTALLED__ = true;
21619
+
21620
+ const _origFetch = window.fetch;
21621
+ window.fetch = async function(input, init) {
21622
+ const url = typeof input === 'string' ? input : (input?.url ?? String(input));
21623
+ const method = (init?.method ?? 'GET').toUpperCase();
21624
+ const rules = window.__PIGGY_DYNAMIC_RESPOND__ || [];
21625
+ for (const rule of rules) {
21626
+ if (matchUrl(url, rule.pattern) && typeof window[rule.fn] === 'function') {
21627
+ try {
21628
+ const r = await window[rule.fn]({ url, method });
21629
+ return new Response(r.body ?? '', {
21630
+ status: r.status ?? 200,
21631
+ headers: { 'Content-Type': r.contentType ?? 'application/json' }
21632
+ });
21633
+ } catch { break; }
21634
+ }
21635
+ }
21636
+ return _origFetch.apply(this, arguments);
21637
+ };
21638
+ })();`;
21639
+ await client.addInitScript(dynamicScript, tabId);
21640
+ await client.evaluate(dynamicScript, tabId);
21641
+ logger_default.success(`[${name}] intercept.respond (dynamic): ${pattern}`);
21642
+ return site;
21643
+ }
21644
+ const script = buildRespondScript(pattern, response.status ?? 200, response.contentType ?? "application/json", response.body);
21645
+ await client.addInitScript(script, tabId);
21646
+ await client.evaluate(script, tabId);
21647
+ logger_default.success(`[${name}] intercept.respond (static): ${pattern} → ${response.status ?? 200}`);
21648
+ return site;
21649
+ },
21650
+ modifyResponse: async (pattern, handler) => {
21651
+ const fnName = `__piggy_modres_${name}_${++_modifyRuleCounter}__`;
21652
+ await client.exposeFunction(fnName, async (response) => {
21653
+ try {
21654
+ const mod = await handler(response);
21655
+ return { success: true, result: mod ?? {} };
21656
+ } catch (e) {
21657
+ return { success: false, error: e.message };
21658
+ }
21659
+ }, tabId);
21660
+ const script = buildModifyResponseScript(pattern, fnName);
21661
+ await client.addInitScript(script, tabId);
21662
+ await client.evaluate(script, tabId);
21663
+ logger_default.success(`[${name}] intercept.modifyResponse: ${pattern}`);
21664
+ return site;
21665
+ },
21405
21666
  clear: async () => {
21406
21667
  await client.clearInterceptRules(tabId);
21407
21668
  logger_default.info(`[${name}] intercept rules cleared`);
@@ -21480,6 +21741,7 @@ function createSiteObject(name, registeredUrl, client, tabId) {
21480
21741
  return site;
21481
21742
  },
21482
21743
  close: async () => {
21744
+ _unsubNavigate();
21483
21745
  keepAliveSites.delete(name);
21484
21746
  if (tabId !== "default") {
21485
21747
  await client.closeTab(tabId);