@browserbridge/bbx 1.8.0 → 1.9.0

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 (39) hide show
  1. package/PRIVACY.md +29 -3
  2. package/README.md +10 -3
  3. package/SECURITY.md +12 -0
  4. package/package.json +7 -1
  5. package/packages/agent-client/src/cli-args.js +35 -0
  6. package/packages/agent-client/src/cli-batch.js +121 -58
  7. package/packages/agent-client/src/cli-output.js +8 -1
  8. package/packages/agent-client/src/cli.js +227 -19
  9. package/packages/agent-client/src/client.js +156 -3
  10. package/packages/agent-client/src/command-registry.js +8 -2
  11. package/packages/agent-client/src/config.js +195 -0
  12. package/packages/agent-client/src/npm-self-update.js +305 -0
  13. package/packages/agent-client/src/remotes.js +4 -2
  14. package/packages/agent-client/src/runtime.js +97 -5
  15. package/packages/agent-client/src/types.ts +38 -0
  16. package/packages/mcp-server/src/handlers-capture.js +29 -2
  17. package/packages/mcp-server/src/handlers-dom.js +54 -3
  18. package/packages/mcp-server/src/handlers-page.js +180 -60
  19. package/packages/mcp-server/src/handlers-utils.js +35 -4
  20. package/packages/mcp-server/src/handlers.js +10 -3
  21. package/packages/mcp-server/src/server.js +75 -6
  22. package/packages/native-host/src/artifact-store.js +353 -0
  23. package/packages/native-host/src/config.js +9 -1
  24. package/packages/native-host/src/daemon-logger.js +3 -1
  25. package/packages/native-host/src/daemon.js +543 -27
  26. package/packages/native-host/src/native-host.js +25 -2
  27. package/packages/protocol/src/capability-values.js +2 -0
  28. package/packages/protocol/src/defaults.js +28 -0
  29. package/packages/protocol/src/errors.js +48 -5
  30. package/packages/protocol/src/incidental-sanitizer.js +197 -0
  31. package/packages/protocol/src/index.js +2 -0
  32. package/packages/protocol/src/protocol.js +316 -6
  33. package/packages/protocol/src/recovery-telemetry.js +302 -0
  34. package/packages/protocol/src/registry.js +143 -6
  35. package/packages/protocol/src/summary.js +116 -25
  36. package/packages/protocol/src/types.ts +489 -4
  37. package/skills/browser-bridge/SKILL.md +22 -8
  38. package/skills/browser-bridge/references/protocol.md +117 -13
  39. package/skills/browser-bridge/references/token-efficiency.md +17 -7
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { applyBudget } from './budget.js';
4
4
  import {
5
+ ARTIFACT_CHUNK_BYTES,
5
6
  BUDGET_PRESETS,
6
7
  DEFAULT_A11Y_MAX_DEPTH,
7
8
  DEFAULT_A11Y_MAX_NODES,
@@ -9,6 +10,7 @@ import {
9
10
  DEFAULT_DEVICE_SCALE_FACTOR,
10
11
  DEFAULT_EVAL_TIMEOUT_MS,
11
12
  DEFAULT_EXTRACT_SETTLE_TIMEOUT_MS,
13
+ DEFAULT_HAR_LIMIT,
12
14
  DEFAULT_LOG_TAIL_LIMIT,
13
15
  DEFAULT_MAX_DEPTH,
14
16
  DEFAULT_MAX_HTML_LENGTH,
@@ -22,11 +24,16 @@ import {
22
24
  DEFAULT_VIEWPORT_WIDTH,
23
25
  DEFAULT_WAIT_TIMEOUT_MS,
24
26
  MAX_EXTRACT_SETTLE_TIMEOUT_MS,
27
+ MAX_ARTIFACT_BYTES,
28
+ MAX_HAR_ENTRIES,
29
+ MAX_SENSITIVE_VALUE_BYTES,
25
30
  } from './defaults.js';
26
31
  import { BridgeError, ERROR_CODES, getErrorRecovery } from './errors.js';
27
32
  import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry.js';
28
33
 
29
34
  /** @typedef {import('./types.js').AccessibilityTreeParams} AccessibilityTreeParams */
35
+ /** @typedef {import('./types.js').ArtifactDeleteParams} ArtifactDeleteParams */
36
+ /** @typedef {import('./types.js').ArtifactReadParams} ArtifactReadParams */
30
37
  /** @typedef {import('./types.js').AccessRequestParams} AccessRequestParams */
31
38
  /** @typedef {import('./types.js').BridgeFailureResponse} BridgeFailureResponse */
32
39
  /** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
@@ -35,9 +42,13 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
35
42
  /** @typedef {import('./types.js').BridgeSuccessResponse} BridgeSuccessResponse */
36
43
  /** @typedef {import('./types.js').CheckedActionParams} CheckedActionParams */
37
44
  /** @typedef {import('./types.js').CdpDispatchKeyEventParams} CdpDispatchKeyEventParams */
45
+ /** @typedef {import('./types.js').CdpDomSnapshotParams} CdpDomSnapshotParams */
38
46
  /** @typedef {import('./types.js').CdpNodeIdParams} CdpNodeIdParams */
39
47
  /** @typedef {import('./types.js').ConsoleParams} ConsoleParams */
40
48
  /** @typedef {import('./types.js').DomQueryParams} DomQueryParams */
49
+ /** @typedef {import('./types.js').DomBaselineCreateParams} DomBaselineCreateParams */
50
+ /** @typedef {import('./types.js').DomBaselineCompareParams} DomBaselineCompareParams */
51
+ /** @typedef {import('./types.js').DomBaselineHandleParams} DomBaselineHandleParams */
41
52
  /** @typedef {import('./types.js').DragParams} DragParams */
42
53
  /** @typedef {import('./types.js').EvaluateParams} EvaluateParams */
43
54
  /** @typedef {import('./types.js').ExtractContentParams} ExtractContentParams */
@@ -45,6 +56,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
45
56
  /** @typedef {import('./types.js').FindByTextParams} FindByTextParams */
46
57
  /** @typedef {import('./types.js').GetHtmlParams} GetHtmlParams */
47
58
  /** @typedef {import('./types.js').HoverParams} HoverParams */
59
+ /** @typedef {import('./types.js').HarExportParams} HarExportParams */
48
60
  /** @typedef {import('./types.js').HandleDialogParams} HandleDialogParams */
49
61
  /** @typedef {import('./types.js').InputActionParams} InputActionParams */
50
62
  /** @typedef {import('./types.js').LogTailParams} LogTailParams */
@@ -52,12 +64,17 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
52
64
  /** @typedef {import('./types.js').NetworkInterceptAddParams} NetworkInterceptAddParams */
53
65
  /** @typedef {import('./types.js').NetworkParams} NetworkParams */
54
66
  /** @typedef {import('./types.js').NormalizedAccessibilityTreeParams} NormalizedAccessibilityTreeParams */
67
+ /** @typedef {import('./types.js').NormalizedArtifactReadParams} NormalizedArtifactReadParams */
55
68
  /** @typedef {import('./types.js').NormalizedAccessRequestParams} NormalizedAccessRequestParams */
56
69
  /** @typedef {import('./types.js').NormalizedCheckedAction} NormalizedCheckedAction */
57
70
  /** @typedef {import('./types.js').NormalizedCdpDispatchKeyEventParams} NormalizedCdpDispatchKeyEventParams */
71
+ /** @typedef {import('./types.js').NormalizedCdpDomSnapshotParams} NormalizedCdpDomSnapshotParams */
58
72
  /** @typedef {import('./types.js').NormalizedCdpNodeIdParams} NormalizedCdpNodeIdParams */
59
73
  /** @typedef {import('./types.js').NormalizedConsoleParams} NormalizedConsoleParams */
60
74
  /** @typedef {import('./types.js').NormalizedDomQuery} NormalizedDomQuery */
75
+ /** @typedef {import('./types.js').NormalizedDomBaselineCreateParams} NormalizedDomBaselineCreateParams */
76
+ /** @typedef {import('./types.js').NormalizedDomBaselineCompareParams} NormalizedDomBaselineCompareParams */
77
+ /** @typedef {import('./types.js').NormalizedDomBaselineHandleParams} NormalizedDomBaselineHandleParams */
61
78
  /** @typedef {import('./types.js').NormalizedDragParams} NormalizedDragParams */
62
79
  /** @typedef {import('./types.js').NormalizedEvaluateParams} NormalizedEvaluateParams */
63
80
  /** @typedef {import('./types.js').NormalizedExtractContentParams} NormalizedExtractContentParams */
@@ -65,6 +82,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
65
82
  /** @typedef {import('./types.js').NormalizedFindByTextParams} NormalizedFindByTextParams */
66
83
  /** @typedef {import('./types.js').NormalizedGetHtmlParams} NormalizedGetHtmlParams */
67
84
  /** @typedef {import('./types.js').NormalizedHoverParams} NormalizedHoverParams */
85
+ /** @typedef {import('./types.js').NormalizedHarExportParams} NormalizedHarExportParams */
68
86
  /** @typedef {import('./types.js').NormalizedHandleDialogParams} NormalizedHandleDialogParams */
69
87
  /** @typedef {import('./types.js').NormalizedInputAction} NormalizedInputAction */
70
88
  /** @typedef {import('./types.js').NormalizedLogTailParams} NormalizedLogTailParams */
@@ -76,6 +94,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
76
94
  /** @typedef {import('./types.js').NormalizedSelectAction} NormalizedSelectAction */
77
95
  /** @typedef {import('./types.js').NormalizedScreenshotParams} NormalizedScreenshotParams */
78
96
  /** @typedef {import('./types.js').NormalizedStorageParams} NormalizedStorageParams */
97
+ /** @typedef {import('./types.js').NormalizedSensitiveReadParams} NormalizedSensitiveReadParams */
79
98
  /** @typedef {import('./types.js').NormalizedStyleQuery} NormalizedStyleQuery */
80
99
  /** @typedef {import('./types.js').NormalizedTabCloseParams} NormalizedTabCloseParams */
81
100
  /** @typedef {import('./types.js').NormalizedTabCreateParams} NormalizedTabCreateParams */
@@ -88,6 +107,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
88
107
  /** @typedef {import('./types.js').SelectActionParams} SelectActionParams */
89
108
  /** @typedef {import('./types.js').ScreenshotParams} ScreenshotParams */
90
109
  /** @typedef {import('./types.js').StorageParams} StorageParams */
110
+ /** @typedef {import('./types.js').SensitiveReadParams} SensitiveReadParams */
91
111
  /** @typedef {import('./types.js').StyleQueryParams} StyleQueryParams */
92
112
  /** @typedef {import('./types.js').TabCloseParams} TabCloseParams */
93
113
  /** @typedef {import('./types.js').TabCreateParams} TabCreateParams */
@@ -244,6 +264,14 @@ export function createSuccess(id, result, meta = {}) {
244
264
  */
245
265
  export function createFailure(id, code, message, details = null, meta = {}) {
246
266
  const recovery = getErrorRecovery(code);
267
+ const effectiveRecovery =
268
+ recovery && meta.method === 'sensitive.read'
269
+ ? {
270
+ retry: false,
271
+ ...(recovery.alternativeMethod ? { alternativeMethod: recovery.alternativeMethod } : {}),
272
+ hint: `Sensitive reads are never retried automatically. ${recovery.hint}`,
273
+ }
274
+ : recovery;
247
275
  return {
248
276
  id,
249
277
  ok: false,
@@ -252,7 +280,7 @@ export function createFailure(id, code, message, details = null, meta = {}) {
252
280
  code,
253
281
  message,
254
282
  details,
255
- ...(recovery && { recovery }),
283
+ ...(effectiveRecovery && { recovery: effectiveRecovery }),
256
284
  },
257
285
  meta: {
258
286
  protocol_version: getProtocolVersion(),
@@ -344,10 +372,28 @@ export function validateBridgeRequest(request) {
344
372
  typeof meta.protocol_version === 'string' ? meta.protocol_version : getProtocolVersion(),
345
373
  token_budget: typeof meta.token_budget === 'number' ? meta.token_budget : null,
346
374
  source: meta.source === 'cli' || meta.source === 'mcp' ? meta.source : undefined,
375
+ automatic_retry: normalizeAutomaticRetryMeta(meta.source, meta.automatic_retry),
347
376
  },
348
377
  };
349
378
  }
350
379
 
380
+ /**
381
+ * @param {unknown} source
382
+ * @param {unknown} value
383
+ * @returns {{ attempt: 2, reason: 'retryable_error' } | undefined}
384
+ */
385
+ function normalizeAutomaticRetryMeta(source, value) {
386
+ if (source !== 'mcp' || !isPlainRecord(value)) return undefined;
387
+ const keys = Object.keys(value).sort();
388
+ return keys.length === 2 &&
389
+ keys[0] === 'attempt' &&
390
+ keys[1] === 'reason' &&
391
+ value.attempt === 2 &&
392
+ value.reason === 'retryable_error'
393
+ ? { attempt: 2, reason: 'retryable_error' }
394
+ : undefined;
395
+ }
396
+
351
397
  /**
352
398
  * Normalize request params early so malformed payloads fail at the shared
353
399
  * protocol boundary before daemon or extension dispatch.
@@ -364,6 +410,13 @@ function normalizeRequestParams(method, params) {
364
410
  return normalizeLogTailParams(params);
365
411
  case 'dom.query':
366
412
  return normalizeDomQuery(params);
413
+ case 'dom.baseline.create':
414
+ return normalizeDomBaselineCreateParams(params);
415
+ case 'dom.baseline.compare':
416
+ return normalizeDomBaselineCompareParams(params);
417
+ case 'dom.baseline.describe':
418
+ case 'dom.baseline.release':
419
+ return normalizeDomBaselineHandleParams(params);
367
420
  case 'page.evaluate':
368
421
  return normalizeEvaluateParams(params);
369
422
  case 'page.get_console':
@@ -374,12 +427,16 @@ function normalizeRequestParams(method, params) {
374
427
  return normalizeWaitForLoadStateParams(params);
375
428
  case 'page.get_storage':
376
429
  return normalizeStorageParams(params);
430
+ case 'sensitive.read':
431
+ return normalizeSensitiveReadParams(params);
377
432
  case 'page.get_text':
378
433
  return normalizePageTextParams(params);
379
434
  case 'page.extract_content':
380
435
  return normalizeExtractContentParams(params);
381
436
  case 'page.get_network':
382
437
  return normalizeNetworkParams(params);
438
+ case 'network.export_har':
439
+ return normalizeHarExportParams(params);
383
440
  case 'network.intercept.add':
384
441
  return normalizeNetworkInterceptAddParams(params);
385
442
  case 'navigation.navigate':
@@ -415,6 +472,8 @@ function normalizeRequestParams(method, params) {
415
472
  case 'cdp.get_box_model':
416
473
  case 'cdp.get_computed_styles_for_node':
417
474
  return normalizeCdpNodeIdParams(params);
475
+ case 'cdp.get_dom_snapshot':
476
+ return normalizeCdpDomSnapshotParams(params);
418
477
  case 'input.set_checked':
419
478
  return normalizeCheckedAction(params);
420
479
  case 'input.select_option':
@@ -424,9 +483,18 @@ function normalizeRequestParams(method, params) {
424
483
  case 'input.drag':
425
484
  return normalizeDragParams(params);
426
485
  case 'screenshot.capture_region':
486
+ return { ...params, ...normalizeScreenshotParams(params) };
427
487
  case 'screenshot.capture_element':
428
488
  case 'screenshot.capture_full_page':
429
- return { ...params, ...normalizeScreenshotParams(params) };
489
+ return {
490
+ ...params,
491
+ ...normalizeScreenshotParams(params),
492
+ ...(params.scale === undefined ? { scale: undefined } : {}),
493
+ };
494
+ case 'artifact.read':
495
+ return normalizeArtifactReadParams(params);
496
+ case 'artifact.delete':
497
+ return normalizeArtifactDeleteParams(params);
430
498
  case 'patch.apply_styles':
431
499
  case 'patch.apply_dom':
432
500
  case 'patch.list':
@@ -510,6 +578,82 @@ export function normalizeDomQuery(params = {}) {
510
578
  };
511
579
  }
512
580
 
581
+ /**
582
+ * @param {DomBaselineCreateParams} [params={}]
583
+ * @returns {NormalizedDomBaselineCreateParams}
584
+ */
585
+ export function normalizeDomBaselineCreateParams(params = {}) {
586
+ const selector = typeof params.selector === 'string' ? params.selector.trim() : '';
587
+ if (selector.length > 2_048) {
588
+ throw new BridgeError(
589
+ ERROR_CODES.INVALID_REQUEST,
590
+ 'selector must not exceed 2,048 characters.'
591
+ );
592
+ }
593
+
594
+ const attributeAllowlist = Array.isArray(params.attributeAllowlist)
595
+ ? [
596
+ ...new Set(
597
+ params.attributeAllowlist
598
+ .filter((name) => typeof name === 'string')
599
+ .map((name) => name.trim().toLowerCase())
600
+ .filter(
601
+ (name) =>
602
+ name.length > 0 &&
603
+ name.length <= 128 &&
604
+ name !== 'value' &&
605
+ !name.startsWith('on') &&
606
+ /^[a-z_][a-z0-9_.:-]*$/u.test(name)
607
+ )
608
+ ),
609
+ ]
610
+ : [];
611
+ if (attributeAllowlist.length > 16) {
612
+ throw new BridgeError(
613
+ ERROR_CODES.INVALID_REQUEST,
614
+ 'attributeAllowlist must contain at most 16 unique attribute names.'
615
+ );
616
+ }
617
+
618
+ return {
619
+ selector: selector || 'body',
620
+ maxNodes: clampInt(params.maxNodes, 1, 1_000, 100),
621
+ maxDepth: clampInt(params.maxDepth, 1, 20, 8),
622
+ textBudget: clampInt(params.textBudget, 32, 1_000, 160),
623
+ attributeAllowlist,
624
+ };
625
+ }
626
+
627
+ /**
628
+ * @param {DomBaselineCompareParams} [params={}]
629
+ * @returns {NormalizedDomBaselineCompareParams}
630
+ */
631
+ export function normalizeDomBaselineCompareParams(params = {}) {
632
+ return {
633
+ baselineId: normalizeDomBaselineId(params.baselineId),
634
+ maxChanges: clampInt(params.maxChanges, 1, 200, 50),
635
+ };
636
+ }
637
+
638
+ /**
639
+ * @param {DomBaselineHandleParams} [params={}]
640
+ * @returns {NormalizedDomBaselineHandleParams}
641
+ */
642
+ export function normalizeDomBaselineHandleParams(params = {}) {
643
+ return { baselineId: normalizeDomBaselineId(params.baselineId) };
644
+ }
645
+
646
+ /**
647
+ * @param {unknown} value
648
+ * @returns {string}
649
+ */
650
+ function normalizeDomBaselineId(value) {
651
+ if (typeof value !== 'string' || !/^baseline_[A-Za-z0-9_-]{32,64}$/u.test(value)) {
652
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'baselineId is invalid.');
653
+ }
654
+ return value;
655
+ }
656
+
513
657
  /**
514
658
  * @param {StyleQueryParams} [params={}]
515
659
  * @returns {NormalizedStyleQuery}
@@ -651,6 +795,25 @@ export function normalizeCdpNodeIdParams(params = {}) {
651
795
  };
652
796
  }
653
797
 
798
+ /**
799
+ * @param {CdpDomSnapshotParams} [params={}]
800
+ * @returns {NormalizedCdpDomSnapshotParams}
801
+ */
802
+ export function normalizeCdpDomSnapshotParams(params = {}) {
803
+ const computedStyles = Array.isArray(params.computedStyles)
804
+ ? params.computedStyles.filter(
805
+ (property) => typeof property === 'string' && property.length > 0 && property.length <= 128
806
+ )
807
+ : [];
808
+ if (computedStyles.length > 100) {
809
+ throw new BridgeError(
810
+ ERROR_CODES.INVALID_REQUEST,
811
+ 'computedStyles must contain at most 100 CSS property names.'
812
+ );
813
+ }
814
+ return { computedStyles };
815
+ }
816
+
654
817
  /**
655
818
  * @param {CheckedActionParams} [params={}]
656
819
  * @returns {NormalizedCheckedAction}
@@ -745,13 +908,30 @@ export function normalizeNavigationAction(params = {}) {
745
908
  * @returns {NormalizedPatchOperation}
746
909
  */
747
910
  export function normalizePatchOperation(params = {}) {
911
+ const operation = typeof params.operation === 'string' ? params.operation : null;
912
+ if (
913
+ operation !== null &&
914
+ ![
915
+ 'set_text',
916
+ 'set_attribute',
917
+ 'remove_attribute',
918
+ 'add_class',
919
+ 'remove_class',
920
+ 'toggle_class',
921
+ ].includes(operation)
922
+ ) {
923
+ throw new BridgeError(
924
+ ERROR_CODES.INVALID_REQUEST,
925
+ `Unsupported DOM patch operation: ${operation}`
926
+ );
927
+ }
748
928
  return {
749
929
  patchId: typeof params.patchId === 'string' ? params.patchId : null,
750
930
  target:
751
931
  params.target && typeof params.target === 'object'
752
932
  ? /** @type {Record<string, unknown>} */ (params.target)
753
933
  : {},
754
- operation: typeof params.operation === 'string' ? params.operation : null,
934
+ operation,
755
935
  name: typeof params.name === 'string' ? params.name : null,
756
936
  declarations:
757
937
  params.declarations && typeof params.declarations === 'object'
@@ -975,9 +1155,42 @@ export function normalizeDragParams(params = {}) {
975
1155
  * @returns {NormalizedStorageParams}
976
1156
  */
977
1157
  export function normalizeStorageParams(params = {}) {
1158
+ const keys = Array.isArray(params.keys)
1159
+ ? params.keys.filter((key) => typeof key === 'string')
1160
+ : null;
1161
+ if (keys && keys.length > 100) {
1162
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'keys must contain at most 100 strings.');
1163
+ }
978
1164
  return {
979
1165
  type: params.type === 'session' ? 'session' : 'local',
980
- keys: Array.isArray(params.keys) ? params.keys.filter((k) => typeof k === 'string') : null,
1166
+ keys,
1167
+ };
1168
+ }
1169
+
1170
+ /**
1171
+ * @param {Record<string, unknown>} params
1172
+ * @returns {NormalizedSensitiveReadParams}
1173
+ */
1174
+ export function normalizeSensitiveReadParams(params) {
1175
+ if (params?.source !== 'local_storage' && params?.source !== 'session_storage') {
1176
+ throw new BridgeError(
1177
+ ERROR_CODES.INVALID_REQUEST,
1178
+ 'source must be local_storage or session_storage.'
1179
+ );
1180
+ }
1181
+ if (typeof params.key !== 'string') {
1182
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'key must be a string.');
1183
+ }
1184
+ if (params.key.length > 2_048) {
1185
+ throw new BridgeError(
1186
+ ERROR_CODES.INVALID_REQUEST,
1187
+ 'key must contain at most 2,048 characters.'
1188
+ );
1189
+ }
1190
+ return {
1191
+ source: params.source,
1192
+ key: params.key,
1193
+ maxBytes: MAX_SENSITIVE_VALUE_BYTES,
981
1194
  };
982
1195
  }
983
1196
 
@@ -1142,6 +1355,13 @@ export function normalizeScreenshotParams(params = {}) {
1142
1355
  throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'format must be png, jpeg, or webp.');
1143
1356
  }
1144
1357
  const numericQuality = Number(params.quality);
1358
+ const delivery = params.delivery ?? 'inline';
1359
+ if (delivery !== 'auto' && delivery !== 'inline' && delivery !== 'artifact') {
1360
+ throw new BridgeError(
1361
+ ERROR_CODES.INVALID_REQUEST,
1362
+ 'delivery must be auto, inline, or artifact.'
1363
+ );
1364
+ }
1145
1365
  return {
1146
1366
  format,
1147
1367
  quality:
@@ -1150,9 +1370,42 @@ export function normalizeScreenshotParams(params = {}) {
1150
1370
  : Number.isFinite(numericQuality)
1151
1371
  ? Math.min(100, Math.max(0, Math.trunc(numericQuality)))
1152
1372
  : 80,
1373
+ delivery,
1374
+ scale:
1375
+ Number.isFinite(Number(params.scale)) && Number(params.scale) > 0
1376
+ ? clampNumber(Number(params.scale), 0.1, 4, 1)
1377
+ : 1,
1378
+ };
1379
+ }
1380
+
1381
+ /**
1382
+ * @param {ArtifactReadParams} [params={}]
1383
+ * @returns {NormalizedArtifactReadParams}
1384
+ */
1385
+ export function normalizeArtifactReadParams(params = {}) {
1386
+ return {
1387
+ artifactId: normalizeArtifactId(params.artifactId),
1388
+ offset: clampInt(params.offset, 0, MAX_ARTIFACT_BYTES, 0),
1389
+ maxBytes: clampInt(params.maxBytes, 1, ARTIFACT_CHUNK_BYTES, ARTIFACT_CHUNK_BYTES),
1153
1390
  };
1154
1391
  }
1155
1392
 
1393
+ /**
1394
+ * @param {ArtifactDeleteParams} [params={}]
1395
+ * @returns {{ artifactId: string }}
1396
+ */
1397
+ export function normalizeArtifactDeleteParams(params = {}) {
1398
+ return { artifactId: normalizeArtifactId(params.artifactId) };
1399
+ }
1400
+
1401
+ /** @param {unknown} value */
1402
+ function normalizeArtifactId(value) {
1403
+ if (typeof value !== 'string' || !/^art_[A-Za-z0-9_-]{32,64}$/u.test(value)) {
1404
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'artifactId is invalid.');
1405
+ }
1406
+ return value;
1407
+ }
1408
+
1156
1409
  /**
1157
1410
  * @param {NetworkParams} [params={}]
1158
1411
  * @returns {NormalizedNetworkParams}
@@ -1184,6 +1437,54 @@ export function normalizeNetworkParams(params = {}) {
1184
1437
  };
1185
1438
  }
1186
1439
 
1440
+ /**
1441
+ * @param {HarExportParams | NormalizedHarExportParams} [params={}]
1442
+ * @returns {NormalizedHarExportParams}
1443
+ */
1444
+ export function normalizeHarExportParams(params = {}) {
1445
+ if (
1446
+ params.limit !== undefined &&
1447
+ (typeof params.limit !== 'number' ||
1448
+ !Number.isSafeInteger(params.limit) ||
1449
+ params.limit < 1 ||
1450
+ params.limit > MAX_HAR_ENTRIES)
1451
+ ) {
1452
+ throw new BridgeError(
1453
+ ERROR_CODES.INVALID_REQUEST,
1454
+ `limit must be an integer between 1 and ${MAX_HAR_ENTRIES}.`
1455
+ );
1456
+ }
1457
+
1458
+ if (
1459
+ params.urlPattern !== undefined &&
1460
+ params.urlPattern !== null &&
1461
+ typeof params.urlPattern !== 'string'
1462
+ ) {
1463
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'urlPattern must be a string.');
1464
+ }
1465
+ const urlPattern = params.urlPattern?.trim() || null;
1466
+ if (urlPattern && urlPattern.length > 2_048) {
1467
+ throw new BridgeError(
1468
+ ERROR_CODES.INVALID_REQUEST,
1469
+ 'urlPattern must contain at most 2048 characters.'
1470
+ );
1471
+ }
1472
+
1473
+ const delivery = params.delivery ?? 'auto';
1474
+ if (delivery !== 'auto' && delivery !== 'inline' && delivery !== 'artifact') {
1475
+ throw new BridgeError(
1476
+ ERROR_CODES.INVALID_REQUEST,
1477
+ 'delivery must be auto, inline, or artifact.'
1478
+ );
1479
+ }
1480
+
1481
+ return {
1482
+ limit: params.limit ?? DEFAULT_HAR_LIMIT,
1483
+ urlPattern,
1484
+ delivery,
1485
+ };
1486
+ }
1487
+
1187
1488
  /**
1188
1489
  * @param {NetworkInterceptAddParams} [params={}]
1189
1490
  * @returns {NormalizedNetworkInterceptAddParams}
@@ -1363,13 +1664,16 @@ export function createRuntimeContext() {
1363
1664
  ELEMENT_NOT_ACTIONABLE: 'Input target is hidden, disabled, inert, or not rendered',
1364
1665
  ELEMENT_OBSCURED: 'Pointer target is blocked by an unrelated element',
1365
1666
  ELEMENT_NOT_FOUND: 'Input selector did not match an element',
1667
+ DOM_BASELINE_NOT_FOUND: 'DOM baseline is missing, expired, or released',
1668
+ DOM_BASELINE_INVALIDATED: 'DOM baseline belongs to a different document or scope',
1669
+ DOM_BASELINE_QUOTA_EXCEEDED: 'DOM baseline retention quota was exceeded',
1366
1670
  INPUT_UNSUPPORTED: 'Execution mode does not support this input operation',
1367
1671
  INPUT_INVALID_TARGET: 'Input target is incompatible with the requested operation',
1368
1672
  INPUT_FOCUS_CHANGED: 'Focus moved before native text dispatch',
1369
1673
  DIALOG_NOT_OPEN: 'No JavaScript dialog is currently observable',
1370
1674
  INVALID_REQUEST: 'Malformed method or params',
1371
1675
  TIMEOUT: 'Operation exceeded time limit',
1372
- RATE_LIMITED: 'Too many requests - back off',
1676
+ CONTENT_SCRIPT_UNAVAILABLE: 'Page is restricted or cannot host the content script',
1373
1677
  INTERNAL_ERROR: 'Unexpected extension error',
1374
1678
  EXTENSION_DISCONNECTED: 'Extension not connected to daemon - check Chrome',
1375
1679
  },
@@ -1377,6 +1681,7 @@ export function createRuntimeContext() {
1377
1681
  'dom.query quick budget first; widen only if truncated',
1378
1682
  "Reuse elementRef; don't re-query",
1379
1683
  'Set attributeAllowlist for focused DOM reads',
1684
+ 'Use dom.baseline.create / compare for bounded semantic DOM change detection; release when done',
1380
1685
  'patch.apply_styles before patch.apply_dom',
1381
1686
  'Verify with get_box_model not screenshots',
1382
1687
  'batch independent reads',
@@ -1390,7 +1695,7 @@ export function createRuntimeContext() {
1390
1695
  'page.get_network for fetch/XHR reads or explicit all-resource CDP capture',
1391
1696
  'page.get_text for full-page content extraction',
1392
1697
  'input.hover before screenshot to inspect hover states',
1393
- 'performance.get_metrics for Core Web Vitals and load timing',
1698
+ 'performance.get_metrics returns raw Chrome/CDP counters whose names and units vary; it is a point sample with no BBX navigation window or LCP/CLS/INP measurement',
1394
1699
  'viewport.resize to test responsive layouts',
1395
1700
  'Prefer screenshot.capture_element, or a tight screenshot.capture_region when element capture cannot express the needed area',
1396
1701
  'page.get_storage reads localStorage/sessionStorage without evaluate',
@@ -1415,12 +1720,17 @@ export function createRuntimeContext() {
1415
1720
  maxHtmlLength: { min: 32, max: 50000, default: DEFAULT_MAX_HTML_LENGTH },
1416
1721
  a11yMaxNodes: { min: 10, max: 5000, default: DEFAULT_A11Y_MAX_NODES },
1417
1722
  networkLimit: { min: 1, max: 500, default: DEFAULT_NETWORK_LIMIT },
1723
+ harLimit: { min: 1, max: MAX_HAR_ENTRIES, default: DEFAULT_HAR_LIMIT },
1418
1724
  consoleLimit: { min: 1, max: 200, default: DEFAULT_CONSOLE_LIMIT },
1419
1725
  pageTextBudget: {
1420
1726
  min: 100,
1421
1727
  max: 100000,
1422
1728
  default: DEFAULT_PAGE_TEXT_BUDGET,
1423
1729
  },
1730
+ baselineMaxNodes: { min: 1, max: 1000, default: 100 },
1731
+ baselineMaxDepth: { min: 1, max: 20, default: 8 },
1732
+ baselineTextBudget: { min: 32, max: 1000, default: 160 },
1733
+ baselineMaxChanges: { min: 1, max: 200, default: 50 },
1424
1734
  },
1425
1735
  };
1426
1736
  }