@buoy-gg/network 7.0.34 → 7.0.36
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.
- package/lib/commonjs/network/components/NetworkCopySettingsView.js +2 -2
- package/lib/commonjs/network/components/NetworkEventDetailView.js +6 -6
- package/lib/commonjs/network/components/NetworkEventItemCompact.js +3 -3
- package/lib/commonjs/network/components/NetworkFilterViewV3.js +3 -3
- package/lib/commonjs/network/components/NetworkHeaderLive.js +2 -2
- package/lib/commonjs/network/components/NetworkListScreen.js +1 -1
- package/lib/commonjs/network/components/NetworkOverrideEditor.js +1 -1
- package/lib/commonjs/network/components/NetworkOverridesScreen.js +1 -1
- package/lib/commonjs/network/components/NetworkSavedScreen.js +1 -1
- package/lib/commonjs/network/components/response-editor/Explorer.js +13 -13
- package/lib/commonjs/network/sync/networkSyncAdapter.js +198 -2
- package/lib/commonjs/network/utils/networkListener.js +156 -2
- package/lib/module/network/components/NetworkCopySettingsView.js +3 -3
- package/lib/module/network/components/NetworkEventDetailView.js +7 -7
- package/lib/module/network/components/NetworkEventItemCompact.js +4 -4
- package/lib/module/network/components/NetworkFilterViewV3.js +4 -4
- package/lib/module/network/components/NetworkHeaderLive.js +3 -3
- package/lib/module/network/components/NetworkListScreen.js +2 -2
- package/lib/module/network/components/NetworkOverrideEditor.js +2 -2
- package/lib/module/network/components/NetworkOverridesScreen.js +2 -2
- package/lib/module/network/components/NetworkSavedScreen.js +2 -2
- package/lib/module/network/components/response-editor/Explorer.js +14 -14
- package/lib/module/network/sync/networkSyncAdapter.js +199 -2
- package/lib/module/network/utils/networkListener.js +156 -2
- package/lib/typescript/network/components/NetworkCopySettingsView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkFilterViewV3.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkOverrideEditor.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkSavedScreen.d.ts.map +1 -1
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts +12 -0
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +11 -27
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -726,13 +726,13 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
726
726
|
maxHeight: 300
|
|
727
727
|
},
|
|
728
728
|
markdownText: {
|
|
729
|
-
fontFamily:
|
|
729
|
+
fontFamily: _sharedUi.monoFont,
|
|
730
730
|
fontSize: 11,
|
|
731
731
|
color: _sharedUi.macOSColors.text.primary,
|
|
732
732
|
lineHeight: 18
|
|
733
733
|
},
|
|
734
734
|
plaintextText: {
|
|
735
|
-
fontFamily:
|
|
735
|
+
fontFamily: _sharedUi.monoFont,
|
|
736
736
|
fontSize: 11,
|
|
737
737
|
color: _sharedUi.macOSColors.text.primary,
|
|
738
738
|
lineHeight: 18
|
|
@@ -870,12 +870,12 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
870
870
|
urlPath: {
|
|
871
871
|
color: _sharedUi.night.color.textSecondary,
|
|
872
872
|
fontSize: 11,
|
|
873
|
-
fontFamily:
|
|
873
|
+
fontFamily: _sharedUi.monoFont
|
|
874
874
|
},
|
|
875
875
|
urlQueryString: {
|
|
876
876
|
color: _sharedUi.night.color.info,
|
|
877
877
|
fontSize: 11,
|
|
878
|
-
fontFamily:
|
|
878
|
+
fontFamily: _sharedUi.monoFont
|
|
879
879
|
},
|
|
880
880
|
// Query Parameters - Collapsible section
|
|
881
881
|
queryParamsCollapsible: {
|
|
@@ -952,7 +952,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
952
952
|
sizeValue: {
|
|
953
953
|
color: _sharedUi.night.color.info,
|
|
954
954
|
fontSize: 10,
|
|
955
|
-
fontFamily:
|
|
955
|
+
fontFamily: _sharedUi.monoFont,
|
|
956
956
|
fontWeight: "600"
|
|
957
957
|
},
|
|
958
958
|
/* ---- Timing phases ---- */
|
|
@@ -997,7 +997,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
997
997
|
width: 72,
|
|
998
998
|
textAlign: "right",
|
|
999
999
|
fontSize: 12,
|
|
1000
|
-
fontFamily:
|
|
1000
|
+
fontFamily: _sharedUi.monoFont,
|
|
1001
1001
|
color: _sharedUi.night.color.text
|
|
1002
1002
|
},
|
|
1003
1003
|
// Collapsible section styles
|
|
@@ -1033,7 +1033,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
1033
1033
|
collapsibleCount: {
|
|
1034
1034
|
color: _sharedUi.night.color.textTertiary,
|
|
1035
1035
|
fontSize: 11.5,
|
|
1036
|
-
fontFamily:
|
|
1036
|
+
fontFamily: _sharedUi.monoFont
|
|
1037
1037
|
},
|
|
1038
1038
|
collapsibleContent: {
|
|
1039
1039
|
padding: 12
|
|
@@ -1107,7 +1107,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
1107
1107
|
filterOptionValue: {
|
|
1108
1108
|
color: _sharedUi.night.color.text,
|
|
1109
1109
|
fontSize: 13,
|
|
1110
|
-
fontFamily:
|
|
1110
|
+
fontFamily: _sharedUi.monoFont
|
|
1111
1111
|
},
|
|
1112
1112
|
filterToggle: {
|
|
1113
1113
|
paddingHorizontal: 10,
|
|
@@ -319,7 +319,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
319
319
|
fontSize: 12,
|
|
320
320
|
color: _sharedUi.night.color.text,
|
|
321
321
|
lineHeight: 16,
|
|
322
|
-
fontFamily:
|
|
322
|
+
fontFamily: _sharedUi.monoFont
|
|
323
323
|
},
|
|
324
324
|
rightSection: {
|
|
325
325
|
flexDirection: "row",
|
|
@@ -334,7 +334,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
334
334
|
bottomRightText: {
|
|
335
335
|
fontSize: 9,
|
|
336
336
|
color: _sharedUi.night.color.textTertiary,
|
|
337
|
-
fontFamily:
|
|
337
|
+
fontFamily: _sharedUi.monoFont
|
|
338
338
|
},
|
|
339
339
|
statusBadge: {
|
|
340
340
|
paddingHorizontal: 4,
|
|
@@ -390,7 +390,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
390
390
|
sizeText: {
|
|
391
391
|
fontSize: 8,
|
|
392
392
|
color: _sharedUi.night.color.textSecondary,
|
|
393
|
-
fontFamily:
|
|
393
|
+
fontFamily: _sharedUi.monoFont
|
|
394
394
|
},
|
|
395
395
|
clientText: {
|
|
396
396
|
fontSize: 8,
|
|
@@ -388,7 +388,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
388
388
|
},
|
|
389
389
|
modeBadgeText: {
|
|
390
390
|
fontSize: 9,
|
|
391
|
-
fontFamily:
|
|
391
|
+
fontFamily: _sharedUi.monoFont,
|
|
392
392
|
fontWeight: "700",
|
|
393
393
|
letterSpacing: 0.5
|
|
394
394
|
},
|
|
@@ -402,7 +402,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
402
402
|
fontSize: 11,
|
|
403
403
|
fontWeight: "600",
|
|
404
404
|
color: _sharedUi.night.color.textSecondary,
|
|
405
|
-
fontFamily:
|
|
405
|
+
fontFamily: _sharedUi.monoFont,
|
|
406
406
|
letterSpacing: 0.3
|
|
407
407
|
},
|
|
408
408
|
modeToggleGroup: {
|
|
@@ -428,7 +428,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
428
428
|
fontSize: 11,
|
|
429
429
|
fontWeight: "700",
|
|
430
430
|
color: _sharedUi.night.color.textSecondary,
|
|
431
|
-
fontFamily:
|
|
431
|
+
fontFamily: _sharedUi.monoFont,
|
|
432
432
|
letterSpacing: 0.3
|
|
433
433
|
},
|
|
434
434
|
modeToggleButtonTextActive: {
|
|
@@ -331,7 +331,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
331
331
|
headerChipValue: {
|
|
332
332
|
fontSize: 12,
|
|
333
333
|
fontWeight: "600",
|
|
334
|
-
fontFamily:
|
|
334
|
+
fontFamily: _sharedUi.monoFont
|
|
335
335
|
},
|
|
336
336
|
headerActionButton: {
|
|
337
337
|
width: 32,
|
|
@@ -370,6 +370,6 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
370
370
|
color: _sharedUi.night.color.onAccent,
|
|
371
371
|
fontSize: 9,
|
|
372
372
|
fontWeight: "700",
|
|
373
|
-
fontFamily:
|
|
373
|
+
fontFamily: _sharedUi.monoFont
|
|
374
374
|
}
|
|
375
375
|
});
|
|
@@ -493,7 +493,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
493
493
|
fontSize: 12,
|
|
494
494
|
color: _sharedUi.macOSColors.text.primary,
|
|
495
495
|
lineHeight: 16,
|
|
496
|
-
fontFamily:
|
|
496
|
+
fontFamily: _sharedUi.monoFont
|
|
497
497
|
},
|
|
498
498
|
urlTextOff: {
|
|
499
499
|
color: _sharedUi.macOSColors.text.muted
|
|
@@ -540,7 +540,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
540
540
|
clearButtonText: {
|
|
541
541
|
fontSize: 12,
|
|
542
542
|
fontWeight: "700",
|
|
543
|
-
fontFamily:
|
|
543
|
+
fontFamily: _sharedUi.monoFont
|
|
544
544
|
},
|
|
545
545
|
expanderIcon: {
|
|
546
546
|
width: 18,
|
|
@@ -627,7 +627,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
627
627
|
fontSize: 10,
|
|
628
628
|
fontWeight: "600",
|
|
629
629
|
marginRight: 4,
|
|
630
|
-
fontFamily:
|
|
630
|
+
fontFamily: _sharedUi.monoFont,
|
|
631
631
|
letterSpacing: 0.4
|
|
632
632
|
},
|
|
633
633
|
labelTextFocused: {
|
|
@@ -643,14 +643,14 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
643
643
|
color: _sharedUi.buoyColors.textMuted,
|
|
644
644
|
fontSize: 10,
|
|
645
645
|
fontWeight: "400",
|
|
646
|
-
fontFamily:
|
|
646
|
+
fontFamily: _sharedUi.monoFont,
|
|
647
647
|
opacity: 0.7
|
|
648
648
|
},
|
|
649
649
|
pageRangeText: {
|
|
650
650
|
color: _sharedUi.buoyColors.textSecondary,
|
|
651
651
|
fontSize: 10,
|
|
652
652
|
fontWeight: "600",
|
|
653
|
-
fontFamily:
|
|
653
|
+
fontFamily: _sharedUi.monoFont
|
|
654
654
|
},
|
|
655
655
|
flexRowGapItemsCenter: {
|
|
656
656
|
flexDirection: "row",
|
|
@@ -708,11 +708,11 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
708
708
|
textNumber: {
|
|
709
709
|
color: _sharedUi.buoyColors.text,
|
|
710
710
|
fontWeight: "600",
|
|
711
|
-
fontFamily:
|
|
711
|
+
fontFamily: _sharedUi.monoFont
|
|
712
712
|
},
|
|
713
713
|
textString: {
|
|
714
714
|
color: _sharedUi.buoyColors.text,
|
|
715
|
-
fontFamily:
|
|
715
|
+
fontFamily: _sharedUi.monoFont
|
|
716
716
|
},
|
|
717
717
|
flexRowGapFullWidth: {
|
|
718
718
|
flexDirection: "row",
|
|
@@ -729,7 +729,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
729
729
|
fontWeight: "600",
|
|
730
730
|
fontSize: 9,
|
|
731
731
|
minWidth: 50,
|
|
732
|
-
fontFamily:
|
|
732
|
+
fontFamily: _sharedUi.monoFont,
|
|
733
733
|
letterSpacing: 0.4,
|
|
734
734
|
opacity: 0.8
|
|
735
735
|
},
|
|
@@ -783,7 +783,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
783
783
|
flex: 1,
|
|
784
784
|
color: _sharedUi.buoyColors.text,
|
|
785
785
|
fontWeight: "400",
|
|
786
|
-
fontFamily:
|
|
786
|
+
fontFamily: _sharedUi.monoFont,
|
|
787
787
|
fontSize: 12,
|
|
788
788
|
paddingVertical: 6,
|
|
789
789
|
paddingHorizontal: 10,
|
|
@@ -839,7 +839,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
839
839
|
readOnlyValue: {
|
|
840
840
|
color: _sharedUi.buoyColors.text,
|
|
841
841
|
fontSize: 13,
|
|
842
|
-
fontFamily:
|
|
842
|
+
fontFamily: _sharedUi.monoFont,
|
|
843
843
|
paddingVertical: 8,
|
|
844
844
|
paddingHorizontal: 12,
|
|
845
845
|
borderRadius: 6,
|
|
@@ -865,7 +865,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
865
865
|
marginLeft: 8,
|
|
866
866
|
color: _sharedUi.buoyColors.warning,
|
|
867
867
|
fontWeight: "500",
|
|
868
|
-
fontFamily:
|
|
868
|
+
fontFamily: _sharedUi.monoFont
|
|
869
869
|
},
|
|
870
870
|
modernToggleButton: {
|
|
871
871
|
flexDirection: "row",
|
|
@@ -904,7 +904,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
904
904
|
color: "#E5E7EB",
|
|
905
905
|
fontSize: 11,
|
|
906
906
|
fontWeight: "600",
|
|
907
|
-
fontFamily:
|
|
907
|
+
fontFamily: _sharedUi.monoFont,
|
|
908
908
|
letterSpacing: 0.3
|
|
909
909
|
},
|
|
910
910
|
toggleStatus: {
|
|
@@ -923,7 +923,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
923
923
|
fontWeight: "700",
|
|
924
924
|
textTransform: "uppercase",
|
|
925
925
|
letterSpacing: 0.8,
|
|
926
|
-
fontFamily:
|
|
926
|
+
fontFamily: _sharedUi.monoFont
|
|
927
927
|
},
|
|
928
928
|
// Pre-computed toggle icon styles to avoid inline objects [[memory:4875251]]
|
|
929
929
|
toggleIconTrue: {
|
|
@@ -962,7 +962,7 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
962
962
|
color: _sharedUi.buoyColors.textSecondary,
|
|
963
963
|
fontSize: 10,
|
|
964
964
|
fontWeight: "600",
|
|
965
|
-
fontFamily:
|
|
965
|
+
fontFamily: _sharedUi.monoFont,
|
|
966
966
|
letterSpacing: 0.4,
|
|
967
967
|
minWidth: 60,
|
|
968
968
|
flexShrink: 0
|
|
@@ -311,13 +311,116 @@ base) {
|
|
|
311
311
|
headers: rule.headers && typeof rule.headers === "object" ? rule.headers : base?.headers,
|
|
312
312
|
// Objects are accepted and serialized so a caller can send JSON directly
|
|
313
313
|
// instead of pre-stringifying it, which is the mistake everyone makes once.
|
|
314
|
-
body:
|
|
314
|
+
body: patchedBody(rule, base),
|
|
315
315
|
failKind: rule.failKind === "timeout" ? "timeout" : undefined,
|
|
316
316
|
delayMs: typeof rule.delayMs === "number" ? rule.delayMs : undefined,
|
|
317
317
|
times: typeof rule.times === "number" ? rule.times : null,
|
|
318
318
|
alternate: rule.alternate === true
|
|
319
319
|
};
|
|
320
320
|
}
|
|
321
|
+
const isPlainObject = v => !!v && typeof v === "object" && !Array.isArray(v);
|
|
322
|
+
/**
|
|
323
|
+
* Objects merge key by key; arrays and primitives are replaced. Collects two
|
|
324
|
+
* dangers instead of writing them: `shrunk` (a non-empty array shorter than
|
|
325
|
+
* the one it replaces) and `breaks` (an array/object field overwritten by null
|
|
326
|
+
* or a primitive — the shape the app renders, changed out from under it). The
|
|
327
|
+
* handler refuses either unless forced.
|
|
328
|
+
*
|
|
329
|
+
* This stays local while the typed-edit RULE is shared: it is the merge itself,
|
|
330
|
+
* not the check, and it reports network-specific dangers the store surfaces do
|
|
331
|
+
* not have. `shapeKind` comes from the shared module so both agree on what
|
|
332
|
+
* counts as an array.
|
|
333
|
+
*/
|
|
334
|
+
function deepMerge(base, patch, path = "", issues = {
|
|
335
|
+
shrunk: [],
|
|
336
|
+
breaks: []
|
|
337
|
+
}) {
|
|
338
|
+
if (Array.isArray(base) && Array.isArray(patch)) {
|
|
339
|
+
if (patch.length > 0 && patch.length < base.length) {
|
|
340
|
+
issues.shrunk.push({
|
|
341
|
+
path: path || "(root)",
|
|
342
|
+
have: base.length,
|
|
343
|
+
sent: patch.length
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return patch;
|
|
347
|
+
}
|
|
348
|
+
if (!isPlainObject(base) || !isPlainObject(patch)) return patch;
|
|
349
|
+
const out = {
|
|
350
|
+
...base
|
|
351
|
+
};
|
|
352
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
353
|
+
const p = path ? `${path}.${k}` : k;
|
|
354
|
+
if (k in base) {
|
|
355
|
+
const from = (0, _sharedUi.shapeKind)(base[k]);
|
|
356
|
+
const to = (0, _sharedUi.shapeKind)(v);
|
|
357
|
+
if ((from === "array" || from === "object") && from !== to) {
|
|
358
|
+
issues.breaks.push({
|
|
359
|
+
path: p,
|
|
360
|
+
from,
|
|
361
|
+
to
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
out[k] = deepMerge(base[k], v, p, issues);
|
|
365
|
+
} else {
|
|
366
|
+
out[k] = v;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Typed-edit violations a bodyPatch would cause against the seed body, or []. */
|
|
373
|
+
function patchViolations(rule, seed) {
|
|
374
|
+
if (rule.body != null || !isPlainObject(rule.bodyPatch) || typeof seed !== "string") return [];
|
|
375
|
+
try {
|
|
376
|
+
return (0, _sharedUi.typedEditViolations)(JSON.parse(seed), rule.bodyPatch, {
|
|
377
|
+
noun: "response body"
|
|
378
|
+
});
|
|
379
|
+
} catch {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The rule's body: explicit `body` wins, else `bodyPatch` deep-merged into the
|
|
386
|
+
* seeded body (from `fromRequestId`, or the rule being replaced), else the seed.
|
|
387
|
+
*
|
|
388
|
+
* `bodyPatch` exists for the same reason `setQueryData` has `merge`: a remote
|
|
389
|
+
* caller reads the real response through a wire that truncates big payloads,
|
|
390
|
+
* edits one field, and pastes the torn object back as `body`. The app then
|
|
391
|
+
* renders a response missing half its fields. With a patch the caller sends
|
|
392
|
+
* `{ stats: [...] }` and the other 30KB stay exactly as the server sent them.
|
|
393
|
+
*/
|
|
394
|
+
function patchedBody(rule, base) {
|
|
395
|
+
if (typeof rule.body === "string") return rule.body;
|
|
396
|
+
if (rule.body != null) return JSON.stringify(rule.body, null, 2);
|
|
397
|
+
const seed = base?.body ?? (typeof rule.id === "string" ? _overrideRulesStore.overrideRulesStore.getState().rules.find(r => r.id === rule.id)?.body : undefined);
|
|
398
|
+
if (isPlainObject(rule.bodyPatch) && typeof seed === "string") {
|
|
399
|
+
try {
|
|
400
|
+
return JSON.stringify(deepMerge(JSON.parse(seed), rule.bodyPatch), null, 2);
|
|
401
|
+
} catch {
|
|
402
|
+
return seed;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return seed;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Replacing a rule by id inherits every field the caller did not send, so a
|
|
410
|
+
* remote driver can flip one thing — `{ id, bodyPatch }`, `{ id, status: 500 }`
|
|
411
|
+
* — without re-sending the urlPattern it never had to know.
|
|
412
|
+
*/
|
|
413
|
+
function existingRule(params) {
|
|
414
|
+
if (!params || typeof params !== "object") return null;
|
|
415
|
+
const raw = params.rule ?? params;
|
|
416
|
+
if (!raw || typeof raw !== "object") return null;
|
|
417
|
+
const id = raw.id;
|
|
418
|
+
if (typeof id !== "string") return null;
|
|
419
|
+
const existing = _overrideRulesStore.overrideRulesStore.getState().rules.find(r => r.id === id);
|
|
420
|
+
return existing ? {
|
|
421
|
+
...existing
|
|
422
|
+
} : null;
|
|
423
|
+
}
|
|
321
424
|
|
|
322
425
|
/**
|
|
323
426
|
* The body a remote surface never received, when it says so.
|
|
@@ -511,7 +614,89 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
|
511
614
|
},
|
|
512
615
|
/** Create a rule, or replace one by id. */
|
|
513
616
|
upsertOverrideRule: params => {
|
|
514
|
-
const base = baseFromRequest(params) ?? preservedBody(params);
|
|
617
|
+
const base = baseFromRequest(params) ?? preservedBody(params) ?? existingRule(params);
|
|
618
|
+
const rawRule = params?.rule ?? params;
|
|
619
|
+
// `bodyPath` + `bodyValue` are sugar over `bodyPatch`, resolved against the
|
|
620
|
+
// real response before anything else looks at the rule — so the keyed
|
|
621
|
+
// expansion, the typed-edit guard and the merge below all run on an
|
|
622
|
+
// ordinary patch and gain no special cases. This is the same form the
|
|
623
|
+
// state tools take; it exists here because a response body is the surface
|
|
624
|
+
// where the caller is LEAST likely to have seen the shape (it read the
|
|
625
|
+
// body over a wire that truncates at 24,000 characters), and therefore
|
|
626
|
+
// most likely to get the nesting wrong.
|
|
627
|
+
if (rawRule && typeof rawRule.bodyPath === "string" && rawRule.body == null) {
|
|
628
|
+
const seed = base?.body ?? (typeof rawRule.id === "string" ? _overrideRulesStore.overrideRulesStore.getState().rules.find(r => r.id === rawRule.id)?.body : undefined);
|
|
629
|
+
if (typeof seed !== "string") {
|
|
630
|
+
return {
|
|
631
|
+
ok: false,
|
|
632
|
+
error: "bodyPath needs something to patch: send fromRequestId (the captured request whose real response it addresses) or the id of an existing rule."
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (rawRule.bodyValue === undefined) {
|
|
636
|
+
return {
|
|
637
|
+
ok: false,
|
|
638
|
+
error: `Refused: sent \`bodyPath\` without \`bodyValue\`. Say what to set "${rawRule.bodyPath}" to.`
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
let parsed;
|
|
642
|
+
try {
|
|
643
|
+
parsed = JSON.parse(seed);
|
|
644
|
+
} catch {
|
|
645
|
+
return {
|
|
646
|
+
ok: false,
|
|
647
|
+
error: "The captured response body is not JSON, so it cannot be addressed by path. Send a whole `body` instead."
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
const built = (0, _sharedUi.patchFromPath)(parsed, rawRule.bodyPath, rawRule.bodyValue);
|
|
651
|
+
if (!built.ok) return {
|
|
652
|
+
ok: false,
|
|
653
|
+
error: `Refused: ${built.error}`
|
|
654
|
+
};
|
|
655
|
+
rawRule.bodyPatch = built.patch;
|
|
656
|
+
delete rawRule.bodyPath;
|
|
657
|
+
delete rawRule.bodyValue;
|
|
658
|
+
}
|
|
659
|
+
if (rawRule && isPlainObject(rawRule.bodyPatch) && rawRule.body == null) {
|
|
660
|
+
const seed = base?.body ?? (typeof rawRule.id === "string" ? _overrideRulesStore.overrideRulesStore.getState().rules.find(r => r.id === rawRule.id)?.body : undefined);
|
|
661
|
+
if (typeof seed !== "string") {
|
|
662
|
+
return {
|
|
663
|
+
ok: false,
|
|
664
|
+
error: "bodyPatch needs something to patch: send fromRequestId (the captured request whose real response the patch is merged into) or the id of an existing rule."
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (rawRule.force !== true) {
|
|
668
|
+
// Address list items in the real response by their own id rather than
|
|
669
|
+
// resending the array: {results:{pikachu:{name:"test123"}}}. Expanded
|
|
670
|
+
// against the seed BEFORE the guard and before the merge, so the
|
|
671
|
+
// typed-edit rule still holds over the result. This is the only form
|
|
672
|
+
// that is correct when the caller read the response through a wire
|
|
673
|
+
// that truncated it — see packages/shared/src/sync/keyedArrays.ts.
|
|
674
|
+
let keyedError;
|
|
675
|
+
try {
|
|
676
|
+
const expanded = (0, _sharedUi.expandKeyedArrays)(JSON.parse(seed), rawRule.bodyPatch);
|
|
677
|
+
keyedError = expanded.error;
|
|
678
|
+
if (expanded.notes.length) rawRule.bodyPatch = expanded.patch;
|
|
679
|
+
} catch {
|
|
680
|
+
/* a non-JSON seed has no lists to address; leave the patch alone */
|
|
681
|
+
}
|
|
682
|
+
// See keyedArrays.ts: an added item whose id disagrees with the key
|
|
683
|
+
// it was sent under is ambiguous, and only this layer can say so.
|
|
684
|
+
if (keyedError) {
|
|
685
|
+
return {
|
|
686
|
+
ok: false,
|
|
687
|
+
error: `Refused: ${keyedError}`
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const violations = patchViolations(rawRule, seed);
|
|
691
|
+
if (violations.length > 0) {
|
|
692
|
+
return {
|
|
693
|
+
ok: false,
|
|
694
|
+
error: (0, _sharedUi.refusalText)(violations, "response body", (0, _sharedUi.suggestedPatch)(rawRule.bodyPatch, violations)),
|
|
695
|
+
violations
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
515
700
|
const draft = readRuleDraft(params, base);
|
|
516
701
|
if (!draft) {
|
|
517
702
|
const wanted = params?.fromRequestId;
|
|
@@ -541,8 +726,19 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
|
541
726
|
enabled: rule.enabled,
|
|
542
727
|
kind: rule.kind,
|
|
543
728
|
status: rule.status,
|
|
729
|
+
times: rule.times,
|
|
544
730
|
bodySize: rule.body ? rule.body.length : 0
|
|
545
731
|
},
|
|
732
|
+
// A rule with no budget overrides EVERY matching request until someone
|
|
733
|
+
// deletes it, and that is rarely what a one-off ask meant. Measured on
|
|
734
|
+
// `api-sim/next-call-fails-once` ("just the next one, then let it work
|
|
735
|
+
// again"): ten of fourteen attempts left the budget off and got a rule
|
|
736
|
+
// that never expires. Saying so is not a nag — it is the difference
|
|
737
|
+
// between a caller that adds `times` on its next turn and one that walks
|
|
738
|
+
// away leaving the app permanently broken.
|
|
739
|
+
...(rule.times === undefined || rule.times === null ? {
|
|
740
|
+
lingers: `This rule has no \`times\` budget, so it overrides EVERY matching request until it is disabled or deleted (id \`${rule.id}\`). If you meant only the next one, set times:1.`
|
|
741
|
+
} : {}),
|
|
546
742
|
...inactiveStamp()
|
|
547
743
|
};
|
|
548
744
|
},
|