@browserbridge/bbx 1.10.0 → 1.11.1

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.
@@ -2,9 +2,9 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
 
5
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- // zod is required at runtime by @modelcontextprotocol/sdk for tool parameter schema
5
+ import { McpServer } from '@modelcontextprotocol/server';
6
+ import { serveStdio } from '@modelcontextprotocol/server/stdio';
7
+ // zod is required at runtime by @modelcontextprotocol/server for tool parameter schema
8
8
  // declarations (z.object, z.string, etc.). It is not used for request/response
9
9
  // validation - that is handled by the protocol package.
10
10
  import * as z from 'zod/v4';
@@ -32,6 +32,7 @@ import {
32
32
  handleInvestigateTool,
33
33
  createToolResult,
34
34
  MAX_BATCH_CALLS,
35
+ runWithMcpRequestEra,
35
36
  } from './handlers.js';
36
37
  import {
37
38
  ARTIFACT_CHUNK_BYTES,
@@ -52,7 +53,7 @@ import {
52
53
  } from '../../protocol/src/index.js';
53
54
  import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
54
55
  import { getMcpServerInstructions } from './guidance.js';
55
- import { isInitiallyEnabledTool, LOADABLE_TOOLSET_TOOLS } from './toolset.js';
56
+ import { isToolEnabledForEra, LOADABLE_TOOLSET_TOOLS } from './toolset.js';
56
57
 
57
58
  export const BUDGET_PRESET_DESCRIPTION = `Budget preset: "quick", "normal", or "deep" (defaults: query ${BUDGET_PRESETS.normal.maxNodes} nodes / depth ${BUDGET_PRESETS.normal.maxDepth} / text ${BUDGET_PRESETS.normal.textBudget}). Numeric fields override the preset when both are provided.`;
58
59
  export const TAB_ID_DESCRIPTION =
@@ -130,18 +131,20 @@ function getInvestigateToolDescription() {
130
131
  }
131
132
 
132
133
  /**
133
- * Create the MCP server with a compact initial surface and on-demand typed tools.
134
+ * Create the MCP server for one negotiated protocol era.
134
135
  *
136
+ * @param {{ era?: 'legacy' | 'modern' }} [options]
135
137
  * @returns {McpServer}
136
138
  */
137
- export function createBridgeMcpServer() {
139
+ export function createBridgeMcpServer(options = {}) {
140
+ const { era = 'legacy' } = options;
138
141
  const server = new McpServer(
139
142
  {
140
143
  name: 'browser-bridge',
141
144
  version: MCP_SERVER_VERSION,
142
145
  },
143
146
  {
144
- instructions: getMcpServerInstructions(),
147
+ instructions: getMcpServerInstructions(era),
145
148
  }
146
149
  );
147
150
 
@@ -149,19 +152,31 @@ export function createBridgeMcpServer() {
149
152
  const registrations = new Map();
150
153
 
151
154
  /**
152
- * Register a tool, then hide it when it is not part of the initial surface.
155
+ * Register a tool, then hide it when it is not part of the era's compact surface.
153
156
  *
154
157
  * Registering first keeps every call site checked against the SDK's own
155
158
  * generic signature instead of a hand-written passthrough type. Disabling
156
- * happens before `connect()`, so an excluded tool never reaches `tools/list`
157
- * but can be enabled later by browser_toolset.
159
+ * happens before `connect()`, so an excluded tool never reaches `tools/list`.
158
160
  *
159
- * @type {typeof server.registerTool}
161
+ * @template {z.ZodObject} InputSchema
162
+ * @param {string} name
163
+ * @param {{
164
+ * title?: string,
165
+ * description?: string,
166
+ * inputSchema: InputSchema,
167
+ * annotations?: import('@modelcontextprotocol/server').ToolAnnotations,
168
+ * _meta?: Record<string, unknown>,
169
+ * }} config
170
+ * @param {import('@modelcontextprotocol/server').AnyToolHandler<InputSchema>} handler
171
+ * @returns {ReturnType<typeof server.registerTool>}
160
172
  */
161
173
  const registerTool = (name, config, handler) => {
162
- const registration = server.registerTool(name, config, handler);
174
+ const wrappedHandler = /** @type {typeof handler} */ (
175
+ (args, context) => runWithMcpRequestEra(era, () => handler(args, context))
176
+ );
177
+ const registration = server.registerTool(name, config, wrappedHandler);
163
178
  registrations.set(name, registration);
164
- if (!isInitiallyEnabledTool(name)) {
179
+ if (!isToolEnabledForEra(name, era)) {
165
180
  registration.disable();
166
181
  }
167
182
  return registration;
@@ -173,12 +188,12 @@ export function createBridgeMcpServer() {
173
188
  title: 'Browser Bridge Status',
174
189
  description:
175
190
  'Check bridge readiness: daemon connectivity, extension state, and window access. Omit destinationId to inspect local and configured remote destinations. If access is not enabled, ask the user to click Enable in the extension popup or side panel, then retry.',
176
- inputSchema: {
191
+ inputSchema: z.object({
177
192
  destinationId: z
178
193
  .string()
179
194
  .optional()
180
195
  .describe('Inspect one destination instead of aggregating all configured destinations.'),
181
- },
196
+ }),
182
197
  },
183
198
  handleStatusTool
184
199
  );
@@ -188,12 +203,12 @@ export function createBridgeMcpServer() {
188
203
  {
189
204
  title: 'Browser Bridge Setup Status',
190
205
  description: 'Check MCP and CLI skill installation status for agent integration.',
191
- inputSchema: {
206
+ inputSchema: z.object({
192
207
  global: z
193
208
  .boolean()
194
209
  .optional()
195
210
  .describe('Check global (true) or local (false) config (default: true)'),
196
- },
211
+ }),
197
212
  },
198
213
  handleSetupTool
199
214
  );
@@ -203,7 +218,7 @@ export function createBridgeMcpServer() {
203
218
  {
204
219
  title: 'Browser Bridge Logs',
205
220
  description: 'Tail recent bridge request logs for debugging connection or routing issues.',
206
- inputSchema: {
221
+ inputSchema: z.object({
207
222
  limit: z
208
223
  .number()
209
224
  .int()
@@ -215,7 +230,7 @@ export function createBridgeMcpServer() {
215
230
  .optional()
216
231
  .describe(BUDGET_PRESET_DESCRIPTION),
217
232
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
218
- },
233
+ }),
219
234
  },
220
235
  handleLogTool
221
236
  );
@@ -225,13 +240,13 @@ export function createBridgeMcpServer() {
225
240
  {
226
241
  title: 'Browser Bridge Health',
227
242
  description: 'Ping the bridge to verify daemon and extension connectivity.',
228
- inputSchema: {
243
+ inputSchema: z.object({
229
244
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
230
245
  intent: z
231
246
  .enum(['inspect', 'interact', 'capture', 'navigate', 'debugger', 'general'])
232
247
  .optional()
233
248
  .describe('Reported operation family shown in the access prompt (defaults to general).'),
234
- },
249
+ }),
235
250
  },
236
251
  handleHealthTool
237
252
  );
@@ -242,7 +257,7 @@ export function createBridgeMcpServer() {
242
257
  title: 'Browser Tabs',
243
258
  description:
244
259
  'List, create, close, or activate browser tabs. List without destinationId aggregates configured destinations; other actions default to local. Only create a page when the user explicitly requests it.',
245
- inputSchema: {
260
+ inputSchema: z.object({
246
261
  action: z
247
262
  .enum(['list', 'create', 'close', 'activate'])
248
263
  .describe('"list" (preferred), "create" (only when needed), "close", or "activate"'),
@@ -258,7 +273,7 @@ export function createBridgeMcpServer() {
258
273
  .string()
259
274
  .optional()
260
275
  .describe('Target one destination; list without it aggregates configured destinations.'),
261
- },
276
+ }),
262
277
  },
263
278
  handleTabsTool
264
279
  );
@@ -269,7 +284,7 @@ export function createBridgeMcpServer() {
269
284
  title: 'Browser DOM',
270
285
  description:
271
286
  'Query, describe, read, search, or wait for DOM elements. Reuse elementRef from prior results. For full-page text, use browser_page action "text". accessibility_tree is debugger-backed and depth-limited - use query/find first.',
272
- inputSchema: {
287
+ inputSchema: z.object({
273
288
  action: z
274
289
  .enum([
275
290
  'query',
@@ -387,7 +402,7 @@ export function createBridgeMcpServer() {
387
402
  .positive()
388
403
  .optional()
389
404
  .describe(`Max HTML chars to return (default: ${DEFAULT_MAX_HTML_LENGTH})`),
390
- },
405
+ }),
391
406
  },
392
407
  handleDomTool
393
408
  );
@@ -398,7 +413,7 @@ export function createBridgeMcpServer() {
398
413
  title: 'Browser Styles And Layout',
399
414
  description:
400
415
  'Read computed styles, matched CSS rules, box model, or hit-test a viewport point. Reuse elementRef from prior queries. For DOM structure, use browser_dom.',
401
- inputSchema: {
416
+ inputSchema: z.object({
402
417
  action: z
403
418
  .enum(['computed', 'matched_rules', 'box_model', 'hit_test'])
404
419
  .describe('Style/layout operation to perform'),
@@ -426,7 +441,7 @@ export function createBridgeMcpServer() {
426
441
  .nonnegative()
427
442
  .optional()
428
443
  .describe('Y coordinate for hit_test (viewport relative)'),
429
- },
444
+ }),
430
445
  },
431
446
  handleStylesLayoutTool
432
447
  );
@@ -437,7 +452,7 @@ export function createBridgeMcpServer() {
437
452
  title: 'Sensitive Browser Read',
438
453
  description:
439
454
  'High-risk exact-value read for one local or session storage key. This operation is logged as Sensitive access, is never batched or retried, and returns the whole value or fails atomically.',
440
- inputSchema: {
455
+ inputSchema: z.object({
441
456
  source: z.enum(['local_storage', 'session_storage']).describe('Exact storage source'),
442
457
  key: z
443
458
  .string()
@@ -445,7 +460,7 @@ export function createBridgeMcpServer() {
445
460
  .describe('Exact storage key, including an empty key if intended'),
446
461
  tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
447
462
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
448
- },
463
+ }),
449
464
  },
450
465
  handleSensitiveReadTool
451
466
  );
@@ -456,7 +471,7 @@ export function createBridgeMcpServer() {
456
471
  title: 'Browser Page State',
457
472
  description:
458
473
  'Read page-level data, export captured network evidence as HAR 1.2, wait for load/URL conditions, or explicitly inspect/handle JavaScript dialogs. The performance action returns a raw CDP Performance.getMetrics point sample: Chrome defines names and units, which vary, while BBX sets no navigation observation window and does not measure LCP, CLS, or INP. For element-level reads, use browser_dom. evaluate, performance, handle_dialog, and source=cdp network capture are debugger-backed.',
459
- inputSchema: {
474
+ inputSchema: z.object({
460
475
  action: z
461
476
  .enum([
462
477
  'state',
@@ -588,7 +603,7 @@ export function createBridgeMcpServer() {
588
603
  .enum(['exact', 'contains', 'regex'])
589
604
  .optional()
590
605
  .describe('URL match mode for wait_for_load (default: exact)'),
591
- },
606
+ }),
592
607
  },
593
608
  handlePageTool
594
609
  );
@@ -599,7 +614,7 @@ export function createBridgeMcpServer() {
599
614
  title: 'Browser Navigation',
600
615
  description:
601
616
  'Navigate to a URL, reload, go back/forward, scroll, or resize the viewport. resize is debugger-backed - use only for exact viewport overrides.',
602
- inputSchema: {
617
+ inputSchema: z.object({
603
618
  action: z
604
619
  .enum(['navigate', 'reload', 'go_back', 'go_forward', 'scroll', 'resize'])
605
620
  .describe('Navigation operation to perform'),
@@ -632,7 +647,7 @@ export function createBridgeMcpServer() {
632
647
  .optional()
633
648
  .describe('Viewport device scale factor (for resize)'),
634
649
  reset: z.boolean().optional().describe('Reset viewport to original size (for resize)'),
635
- },
650
+ }),
636
651
  },
637
652
  handleNavigationTool
638
653
  );
@@ -643,7 +658,7 @@ export function createBridgeMcpServer() {
643
658
  title: 'Browser Input',
644
659
  description:
645
660
  'Dispatch browser input. Targeted click, focus, type, fill, press_key, set_checked, select_option, hover, and drag actions perform actionability checks and report resolution/execution metadata. cdp_press_key and scroll_into_view use separate contracts. Reuse elementRef values and verify application state after mutations.',
646
- inputSchema: {
661
+ inputSchema: z.object({
647
662
  action: z
648
663
  .enum([
649
664
  'click',
@@ -744,7 +759,7 @@ export function createBridgeMcpServer() {
744
759
  .describe('Drag destination selector (alternative to destinationElementRef)'),
745
760
  offsetX: z.number().optional().describe('Drag drop offset X (default: 0)'),
746
761
  offsetY: z.number().optional().describe('Drag drop offset Y (default: 0)'),
747
- },
762
+ }),
748
763
  },
749
764
  handleInputTool
750
765
  );
@@ -755,7 +770,7 @@ export function createBridgeMcpServer() {
755
770
  title: 'Browser Patch',
756
771
  description:
757
772
  'Apply or rollback document-local style and DOM patches for live prototyping before editing source. commit_baseline keeps current changes but discards rollback history. Set verify=true to get computed results inline.',
758
- inputSchema: {
773
+ inputSchema: z.object({
759
774
  action: z
760
775
  .enum(['apply_styles', 'apply_dom', 'list', 'rollback', 'commit_baseline'])
761
776
  .describe('Patch operation to perform'),
@@ -795,7 +810,7 @@ export function createBridgeMcpServer() {
795
810
  .describe(
796
811
  'Return computed result inline after applying, eliminating a verification round-trip'
797
812
  ),
798
- },
813
+ }),
799
814
  },
800
815
  handlePatchTool
801
816
  );
@@ -806,7 +821,7 @@ export function createBridgeMcpServer() {
806
821
  title: 'Browser Capture',
807
822
  description:
808
823
  'Capture screenshots or CDP snapshots. Debugger-backed and token-expensive - use only when structured reads (browser_dom, browser_styles_layout) are insufficient. Prefer element, then tight region; full_page only for document-level context.',
809
- inputSchema: {
824
+ inputSchema: z.object({
810
825
  action: z
811
826
  .enum([
812
827
  'element',
@@ -872,7 +887,7 @@ export function createBridgeMcpServer() {
872
887
  .max(4)
873
888
  .optional()
874
889
  .describe('Screenshot capture scale (0.1 to 4)'),
875
- },
890
+ }),
876
891
  },
877
892
  handleCaptureTool
878
893
  );
@@ -882,8 +897,8 @@ export function createBridgeMcpServer() {
882
897
  {
883
898
  title: 'Browser Artifact',
884
899
  description:
885
- 'Read (chunked base64) or delete a daemon-owned artifact produced by artifact delivery (screenshots, HAR exports). Artifacts are scoped to this MCP session and expire quickly. Page reads with offset until nextOffset is null, then verify the reassembled bytes against sha256.',
886
- inputSchema: {
900
+ 'Read (chunked base64) or delete a daemon-owned artifact produced by artifact delivery (screenshots, HAR exports). Artifacts are scoped to the current local MCP process and expire quickly. Page reads with offset until nextOffset is null, then verify the reassembled bytes against sha256.',
901
+ inputSchema: z.object({
887
902
  action: z.enum(['read', 'delete']).describe('Artifact operation to perform'),
888
903
  artifactId: z
889
904
  .string()
@@ -907,7 +922,7 @@ export function createBridgeMcpServer() {
907
922
  .enum(['quick', 'normal', 'deep'])
908
923
  .optional()
909
924
  .describe(BUDGET_PRESET_DESCRIPTION),
910
- },
925
+ }),
911
926
  },
912
927
  handleArtifactTool
913
928
  );
@@ -918,7 +933,7 @@ export function createBridgeMcpServer() {
918
933
  title: 'Browser Network Intercept',
919
934
  description:
920
935
  'Manage CDP Fetch request interception rules: add (block, fulfill, or continue matching requests), list, remove, or clear. Debugger-backed; rules persist until removed or cleared.',
921
- inputSchema: {
936
+ inputSchema: z.object({
922
937
  action: z
923
938
  .enum(['add', 'list', 'remove', 'clear'])
924
939
  .describe('Intercept operation to perform'),
@@ -951,7 +966,7 @@ export function createBridgeMcpServer() {
951
966
  .optional()
952
967
  .describe('Response headers for fulfill'),
953
968
  ruleId: z.string().min(1).optional().describe('Rule ID (required for remove)'),
954
- },
969
+ }),
955
970
  },
956
971
  handleInterceptTool
957
972
  );
@@ -968,7 +983,7 @@ export function createBridgeMcpServer() {
968
983
  idempotentHint: true,
969
984
  openWorldHint: true,
970
985
  },
971
- inputSchema: {
986
+ inputSchema: z.object({
972
987
  calls: z
973
988
  .array(
974
989
  z.object({
@@ -988,7 +1003,7 @@ export function createBridgeMcpServer() {
988
1003
  .min(1)
989
1004
  .max(MAX_BATCH_CALLS)
990
1005
  .describe('Calls to execute in parallel'),
991
- },
1006
+ }),
992
1007
  },
993
1008
  handleBatchTool
994
1009
  );
@@ -999,7 +1014,7 @@ export function createBridgeMcpServer() {
999
1014
  title: 'Raw Browser Bridge Call',
1000
1015
  description:
1001
1016
  'Primary Browser Bridge tool for permission-ask hosts: call any bridge method directly by name so the user can approve one BBX MCP tool instead of each specialized tool separately.',
1002
- inputSchema: {
1017
+ inputSchema: z.object({
1003
1018
  method: z.string().describe('Bridge method name (e.g., "dom.query", "input.click")'),
1004
1019
  params: z
1005
1020
  .record(z.string(), z.unknown())
@@ -1011,7 +1026,7 @@ export function createBridgeMcpServer() {
1011
1026
  .enum(['quick', 'normal', 'deep'])
1012
1027
  .optional()
1013
1028
  .describe(BUDGET_PRESET_DESCRIPTION),
1014
- },
1029
+ }),
1015
1030
  },
1016
1031
  handleRawCallTool
1017
1032
  );
@@ -1022,9 +1037,9 @@ export function createBridgeMcpServer() {
1022
1037
  title: 'Browser Bridge Runtime Context',
1023
1038
  description:
1024
1039
  'Return runtime context: budget presets, method groups, and active limits. Call to discover defaults before inspecting a page.',
1025
- inputSchema: {
1040
+ inputSchema: z.object({
1026
1041
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
1027
- },
1042
+ }),
1028
1043
  },
1029
1044
  handleSkillTool
1030
1045
  );
@@ -1035,9 +1050,9 @@ export function createBridgeMcpServer() {
1035
1050
  title: 'Request Browser Bridge Access',
1036
1051
  description:
1037
1052
  'Request window access for Browser Bridge. Surfaces an Enable prompt in the extension popup or side panel. Use once per window; if access is already pending, ask the user to enable that window instead of requesting again.',
1038
- inputSchema: {
1053
+ inputSchema: z.object({
1039
1054
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
1040
- },
1055
+ }),
1041
1056
  },
1042
1057
  handleAccessTool
1043
1058
  );
@@ -1056,7 +1071,7 @@ export function createBridgeMcpServer() {
1056
1071
  _meta: {
1057
1072
  delegationHint: createInvestigateDelegationHint(),
1058
1073
  },
1059
- inputSchema: {
1074
+ inputSchema: z.object({
1060
1075
  objective: z
1061
1076
  .string()
1062
1077
  .describe('What to find, verify, or extract from the current page (natural language).'),
@@ -1074,7 +1089,7 @@ export function createBridgeMcpServer() {
1074
1089
  .string()
1075
1090
  .optional()
1076
1091
  .describe('Optional CSS selector to scope the investigation to a subtree.'),
1077
- },
1092
+ }),
1078
1093
  },
1079
1094
  handleInvestigateTool
1080
1095
  );
@@ -1084,12 +1099,12 @@ export function createBridgeMcpServer() {
1084
1099
  {
1085
1100
  title: 'Load Browser Bridge Tool',
1086
1101
  description:
1087
- 'Load one specialized typed Browser Bridge tool by exact name. The tool appears in tools/list automatically and remains enabled for this MCP session.',
1088
- inputSchema: {
1102
+ 'Load one specialized typed Browser Bridge tool by exact name. The tool appears in tools/list automatically and remains enabled for this legacy MCP connection.',
1103
+ inputSchema: z.object({
1089
1104
  tool: z
1090
1105
  .enum(LOADABLE_TOOLSET_TOOLS)
1091
1106
  .describe('Exact specialized Browser Bridge tool to load.'),
1092
- },
1107
+ }),
1093
1108
  },
1094
1109
  ({ tool }) => {
1095
1110
  const registration = registrations.get(tool);
@@ -1106,11 +1121,11 @@ export function createBridgeMcpServer() {
1106
1121
  }
1107
1122
 
1108
1123
  /**
1109
- * @returns {Promise<void>}
1124
+ * @param {{ serve?: typeof serveStdio }} [options]
1125
+ * @returns {void}
1110
1126
  */
1111
- export async function startBridgeMcpServer() {
1127
+ export function startBridgeMcpServer(options = {}) {
1128
+ const { serve = serveStdio } = options;
1112
1129
  applyWindowsTcpTransportDefaults();
1113
- const server = createBridgeMcpServer();
1114
- const transport = new StdioServerTransport();
1115
- await server.connect(transport);
1130
+ serve(({ era }) => createBridgeMcpServer({ era }));
1116
1131
  }
@@ -16,6 +16,21 @@ export const INITIAL_TOOLSET_TOOLS = Object.freeze([
16
16
  'browser_toolset',
17
17
  ]);
18
18
 
19
+ /**
20
+ * Modern MCP tool lists cannot change as a side effect of another request.
21
+ * Keep the compact surface static and use browser_call for full protocol reach.
22
+ *
23
+ * @type {readonly string[]}
24
+ */
25
+ export const MODERN_TOOLSET_TOOLS = Object.freeze([
26
+ 'browser_access',
27
+ 'browser_batch',
28
+ 'browser_call',
29
+ 'browser_health',
30
+ 'browser_skill',
31
+ 'browser_status',
32
+ ]);
33
+
19
34
  /** @typedef {'browser_dom' | 'browser_styles_layout' | 'browser_page' | 'browser_logs' | 'browser_input' | 'browser_navigation' | 'browser_tabs' | 'browser_capture' | 'browser_artifact' | 'browser_patch' | 'browser_intercept' | 'browser_investigate' | 'browser_sensitive_read' | 'browser_setup' | 'browser_skill'} LoadableToolName */
20
35
 
21
36
  /** @type {readonly LoadableToolName[]} */
@@ -38,6 +53,7 @@ export const LOADABLE_TOOLSET_TOOLS = Object.freeze([
38
53
  ]);
39
54
 
40
55
  const INITIAL_TOOLSET = new Set(INITIAL_TOOLSET_TOOLS);
56
+ const MODERN_TOOLSET = new Set(MODERN_TOOLSET_TOOLS);
41
57
 
42
58
  /**
43
59
  * @param {string} toolName
@@ -46,3 +62,12 @@ const INITIAL_TOOLSET = new Set(INITIAL_TOOLSET_TOOLS);
46
62
  export function isInitiallyEnabledTool(toolName) {
47
63
  return INITIAL_TOOLSET.has(toolName);
48
64
  }
65
+
66
+ /**
67
+ * @param {string} toolName
68
+ * @param {'legacy' | 'modern'} era
69
+ * @returns {boolean}
70
+ */
71
+ export function isToolEnabledForEra(toolName, era) {
72
+ return era === 'modern' ? MODERN_TOOLSET.has(toolName) : INITIAL_TOOLSET.has(toolName);
73
+ }
@@ -17,15 +17,17 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
17
  const repoRoot = path.resolve(__dirname, '../../..');
18
18
 
19
19
  /**
20
- * npm exec may run package lifecycle hooks while resolving the local bin. Keep
21
- * transient `npm exec -- bbx ...` invocations from rewriting host setup or
22
- * replacing a working daemon.
20
+ * Host setup is appropriate for a global CLI install, not when this package is
21
+ * an ordinary project dependency. npm exec is also always non-mutating. The
22
+ * explicit force switch is reserved for controlled packaging/tests.
23
23
  *
24
24
  * @param {NodeJS.ProcessEnv} [env=process.env]
25
25
  * @returns {boolean}
26
26
  */
27
- function shouldSkipPostinstall(env = process.env) {
28
- return env.npm_command === 'exec';
27
+ export function shouldSkipPostinstall(env = process.env) {
28
+ const globalInstall = env.npm_config_global === 'true' || env.npm_config_global === '1';
29
+ const explicitlyEnabled = env.BBX_POSTINSTALL_FORCE === '1';
30
+ return env.npm_command === 'exec' || (!globalInstall && !explicitlyEnabled);
29
31
  }
30
32
 
31
33
  /**
@@ -9,6 +9,7 @@ import { getBridgeDir } from './config.js';
9
9
  export const BRIDGE_AUTH_TOKEN_ENV = 'BBX_AUTH_TOKEN';
10
10
  export const BRIDGE_AUTH_TOKEN_FILE_ENV = 'BBX_AUTH_TOKEN_FILE';
11
11
  const TOKEN_FILENAME = 'daemon.auth';
12
+ const EXTENSION_TOKEN_FILENAME = 'daemon.extension.auth';
12
13
  const TOKEN_BYTES = 32;
13
14
  const TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,256}$/u;
14
15
  const UUID_TOKEN_PATTERN =
@@ -22,6 +23,17 @@ export function getBridgeAuthTokenPath(env = process.env) {
22
23
  return path.join(getBridgeDir(env), TOKEN_FILENAME);
23
24
  }
24
25
 
26
+ /**
27
+ * Private credential used only by the Chrome native host when registering the
28
+ * extension role. Remote agent configurations never receive this token.
29
+ *
30
+ * @param {NodeJS.ProcessEnv} [env=process.env]
31
+ * @returns {string}
32
+ */
33
+ export function getBridgeExtensionAuthTokenPath(env = process.env) {
34
+ return path.join(getBridgeDir(env), EXTENSION_TOKEN_FILENAME);
35
+ }
36
+
25
37
  /**
26
38
  * @param {unknown} value
27
39
  * @returns {string | null}
@@ -91,6 +103,25 @@ export async function readBridgeAuthToken(options = {}) {
91
103
  }
92
104
  const tokenPath = options.tokenPath ?? getBridgeAuthTokenPath();
93
105
  const readFile = options.readFile ?? fs.promises.readFile.bind(fs.promises);
106
+ return readStoredBridgeAuthToken(tokenPath, readFile);
107
+ }
108
+
109
+ /**
110
+ * @param {{ tokenPath?: string, readFile?: typeof fs.promises.readFile }} [options={}]
111
+ * @returns {Promise<string | null>}
112
+ */
113
+ export async function readBridgeExtensionAuthToken(options = {}) {
114
+ const tokenPath = options.tokenPath ?? getBridgeExtensionAuthTokenPath();
115
+ const readFile = options.readFile ?? fs.promises.readFile.bind(fs.promises);
116
+ return readStoredBridgeAuthToken(tokenPath, readFile);
117
+ }
118
+
119
+ /**
120
+ * @param {string} tokenPath
121
+ * @param {typeof fs.promises.readFile} readFile
122
+ * @returns {Promise<string | null>}
123
+ */
124
+ async function readStoredBridgeAuthToken(tokenPath, readFile) {
94
125
  try {
95
126
  return normalizeBridgeAuthToken(await readFile(tokenPath, 'utf8'));
96
127
  } catch (error) {
@@ -136,12 +167,43 @@ export async function writeBridgeAuthToken(token, options = {}) {
136
167
  */
137
168
  export async function ensureBridgeAuthToken(options = {}) {
138
169
  const tokenPath = options.tokenPath ?? getBridgeAuthTokenPath();
170
+ return ensureStoredBridgeAuthToken(tokenPath, options);
171
+ }
172
+
173
+ /**
174
+ * @param {{
175
+ * tokenPath?: string,
176
+ * readFile?: typeof fs.promises.readFile,
177
+ * writeFile?: typeof fs.promises.writeFile,
178
+ * mkdir?: typeof fs.promises.mkdir,
179
+ * chmod?: typeof fs.promises.chmod,
180
+ * randomBytesFn?: typeof randomBytes
181
+ * }} [options={}]
182
+ * @returns {Promise<string>}
183
+ */
184
+ export async function ensureBridgeExtensionAuthToken(options = {}) {
185
+ const tokenPath = options.tokenPath ?? getBridgeExtensionAuthTokenPath();
186
+ return ensureStoredBridgeAuthToken(tokenPath, options);
187
+ }
188
+
189
+ /**
190
+ * @param {string} tokenPath
191
+ * @param {{
192
+ * readFile?: typeof fs.promises.readFile,
193
+ * writeFile?: typeof fs.promises.writeFile,
194
+ * mkdir?: typeof fs.promises.mkdir,
195
+ * chmod?: typeof fs.promises.chmod,
196
+ * randomBytesFn?: typeof randomBytes
197
+ * }} options
198
+ * @returns {Promise<string>}
199
+ */
200
+ async function ensureStoredBridgeAuthToken(tokenPath, options) {
139
201
  const readFile = options.readFile ?? fs.promises.readFile.bind(fs.promises);
140
202
  const writeFile = options.writeFile ?? fs.promises.writeFile.bind(fs.promises);
141
203
  const mkdir = options.mkdir ?? fs.promises.mkdir.bind(fs.promises);
142
204
  const chmod = options.chmod ?? fs.promises.chmod.bind(fs.promises);
143
205
  const randomBytesFn = options.randomBytesFn ?? randomBytes;
144
- const existing = await readBridgeAuthToken({ tokenPath, readFile });
206
+ const existing = await readStoredBridgeAuthToken(tokenPath, readFile);
145
207
  if (existing) {
146
208
  return existing;
147
209
  }