@buoy-gg/network 7.0.12 → 7.0.14

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 (136) hide show
  1. package/lib/commonjs/index.js +52 -0
  2. package/lib/commonjs/network/components/NetworkDetailStepper.js +26 -14
  3. package/lib/commonjs/network/components/NetworkEventDetailView.js +86 -14
  4. package/lib/commonjs/network/components/NetworkEventItemCompact.js +4 -0
  5. package/lib/commonjs/network/components/NetworkHeaderLive.js +32 -7
  6. package/lib/commonjs/network/components/NetworkHeaderMenu.js +183 -0
  7. package/lib/commonjs/network/components/NetworkListScreen.js +134 -8
  8. package/lib/commonjs/network/components/NetworkModal.js +198 -51
  9. package/lib/commonjs/network/components/NetworkNumberPrompt.js +178 -0
  10. package/lib/commonjs/network/components/NetworkOverrideEditor.js +797 -0
  11. package/lib/commonjs/network/components/NetworkOverrideHeaderButton.js +102 -0
  12. package/lib/commonjs/network/components/NetworkOverridesButton.js +88 -0
  13. package/lib/commonjs/network/components/NetworkOverridesScreen.js +489 -0
  14. package/lib/commonjs/network/components/response-editor/Explorer.js +973 -0
  15. package/lib/commonjs/network/components/response-editor/deleteNestedDataByPath.js +56 -0
  16. package/lib/commonjs/network/components/response-editor/editorContext.js +33 -0
  17. package/lib/commonjs/network/components/response-editor/updateNestedDataByPath.js +61 -0
  18. package/lib/commonjs/network/hooks/useNetworkSelection.js +53 -0
  19. package/lib/commonjs/network/hooks/useOverrideRules.js +36 -0
  20. package/lib/commonjs/network/overrides/applyToFetch.js +80 -0
  21. package/lib/commonjs/network/overrides/applyToXhr.js +190 -0
  22. package/lib/commonjs/network/overrides/engine.js +70 -0
  23. package/lib/commonjs/network/overrides/matchRule.js +81 -0
  24. package/lib/commonjs/network/overrides/overrideRulesStore.js +550 -0
  25. package/lib/commonjs/network/overrides/presets.js +70 -0
  26. package/lib/commonjs/network/overrides/resolveOverride.js +158 -0
  27. package/lib/commonjs/network/overrides/types.js +85 -0
  28. package/lib/commonjs/network/sync/networkSyncAdapter.js +230 -1
  29. package/lib/commonjs/network/types/index.js +5 -1
  30. package/lib/commonjs/network/utils/networkEventStore.js +52 -6
  31. package/lib/commonjs/network/utils/networkListener.js +90 -10
  32. package/lib/commonjs/network/utils/networkSelectionStore.js +193 -0
  33. package/lib/commonjs/preset.js +11 -0
  34. package/lib/module/index.js +19 -0
  35. package/lib/module/network/components/NetworkDetailStepper.js +27 -15
  36. package/lib/module/network/components/NetworkEventDetailView.js +87 -15
  37. package/lib/module/network/components/NetworkEventItemCompact.js +5 -1
  38. package/lib/module/network/components/NetworkHeaderLive.js +32 -7
  39. package/lib/module/network/components/NetworkHeaderMenu.js +177 -0
  40. package/lib/module/network/components/NetworkListScreen.js +135 -9
  41. package/lib/module/network/components/NetworkModal.js +200 -53
  42. package/lib/module/network/components/NetworkNumberPrompt.js +174 -0
  43. package/lib/module/network/components/NetworkOverrideEditor.js +794 -0
  44. package/lib/module/network/components/NetworkOverrideHeaderButton.js +99 -0
  45. package/lib/module/network/components/NetworkOverridesButton.js +85 -0
  46. package/lib/module/network/components/NetworkOverridesScreen.js +485 -0
  47. package/lib/module/network/components/response-editor/Explorer.js +972 -0
  48. package/lib/module/network/components/response-editor/deleteNestedDataByPath.js +51 -0
  49. package/lib/module/network/components/response-editor/editorContext.js +29 -0
  50. package/lib/module/network/components/response-editor/updateNestedDataByPath.js +56 -0
  51. package/lib/module/network/hooks/useNetworkSelection.js +46 -0
  52. package/lib/module/network/hooks/useOverrideRules.js +32 -0
  53. package/lib/module/network/overrides/applyToFetch.js +72 -0
  54. package/lib/module/network/overrides/applyToXhr.js +185 -0
  55. package/lib/module/network/overrides/engine.js +64 -0
  56. package/lib/module/network/overrides/matchRule.js +75 -0
  57. package/lib/module/network/overrides/overrideRulesStore.js +547 -0
  58. package/lib/module/network/overrides/presets.js +65 -0
  59. package/lib/module/network/overrides/resolveOverride.js +152 -0
  60. package/lib/module/network/overrides/types.js +80 -0
  61. package/lib/module/network/sync/networkSyncAdapter.js +230 -1
  62. package/lib/module/network/types/index.js +3 -1
  63. package/lib/module/network/utils/networkEventStore.js +52 -6
  64. package/lib/module/network/utils/networkListener.js +91 -10
  65. package/lib/module/network/utils/networkSelectionStore.js +188 -0
  66. package/lib/module/preset.js +13 -1
  67. package/lib/typescript/index.d.ts +9 -0
  68. package/lib/typescript/index.d.ts.map +1 -1
  69. package/lib/typescript/network/components/NetworkDetailStepper.d.ts +8 -5
  70. package/lib/typescript/network/components/NetworkDetailStepper.d.ts.map +1 -1
  71. package/lib/typescript/network/components/NetworkEventDetailView.d.ts +36 -1
  72. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  73. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +6 -0
  74. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  75. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +13 -3
  76. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  77. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +46 -0
  78. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -0
  79. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  80. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  81. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts +33 -0
  82. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts.map +1 -0
  83. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts +17 -0
  84. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts.map +1 -0
  85. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts +27 -0
  86. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts.map +1 -0
  87. package/lib/typescript/network/components/NetworkOverridesButton.d.ts +17 -0
  88. package/lib/typescript/network/components/NetworkOverridesButton.d.ts.map +1 -0
  89. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts +29 -0
  90. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -0
  91. package/lib/typescript/network/components/response-editor/Explorer.d.ts +17 -0
  92. package/lib/typescript/network/components/response-editor/Explorer.d.ts.map +1 -0
  93. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts +9 -0
  94. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts.map +1 -0
  95. package/lib/typescript/network/components/response-editor/editorContext.d.ts +29 -0
  96. package/lib/typescript/network/components/response-editor/editorContext.d.ts.map +1 -0
  97. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts +10 -0
  98. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts.map +1 -0
  99. package/lib/typescript/network/hooks/useNetworkSelection.d.ts +26 -0
  100. package/lib/typescript/network/hooks/useNetworkSelection.d.ts.map +1 -0
  101. package/lib/typescript/network/hooks/useOverrideRules.d.ts +29 -0
  102. package/lib/typescript/network/hooks/useOverrideRules.d.ts.map +1 -0
  103. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts +16 -0
  104. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts.map +1 -0
  105. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts +19 -0
  106. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts.map +1 -0
  107. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts +22 -0
  108. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts.map +1 -0
  109. package/lib/typescript/network/overrides/applyToFetch.d.ts +50 -0
  110. package/lib/typescript/network/overrides/applyToFetch.d.ts.map +1 -0
  111. package/lib/typescript/network/overrides/applyToXhr.d.ts +43 -0
  112. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -0
  113. package/lib/typescript/network/overrides/engine.d.ts +47 -0
  114. package/lib/typescript/network/overrides/engine.d.ts.map +1 -0
  115. package/lib/typescript/network/overrides/matchRule.d.ts +22 -0
  116. package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -0
  117. package/lib/typescript/network/overrides/overrideRulesStore.d.ts +205 -0
  118. package/lib/typescript/network/overrides/overrideRulesStore.d.ts.map +1 -0
  119. package/lib/typescript/network/overrides/presets.d.ts +38 -0
  120. package/lib/typescript/network/overrides/presets.d.ts.map +1 -0
  121. package/lib/typescript/network/overrides/resolveOverride.d.ts +46 -0
  122. package/lib/typescript/network/overrides/resolveOverride.d.ts.map +1 -0
  123. package/lib/typescript/network/overrides/types.d.ts +169 -0
  124. package/lib/typescript/network/overrides/types.d.ts.map +1 -0
  125. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +73 -0
  126. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  127. package/lib/typescript/network/types/index.d.ts +10 -0
  128. package/lib/typescript/network/types/index.d.ts.map +1 -1
  129. package/lib/typescript/network/utils/networkEventStore.d.ts +14 -0
  130. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  131. package/lib/typescript/network/utils/networkListener.d.ts +3 -0
  132. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  133. package/lib/typescript/network/utils/networkSelectionStore.d.ts +117 -0
  134. package/lib/typescript/network/utils/networkSelectionStore.d.ts.map +1 -0
  135. package/lib/typescript/preset.d.ts.map +1 -1
  136. package/package.json +2 -2
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.findMatchingRule = findMatchingRule;
7
+ exports.isSpent = isSpent;
8
+ exports.resolveOverride = resolveOverride;
9
+ exports.shouldApply = shouldApply;
10
+ exports.toOutcome = toOutcome;
11
+ var _matchRule = require("./matchRule");
12
+ var _types = require("./types");
13
+ /**
14
+ * Decide whether a request is overridden — the whole decision, in one pure
15
+ * function.
16
+ *
17
+ * Pure on purpose: this is the piece worth unit-testing, and it runs inside the
18
+ * app's own `XMLHttpRequest.send()`, so it must be cheap, total, and incapable
19
+ * of throwing. Everything that could surprise (defaults, clamping, content-type
20
+ * inference) is resolved HERE, so `applyToXhr` never makes a decision — it just
21
+ * performs the outcome.
22
+ */
23
+
24
+ /**
25
+ * Traffic that must never be overridden, whatever the user's rules say.
26
+ *
27
+ * A `*` pattern is a completely reasonable thing for a developer to write
28
+ * ("break everything and see what the app does"), and without this it would
29
+ * also break Buoy's own licence checks — turning "I tested an error state" into
30
+ * "the dev tools stopped working and I don't know why". Metro/debugger traffic
31
+ * is already excluded upstream by `networkListener.shouldIgnoreUrl`.
32
+ */
33
+ const NEVER_OVERRIDE = [/(^|\.)keygen\.sh/i];
34
+ function isProtectedUrl(url) {
35
+ return NEVER_OVERRIDE.some(pattern => pattern.test(url));
36
+ }
37
+
38
+ /**
39
+ * Guess a `content-type` when the rule didn't set one.
40
+ *
41
+ * Getting this wrong is not cosmetic: a client that trusts the header (axios's
42
+ * default transform, any `response.json()` wrapper) will hand the app a string
43
+ * where it expected an object, and the override looks broken rather than the
44
+ * header looking wrong.
45
+ */
46
+ function inferContentType(body) {
47
+ const trimmed = body.trim();
48
+ if (trimmed === "") return "text/plain; charset=utf-8";
49
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
50
+ try {
51
+ JSON.parse(trimmed);
52
+ return "application/json";
53
+ } catch {
54
+ // Looks like JSON, isn't. Fall through — mislabelling it would be worse.
55
+ }
56
+ }
57
+ return "text/plain; charset=utf-8";
58
+ }
59
+ function hasHeader(headers, name) {
60
+ const wanted = name.toLowerCase();
61
+ return Object.keys(headers).some(key => key.toLowerCase() === wanted);
62
+ }
63
+
64
+ /** Normalize a matched rule into the exact instructions the interceptor needs. */
65
+ function toOutcome(rule) {
66
+ const delayMs = Math.max(0, rule.delayMs ?? 0);
67
+ if (rule.kind === "delay") {
68
+ return {
69
+ kind: "delay",
70
+ rule,
71
+ delayMs
72
+ };
73
+ }
74
+ if (rule.kind === "fail") {
75
+ return {
76
+ kind: "fail",
77
+ rule,
78
+ delayMs,
79
+ failKind: rule.failKind ?? "network"
80
+ };
81
+ }
82
+ const body = rule.body ?? "";
83
+ const headers = {
84
+ ...(rule.headers ?? {})
85
+ };
86
+ if (!hasHeader(headers, "content-type")) {
87
+ headers["content-type"] = inferContentType(body);
88
+ }
89
+ return {
90
+ kind: "respond",
91
+ rule,
92
+ delayMs,
93
+ // Clamped, not rejected: whatwg-fetch's Response constructor THROWS outside
94
+ // [200, 599], which would crash the app's own fetch instead of overriding
95
+ // it. The editor constrains the input; this guarantees it.
96
+ status: Math.min(_types.STATUS_MAX, Math.max(_types.STATUS_MIN, rule.status ?? 200)),
97
+ statusText: rule.statusText ?? "",
98
+ headers,
99
+ body
100
+ };
101
+ }
102
+
103
+ /** A rule that has burned through its `times` budget no longer applies. */
104
+ function isSpent(rule) {
105
+ return typeof rule.times === "number" && rule.hits >= rule.times;
106
+ }
107
+
108
+ /**
109
+ * Is this rule in a phase where it should actually fire?
110
+ *
111
+ * Only `alternate` can answer no. It reads the CURRENT `seen` count, before
112
+ * this match is recorded, so an untouched rule (`seen === 0`) lets the first
113
+ * request through and overrides the second.
114
+ */
115
+ function shouldApply(rule) {
116
+ if (!rule.alternate) return true;
117
+ return rule.seen % 2 === 1;
118
+ }
119
+
120
+ /**
121
+ * The first enabled rule whose pattern covers this request, phase ignored.
122
+ *
123
+ * Separate from `resolveOverride` because an alternating rule has to advance
124
+ * its phase on the requests it deliberately skips — the caller needs the rule
125
+ * back even when the answer is "not this time".
126
+ */
127
+ function findMatchingRule(url, method, rules) {
128
+ if (rules.length === 0) return null;
129
+ // Chrome skips OPTIONS too (`NetworkPersistenceManager.ts:953`). A faked
130
+ // preflight fails CORS in a way that looks nothing like the rule you wrote.
131
+ if (method.toUpperCase() === "OPTIONS") return null;
132
+ if (isProtectedUrl(url)) return null;
133
+ for (const rule of rules) {
134
+ if (!rule.enabled) continue;
135
+ if (isSpent(rule)) continue;
136
+ if (!(0, _matchRule.ruleMatches)(rule, url, method)) continue;
137
+ return rule;
138
+ }
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * First ENABLED matching rule wins outright — no merging across rules.
144
+ *
145
+ * Chrome cascades header overrides from the most general path segment to the
146
+ * most specific, but that only exists because its rules are directory-nested
147
+ * files. A flat list with a cascade would be a rule whose effect you cannot see
148
+ * by reading it, which is exactly the wrong trade on a phone screen.
149
+ *
150
+ * @param url Full request URL, query string included.
151
+ * @param method HTTP method as the caller sent it (case-insensitive).
152
+ * @param rules Ordered rule list; earlier entries win.
153
+ */
154
+ function resolveOverride(url, method, rules) {
155
+ const rule = findMatchingRule(url, method, rules);
156
+ if (!rule || !shouldApply(rule)) return null;
157
+ return toOutcome(rule);
158
+ }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.STATUS_MIN = exports.STATUS_MAX = exports.MAX_RULES = exports.FREE_TIER_RULE_LIMIT = void 0;
7
+ exports.markForOutcome = markForOutcome;
8
+ /**
9
+ * Network response overrides — the rule model.
10
+ *
11
+ * This is Buoy's answer to Chrome DevTools' "Local Overrides", reshaped for
12
+ * mobile. Two deliberate differences from Chrome are worth knowing, because
13
+ * they explain the whole shape of this file:
14
+ *
15
+ * 1. Chrome's rules ARE a directory tree — a body override is a file at
16
+ * `<host>/<path>`, header overrides live in per-directory `.headers` files,
17
+ * and its general→specific header cascade exists only to hang off that
18
+ * nesting. We have no filesystem, so rules are a flat ordered list and the
19
+ * FIRST enabled match wins outright. That's what Charles/Proxyman/MSW do,
20
+ * and it's what a developer can reason about on a phone screen.
21
+ *
22
+ * 2. Chrome CANNOT change a status code. `NetworkManager.ts` computes
23
+ * `responseCode = isBodyOverridden ? 200 : (responseStatusCode || 200)`, so
24
+ * overriding a body forces 200, and there is no latency or failure
25
+ * simulation at all. Chrome's feature is "serve different bytes". Forcing a
26
+ * 500, a timeout or a 3s stall is the entire mobile use case, so `status`,
27
+ * `delayMs` and `kind: "fail"` below are the point of the exercise.
28
+ */
29
+
30
+ /**
31
+ * What a rule does when it matches.
32
+ *
33
+ * - `respond` — synthesize a response. The request never leaves the device.
34
+ * - `fail` — synthesize a transport failure (offline / timeout). Also never
35
+ * leaves the device.
36
+ * - `delay` — let the real request run, just late. Pure latency injection.
37
+ */
38
+
39
+ /**
40
+ * Which transport failure to fake.
41
+ *
42
+ * `network` surfaces the way a real offline request does — `fetch` rejects with
43
+ * `TypeError: Network request failed`. This is also how you simulate "status 0":
44
+ * you can't, via `respond`, because whatwg-fetch's `Response` constructor throws
45
+ * a RangeError outside 200–599 (see STATUS_MIN/STATUS_MAX).
46
+ */
47
+
48
+ /**
49
+ * Status codes a synthesized response may use.
50
+ *
51
+ * NOT an arbitrary product decision: React Native's `fetch` is the whatwg-fetch
52
+ * polyfill, whose `Response` constructor throws
53
+ * `RangeError: The status provided is outside the range [200, 599]`. A rule set
54
+ * to 0 would therefore crash the app's own fetch call rather than override it.
55
+ * Use `kind: "fail"` for connection-level failures.
56
+ */
57
+ const STATUS_MIN = exports.STATUS_MIN = 200;
58
+ const STATUS_MAX = exports.STATUS_MAX = 599;
59
+
60
+ /** Bounds the persisted list so a runaway UI can't fill storage. */
61
+ const MAX_RULES = exports.MAX_RULES = 50;
62
+
63
+ /** Free tier gets one active rule; Pro is unlimited (up to MAX_RULES). */
64
+ const FREE_TIER_RULE_LIMIT = exports.FREE_TIER_RULE_LIMIT = 1;
65
+
66
+ /**
67
+ * Stamped onto the `NetworkEvent` a rule affected, so the list can badge the
68
+ * row and the detail view can offer "edit / disable the rule that did this".
69
+ */
70
+
71
+ /**
72
+ * A matched rule, normalized into exactly what the interceptor needs — defaults
73
+ * resolved, status clamped, content-type inferred. Keeping normalization in the
74
+ * resolver means `applyToXhr` never has to make a decision.
75
+ */
76
+
77
+ /** The marker recorded for an outcome. */
78
+ function markForOutcome(outcome) {
79
+ return {
80
+ ruleId: outcome.rule.id,
81
+ ruleName: outcome.rule.name,
82
+ kind: outcome.kind,
83
+ delayMs: outcome.delayMs || undefined
84
+ };
85
+ }
@@ -6,6 +6,10 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.networkSyncAdapter = exports.SNAPSHOT_BODY_INLINE_LIMIT = void 0;
7
7
  var _networkEventStore = require("../utils/networkEventStore");
8
8
  var _networkSavedStore = require("../utils/networkSavedStore");
9
+ var _overrideRulesStore = require("../overrides/overrideRulesStore");
10
+ var _presets = require("../overrides/presets");
11
+ var _engine = require("../overrides/engine");
12
+ var _networkListener = require("../utils/networkListener");
9
13
  /**
10
14
  * Request/response bodies larger than this (using the cached size proxy already
11
15
  * on the event — no per-snapshot stringify) are NOT streamed in the list
@@ -31,9 +35,116 @@ const stripLargeBodies = event => {
31
35
  if (stripResponse) stripped.responseData = undefined;
32
36
  return stripped;
33
37
  };
38
+
39
+ /**
40
+ * Drop oversized rule bodies from the snapshot, flagging what was dropped.
41
+ *
42
+ * Same limit and same reasoning as `stripLargeBodies` above — and the same
43
+ * hazard, only worse: an override body is authored once and then rides EVERY
44
+ * snapshot for as long as the rule exists, whereas an event ages out of the
45
+ * list. `draftFromEvent` seeds a rule with the real response, so 500KB is an
46
+ * ordinary size here, not a pathological one.
47
+ */
48
+ const stripLargeRuleBodies = state => {
49
+ let changed = false;
50
+ const rules = state.rules.map(rule => {
51
+ if (!rule.body || rule.body.length <= SNAPSHOT_BODY_INLINE_LIMIT) {
52
+ return rule;
53
+ }
54
+ changed = true;
55
+ return {
56
+ ...rule,
57
+ body: undefined,
58
+ bodyOmitted: true
59
+ };
60
+ });
61
+ // Identity is preserved when nothing was stripped: the desktop mirror feeds
62
+ // this straight into a `useEffect` dependency, and a fresh object every
63
+ // snapshot would re-run it several times a second.
64
+ return changed ? {
65
+ ...state,
66
+ rules
67
+ } : state;
68
+ };
34
69
  const resolveEvent = id => _networkEventStore.networkEventStore.getEventById(id) ?? _networkSavedStore.networkSavedStore.getEventById(id);
35
70
  const readId = params => params && typeof params === "object" ? params.id : undefined;
36
71
  const readBool = (params, key) => !!(params && typeof params === "object" && params[key]);
72
+ const VALID_KINDS = ["respond", "fail", "delay"];
73
+
74
+ /**
75
+ * Turn an override rule sent over the wire into a draft the store will accept.
76
+ *
77
+ * Validated rather than trusted: these params arrive from the dashboard and
78
+ * from MCP clients, and a malformed rule doesn't just fail — it gets PERSISTED
79
+ * and then re-applied to real traffic on every launch. `urlPattern` is the one
80
+ * field with no sane default, so its absence rejects the whole call.
81
+ */
82
+ function readRuleDraft(params,
83
+ /**
84
+ * Prefill from a captured request, as the UI's Override button does. Any
85
+ * field the caller sent explicitly still wins over it.
86
+ */
87
+ base) {
88
+ if (!params || typeof params !== "object") return null;
89
+ const raw = params.rule ?? params;
90
+ if (!raw || typeof raw !== "object") return null;
91
+ const rule = raw;
92
+ const urlPattern = typeof rule.urlPattern === "string" && rule.urlPattern.trim() ? rule.urlPattern.trim() : base?.urlPattern ?? "";
93
+ if (!urlPattern) return null;
94
+ const kind = VALID_KINDS.includes(rule.kind) ? rule.kind : base?.kind ?? "respond";
95
+ return {
96
+ id: typeof rule.id === "string" ? rule.id : undefined,
97
+ enabled: rule.enabled !== false,
98
+ name: typeof rule.name === "string" ? rule.name : base?.name,
99
+ urlPattern,
100
+ methods: Array.isArray(rule.methods) ? rule.methods.filter(m => typeof m === "string").map(m => m.toUpperCase()) : base?.methods,
101
+ kind,
102
+ status: typeof rule.status === "number" ? rule.status : base?.status,
103
+ statusText: typeof rule.statusText === "string" ? rule.statusText : base?.statusText,
104
+ headers: rule.headers && typeof rule.headers === "object" ? rule.headers : base?.headers,
105
+ // Objects are accepted and serialized so a caller can send JSON directly
106
+ // instead of pre-stringifying it, which is the mistake everyone makes once.
107
+ body: typeof rule.body === "string" ? rule.body : rule.body != null ? JSON.stringify(rule.body, null, 2) : base?.body,
108
+ failKind: rule.failKind === "timeout" ? "timeout" : undefined,
109
+ delayMs: typeof rule.delayMs === "number" ? rule.delayMs : undefined,
110
+ times: typeof rule.times === "number" ? rule.times : null,
111
+ alternate: rule.alternate === true
112
+ };
113
+ }
114
+
115
+ /**
116
+ * The body a remote surface never received, when it says so.
117
+ *
118
+ * A dashboard editing a rule whose body was stripped from the snapshot has
119
+ * nothing to send back for it. Without this the save would write `body:
120
+ * undefined` and silently destroy the payload — the one field a rule exists for.
121
+ */
122
+ function preservedBody(params) {
123
+ if (!params || typeof params !== "object") return null;
124
+ const raw = params.rule ?? params;
125
+ if (!raw || typeof raw !== "object") return null;
126
+ const rule = raw;
127
+ if (rule.bodyOmitted !== true || typeof rule.id !== "string") return null;
128
+ const existing = _overrideRulesStore.overrideRulesStore.getState().rules.find(candidate => candidate.id === rule.id);
129
+ return existing ? {
130
+ body: existing.body
131
+ } : null;
132
+ }
133
+
134
+ /**
135
+ * Resolve `fromRequestId` into the same draft the UI's Override button builds.
136
+ *
137
+ * Worth routing through `draftFromEvent` rather than letting a caller paste the
138
+ * URL: it drops the query string to `*`, which is the difference between a rule
139
+ * that fires and one that sits at 0 hits against a cache-busting client.
140
+ */
141
+ function baseFromRequest(params) {
142
+ if (!params || typeof params !== "object") return null;
143
+ const id = params.fromRequestId;
144
+ if (typeof id !== "string" || !id) return null;
145
+ const event = resolveEvent(id);
146
+ return event ? (0, _presets.draftFromEvent)(event) : null;
147
+ }
37
148
 
38
149
  /**
39
150
  * Sync adapter for the network tool, consumed by @buoy-gg/external-sync's
@@ -53,9 +164,15 @@ const readBool = (params, key) => !!(params && typeof params === "object" && par
53
164
  * `NetworkPanel`/`NetworkInline` accept either shape.
54
165
  */
55
166
  const networkSyncAdapter = exports.networkSyncAdapter = {
56
- version: 3,
167
+ version: 4,
57
168
  getSnapshot: () => ({
58
169
  events: _networkEventStore.networkEventStore.getEvents().map(stripLargeBodies),
170
+ /**
171
+ * Response-override rules live on the DEVICE — that's the only place they
172
+ * can run, since a rule is applied inside the app's own XMLHttpRequest.
173
+ * Remote surfaces mirror this and forward their edits back as actions.
174
+ */
175
+ overrides: stripLargeRuleBodies(_overrideRulesStore.overrideRulesStore.getState()),
59
176
  // Saved snapshots go through the same body strip. They are few (capped in
60
177
  // the store) but they ride on EVERY snapshot, so a fat one would land in
61
178
  // the same 200ms loop that the body-stripping exists to protect.
@@ -69,9 +186,13 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
69
186
  // both stores drive the snapshot.
70
187
  const unsubscribeEvents = _networkEventStore.networkEventStore.subscribeToEvents(onChange);
71
188
  const unsubscribeSaved = _networkSavedStore.networkSavedStore.subscribe(onChange);
189
+ // Same reasoning for rules: editing one with no traffic in flight must
190
+ // still reach the dashboard. Subscribing also hydrates the rule list.
191
+ const unsubscribeOverrides = _overrideRulesStore.overrideRulesStore.subscribe(onChange);
72
192
  return () => {
73
193
  unsubscribeEvents();
74
194
  unsubscribeSaved();
195
+ unsubscribeOverrides();
75
196
  };
76
197
  },
77
198
  actions: {
@@ -142,6 +263,114 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
142
263
  return {
143
264
  ok: true
144
265
  };
266
+ },
267
+ // =========================================================================
268
+ // RESPONSE OVERRIDES
269
+ // =========================================================================
270
+
271
+ /** Master switch — turns every rule off without losing any of them. */
272
+ setOverridesEnabled: params => {
273
+ _overrideRulesStore.overrideRulesStore.setEnabled(readBool(params, "enabled"));
274
+ return {
275
+ ok: true,
276
+ enabled: _overrideRulesStore.overrideRulesStore.getState().enabled
277
+ };
278
+ },
279
+ /** Create a rule, or replace one by id. */
280
+ upsertOverrideRule: params => {
281
+ const base = baseFromRequest(params) ?? preservedBody(params);
282
+ const draft = readRuleDraft(params, base);
283
+ if (!draft) {
284
+ const wanted = params?.fromRequestId;
285
+ return {
286
+ ok: false,
287
+ error: wanted ? `No captured request \`${wanted}\` — it may have been cleared. Send a urlPattern instead.` : "A rule needs a non-empty urlPattern."
288
+ };
289
+ }
290
+ const rule = _overrideRulesStore.overrideRulesStore.upsertRule(draft);
291
+ if (!rule) return {
292
+ ok: false,
293
+ error: "Override rule limit reached."
294
+ };
295
+ // A rule pushed from a remote surface is meant to take effect now;
296
+ // leaving it dark under an OFF master switch reads as a silent failure.
297
+ if (rule.enabled) _overrideRulesStore.overrideRulesStore.setEnabled(true);
298
+ // A RECEIPT, not the rule. `fromRequestId` seeds the real response as the
299
+ // body, so echoing the whole rule sent 380KB back down the socket for a
300
+ // Pokémon payload — and no caller reads it: the desktop mirror gets the
301
+ // rule from the next snapshot, and MCP only needs the id to report and
302
+ // remove it.
303
+ return {
304
+ ok: true,
305
+ rule: {
306
+ id: rule.id,
307
+ urlPattern: rule.urlPattern,
308
+ enabled: rule.enabled,
309
+ kind: rule.kind,
310
+ status: rule.status,
311
+ bodySize: rule.body ? rule.body.length : 0
312
+ }
313
+ };
314
+ },
315
+ setOverrideRuleEnabled: params => {
316
+ const id = readId(params);
317
+ if (!id) return {
318
+ ok: false,
319
+ error: "Missing rule id."
320
+ };
321
+ _overrideRulesStore.overrideRulesStore.setRuleEnabled(id, readBool(params, "enabled"));
322
+ return {
323
+ ok: true
324
+ };
325
+ },
326
+ deleteOverrideRule: params => {
327
+ const id = readId(params);
328
+ if (!id) return {
329
+ ok: false,
330
+ error: "Missing rule id."
331
+ };
332
+ _overrideRulesStore.overrideRulesStore.removeRule(id);
333
+ return {
334
+ ok: true
335
+ };
336
+ },
337
+ clearOverrideRules: () => {
338
+ _overrideRulesStore.overrideRulesStore.clearRules();
339
+ return {
340
+ ok: true
341
+ };
342
+ },
343
+ /** Read the rules back — hit counts included, which the snapshot coalesces. */
344
+ listOverrideRules: () => _overrideRulesStore.overrideRulesStore.getState(),
345
+ /**
346
+ * The full body of one rule, for the surfaces the snapshot withheld it from
347
+ * (see `stripLargeRuleBodies`). Fetched when the user opens the rule, which
348
+ * is the only moment it's needed.
349
+ */
350
+ getOverrideRuleBody: params => {
351
+ const id = readId(params);
352
+ if (!id) return null;
353
+ // `getState().rules`, not `getRules()` — the latter reports nothing while
354
+ // the master switch is off, and a rule is perfectly editable then.
355
+ const rule = _overrideRulesStore.overrideRulesStore.getState().rules.find(candidate => candidate.id === id);
356
+ if (!rule) return null;
357
+ return {
358
+ body: rule.body ?? null
359
+ };
360
+ },
361
+ /** Why isn't my rule firing? Reports the engine's own view of the world. */
362
+ debugOverrides: params => {
363
+ const url = (params && typeof params === "object" ? params.url : undefined) ?? "https://example.com/";
364
+ return {
365
+ interceptorInstalled: (0, _networkListener.isNetworkListening)(),
366
+ listenerCount: (0, _networkListener.getNetworkListenerCount)(),
367
+ engine: (0, _engine.inspectEngine)(url, "GET"),
368
+ store: {
369
+ enabled: _overrideRulesStore.overrideRulesStore.getState().enabled,
370
+ ruleCount: _overrideRulesStore.overrideRulesStore.getState().rules.length,
371
+ rulesVisibleToEngine: _overrideRulesStore.overrideRulesStore.getRules().length
372
+ }
373
+ };
145
374
  }
146
375
  }
147
376
  };
@@ -1 +1,5 @@
1
- "use strict";
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
@@ -39,6 +39,13 @@ var _networkSavedStore = require("./networkSavedStore");
39
39
 
40
40
  class NetworkEventStore extends _sharedUi.BaseEventStore {
41
41
  pendingRequests = new Map();
42
+ /**
43
+ * Recently seen `METHOD:url` keys → when, and which event id we kept.
44
+ *
45
+ * The timestamp powers the dual-interception dedup below. The id is what lets
46
+ * a DROPPED duplicate still contribute something the kept event doesn't know
47
+ * — see `applyOverrideMark`.
48
+ */
42
49
  recentRequests = new Map();
43
50
  rawListenerUnsubscribe = null;
44
51
  constructor() {
@@ -102,19 +109,31 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
102
109
  // Check for duplicate request based on URL, method, and timing
103
110
  const requestKey = `${request.method}:${request.url}`;
104
111
  const now = Date.now();
105
- const lastRequestTime = this.recentRequests.get(requestKey);
112
+ const recent = this.recentRequests.get(requestKey);
106
113
 
107
114
  // If same request within 50ms, likely a duplicate from XHR/fetch dual interception
108
- if (lastRequestTime && now - lastRequestTime < 50) {
115
+ if (recent && now - recent.at < 50) {
116
+ // …but the duplicate is not always redundant. On React Native a
117
+ // `fetch()` is seen twice: once by the fetch wrapper, which is what
118
+ // creates this event, and again by the XHR wrapper underneath it —
119
+ // and ONLY the XHR layer resolves overrides, because that's where the
120
+ // request can actually be intercepted. So the mark arrives on the copy
121
+ // we're about to throw away. Move it across before dropping.
122
+ if (event.override) {
123
+ this.applyOverrideMark(recent.id, event.override);
124
+ }
109
125
  return;
110
126
  }
111
- this.recentRequests.set(requestKey, now);
127
+ this.recentRequests.set(requestKey, {
128
+ at: now,
129
+ id: request.id
130
+ });
112
131
 
113
132
  // Clean up old entries to prevent memory leak
114
133
  if (this.recentRequests.size > 100) {
115
134
  const cutoff = now - 5000;
116
- for (const [key, time] of this.recentRequests.entries()) {
117
- if (time < cutoff) {
135
+ for (const [key, entry] of this.recentRequests.entries()) {
136
+ if (entry.at < cutoff) {
118
137
  this.recentRequests.delete(key);
119
138
  }
120
139
  }
@@ -148,7 +167,10 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
148
167
  responseHeaders: {},
149
168
  requestClient: request.client,
150
169
  operationName,
151
- graphqlVariables
170
+ graphqlVariables,
171
+ // Present when this request came straight through the XHR wrapper
172
+ // (axios, raw XHR). For `fetch` it arrives later — see the dedup branch.
173
+ override: event.override
152
174
  };
153
175
  this.pendingRequests.set(request.id, networkEvent);
154
176
  this.events = [networkEvent, ...this.events].slice(0, this.maxEvents);
@@ -177,6 +199,10 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
177
199
  updatedEvent.error = event.error.message;
178
200
  updatedEvent.status = updatedEvent.status || 0;
179
201
  }
202
+
203
+ // A pure-XHR request carries its mark all the way through; a `fetch`
204
+ // one was marked at request time and is preserved by the spread above.
205
+ if (event.override) updatedEvent.override = event.override;
180
206
  this.events = this.events.map((e, i) => i === index ? updatedEvent : e);
181
207
  this.pendingRequests.delete(request.id);
182
208
  // Refresh any pinned/saved snapshot of this request — a request pinned
@@ -188,6 +214,26 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
188
214
  }
189
215
  }
190
216
  }
217
+
218
+ /**
219
+ * Stamp an override mark onto an already-recorded event.
220
+ *
221
+ * Only ever moves a mark from a dropped duplicate onto the event that was
222
+ * kept, so it never overwrites one that is already there.
223
+ */
224
+ applyOverrideMark(id, override) {
225
+ const index = this.events.findIndex(e => e.id === id);
226
+ if (index === -1 || this.events[index].override) return;
227
+ const updatedEvent = {
228
+ ...this.events[index],
229
+ override
230
+ };
231
+ this.events = this.events.map((e, i) => i === index ? updatedEvent : e);
232
+ const pending = this.pendingRequests.get(id);
233
+ if (pending) this.pendingRequests.set(id, updatedEvent);
234
+ this.notify(updatedEvent);
235
+ this.notifyArrayListeners();
236
+ }
191
237
  extractHost(url) {
192
238
  try {
193
239
  const urlObj = new URL(url);