@browserbridge/bbx 1.3.0 → 1.5.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.
- package/README.md +3 -1
- package/package.json +2 -2
- package/packages/agent-client/src/cli.js +45 -16
- package/packages/agent-client/src/client.js +74 -20
- package/packages/agent-client/src/command-registry.js +2 -3
- package/packages/agent-client/src/mcp-config.js +30 -27
- package/packages/agent-client/src/runtime.js +2 -10
- package/packages/agent-client/src/types.ts +10 -1
- package/packages/mcp-server/src/guidance.js +241 -0
- package/packages/mcp-server/src/handlers-capture.js +74 -11
- package/packages/mcp-server/src/handlers-dom.js +48 -0
- package/packages/mcp-server/src/handlers-navigation.js +22 -2
- package/packages/mcp-server/src/handlers-page.js +10 -9
- package/packages/mcp-server/src/handlers-utils.js +47 -1
- package/packages/mcp-server/src/server.js +111 -29
- package/packages/native-host/src/auth-token.js +92 -0
- package/packages/native-host/src/daemon-process.js +26 -4
- package/packages/native-host/src/daemon.js +174 -28
- package/packages/native-host/src/framing.js +7 -2
- package/packages/native-host/src/native-host.js +18 -2
- package/packages/protocol/src/defaults.js +3 -0
- package/packages/protocol/src/json-lines.js +29 -1
- package/packages/protocol/src/protocol.js +6 -1
- package/packages/protocol/src/types.ts +2 -0
- package/skills/browser-bridge/SKILL.md +21 -5
- package/skills/browser-bridge/agents/openai.yaml +1 -1
- package/skills/browser-bridge/references/interaction.md +6 -6
- package/skills/browser-bridge/references/protocol.md +57 -54
- package/skills/browser-bridge/references/ui-workflows.md +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
3
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
7
|
// zod is required at runtime by @modelcontextprotocol/sdk for tool parameter schema
|
|
@@ -38,11 +40,27 @@ import {
|
|
|
38
40
|
getMethodsByMaxComplexity,
|
|
39
41
|
} from '../../protocol/src/index.js';
|
|
40
42
|
import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
|
|
43
|
+
import { MCP_SERVER_INSTRUCTIONS, registerBridgeMcpGuidance } from './guidance.js';
|
|
41
44
|
|
|
42
45
|
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.`;
|
|
43
46
|
export const TAB_ID_DESCRIPTION =
|
|
44
47
|
'Target a specific tab instead of the active tab in the enabled window.';
|
|
45
48
|
|
|
49
|
+
const MCP_SERVER_VERSION = loadPackageVersion();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function loadPackageVersion() {
|
|
55
|
+
try {
|
|
56
|
+
const raw = fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8');
|
|
57
|
+
const parsed = JSON.parse(raw);
|
|
58
|
+
return parsed && typeof parsed.version === 'string' ? parsed.version : '0.0.0';
|
|
59
|
+
} catch {
|
|
60
|
+
return '0.0.0';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
/** @type {readonly import('../../protocol/src/types.js').BridgeMethod[]} */
|
|
47
65
|
const INVESTIGATE_SUBAGENT_BRIDGE_METHODS = Object.freeze(
|
|
48
66
|
getMethodsByMaxComplexity('low').filter(
|
|
@@ -75,10 +93,15 @@ const INVESTIGATE_DELEGATION_HINT = Object.freeze({
|
|
|
75
93
|
* @returns {McpServer}
|
|
76
94
|
*/
|
|
77
95
|
export function createBridgeMcpServer() {
|
|
78
|
-
const server = new McpServer(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
const server = new McpServer(
|
|
97
|
+
{
|
|
98
|
+
name: 'browser-bridge',
|
|
99
|
+
version: MCP_SERVER_VERSION,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
instructions: MCP_SERVER_INSTRUCTIONS,
|
|
103
|
+
}
|
|
104
|
+
);
|
|
82
105
|
|
|
83
106
|
server.registerTool(
|
|
84
107
|
'browser_status',
|
|
@@ -114,6 +137,8 @@ export function createBridgeMcpServer() {
|
|
|
114
137
|
inputSchema: {
|
|
115
138
|
limit: z
|
|
116
139
|
.number()
|
|
140
|
+
.int()
|
|
141
|
+
.positive()
|
|
117
142
|
.optional()
|
|
118
143
|
.describe(`Maximum log entries to return (default: ${DEFAULT_CONSOLE_LIMIT})`),
|
|
119
144
|
budgetPreset: z
|
|
@@ -147,7 +172,7 @@ export function createBridgeMcpServer() {
|
|
|
147
172
|
.describe('"list" (preferred), "create" (only when needed), or "close"'),
|
|
148
173
|
url: z.string().optional().describe('URL for create action'),
|
|
149
174
|
active: z.boolean().optional().describe('Focus the new tab (default: true)'),
|
|
150
|
-
tabId: z.number().optional().describe('Tab ID (required for close)'),
|
|
175
|
+
tabId: z.number().int().positive().optional().describe('Tab ID (required for close)'),
|
|
151
176
|
},
|
|
152
177
|
},
|
|
153
178
|
handleTabsTool
|
|
@@ -173,7 +198,7 @@ export function createBridgeMcpServer() {
|
|
|
173
198
|
'accessibility_tree',
|
|
174
199
|
])
|
|
175
200
|
.describe('DOM operation to perform'),
|
|
176
|
-
tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
|
|
201
|
+
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
177
202
|
budgetPreset: z
|
|
178
203
|
.enum(['quick', 'normal', 'deep'])
|
|
179
204
|
.optional()
|
|
@@ -189,14 +214,20 @@ export function createBridgeMcpServer() {
|
|
|
189
214
|
withinRef: z.string().optional().describe('Scope query to this elementRef subtree'),
|
|
190
215
|
maxNodes: z
|
|
191
216
|
.number()
|
|
217
|
+
.int()
|
|
218
|
+
.positive()
|
|
192
219
|
.optional()
|
|
193
220
|
.describe(`Maximum nodes to return (default: ${DEFAULT_MAX_NODES})`),
|
|
194
221
|
maxDepth: z
|
|
195
222
|
.number()
|
|
223
|
+
.int()
|
|
224
|
+
.positive()
|
|
196
225
|
.optional()
|
|
197
226
|
.describe(`Maximum tree depth (default: ${DEFAULT_MAX_DEPTH})`),
|
|
198
227
|
textBudget: z
|
|
199
228
|
.number()
|
|
229
|
+
.int()
|
|
230
|
+
.positive()
|
|
200
231
|
.optional()
|
|
201
232
|
.describe(`Max chars of text content per node (default: ${DEFAULT_TEXT_BUDGET})`),
|
|
202
233
|
includeBbox: z
|
|
@@ -216,7 +247,12 @@ export function createBridgeMcpServer() {
|
|
|
216
247
|
.boolean()
|
|
217
248
|
.optional()
|
|
218
249
|
.describe('Require exact text match (default: false, substring match)'),
|
|
219
|
-
maxResults: z
|
|
250
|
+
maxResults: z
|
|
251
|
+
.number()
|
|
252
|
+
.int()
|
|
253
|
+
.positive()
|
|
254
|
+
.optional()
|
|
255
|
+
.describe('Maximum search results (default: 10)'),
|
|
220
256
|
role: z.string().optional().describe('ARIA role to search for (for find_role action)'),
|
|
221
257
|
name: z.string().optional().describe('Accessible name to match with role'),
|
|
222
258
|
state: z
|
|
@@ -225,6 +261,8 @@ export function createBridgeMcpServer() {
|
|
|
225
261
|
.describe('Expected element state (for wait action)'),
|
|
226
262
|
timeoutMs: z
|
|
227
263
|
.number()
|
|
264
|
+
.int()
|
|
265
|
+
.positive()
|
|
228
266
|
.optional()
|
|
229
267
|
.describe(`Timeout for wait operations (default: ${DEFAULT_WAIT_TIMEOUT_MS})`),
|
|
230
268
|
outer: z
|
|
@@ -233,6 +271,8 @@ export function createBridgeMcpServer() {
|
|
|
233
271
|
.describe('Return outerHTML instead of innerHTML (default: false)'),
|
|
234
272
|
maxLength: z
|
|
235
273
|
.number()
|
|
274
|
+
.int()
|
|
275
|
+
.positive()
|
|
236
276
|
.optional()
|
|
237
277
|
.describe(`Max HTML chars to return (default: ${DEFAULT_MAX_HTML_LENGTH})`),
|
|
238
278
|
},
|
|
@@ -250,7 +290,7 @@ export function createBridgeMcpServer() {
|
|
|
250
290
|
action: z
|
|
251
291
|
.enum(['computed', 'matched_rules', 'box_model', 'hit_test'])
|
|
252
292
|
.describe('Style/layout operation to perform'),
|
|
253
|
-
tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
|
|
293
|
+
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
254
294
|
budgetPreset: z
|
|
255
295
|
.enum(['quick', 'normal', 'deep'])
|
|
256
296
|
.optional()
|
|
@@ -261,8 +301,16 @@ export function createBridgeMcpServer() {
|
|
|
261
301
|
.array(z.string())
|
|
262
302
|
.optional()
|
|
263
303
|
.describe('Style properties to fetch (omitting returns all - expensive)'),
|
|
264
|
-
x: z
|
|
265
|
-
|
|
304
|
+
x: z
|
|
305
|
+
.number()
|
|
306
|
+
.nonnegative()
|
|
307
|
+
.optional()
|
|
308
|
+
.describe('X coordinate for hit_test (viewport relative)'),
|
|
309
|
+
y: z
|
|
310
|
+
.number()
|
|
311
|
+
.nonnegative()
|
|
312
|
+
.optional()
|
|
313
|
+
.describe('Y coordinate for hit_test (viewport relative)'),
|
|
266
314
|
},
|
|
267
315
|
},
|
|
268
316
|
handleStylesLayoutTool
|
|
@@ -287,7 +335,7 @@ export function createBridgeMcpServer() {
|
|
|
287
335
|
'performance',
|
|
288
336
|
])
|
|
289
337
|
.describe('Page operation to perform'),
|
|
290
|
-
tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
|
|
338
|
+
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
291
339
|
budgetPreset: z
|
|
292
340
|
.enum(['quick', 'normal', 'deep'])
|
|
293
341
|
.optional()
|
|
@@ -299,6 +347,8 @@ export function createBridgeMcpServer() {
|
|
|
299
347
|
awaitPromise: z.boolean().optional().describe('Await returned promises (default: false)'),
|
|
300
348
|
timeoutMs: z
|
|
301
349
|
.number()
|
|
350
|
+
.int()
|
|
351
|
+
.positive()
|
|
302
352
|
.optional()
|
|
303
353
|
.describe(`Timeout for evaluate/wait operations (default: ${DEFAULT_WAIT_TIMEOUT_MS})`),
|
|
304
354
|
returnByValue: z
|
|
@@ -312,6 +362,8 @@ export function createBridgeMcpServer() {
|
|
|
312
362
|
clear: z.boolean().optional().describe('Clear buffer after reading (default: false)'),
|
|
313
363
|
limit: z
|
|
314
364
|
.number()
|
|
365
|
+
.int()
|
|
366
|
+
.positive()
|
|
315
367
|
.optional()
|
|
316
368
|
.describe(`Maximum entries to return (default: ${DEFAULT_CONSOLE_LIMIT})`),
|
|
317
369
|
type: z
|
|
@@ -324,6 +376,8 @@ export function createBridgeMcpServer() {
|
|
|
324
376
|
.describe('Specific storage keys to fetch (omitting returns all)'),
|
|
325
377
|
textBudget: z
|
|
326
378
|
.number()
|
|
379
|
+
.int()
|
|
380
|
+
.positive()
|
|
327
381
|
.optional()
|
|
328
382
|
.describe(`Max chars for page text (default: ${DEFAULT_PAGE_TEXT_BUDGET})`),
|
|
329
383
|
urlPattern: z.string().optional().describe('Filter network entries by URL pattern'),
|
|
@@ -342,14 +396,19 @@ export function createBridgeMcpServer() {
|
|
|
342
396
|
action: z
|
|
343
397
|
.enum(['navigate', 'reload', 'go_back', 'go_forward', 'scroll', 'resize'])
|
|
344
398
|
.describe('Navigation operation to perform'),
|
|
345
|
-
tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
|
|
399
|
+
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
346
400
|
budgetPreset: z
|
|
347
401
|
.enum(['quick', 'normal', 'deep'])
|
|
348
402
|
.optional()
|
|
349
403
|
.describe(BUDGET_PRESET_DESCRIPTION),
|
|
350
404
|
url: z.string().optional().describe('URL to navigate to (for navigate action)'),
|
|
351
405
|
waitForLoad: z.boolean().optional().describe('Wait for load event (default: true)'),
|
|
352
|
-
timeoutMs: z
|
|
406
|
+
timeoutMs: z
|
|
407
|
+
.number()
|
|
408
|
+
.int()
|
|
409
|
+
.positive()
|
|
410
|
+
.optional()
|
|
411
|
+
.describe('Timeout for navigation (default: 30000)'),
|
|
353
412
|
top: z.number().optional().describe('Scroll target Y position (pixels)'),
|
|
354
413
|
left: z.number().optional().describe('Scroll target X position (pixels)'),
|
|
355
414
|
behavior: z.enum(['auto', 'smooth']).optional().describe('Scroll behavior (default: auto)'),
|
|
@@ -357,8 +416,13 @@ export function createBridgeMcpServer() {
|
|
|
357
416
|
.boolean()
|
|
358
417
|
.optional()
|
|
359
418
|
.describe('Scroll relative to current position (default: false)'),
|
|
360
|
-
width: z.number().optional().describe('Viewport width in pixels'),
|
|
361
|
-
height: z.number().optional().describe('Viewport height in pixels'),
|
|
419
|
+
width: z.number().int().positive().optional().describe('Viewport width in pixels'),
|
|
420
|
+
height: z.number().int().positive().optional().describe('Viewport height in pixels'),
|
|
421
|
+
deviceScaleFactor: z
|
|
422
|
+
.number()
|
|
423
|
+
.nonnegative()
|
|
424
|
+
.optional()
|
|
425
|
+
.describe('Viewport device scale factor (for resize)'),
|
|
362
426
|
reset: z.boolean().optional().describe('Reset viewport to original size (for resize)'),
|
|
363
427
|
},
|
|
364
428
|
},
|
|
@@ -386,7 +450,7 @@ export function createBridgeMcpServer() {
|
|
|
386
450
|
'scroll_into_view',
|
|
387
451
|
])
|
|
388
452
|
.describe('Input operation to perform'),
|
|
389
|
-
tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
|
|
453
|
+
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
390
454
|
budgetPreset: z
|
|
391
455
|
.enum(['quick', 'normal', 'deep'])
|
|
392
456
|
.optional()
|
|
@@ -400,7 +464,13 @@ export function createBridgeMcpServer() {
|
|
|
400
464
|
.enum(['left', 'middle', 'right'])
|
|
401
465
|
.optional()
|
|
402
466
|
.describe('Mouse button for click (default: left)'),
|
|
403
|
-
clickCount: z
|
|
467
|
+
clickCount: z
|
|
468
|
+
.number()
|
|
469
|
+
.int()
|
|
470
|
+
.min(1)
|
|
471
|
+
.max(2)
|
|
472
|
+
.optional()
|
|
473
|
+
.describe('Click count (1=single, 2=double)'),
|
|
404
474
|
text: z.string().max(100000).optional().describe('Text to type (for type action)'),
|
|
405
475
|
clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
|
|
406
476
|
submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
|
|
@@ -423,10 +493,15 @@ export function createBridgeMcpServer() {
|
|
|
423
493
|
.optional()
|
|
424
494
|
.describe('Option labels to select (alternative to values)'),
|
|
425
495
|
indexes: z
|
|
426
|
-
.array(z.number())
|
|
496
|
+
.array(z.number().int().nonnegative())
|
|
427
497
|
.optional()
|
|
428
498
|
.describe('Option indexes to select (alternative to values/labels)'),
|
|
429
|
-
duration: z
|
|
499
|
+
duration: z
|
|
500
|
+
.number()
|
|
501
|
+
.int()
|
|
502
|
+
.nonnegative()
|
|
503
|
+
.optional()
|
|
504
|
+
.describe('Hover duration in ms (default: 100)'),
|
|
430
505
|
sourceElementRef: z.string().optional().describe('Drag source element (for drag action)'),
|
|
431
506
|
sourceSelector: z
|
|
432
507
|
.string()
|
|
@@ -457,7 +532,7 @@ export function createBridgeMcpServer() {
|
|
|
457
532
|
action: z
|
|
458
533
|
.enum(['apply_styles', 'apply_dom', 'list', 'rollback', 'commit_baseline'])
|
|
459
534
|
.describe('Patch operation to perform'),
|
|
460
|
-
tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
|
|
535
|
+
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
461
536
|
budgetPreset: z
|
|
462
537
|
.enum(['quick', 'normal', 'deep'])
|
|
463
538
|
.optional()
|
|
@@ -517,7 +592,7 @@ export function createBridgeMcpServer() {
|
|
|
517
592
|
.describe(
|
|
518
593
|
'element (preferred), region (tight crop), full_page (document-level only), or cdp_* for low-level data'
|
|
519
594
|
),
|
|
520
|
-
tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
|
|
595
|
+
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
521
596
|
budgetPreset: z
|
|
522
597
|
.enum(['quick', 'normal', 'deep'])
|
|
523
598
|
.optional()
|
|
@@ -527,13 +602,18 @@ export function createBridgeMcpServer() {
|
|
|
527
602
|
.optional()
|
|
528
603
|
.describe('Element reference (for element action, preferred)'),
|
|
529
604
|
selector: z.string().optional().describe('CSS selector (used if no elementRef)'),
|
|
530
|
-
nodeId: z
|
|
605
|
+
nodeId: z
|
|
606
|
+
.number()
|
|
607
|
+
.int()
|
|
608
|
+
.positive()
|
|
609
|
+
.optional()
|
|
610
|
+
.describe('CDP node id for cdp_box_model/cdp_computed_styles'),
|
|
531
611
|
rect: z
|
|
532
612
|
.object({
|
|
533
|
-
x: z.number().describe('Region left edge (viewport pixels)'),
|
|
534
|
-
y: z.number().describe('Region top edge (viewport pixels)'),
|
|
535
|
-
width: z.number().describe('Region width (pixels)'),
|
|
536
|
-
height: z.number().describe('Region height (pixels)'),
|
|
613
|
+
x: z.number().nonnegative().describe('Region left edge (viewport pixels)'),
|
|
614
|
+
y: z.number().nonnegative().describe('Region top edge (viewport pixels)'),
|
|
615
|
+
width: z.number().positive().describe('Region width (pixels)'),
|
|
616
|
+
height: z.number().positive().describe('Region height (pixels)'),
|
|
537
617
|
})
|
|
538
618
|
.optional()
|
|
539
619
|
.describe('Viewport region for region action (keep crop tight)'),
|
|
@@ -557,7 +637,7 @@ export function createBridgeMcpServer() {
|
|
|
557
637
|
.record(z.string(), z.unknown())
|
|
558
638
|
.optional()
|
|
559
639
|
.describe('Method params for this call'),
|
|
560
|
-
tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
|
|
640
|
+
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
561
641
|
budgetPreset: z
|
|
562
642
|
.enum(['quick', 'normal', 'deep'])
|
|
563
643
|
.optional()
|
|
@@ -583,7 +663,7 @@ export function createBridgeMcpServer() {
|
|
|
583
663
|
.record(z.string(), z.unknown())
|
|
584
664
|
.optional()
|
|
585
665
|
.describe('Method parameters as object'),
|
|
586
|
-
tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
|
|
666
|
+
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
587
667
|
},
|
|
588
668
|
},
|
|
589
669
|
handleRawCallTool
|
|
@@ -645,7 +725,7 @@ export function createBridgeMcpServer() {
|
|
|
645
725
|
'"normal" (state + DOM + text, default), ' +
|
|
646
726
|
'"deep" (state + DOM + text + console + network).'
|
|
647
727
|
),
|
|
648
|
-
tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
|
|
728
|
+
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
649
729
|
selector: z
|
|
650
730
|
.string()
|
|
651
731
|
.optional()
|
|
@@ -655,6 +735,8 @@ export function createBridgeMcpServer() {
|
|
|
655
735
|
handleInvestigateTool
|
|
656
736
|
);
|
|
657
737
|
|
|
738
|
+
registerBridgeMcpGuidance(server);
|
|
739
|
+
|
|
658
740
|
return server;
|
|
659
741
|
}
|
|
660
742
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { getBridgeDir } from './config.js';
|
|
8
|
+
|
|
9
|
+
const TOKEN_FILENAME = 'daemon.auth';
|
|
10
|
+
const TOKEN_BYTES = 32;
|
|
11
|
+
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,256}$/u;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function getBridgeAuthTokenPath(env = process.env) {
|
|
18
|
+
return path.join(getBridgeDir(env), TOKEN_FILENAME);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {unknown} value
|
|
23
|
+
* @returns {string | null}
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeBridgeAuthToken(value) {
|
|
26
|
+
if (typeof value !== 'string') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const token = value.trim();
|
|
30
|
+
return TOKEN_PATTERN.test(token) ? token : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {{ tokenPath?: string, readFile?: typeof fs.promises.readFile }} [options={}]
|
|
35
|
+
* @returns {Promise<string | null>}
|
|
36
|
+
*/
|
|
37
|
+
export async function readBridgeAuthToken(options = {}) {
|
|
38
|
+
const tokenPath = options.tokenPath ?? getBridgeAuthTokenPath();
|
|
39
|
+
const readFile = options.readFile ?? fs.promises.readFile.bind(fs.promises);
|
|
40
|
+
try {
|
|
41
|
+
return normalizeBridgeAuthToken(await readFile(tokenPath, 'utf8'));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (isMissingFileError(error)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {{
|
|
52
|
+
* tokenPath?: string,
|
|
53
|
+
* readFile?: typeof fs.promises.readFile,
|
|
54
|
+
* writeFile?: typeof fs.promises.writeFile,
|
|
55
|
+
* mkdir?: typeof fs.promises.mkdir,
|
|
56
|
+
* chmod?: typeof fs.promises.chmod,
|
|
57
|
+
* randomBytesFn?: typeof randomBytes
|
|
58
|
+
* }} [options={}]
|
|
59
|
+
* @returns {Promise<string>}
|
|
60
|
+
*/
|
|
61
|
+
export async function ensureBridgeAuthToken(options = {}) {
|
|
62
|
+
const tokenPath = options.tokenPath ?? getBridgeAuthTokenPath();
|
|
63
|
+
const readFile = options.readFile ?? fs.promises.readFile.bind(fs.promises);
|
|
64
|
+
const writeFile = options.writeFile ?? fs.promises.writeFile.bind(fs.promises);
|
|
65
|
+
const mkdir = options.mkdir ?? fs.promises.mkdir.bind(fs.promises);
|
|
66
|
+
const chmod = options.chmod ?? fs.promises.chmod.bind(fs.promises);
|
|
67
|
+
const randomBytesFn = options.randomBytesFn ?? randomBytes;
|
|
68
|
+
const existing = await readBridgeAuthToken({ tokenPath, readFile });
|
|
69
|
+
if (existing) {
|
|
70
|
+
return existing;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const token = randomBytesFn(TOKEN_BYTES).toString('base64url');
|
|
74
|
+
await mkdir(path.dirname(tokenPath), { recursive: true });
|
|
75
|
+
await writeFile(tokenPath, `${token}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
76
|
+
if (process.platform !== 'win32') {
|
|
77
|
+
await chmod(tokenPath, 0o600).catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
return token;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {unknown} error
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
function isMissingFileError(error) {
|
|
87
|
+
return Boolean(
|
|
88
|
+
error &&
|
|
89
|
+
typeof error === 'object' &&
|
|
90
|
+
/** @type {{ code?: unknown }} */ (error).code === 'ENOENT'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
|
|
9
9
|
import { pingExistingDaemon } from './daemon.js';
|
|
10
10
|
import {
|
|
11
|
+
applyWindowsTcpTransportDefaults,
|
|
11
12
|
createSocketBridgeTransport,
|
|
12
13
|
formatBridgeTransport,
|
|
13
14
|
getBridgeTransport,
|
|
@@ -124,7 +125,7 @@ export async function clearDaemonPidFile(options = {}) {
|
|
|
124
125
|
*/
|
|
125
126
|
export async function stopBridgeDaemon(options = {}) {
|
|
126
127
|
const {
|
|
127
|
-
transport =
|
|
128
|
+
transport = undefined,
|
|
128
129
|
socketPath = undefined,
|
|
129
130
|
pidPath = getDaemonPidPath(),
|
|
130
131
|
timeoutMs = DEFAULT_DAEMON_RESTART_TIMEOUT_MS,
|
|
@@ -136,7 +137,7 @@ export async function stopBridgeDaemon(options = {}) {
|
|
|
136
137
|
rmFn = fs.promises.rm,
|
|
137
138
|
sleepFn = sleep,
|
|
138
139
|
} = options;
|
|
139
|
-
const resolvedTransport =
|
|
140
|
+
const resolvedTransport = resolveDaemonTransport({ transport, socketPath });
|
|
140
141
|
const resolvedSocketPath =
|
|
141
142
|
resolvedTransport.type === 'socket' ? resolvedTransport.socketPath : '';
|
|
142
143
|
|
|
@@ -238,7 +239,7 @@ export async function restartBridgeDaemonIfRunning(options = {}) {
|
|
|
238
239
|
*/
|
|
239
240
|
async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
|
|
240
241
|
const {
|
|
241
|
-
transport =
|
|
242
|
+
transport = undefined,
|
|
242
243
|
socketPath = undefined,
|
|
243
244
|
pidPath = getDaemonPidPath(),
|
|
244
245
|
timeoutMs = DEFAULT_DAEMON_RESTART_TIMEOUT_MS,
|
|
@@ -248,7 +249,7 @@ async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
|
|
|
248
249
|
sleepFn = sleep,
|
|
249
250
|
spawnDaemonFn = spawnBridgeDaemonProcess,
|
|
250
251
|
} = options;
|
|
251
|
-
const resolvedTransport =
|
|
252
|
+
const resolvedTransport = resolveDaemonTransport({ transport, socketPath });
|
|
252
253
|
|
|
253
254
|
spawnDaemonFn();
|
|
254
255
|
|
|
@@ -271,6 +272,27 @@ async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
|
|
|
271
272
|
};
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Mirror the daemon entrypoint transport defaults so restart polling targets the
|
|
277
|
+
* same endpoint the spawned process listens on.
|
|
278
|
+
*
|
|
279
|
+
* @param {{ transport?: BridgeTransport, socketPath?: string }} options
|
|
280
|
+
* @returns {BridgeTransport}
|
|
281
|
+
*/
|
|
282
|
+
function resolveDaemonTransport(options) {
|
|
283
|
+
const { transport, socketPath } = options;
|
|
284
|
+
if (socketPath) {
|
|
285
|
+
return createSocketBridgeTransport(socketPath);
|
|
286
|
+
}
|
|
287
|
+
if (transport) {
|
|
288
|
+
return transport;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const env = { ...process.env };
|
|
292
|
+
applyWindowsTcpTransportDefaults(env);
|
|
293
|
+
return getBridgeTransport(env);
|
|
294
|
+
}
|
|
295
|
+
|
|
274
296
|
/**
|
|
275
297
|
* @param {BridgeTransport} transport
|
|
276
298
|
* @returns {Promise<number | null>}
|