nothing-browser 0.0.10 → 0.0.12

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