@buoy-gg/network 7.0.39 → 7.0.41

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.
Files changed (56) hide show
  1. package/README.md +6 -0
  2. package/lib/commonjs/index.js +7 -0
  3. package/lib/commonjs/network/components/NetworkListScreen.js +2 -3
  4. package/lib/commonjs/network/components/NetworkListScreen.web.js +2 -3
  5. package/lib/commonjs/network/components/NetworkModal.js +1 -1
  6. package/lib/commonjs/network/conditions/xhr.js +26 -1
  7. package/lib/commonjs/network/overrides/applyToXhr.js +99 -42
  8. package/lib/commonjs/network/overrides/matchRule.js +7 -1
  9. package/lib/commonjs/network/overrides/overriddenRows.js +37 -0
  10. package/lib/commonjs/network/overrides/resolveOverride.js +1 -1
  11. package/lib/commonjs/network/utils/networkEventStore.js +24 -6
  12. package/lib/commonjs/network/utils/networkListener.js +106 -11
  13. package/lib/commonjs/network/utils/networkSavedStore.js +4 -4
  14. package/lib/commonjs/network/utils/requestId.js +16 -0
  15. package/lib/commonjs/network/utils/resolveRequestUrl.js +15 -0
  16. package/lib/commonjs/network/utils/startNetworkCapture.js +24 -0
  17. package/lib/commonjs/preset.js +3 -12
  18. package/lib/module/index.js +1 -0
  19. package/lib/module/network/components/NetworkListScreen.js +2 -3
  20. package/lib/module/network/components/NetworkListScreen.web.js +2 -3
  21. package/lib/module/network/components/NetworkModal.js +1 -1
  22. package/lib/module/network/conditions/xhr.js +27 -2
  23. package/lib/module/network/overrides/applyToXhr.js +98 -42
  24. package/lib/module/network/overrides/matchRule.js +7 -1
  25. package/lib/module/network/overrides/overriddenRows.js +33 -0
  26. package/lib/module/network/overrides/resolveOverride.js +1 -1
  27. package/lib/module/network/utils/networkEventStore.js +25 -7
  28. package/lib/module/network/utils/networkListener.js +106 -12
  29. package/lib/module/network/utils/networkSavedStore.js +4 -4
  30. package/lib/module/network/utils/requestId.js +12 -0
  31. package/lib/module/network/utils/resolveRequestUrl.js +11 -0
  32. package/lib/module/network/utils/startNetworkCapture.js +21 -0
  33. package/lib/module/preset.js +3 -12
  34. package/lib/typescript/index.d.ts +1 -0
  35. package/lib/typescript/index.d.ts.map +1 -1
  36. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  37. package/lib/typescript/network/components/NetworkListScreen.web.d.ts.map +1 -1
  38. package/lib/typescript/network/conditions/xhr.d.ts.map +1 -1
  39. package/lib/typescript/network/overrides/applyToXhr.d.ts +1 -0
  40. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
  41. package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -1
  42. package/lib/typescript/network/overrides/overriddenRows.d.ts +16 -0
  43. package/lib/typescript/network/overrides/overriddenRows.d.ts.map +1 -0
  44. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  45. package/lib/typescript/network/utils/networkListener.d.ts +3 -4
  46. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  47. package/lib/typescript/network/utils/networkSavedStore.d.ts +4 -4
  48. package/lib/typescript/network/utils/requestId.d.ts +2 -0
  49. package/lib/typescript/network/utils/requestId.d.ts.map +1 -0
  50. package/lib/typescript/network/utils/resolveRequestUrl.d.ts +3 -0
  51. package/lib/typescript/network/utils/resolveRequestUrl.d.ts.map +1 -0
  52. package/lib/typescript/network/utils/startNetworkCapture.d.ts +6 -0
  53. package/lib/typescript/network/utils/startNetworkCapture.d.ts.map +1 -0
  54. package/lib/typescript/preset.d.ts.map +1 -1
  55. package/lib/web/index.mjs +146 -0
  56. package/package.json +32 -10
package/README.md CHANGED
@@ -93,3 +93,9 @@ Each call keeps the profile selected when it starts. Offline takes precedence ov
93
93
  Conditions remain active when the panel is closed or its list is paused. They reset on a full JS reload or account access loss and are never persisted. Captured requests retain their condition stamp for saved snapshots, detail views and export. Desktop and MCP can control a connected device that advertises the conditions actions.
94
94
 
95
95
  Network conditions are a development preview. The native compatibility matrix across transports, runtimes and devices is still being worked through, so confirm the behaviour you rely on in your own build before you depend on it.
96
+
97
+ ## Web support (unreleased)
98
+
99
+ Browser fetch and XHR use the shared capture, rules, conditions, and request panels. CORS still controls which response data the page can read. The browser build is available in this checkout and has not been published yet. See the [web setup guide](https://buoy.gg/buoy/latest/docs/web-preview) for registration, dependencies, and browser boundaries.
100
+
101
+ The capture control reads “Pause network capture” while capture is enabled and “Resume network capture” while it is paused. Existing requests remain visible while capture is paused.
@@ -243,6 +243,12 @@ Object.defineProperty(exports, "splitPinned", {
243
243
  return _withPinnedFirst.splitPinned;
244
244
  }
245
245
  });
246
+ Object.defineProperty(exports, "startNetworkCapture", {
247
+ enumerable: true,
248
+ get: function () {
249
+ return _startNetworkCapture.startNetworkCapture;
250
+ }
251
+ });
246
252
  Object.defineProperty(exports, "subscribeNetworkConditions", {
247
253
  enumerable: true,
248
254
  get: function () {
@@ -298,6 +304,7 @@ Object.defineProperty(exports, "useTickEveryMinute", {
298
304
  }
299
305
  });
300
306
  var _preset = require("./preset");
307
+ var _startNetworkCapture = require("./network/utils/startNetworkCapture");
301
308
  var _NetworkModal = require("./network/components/NetworkModal");
302
309
  var _NetworkEventDetailView = require("./network/components/NetworkEventDetailView");
303
310
  var _NetworkEventItemCompact = require("./network/components/NetworkEventItemCompact");
@@ -12,7 +12,7 @@ var _NetworkUpgradeBanner = require("./NetworkUpgradeBanner");
12
12
  var _useNetworkEventList = require("../hooks/useNetworkEventList");
13
13
  var _useNetworkSaved = require("../hooks/useNetworkSaved");
14
14
  var _useOverrideRules = require("../hooks/useOverrideRules");
15
- var _matchRule = require("../overrides/matchRule");
15
+ var _overriddenRows = require("../overrides/overriddenRows");
16
16
  var _withPinnedFirst = require("../utils/withPinnedFirst");
17
17
  var _jsxRuntime = require("react/jsx-runtime");
18
18
  /**
@@ -196,8 +196,7 @@ function NetworkListScreen({
196
196
  // Paused rules still populate the strip — that's how you find the thing you
197
197
  // paused in order to resume it. The header says PAUSED instead.
198
198
  const activeRules = rules.filter(rule => rule.enabled && !isRuleSpent(rule));
199
- const isOverridden = event => activeRules.some(rule => (0, _matchRule.ruleMatches)(rule, event.url, String(event.method || "GET")));
200
- const overridden = activeRules.length ? [...pinned, ...rest].filter(isOverridden) : [];
199
+ const overridden = (0, _overriddenRows.overriddenEvents)([...pinned, ...rest], activeRules);
201
200
  const overriddenIds = new Set(overridden.map(event => event.id));
202
201
  const pinnedRows = pinned.filter(event => !overriddenIds.has(event.id));
203
202
  const restRows = overriddenIds.size ? rest.filter(event => !overriddenIds.has(event.id)) : rest;
@@ -9,7 +9,7 @@ var _NetworkEventItemCompact = require("./NetworkEventItemCompact.web");
9
9
  var _useNetworkEventList = require("../hooks/useNetworkEventList");
10
10
  var _useNetworkSaved = require("../hooks/useNetworkSaved");
11
11
  var _useOverrideRules = require("../hooks/useOverrideRules");
12
- var _matchRule = require("../overrides/matchRule");
12
+ var _overriddenRows = require("../overrides/overriddenRows");
13
13
  var _withPinnedFirst = require("../utils/withPinnedFirst");
14
14
  var _jsxRuntime = require("react/jsx-runtime");
15
15
  /**
@@ -387,8 +387,7 @@ function NetworkListScreen({
387
387
  // Hoist anything an active rule covers. Done after `splitPinned` so a request
388
388
  // that is both pinned and overridden appears once, in the louder section.
389
389
  const activeRules = rules.filter(rule => rule.enabled && !isRuleSpent(rule));
390
- const isOverridden = event => activeRules.some(rule => (0, _matchRule.ruleMatches)(rule, event.url, String(event.method || "GET")));
391
- const overridden = activeRules.length ? [...pinned, ...rest].filter(isOverridden) : [];
390
+ const overridden = (0, _overriddenRows.overriddenEvents)([...pinned, ...rest], activeRules);
392
391
  const overriddenIds = new Set(overridden.map(event => event.id));
393
392
  const pinnedRows = pinned.filter(event => !overriddenIds.has(event.id));
394
393
  const restRows = overriddenIds.size ? rest.filter(event => !overriddenIds.has(event.id)) : rest;
@@ -501,7 +501,7 @@ function NetworkModalInner({
501
501
  }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
502
502
  isEnabled: !paused,
503
503
  onToggle: () => setPaused(prev => !prev),
504
- accessibilityLabel: "Pause network list",
504
+ accessibilityLabel: paused ? "Resume network capture" : "Pause network capture",
505
505
  testID: "buoy-network-power"
506
506
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkClearButton, {
507
507
  paused: paused
@@ -7,7 +7,32 @@ exports.runConditionXhr = runConditionXhr;
7
7
  var _applyToXhr = require("../overrides/applyToXhr");
8
8
  /** Active simulation uses a send-to-completion deadline, including the wait. */
9
9
  function runConditionXhr(target, url, condition, outcome, dispatch) {
10
- if (!(0, _applyToXhr.canSynthesize)(target)) throw new Error("Network conditions require React Native XMLHttpRequest.");
10
+ if (!(0, _applyToXhr.canSynthesize)(target)) {
11
+ const rule = {
12
+ id: "condition",
13
+ enabled: true,
14
+ urlPattern: url,
15
+ kind: "delay",
16
+ hits: 0,
17
+ seen: 0,
18
+ createdAt: Date.now()
19
+ };
20
+ const simulation = condition.profile === "offline" ? {
21
+ kind: "fail",
22
+ rule,
23
+ failKind: "network",
24
+ delayMs: 0
25
+ } : outcome ? {
26
+ ...outcome,
27
+ delayMs: condition.delayMs + outcome.delayMs
28
+ } : {
29
+ kind: "delay",
30
+ rule,
31
+ delayMs: condition.delayMs
32
+ };
33
+ if (!(0, _applyToXhr.runOverride)(target, url, simulation, dispatch)) throw new Error("Network conditions are unavailable for this XMLHttpRequest implementation.");
34
+ return;
35
+ }
11
36
  const xhr = target;
12
37
  if (xhr.readyState !== 1 || xhr._sent) throw new Error("Request is not open or has already been sent.");
13
38
  const started = Date.now();
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.canSynthesize = canSynthesize;
7
7
  exports.canSynthesizeStandard = canSynthesizeStandard;
8
8
  exports.completeNativeOverride = completeNativeOverride;
9
+ exports.hasPendingStandardRequest = hasPendingStandardRequest;
9
10
  exports.runOverride = runOverride;
10
11
  /**
11
12
  * Perform an override outcome against an XMLHttpRequest — React Native's OR a
@@ -286,6 +287,7 @@ function respondStandard(xhr, url, outcome, isAborted) {
286
287
  setStandardReadyState(xhr, LOADING);
287
288
  if (isAborted()) return;
288
289
  setStandardReadyState(xhr, DONE);
290
+ if (isAborted()) return;
289
291
  fireEvent(xhr, "load");
290
292
  fireEvent(xhr, "loadend");
291
293
  }
@@ -314,60 +316,115 @@ function failStandard(xhr, outcome) {
314
316
  * `delay` pass-through hands the request to the real network, where the
315
317
  * prototype `abort()` must be reachable again.
316
318
  */
319
+ const pendingStandardRequests = new WeakMap();
320
+ function hasPendingStandardRequest(xhr) {
321
+ return pendingStandardRequests.has(xhr);
322
+ }
317
323
  function runStandardOverride(xhr, url, outcome, passThrough) {
318
- let timer = null;
319
- let aborted = false;
324
+ const started = Date.now();
325
+ const generation = {};
326
+ let timer;
327
+ let deadline;
320
328
  let settled = false;
321
- const restoreAbort = () => {
322
- try {
323
- delete xhr.abort;
324
- } catch {
325
- // Non-configurable shadow can't happen (we define it configurable), but
326
- // never let cleanup throw into app code.
329
+ let aborted = false;
330
+ let dispatched = false;
331
+ let timeout = xhr.timeout;
332
+ const originalAbort = xhr.abort;
333
+ const originalOpen = xhr.open;
334
+ const names = ['abort', 'open', 'timeout', 'readyState', 'status', 'statusText', 'response', 'responseText', 'responseURL', 'getAllResponseHeaders', 'getResponseHeader'];
335
+ const descriptors = new Map(names.map(name => [name, Object.getOwnPropertyDescriptor(xhr, name)]));
336
+ let prototype = Object.getPrototypeOf(xhr);
337
+ let nativeTimeout;
338
+ while (prototype && !nativeTimeout) {
339
+ nativeTimeout = Object.getOwnPropertyDescriptor(prototype, 'timeout');
340
+ prototype = Object.getPrototypeOf(prototype);
341
+ }
342
+ const finish = () => {
343
+ settled = true;
344
+ if (pendingStandardRequests.get(xhr) === generation) pendingStandardRequests.delete(xhr);
345
+ clearTimeout(timer);
346
+ clearTimeout(deadline);
347
+ xhr.removeEventListener('loadend', finish);
348
+ };
349
+ const restore = () => {
350
+ finish();
351
+ for (const [name, descriptor] of descriptors) {
352
+ if (descriptor) Object.defineProperty(xhr, name, descriptor);else delete xhr[name];
327
353
  }
354
+ xhr.timeout = timeout;
328
355
  };
329
- Object.defineProperty(xhr, "abort", {
356
+ const scheduleDeadline = () => {
357
+ clearTimeout(deadline);
358
+ if (dispatched) {
359
+ nativeTimeout?.set?.call(xhr, timeout > 0 ? Math.max(1, timeout - (Date.now() - started)) : 0);
360
+ } else if (timeout > 0) {
361
+ deadline = setTimeout(() => {
362
+ if (settled) return;
363
+ clearTimeout(timer);
364
+ failStandard(xhr, {
365
+ ...outcome,
366
+ kind: 'fail',
367
+ failKind: 'timeout'
368
+ });
369
+ finish();
370
+ }, Math.max(0, timeout - (Date.now() - started)));
371
+ }
372
+ };
373
+ Object.defineProperty(xhr, 'timeout', {
374
+ configurable: true,
375
+ get: () => timeout,
376
+ set: value => {
377
+ timeout = Math.max(0, Number(value) || 0);
378
+ scheduleDeadline();
379
+ }
380
+ });
381
+ Object.defineProperty(xhr, 'open', {
382
+ configurable: true,
383
+ value: (...args) => {
384
+ restore();
385
+ return Reflect.apply(originalOpen, xhr, args);
386
+ }
387
+ });
388
+ Object.defineProperty(xhr, 'abort', {
330
389
  configurable: true,
331
- writable: true,
332
390
  value: () => {
333
- if (settled || aborted) return;
334
- aborted = true;
335
- settled = true;
336
- if (timer !== null) {
337
- clearTimeout(timer);
338
- timer = null;
391
+ if (dispatched || settled) {
392
+ restore();
393
+ originalAbort.call(xhr);
394
+ return;
339
395
  }
340
- restoreAbort();
341
- // The in-flight abort sequence: DONE, then 'abort', then 'loadend' —
342
- // whatwg's request-error steps, which is also what RN's setReadyState
343
- // dispatches off `_aborted`.
396
+ aborted = true;
397
+ clearTimeout(timer);
344
398
  setStandardReadyState(xhr, DONE);
345
- fireEvent(xhr, "abort");
346
- fireEvent(xhr, "loadend");
399
+ fireEvent(xhr, 'abort');
400
+ fireEvent(xhr, 'loadend');
401
+ finish();
402
+ if (!pendingStandardRequests.has(xhr)) defineOwn(xhr, 'readyState', 0);
347
403
  }
348
404
  });
349
- const fire = () => {
350
- timer = null;
351
- if (aborted) return;
352
- if (outcome.kind === "delay") {
353
- // The real request is about to be genuinely in flight — put the
354
- // prototype `abort()` back FIRST so cancelling reaches the network.
355
- restoreAbort();
356
- passThrough();
405
+ pendingStandardRequests.set(xhr, generation);
406
+ xhr.addEventListener('loadend', finish);
407
+ scheduleDeadline();
408
+ timer = setTimeout(() => {
409
+ if (settled || aborted) return;
410
+ if (outcome.kind === 'delay') {
411
+ dispatched = true;
412
+ scheduleDeadline();
413
+ try {
414
+ passThrough();
415
+ } catch {
416
+ failStandard(xhr, {
417
+ ...outcome,
418
+ kind: 'fail',
419
+ failKind: 'network'
420
+ });
421
+ finish();
422
+ }
357
423
  return;
358
424
  }
359
- if (outcome.kind === "fail") {
360
- failStandard(xhr, outcome);
361
- } else {
362
- respondStandard(xhr, url, outcome, () => aborted);
363
- }
364
- settled = true;
365
- restoreAbort();
366
- };
367
-
368
- // ALWAYS asynchronous, even at 0ms — same contract as the RN path: callers
369
- // legitimately attach handlers after `send()` returns (axios does).
370
- timer = setTimeout(fire, outcome.delayMs);
425
+ if (outcome.kind === 'fail') failStandard(xhr, outcome);else respondStandard(xhr, url, outcome, () => aborted || pendingStandardRequests.get(xhr) !== generation);
426
+ finish();
427
+ }, outcome.delayMs);
371
428
  return true;
372
429
  }
373
430
 
@@ -57,7 +57,13 @@ function compilePattern(pattern) {
57
57
  // Anchored, like Chrome's `^${...}$` — a pattern describes the whole URL, so
58
58
  // `pokeapi.co` matches nothing and `*pokeapi.co*` matches everything on that
59
59
  // host. Predictable beats clever; the rule editor writes full URLs for you.
60
- const regex = new RegExp(`^${escapeRegex(pattern)}$`);
60
+ //
61
+ // One exception: a pattern that starts with `/` is a PATH. Anchored to the
62
+ // full URL it could never match anything (every URL starts with a scheme),
63
+ // so it means "this path on any origin" — `/api/rewards` matches
64
+ // `http://localhost:3002/api/rewards`, and `/api/rewards*` also its queries.
65
+ const source = pattern.startsWith("/") ? `[a-zA-Z][a-zA-Z0-9+.-]*://[^/]*${escapeRegex(pattern)}` : escapeRegex(pattern);
66
+ const regex = new RegExp(`^${source}$`);
61
67
  // Unbounded growth isn't possible in practice (patterns come from a capped
62
68
  // rule list), but a user editing a pattern character by character churns
63
69
  // entries, so keep the cache from becoming a leak.
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.overriddenEvents = overriddenEvents;
7
+ var _matchRule = require("./matchRule");
8
+ /**
9
+ * Which captured requests belong in the list's OVERRIDDEN strip.
10
+ *
11
+ * A row belongs there when an active rule actually changed it (it carries the
12
+ * rule's override mark) or matched it after the rule existed (while overrides
13
+ * are paused those pass through, and the strip is how you find the rule again).
14
+ * Before a rule existed nothing overrode anything, so older matching requests
15
+ * are real server responses and stay in the list, with one exception: the
16
+ * newest of them, which is the request the rule was built from. That one hoists
17
+ * right away, so creating a rule shows where it points without waiting for the
18
+ * endpoint to be called again.
19
+ */
20
+ function overriddenEvents(events, activeRules) {
21
+ if (activeRules.length === 0) return [];
22
+ const activeIds = new Set(activeRules.map(rule => rule.id));
23
+ const picked = new Set();
24
+ for (const rule of activeRules) {
25
+ let source;
26
+ for (const event of events) {
27
+ if (event.override && activeIds.has(event.override.ruleId)) {
28
+ picked.add(event.id);
29
+ continue;
30
+ }
31
+ if (!(0, _matchRule.ruleMatches)(rule, event.url, String(event.method || "GET"))) continue;
32
+ if (event.timestamp >= rule.createdAt) picked.add(event.id);else if (!source || event.timestamp > source.timestamp) source = event;
33
+ }
34
+ if (source) picked.add(source.id);
35
+ }
36
+ return events.filter(event => picked.has(event.id));
37
+ }
@@ -30,7 +30,7 @@ var _types = require("./types");
30
30
  * "the dev tools stopped working and I don't know why". Metro/debugger traffic
31
31
  * is already excluded upstream by `networkListener.shouldIgnoreUrl`.
32
32
  */
33
- const NEVER_OVERRIDE = [/(^|\.)keygen\.sh/i];
33
+ const NEVER_OVERRIDE = [/(^|\.)keygen\.sh/i, /^https?:\/\/(www\.)?buoy\.gg\/api\/license(\/|$)/i];
34
34
  function isProtectedUrl(url) {
35
35
  return NEVER_OVERRIDE.some(pattern => pattern.test(url));
36
36
  }
@@ -140,9 +140,8 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
140
140
  }
141
141
 
142
142
  // If no other listeners exist, stop the network interceptor
143
- const listener = (0, _networkListener.networkListener)();
144
- if (listener.listenerCount === 0) {
145
- listener.stopListening();
143
+ if ((0, _networkListener.getNetworkListenerCount)() === 0) {
144
+ (0, _networkListener.stopNetworkListener)();
146
145
  }
147
146
  }
148
147
 
@@ -155,9 +154,8 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
155
154
  disableCapture() {
156
155
  this.mirrorMode = true;
157
156
  this.takeBootBuffer();
158
- const listener = (0, _networkListener.networkListener)();
159
- if (listener.listenerCount === 0 && listener.isActive) {
160
- listener.stopListening();
157
+ if ((0, _networkListener.getNetworkListenerCount)() === 0) {
158
+ (0, _networkListener.stopNetworkListener)();
161
159
  }
162
160
  super.disableCapture();
163
161
  }
@@ -204,6 +202,8 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
204
202
  request
205
203
  } = event;
206
204
  if (event.type === "request") {
205
+ // Replayed request notifications must not reset a completed row or add a second key.
206
+ if (this.events.some(item => item.id === request.id)) return;
207
207
  // Build full URL with query params if present
208
208
  const queryString = request.params ? `?${new URLSearchParams(request.params).toString()}` : "";
209
209
  const fullUrl = `${request.url}${queryString}`;
@@ -242,6 +242,24 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
242
242
  this.events = [networkEvent, ...this.events].slice(0, this.maxEvents);
243
243
  this.notify(networkEvent);
244
244
  this.notifyArrayListeners();
245
+ } else if (event.type === "request-body") {
246
+ const current = this.events.find(item => item.id === request.id);
247
+ if (!current) return;
248
+ const updated = {
249
+ ...current,
250
+ requestData: request.data,
251
+ requestSize: this.getDataSize(request.data)
252
+ };
253
+ if (request.client === "graphql") {
254
+ updated.operationName = (0, _extractOperationName.extractOperationName)(request.data) || undefined;
255
+ const variables = request.data?.variables;
256
+ if (variables && typeof variables === "object") updated.graphqlVariables = variables;
257
+ }
258
+ this.events = this.events.map(item => item.id === request.id ? updated : item);
259
+ if (this.pendingRequests.has(request.id)) this.pendingRequests.set(request.id, updated);
260
+ _networkSavedStore.networkSavedStore.syncFromLive(updated);
261
+ this.notify(updated);
262
+ this.notifyArrayListeners();
245
263
  } else if (event.type === "response" || event.type === "error") {
246
264
  const index = this.events.findIndex(e => e.id === request.id);
247
265
  if (index !== -1) {
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.stopNetworkListener = exports.startNetworkListener = exports.removeAllNetworkListeners = exports.networkListener = exports.isNetworkListening = exports.isNetworkInterceptionLive = exports.getNetworkListenerCount = exports.addNetworkListener = exports.BUOY_XHR_INTERCEPTOR_FLAG = exports.BUOY_FETCH_INTERCEPTOR_FLAG = void 0;
7
+ var _resolveRequestUrl = require("./resolveRequestUrl");
8
+ var _requestId = require("./requestId");
7
9
  var _xhr = require("../conditions/xhr");
8
10
  var _store = require("../conditions/store");
9
11
  var _wait = require("../conditions/wait");
@@ -17,6 +19,59 @@ var _httpCompletion = require("./httpCompletion");
17
19
  * Simple and reliable network interception for React Native
18
20
  */
19
21
 
22
+ let devOrigin;
23
+
24
+ /**
25
+ * Headers another Buoy tool adds to a request inside our wrapper.
26
+ *
27
+ * Impersonate patches fetch/XHR at boot, so this listener usually wraps it
28
+ * and records the request before the impersonation header is added. The
29
+ * request on the wire carried the header and the Network row did not. Tools
30
+ * that add headers register a function here, keyed by URL, so the row shows
31
+ * what was actually sent.
32
+ */
33
+ function injectedHeaders(url) {
34
+ const injectors = globalThis.__buoyHeaderInjectors;
35
+ if (!injectors || injectors.length === 0) return undefined;
36
+ let out;
37
+ for (const inject of injectors) {
38
+ try {
39
+ const h = inject(url);
40
+ if (h) out = {
41
+ ...out,
42
+ ...h
43
+ };
44
+ } catch {
45
+ /* a broken injector must not break capture */
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+
51
+ /**
52
+ * Origin of the Metro dev server this JS bundle was loaded from, read once from
53
+ * the bundle URL React Native reports. `null` in release builds (the bundle is
54
+ * a file) and anywhere react-native isn't present.
55
+ */
56
+ function devServerOrigin() {
57
+ if (devOrigin !== undefined) return devOrigin;
58
+ devOrigin = null;
59
+ try {
60
+ if (typeof __DEV__ !== "undefined" && !__DEV__) return devOrigin;
61
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
62
+ const rn = require("react-native");
63
+ const sc = rn?.NativeModules?.SourceCode;
64
+ const url = sc?.getConstants?.().scriptURL ?? sc?.scriptURL;
65
+ if (typeof url === "string" && /^https?:\/\//.test(url)) {
66
+ const u = new URL(url);
67
+ devOrigin = `${u.protocol}//${u.host}`;
68
+ }
69
+ } catch {
70
+ devOrigin = null;
71
+ }
72
+ return devOrigin;
73
+ }
74
+
20
75
  /**
21
76
  * True only while `originalFetch` is being CALLED (not awaited).
22
77
  *
@@ -122,7 +177,6 @@ function isInternalBuoyHost(url) {
122
177
  class NetworkListener {
123
178
  listeners = [];
124
179
  isListening = false;
125
- requestCounter = 1000;
126
180
 
127
181
  // Store original methods
128
182
 
@@ -166,8 +220,15 @@ class NetworkListener {
166
220
  try {
167
221
  const parsed = new URL(url);
168
222
  if (parsed.hostname === "keygen.sh" || parsed.hostname.endsWith(".keygen.sh")) return true;
223
+ // Buoy's own license endpoint (7.0.39+): the same traffic, new address.
224
+ if ((parsed.hostname === "buoy.gg" || parsed.hostname === "www.buoy.gg") && parsed.pathname.startsWith("/api/license")) return true;
169
225
  const local = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "10.0.2.2";
170
226
  if (local && ["8081", "8802"].includes(parsed.port)) return true;
227
+ // The dev server this bundle actually came from, whatever its host and
228
+ // port (a LAN address, --port 8082…): Metro's own traffic — symbolicate,
229
+ // logs, HMR, the bundle — is not the app's.
230
+ const dev = devServerOrigin();
231
+ if (dev && `${parsed.protocol}//${parsed.host}` === dev) return true;
171
232
  } catch {
172
233
  return false;
173
234
  }
@@ -623,7 +684,7 @@ class NetworkListener {
623
684
  this.originalFetch = globalThis.fetch;
624
685
  const generation = ++this.fetchGeneration;
625
686
  const wrappedFetch = async (input, init) => {
626
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
687
+ const url = (0, _resolveRequestUrl.resolveRequestUrl)(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
627
688
 
628
689
  // A newer install supersedes this wrapper. If it is still stranded in
629
690
  // another tool's chain it must pass through silently, or one request
@@ -637,7 +698,7 @@ class NetworkListener {
637
698
  return passthroughFetch(input, init);
638
699
  }
639
700
  const startTime = Date.now();
640
- const requestId = `fetch_${++self.requestCounter}`;
701
+ const requestId = (0, _requestId.nextRequestId)("fetch");
641
702
  const inputRequest = typeof input === "object" && "method" in input ? input : undefined;
642
703
  const method = init?.method ?? inputRequest?.method ?? "GET";
643
704
  const signal = init?.signal !== undefined ? init.signal : inputRequest?.signal;
@@ -661,9 +722,12 @@ class NetworkListener {
661
722
  requestHeaders[key] = value;
662
723
  });
663
724
  } else {
664
- requestHeaders = headers;
725
+ requestHeaders = {
726
+ ...headers
727
+ };
665
728
  }
666
729
  }
730
+ Object.assign(requestHeaders, injectedHeaders(url));
667
731
 
668
732
  // Parse request body
669
733
  // Explicitly typed: the late emit for a streamed reply reads this from
@@ -712,6 +776,35 @@ class NetworkListener {
712
776
  override,
713
777
  condition
714
778
  });
779
+
780
+ // Clone before dispatch; inspect the body without delaying or consuming the app's upload.
781
+ if (init?.body === undefined && inputRequest?.body && !inputRequest.bodyUsed) {
782
+ try {
783
+ const bodyCopy = inputRequest.clone();
784
+ void self.readStreamedBody(bodyCopy).then(({
785
+ body
786
+ }) => {
787
+ try {
788
+ requestData = JSON.parse(body);
789
+ } catch {
790
+ requestData = body;
791
+ }
792
+ self.emit({
793
+ type: "request-body",
794
+ timestamp: new Date(),
795
+ request: {
796
+ id: requestId || "unknown",
797
+ url: cleanUrl,
798
+ method,
799
+ data: requestData,
800
+ client: clientType
801
+ }
802
+ });
803
+ }).catch(() => {});
804
+ } catch {
805
+ // An unreadable body must not prevent the app's request.
806
+ }
807
+ }
715
808
  try {
716
809
  if (condition || outcome) await (0, _wait.waitBeforeDispatch)((condition?.delayMs ?? 0) + (outcome?.delayMs ?? 0), signal);
717
810
  (0, _wait.throwIfAborted)(signal);
@@ -910,9 +1003,9 @@ class NetworkListener {
910
1003
  if (generation === self.xhrGeneration) {
911
1004
  pendingCapture.get(this)?.();
912
1005
  const xhr = this;
913
- xhr._buoyRequestId = `xhr_${++self.requestCounter}`;
1006
+ xhr._buoyRequestId = (0, _requestId.nextRequestId)("xhr");
914
1007
  xhr._method = method;
915
- xhr._url = url;
1008
+ xhr._url = (0, _resolveRequestUrl.resolveRequestUrl)(url);
916
1009
  xhr._buoyStartTime = Date.now();
917
1010
  xhr._requestHeaders = {};
918
1011
  }
@@ -934,6 +1027,7 @@ class NetworkListener {
934
1027
  if (generation !== self.xhrGeneration || insideFetchHandoff) {
935
1028
  return passthroughSend.call(this, data);
936
1029
  }
1030
+ if ((0, _applyToXhr.hasPendingStandardRequest)(this)) throw new DOMException("The request has already been sent.", "InvalidStateError");
937
1031
  if (this.readyState !== 1 || this._sent) {
938
1032
  return passthroughSend.call(this, data);
939
1033
  }
@@ -945,6 +1039,7 @@ class NetworkListener {
945
1039
  xhr._buoyStartTime = startTime;
946
1040
  xhr._buoyTtfb = undefined;
947
1041
  const requestHeaders = xhr._requestHeaders || {};
1042
+ Object.assign(requestHeaders, injectedHeaders(url));
948
1043
 
949
1044
  // Skip ignored URLs
950
1045
  if (self.shouldIgnoreUrl(url)) {
@@ -1207,7 +1302,7 @@ const startNetworkListener = () => getNetworkListener().startListening();
1207
1302
  * Stop network traffic monitoring
1208
1303
  */
1209
1304
  exports.startNetworkListener = startNetworkListener;
1210
- const stopNetworkListener = () => getNetworkListener().stopListening();
1305
+ const stopNetworkListener = () => _networkListener?.stopListening();
1211
1306
 
1212
1307
  /**
1213
1308
  * Add a listener for network events
@@ -1232,7 +1327,7 @@ const addNetworkListener = listener => getNetworkListener().addListener(listener
1232
1327
  * Remove all registered network event listeners
1233
1328
  */
1234
1329
  exports.addNetworkListener = addNetworkListener;
1235
- const removeAllNetworkListeners = () => getNetworkListener().removeAllListeners();
1330
+ const removeAllNetworkListeners = () => _networkListener?.removeAllListeners();
1236
1331
 
1237
1332
  /**
1238
1333
  * Check if network monitoring is currently active
@@ -1240,7 +1335,7 @@ const removeAllNetworkListeners = () => getNetworkListener().removeAllListeners(
1240
1335
  * @returns True if currently intercepting network traffic
1241
1336
  */
1242
1337
  exports.removeAllNetworkListeners = removeAllNetworkListeners;
1243
- const isNetworkListening = () => getNetworkListener().isActive;
1338
+ const isNetworkListening = () => _networkListener?.isActive ?? false;
1244
1339
 
1245
1340
  /**
1246
1341
  * Get the number of registered network event listeners
@@ -1248,7 +1343,7 @@ const isNetworkListening = () => getNetworkListener().isActive;
1248
1343
  * @returns Number of active listeners
1249
1344
  */
1250
1345
  exports.isNetworkListening = isNetworkListening;
1251
- const getNetworkListenerCount = () => getNetworkListener().listenerCount;
1346
+ const getNetworkListenerCount = () => _networkListener?.listenerCount ?? 0;
1252
1347
 
1253
1348
  /**
1254
1349
  * Whether both interceptors are actually reachable through the live global
@@ -1256,5 +1351,5 @@ const getNetworkListenerCount = () => getNetworkListener().listenerCount;
1256
1351
  * even after another tool re-assigns `globalThis.fetch` over the wrapper).
1257
1352
  */
1258
1353
  exports.getNetworkListenerCount = getNetworkListenerCount;
1259
- const isNetworkInterceptionLive = () => getNetworkListener().isInterceptionLive();
1354
+ const isNetworkInterceptionLive = () => _networkListener?.isInterceptionLive() ?? false;
1260
1355
  exports.isNetworkInterceptionLive = isNetworkInterceptionLive;
@@ -22,10 +22,10 @@ var _sharedUi = require("@buoy-gg/shared-ui");
22
22
  * A record exists while either flag is true and is dropped when both go false,
23
23
  * so all four states fall out without special cases.
24
24
  *
25
- * IDENTITY WARNING: `networkListener` mints request IDs from a per-runtime
26
- * counter (`fetch_1`, `xhr_2`), so after an app reload `fetch_1` is a
27
- * DIFFERENT request. Records are therefore keyed on a generated `key`, carry
28
- * the `sessionId` that captured them, and only trust `liveId` while that
25
+ * Request IDs include a runtime session and a counter. Saved records also
26
+ * track their owning session because live rows disappear on app reload.
27
+ * Records use a generated `key`, carry the `sessionId` that captured them,
28
+ * and only trust `liveId` while that
29
29
  * session is the current one. Snapshots restored from an earlier session get
30
30
  * their event `id` rewritten to `saved:<key>` so a restored pin can never
31
31
  * resolve to (or dedupe against) an unrelated live request.