@browserbridge/bbx 1.7.4 → 1.7.6

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 (34) hide show
  1. package/package.json +6 -6
  2. package/packages/agent-client/src/atomic-write.js +99 -0
  3. package/packages/agent-client/src/cli-args.js +152 -0
  4. package/packages/agent-client/src/cli-batch.js +140 -0
  5. package/packages/agent-client/src/cli-helpers.js +20 -3
  6. package/packages/agent-client/src/cli-output.js +57 -0
  7. package/packages/agent-client/src/cli-proxy-remote.js +350 -0
  8. package/packages/agent-client/src/cli-setup-commands.js +404 -0
  9. package/packages/agent-client/src/cli.js +63 -906
  10. package/packages/agent-client/src/command-registry.js +12 -6
  11. package/packages/agent-client/src/install.js +39 -4
  12. package/packages/agent-client/src/mcp-config.js +28 -14
  13. package/packages/agent-client/src/remotes.js +93 -15
  14. package/packages/agent-client/src/runtime.js +6 -4
  15. package/packages/mcp-server/src/bin.js +10 -1
  16. package/packages/mcp-server/src/guidance.js +1 -1
  17. package/packages/mcp-server/src/handlers-capture.js +8 -5
  18. package/packages/mcp-server/src/handlers-dom.js +2 -2
  19. package/packages/mcp-server/src/handlers-navigation.js +20 -9
  20. package/packages/mcp-server/src/handlers-page.js +111 -111
  21. package/packages/mcp-server/src/handlers-utils.js +268 -8
  22. package/packages/mcp-server/src/handlers.js +31 -10
  23. package/packages/mcp-server/src/lifecycle.js +413 -0
  24. package/packages/mcp-server/src/server.js +27 -19
  25. package/packages/native-host/src/auth-token.js +1 -1
  26. package/packages/native-host/src/config.js +9 -4
  27. package/packages/native-host/src/daemon-process.js +8 -1
  28. package/packages/protocol/src/capabilities.js +33 -110
  29. package/packages/protocol/src/capability-values.js +45 -0
  30. package/packages/protocol/src/defaults.js +19 -2
  31. package/packages/protocol/src/protocol.js +106 -5
  32. package/packages/protocol/src/registry.js +299 -86
  33. package/packages/protocol/src/types.ts +19 -1
  34. package/skills/browser-bridge/SKILL.md +1 -1
@@ -1,122 +1,45 @@
1
1
  // @ts-check
2
2
 
3
+ import { CAPABILITIES, DEFAULT_CAPABILITIES } from './capability-values.js';
4
+ import { BRIDGE_METHODS, BRIDGE_METHOD_REGISTRY } from './registry.js';
5
+
3
6
  /** @typedef {import('./types.js').BridgeMethod} CapabilityMethod */
4
7
  /** @typedef {import('./types.js').Capability} Capability */
5
8
 
6
- export const CAPABILITIES = Object.freeze({
7
- PAGE_READ: 'page.read',
8
- PAGE_EVALUATE: 'page.evaluate',
9
- DOM_READ: 'dom.read',
10
- STYLES_READ: 'styles.read',
11
- LAYOUT_READ: 'layout.read',
12
- VIEWPORT_CONTROL: 'viewport.control',
13
- NAVIGATION_CONTROL: 'navigation.control',
14
- SCREENSHOT_PARTIAL: 'screenshot.partial',
15
- PATCH_DOM: 'patch.dom',
16
- PATCH_STYLES: 'patch.styles',
17
- CDP_DOM_SNAPSHOT: 'cdp.dom_snapshot',
18
- CDP_BOX_MODEL: 'cdp.box_model',
19
- CDP_STYLES: 'cdp.styles',
20
- CDP_INPUT: 'cdp.input',
21
- AUTOMATION_INPUT: 'automation.input',
22
- TABS_MANAGE: 'tabs.manage',
23
- PERFORMANCE_READ: 'performance.read',
24
- NETWORK_READ: 'network.read',
25
- NETWORK_INTERCEPT: 'network.intercept',
26
- });
9
+ export { CAPABILITIES, DEFAULT_CAPABILITIES };
10
+
11
+ /** @type {readonly CapabilityMethod[]} */
12
+ const LEGACY_METHOD_CAPABILITY_PREFIX = [
13
+ 'access.request',
14
+ 'tabs.list',
15
+ 'tabs.create',
16
+ 'tabs.close',
17
+ 'tabs.activate',
18
+ 'skill.get_runtime_context',
19
+ 'setup.get_status',
20
+ 'setup.install',
21
+ ];
22
+
23
+ /** @type {readonly CapabilityMethod[]} */
24
+ const LEGACY_METHOD_CAPABILITY_SUFFIX = ['log.tail', 'health.ping', 'daemon.metrics'];
27
25
 
28
- export const DEFAULT_CAPABILITIES = Object.freeze([
29
- CAPABILITIES.PAGE_READ,
30
- CAPABILITIES.PAGE_EVALUATE,
31
- CAPABILITIES.DOM_READ,
32
- CAPABILITIES.STYLES_READ,
33
- CAPABILITIES.LAYOUT_READ,
34
- CAPABILITIES.VIEWPORT_CONTROL,
35
- CAPABILITIES.NAVIGATION_CONTROL,
36
- CAPABILITIES.SCREENSHOT_PARTIAL,
37
- CAPABILITIES.PATCH_DOM,
38
- CAPABILITIES.PATCH_STYLES,
39
- CAPABILITIES.AUTOMATION_INPUT,
40
- CAPABILITIES.CDP_DOM_SNAPSHOT,
41
- CAPABILITIES.CDP_BOX_MODEL,
42
- CAPABILITIES.CDP_STYLES,
43
- CAPABILITIES.CDP_INPUT,
44
- CAPABILITIES.TABS_MANAGE,
45
- CAPABILITIES.PERFORMANCE_READ,
46
- CAPABILITIES.NETWORK_READ,
47
- CAPABILITIES.NETWORK_INTERCEPT,
26
+ const LEGACY_POSITIONED_METHODS = new Set([
27
+ ...LEGACY_METHOD_CAPABILITY_PREFIX,
28
+ ...LEGACY_METHOD_CAPABILITY_SUFFIX,
48
29
  ]);
49
30
 
50
31
  /** @type {Readonly<Record<CapabilityMethod, Capability | null>>} */
51
- export const METHOD_CAPABILITIES = Object.freeze({
52
- 'access.request': null,
53
- 'tabs.list': null,
54
- 'tabs.create': CAPABILITIES.TABS_MANAGE,
55
- 'tabs.close': CAPABILITIES.TABS_MANAGE,
56
- 'tabs.activate': CAPABILITIES.TABS_MANAGE,
57
- 'skill.get_runtime_context': null,
58
- 'setup.get_status': null,
59
- 'setup.install': null,
60
- 'page.get_state': CAPABILITIES.PAGE_READ,
61
- 'page.evaluate': CAPABILITIES.PAGE_EVALUATE,
62
- 'page.get_console': CAPABILITIES.PAGE_READ,
63
- 'page.wait_for_load_state': CAPABILITIES.PAGE_READ,
64
- 'page.get_storage': CAPABILITIES.PAGE_READ,
65
- 'page.get_text': CAPABILITIES.PAGE_READ,
66
- 'page.get_network': CAPABILITIES.NETWORK_READ,
67
- // Interception mutates traffic (block/forge responses) - distinct from
68
- // passive network reads so hosts can gate it independently.
69
- 'network.intercept.add': CAPABILITIES.NETWORK_INTERCEPT,
70
- 'network.intercept.remove': CAPABILITIES.NETWORK_INTERCEPT,
71
- 'network.intercept.list': CAPABILITIES.NETWORK_INTERCEPT,
72
- 'network.intercept.clear': CAPABILITIES.NETWORK_INTERCEPT,
73
- 'navigation.navigate': CAPABILITIES.NAVIGATION_CONTROL,
74
- 'navigation.reload': CAPABILITIES.NAVIGATION_CONTROL,
75
- 'navigation.go_back': CAPABILITIES.NAVIGATION_CONTROL,
76
- 'navigation.go_forward': CAPABILITIES.NAVIGATION_CONTROL,
77
- 'dom.query': CAPABILITIES.DOM_READ,
78
- 'dom.describe': CAPABILITIES.DOM_READ,
79
- 'dom.get_text': CAPABILITIES.DOM_READ,
80
- 'dom.get_attributes': CAPABILITIES.DOM_READ,
81
- 'dom.wait_for': CAPABILITIES.DOM_READ,
82
- 'dom.find_by_text': CAPABILITIES.DOM_READ,
83
- 'dom.find_by_role': CAPABILITIES.DOM_READ,
84
- 'dom.get_html': CAPABILITIES.DOM_READ,
85
- 'dom.get_accessibility_tree': CAPABILITIES.DOM_READ,
86
- 'layout.get_box_model': CAPABILITIES.LAYOUT_READ,
87
- 'layout.hit_test': CAPABILITIES.LAYOUT_READ,
88
- 'styles.get_computed': CAPABILITIES.STYLES_READ,
89
- 'styles.get_matched_rules': CAPABILITIES.STYLES_READ,
90
- 'viewport.scroll': CAPABILITIES.VIEWPORT_CONTROL,
91
- 'viewport.resize': CAPABILITIES.VIEWPORT_CONTROL,
92
- 'input.click': CAPABILITIES.AUTOMATION_INPUT,
93
- 'input.focus': CAPABILITIES.AUTOMATION_INPUT,
94
- 'input.type': CAPABILITIES.AUTOMATION_INPUT,
95
- 'input.fill': CAPABILITIES.AUTOMATION_INPUT,
96
- 'input.press_key': CAPABILITIES.AUTOMATION_INPUT,
97
- 'input.set_checked': CAPABILITIES.AUTOMATION_INPUT,
98
- 'input.select_option': CAPABILITIES.AUTOMATION_INPUT,
99
- 'input.hover': CAPABILITIES.AUTOMATION_INPUT,
100
- 'input.drag': CAPABILITIES.AUTOMATION_INPUT,
101
- 'input.scroll_into_view': CAPABILITIES.AUTOMATION_INPUT,
102
- 'screenshot.capture_region': CAPABILITIES.SCREENSHOT_PARTIAL,
103
- 'screenshot.capture_element': CAPABILITIES.SCREENSHOT_PARTIAL,
104
- 'screenshot.capture_full_page': CAPABILITIES.SCREENSHOT_PARTIAL,
105
- 'patch.apply_styles': CAPABILITIES.PATCH_STYLES,
106
- 'patch.apply_dom': CAPABILITIES.PATCH_DOM,
107
- 'patch.list': CAPABILITIES.PATCH_DOM,
108
- 'patch.rollback': CAPABILITIES.PATCH_DOM,
109
- 'patch.commit_session_baseline': CAPABILITIES.PATCH_DOM,
110
- 'cdp.get_document': CAPABILITIES.CDP_DOM_SNAPSHOT,
111
- 'cdp.get_dom_snapshot': CAPABILITIES.CDP_DOM_SNAPSHOT,
112
- 'cdp.get_box_model': CAPABILITIES.CDP_BOX_MODEL,
113
- 'cdp.get_computed_styles_for_node': CAPABILITIES.CDP_STYLES,
114
- 'cdp.dispatch_key_event': CAPABILITIES.CDP_INPUT,
115
- 'performance.get_metrics': CAPABILITIES.PERFORMANCE_READ,
116
- 'log.tail': null,
117
- 'health.ping': null,
118
- 'daemon.metrics': null,
119
- });
32
+ export const METHOD_CAPABILITIES = Object.freeze(
33
+ /** @type {Record<CapabilityMethod, Capability | null>} */ (
34
+ Object.fromEntries(
35
+ [
36
+ ...LEGACY_METHOD_CAPABILITY_PREFIX,
37
+ ...BRIDGE_METHODS.filter((method) => !LEGACY_POSITIONED_METHODS.has(method)),
38
+ ...LEGACY_METHOD_CAPABILITY_SUFFIX,
39
+ ].map((method) => [method, BRIDGE_METHOD_REGISTRY[method].capability])
40
+ )
41
+ )
42
+ );
120
43
 
121
44
  /**
122
45
  * @param {unknown} value
@@ -0,0 +1,45 @@
1
+ // @ts-check
2
+
3
+ export const CAPABILITIES = Object.freeze({
4
+ PAGE_READ: 'page.read',
5
+ PAGE_EVALUATE: 'page.evaluate',
6
+ DOM_READ: 'dom.read',
7
+ STYLES_READ: 'styles.read',
8
+ LAYOUT_READ: 'layout.read',
9
+ VIEWPORT_CONTROL: 'viewport.control',
10
+ NAVIGATION_CONTROL: 'navigation.control',
11
+ SCREENSHOT_PARTIAL: 'screenshot.partial',
12
+ PATCH_DOM: 'patch.dom',
13
+ PATCH_STYLES: 'patch.styles',
14
+ CDP_DOM_SNAPSHOT: 'cdp.dom_snapshot',
15
+ CDP_BOX_MODEL: 'cdp.box_model',
16
+ CDP_STYLES: 'cdp.styles',
17
+ CDP_INPUT: 'cdp.input',
18
+ AUTOMATION_INPUT: 'automation.input',
19
+ TABS_MANAGE: 'tabs.manage',
20
+ PERFORMANCE_READ: 'performance.read',
21
+ NETWORK_READ: 'network.read',
22
+ NETWORK_INTERCEPT: 'network.intercept',
23
+ });
24
+
25
+ export const DEFAULT_CAPABILITIES = Object.freeze([
26
+ CAPABILITIES.PAGE_READ,
27
+ CAPABILITIES.PAGE_EVALUATE,
28
+ CAPABILITIES.DOM_READ,
29
+ CAPABILITIES.STYLES_READ,
30
+ CAPABILITIES.LAYOUT_READ,
31
+ CAPABILITIES.VIEWPORT_CONTROL,
32
+ CAPABILITIES.NAVIGATION_CONTROL,
33
+ CAPABILITIES.SCREENSHOT_PARTIAL,
34
+ CAPABILITIES.PATCH_DOM,
35
+ CAPABILITIES.PATCH_STYLES,
36
+ CAPABILITIES.AUTOMATION_INPUT,
37
+ CAPABILITIES.CDP_DOM_SNAPSHOT,
38
+ CAPABILITIES.CDP_BOX_MODEL,
39
+ CAPABILITIES.CDP_STYLES,
40
+ CAPABILITIES.CDP_INPUT,
41
+ CAPABILITIES.TABS_MANAGE,
42
+ CAPABILITIES.PERFORMANCE_READ,
43
+ CAPABILITIES.NETWORK_READ,
44
+ CAPABILITIES.NETWORK_INTERCEPT,
45
+ ]);
@@ -1,5 +1,7 @@
1
1
  // @ts-check
2
2
 
3
+ import { BRIDGE_METHODS, BRIDGE_METHOD_REGISTRY } from './registry.js';
4
+
3
5
  /**
4
6
  * Shared Browser Bridge defaults used by the protocol, MCP layer, and runtime
5
7
  * guidance. Keep this as the source of truth for default limits and presets.
@@ -20,6 +22,8 @@ export const DEFAULT_CONSOLE_LIMIT = 50;
20
22
  export const DEFAULT_VIEWPORT_WIDTH = 1280;
21
23
  export const DEFAULT_VIEWPORT_HEIGHT = 720;
22
24
  export const DEFAULT_DEVICE_SCALE_FACTOR = 0;
25
+ /** @type {'continue'} */
26
+ export const DEFAULT_NETWORK_INTERCEPT_ACTION = 'continue';
23
27
 
24
28
  /** Maximum size of a Chrome native messaging message in bytes. */
25
29
  export const MAX_NATIVE_MESSAGE_BYTES = 1_048_576;
@@ -68,8 +72,8 @@ export const BUDGET_PRESETS = Object.freeze({
68
72
  deep: { maxNodes: 100, maxDepth: 8, textBudget: 2000, tokenBudget: 4000 },
69
73
  });
70
74
 
71
- /** @type {ReadonlySet<string>} */
72
- export const DEBUGGER_BACKED_METHODS = new Set([
75
+ /** @type {readonly import('./types.js').BridgeMethod[]} */
76
+ const LEGACY_DEBUGGER_BACKED_METHOD_ORDER = [
73
77
  'page.evaluate',
74
78
  'dom.get_accessibility_tree',
75
79
  'viewport.resize',
@@ -86,6 +90,19 @@ export const DEBUGGER_BACKED_METHODS = new Set([
86
90
  'cdp.get_box_model',
87
91
  'cdp.get_computed_styles_for_node',
88
92
  'cdp.dispatch_key_event',
93
+ ];
94
+
95
+ const LEGACY_DEBUGGER_BACKED_METHODS = new Set(LEGACY_DEBUGGER_BACKED_METHOD_ORDER);
96
+
97
+ /** @type {ReadonlySet<string>} */
98
+ export const DEBUGGER_BACKED_METHODS = new Set([
99
+ ...LEGACY_DEBUGGER_BACKED_METHOD_ORDER.filter(
100
+ (method) => BRIDGE_METHOD_REGISTRY[method].debuggerBacked
101
+ ),
102
+ ...BRIDGE_METHODS.filter(
103
+ (method) =>
104
+ BRIDGE_METHOD_REGISTRY[method].debuggerBacked && !LEGACY_DEBUGGER_BACKED_METHODS.has(method)
105
+ ),
89
106
  ]);
90
107
 
91
108
  /**
@@ -6,12 +6,14 @@ import {
6
6
  DEFAULT_A11Y_MAX_DEPTH,
7
7
  DEFAULT_A11Y_MAX_NODES,
8
8
  DEFAULT_CONSOLE_LIMIT,
9
+ DEFAULT_DEVICE_SCALE_FACTOR,
9
10
  DEFAULT_EVAL_TIMEOUT_MS,
10
11
  DEFAULT_LOG_TAIL_LIMIT,
11
12
  DEFAULT_MAX_DEPTH,
12
13
  DEFAULT_MAX_HTML_LENGTH,
13
14
  DEFAULT_MAX_NODES,
14
15
  DEFAULT_NAV_TIMEOUT_MS,
16
+ DEFAULT_NETWORK_INTERCEPT_ACTION,
15
17
  DEFAULT_NETWORK_LIMIT,
16
18
  DEFAULT_PAGE_TEXT_BUDGET,
17
19
  DEFAULT_TEXT_BUDGET,
@@ -42,6 +44,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
42
44
  /** @typedef {import('./types.js').InputActionParams} InputActionParams */
43
45
  /** @typedef {import('./types.js').LogTailParams} LogTailParams */
44
46
  /** @typedef {import('./types.js').NavigationActionParams} NavigationActionParams */
47
+ /** @typedef {import('./types.js').NetworkInterceptAddParams} NetworkInterceptAddParams */
45
48
  /** @typedef {import('./types.js').NetworkParams} NetworkParams */
46
49
  /** @typedef {import('./types.js').NormalizedAccessibilityTreeParams} NormalizedAccessibilityTreeParams */
47
50
  /** @typedef {import('./types.js').NormalizedCheckedAction} NormalizedCheckedAction */
@@ -58,6 +61,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
58
61
  /** @typedef {import('./types.js').NormalizedInputAction} NormalizedInputAction */
59
62
  /** @typedef {import('./types.js').NormalizedLogTailParams} NormalizedLogTailParams */
60
63
  /** @typedef {import('./types.js').NormalizedNavigationAction} NormalizedNavigationAction */
64
+ /** @typedef {import('./types.js').NormalizedNetworkInterceptAddParams} NormalizedNetworkInterceptAddParams */
61
65
  /** @typedef {import('./types.js').NormalizedNetworkParams} NormalizedNetworkParams */
62
66
  /** @typedef {import('./types.js').NormalizedPageTextParams} NormalizedPageTextParams */
63
67
  /** @typedef {import('./types.js').NormalizedPatchOperation} NormalizedPatchOperation */
@@ -151,6 +155,30 @@ function clampInt(value, min, max, fallback) {
151
155
  return Math.min(Math.max(integer, min), max);
152
156
  }
153
157
 
158
+ /**
159
+ * Clamp a finite number without discarding its fractional component.
160
+ *
161
+ * @param {unknown} value
162
+ * @param {number} min
163
+ * @param {number} max
164
+ * @param {number} fallback
165
+ * @returns {number}
166
+ */
167
+ function clampNumber(value, min, max, fallback) {
168
+ const numeric = typeof value === 'number' && Number.isFinite(value) ? value : fallback;
169
+ return Math.min(Math.max(numeric, min), max);
170
+ }
171
+
172
+ /**
173
+ * @param {unknown} value
174
+ * @returns {value is Record<string, unknown>}
175
+ */
176
+ function isPlainRecord(value) {
177
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
178
+ const prototype = Object.getPrototypeOf(value);
179
+ return prototype === Object.prototype || prototype === null;
180
+ }
181
+
154
182
  /** @type {ReadonlyArray<BridgeMethod>} */
155
183
  export const METHODS = BRIDGE_METHODS;
156
184
 
@@ -336,6 +364,8 @@ function normalizeRequestParams(method, params) {
336
364
  return normalizePageTextParams(params);
337
365
  case 'page.get_network':
338
366
  return normalizeNetworkParams(params);
367
+ case 'network.intercept.add':
368
+ return normalizeNetworkInterceptAddParams(params);
339
369
  case 'navigation.navigate':
340
370
  case 'navigation.reload':
341
371
  case 'navigation.go_back':
@@ -643,7 +673,7 @@ export function normalizeNavigationAction(params = {}) {
643
673
  return {
644
674
  url,
645
675
  waitForLoad: params.waitForLoad !== false,
646
- timeoutMs: clampInt(params.timeoutMs, 500, 120_000, 15_000),
676
+ timeoutMs: clampInt(params.timeoutMs, 500, 120_000, DEFAULT_NAV_TIMEOUT_MS),
647
677
  };
648
678
  }
649
679
 
@@ -682,11 +712,17 @@ export function normalizeEvaluateParams(params = {}) {
682
712
  `Expression too large (${expression.length} chars). Maximum is 100,000.`
683
713
  );
684
714
  }
715
+ if (params.returnByValue === false) {
716
+ throw new BridgeError(
717
+ ERROR_CODES.INVALID_REQUEST,
718
+ 'returnByValue=false is not supported because remote CDP objects cannot be consumed.'
719
+ );
720
+ }
685
721
  return {
686
722
  expression,
687
723
  awaitPromise: Boolean(params.awaitPromise),
688
- timeoutMs: clampInt(params.timeoutMs, 100, 30_000, 5_000),
689
- returnByValue: params.returnByValue !== false,
724
+ timeoutMs: clampInt(params.timeoutMs, 100, 30_000, DEFAULT_EVAL_TIMEOUT_MS),
725
+ returnByValue: true,
690
726
  };
691
727
  }
692
728
 
@@ -829,7 +865,7 @@ export function normalizeStorageParams(params = {}) {
829
865
  export function normalizeWaitForLoadStateParams(params = {}) {
830
866
  return {
831
867
  waitForLoad: params.waitForLoad !== false,
832
- timeoutMs: clampInt(params.timeoutMs, 500, 120_000, 15_000),
868
+ timeoutMs: clampInt(params.timeoutMs, 500, 120_000, DEFAULT_NAV_TIMEOUT_MS),
833
869
  };
834
870
  }
835
871
 
@@ -898,6 +934,71 @@ export function normalizeNetworkParams(params = {}) {
898
934
  };
899
935
  }
900
936
 
937
+ /**
938
+ * @param {NetworkInterceptAddParams} [params={}]
939
+ * @returns {NormalizedNetworkInterceptAddParams}
940
+ */
941
+ export function normalizeNetworkInterceptAddParams(params = {}) {
942
+ if (typeof params.urlPattern !== 'string' || !params.urlPattern) {
943
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'urlPattern is required.');
944
+ }
945
+
946
+ const action = params.action ?? DEFAULT_NETWORK_INTERCEPT_ACTION;
947
+ if (action !== 'fulfill' && action !== 'continue' && action !== 'block') {
948
+ throw new BridgeError(
949
+ ERROR_CODES.INVALID_REQUEST,
950
+ 'action must be one of fulfill, continue, or block.'
951
+ );
952
+ }
953
+ if (
954
+ params.statusCode !== undefined &&
955
+ (!Number.isInteger(params.statusCode) || params.statusCode < 100 || params.statusCode > 599)
956
+ ) {
957
+ throw new BridgeError(
958
+ ERROR_CODES.INVALID_REQUEST,
959
+ 'statusCode must be an integer between 100 and 599.'
960
+ );
961
+ }
962
+ if (params.body !== undefined && typeof params.body !== 'string') {
963
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'body must be a string.');
964
+ }
965
+
966
+ /** @type {Record<string, string> | undefined} */
967
+ let headers;
968
+ if (params.headers !== undefined) {
969
+ if (!isPlainRecord(params.headers)) {
970
+ throw new BridgeError(
971
+ ERROR_CODES.INVALID_REQUEST,
972
+ 'headers must be an object containing string values.'
973
+ );
974
+ }
975
+ headers = {};
976
+ for (const [name, value] of Object.entries(params.headers)) {
977
+ if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
978
+ throw new BridgeError(
979
+ ERROR_CODES.INVALID_REQUEST,
980
+ `Invalid header name: ${name || '(empty)'}.`
981
+ );
982
+ }
983
+ if (typeof value !== 'string' || /[\r\n]/.test(value)) {
984
+ throw new BridgeError(
985
+ ERROR_CODES.INVALID_REQUEST,
986
+ `Header ${name} must have a string value without newlines.`
987
+ );
988
+ }
989
+ headers[name] = value;
990
+ }
991
+ }
992
+
993
+ return {
994
+ urlPattern: params.urlPattern,
995
+ action,
996
+ ...(params.statusCode === undefined ? {} : { statusCode: params.statusCode }),
997
+ ...(params.body === undefined ? {} : { body: params.body }),
998
+ ...(headers === undefined ? {} : { headers }),
999
+ };
1000
+ }
1001
+
901
1002
  /**
902
1003
  * @param {PageTextParams} [params={}]
903
1004
  * @returns {NormalizedPageTextParams}
@@ -926,7 +1027,7 @@ export function normalizeViewportResizeParams(params = {}) {
926
1027
  return {
927
1028
  width: clampInt(params.width, 320, 7680, DEFAULT_VIEWPORT_WIDTH),
928
1029
  height: clampInt(params.height, 200, 4320, DEFAULT_VIEWPORT_HEIGHT),
929
- deviceScaleFactor: clampInt(params.deviceScaleFactor, 0, 4, 0),
1030
+ deviceScaleFactor: clampNumber(params.deviceScaleFactor, 0, 4, DEFAULT_DEVICE_SCALE_FACTOR),
930
1031
  reset: Boolean(params.reset),
931
1032
  };
932
1033
  }