@browserbridge/bbx 1.0.1 → 1.1.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 +4 -4
- package/package.json +11 -13
- package/packages/agent-client/src/cli-helpers.js +33 -0
- package/packages/agent-client/src/cli.js +116 -41
- package/packages/agent-client/src/client.js +29 -4
- package/packages/agent-client/src/command-registry.js +3 -0
- package/packages/agent-client/src/detect.js +159 -48
- package/packages/agent-client/src/install.js +24 -1
- package/packages/agent-client/src/mcp-config.js +29 -10
- package/packages/agent-client/src/setup-status.js +12 -4
- package/packages/mcp-server/src/bin.js +57 -5
- package/packages/mcp-server/src/handlers.js +28 -7
- package/packages/mcp-server/src/server.js +12 -2
- package/packages/native-host/bin/bridge-daemon.js +33 -4
- package/packages/native-host/bin/install-manifest.js +24 -2
- package/packages/native-host/src/config.js +131 -6
- package/packages/native-host/src/daemon-process.js +396 -0
- package/packages/native-host/src/daemon.js +217 -68
- package/packages/native-host/src/framing.js +131 -11
- package/packages/native-host/src/install-manifest.js +121 -7
- package/packages/native-host/src/native-host.js +110 -73
- package/packages/protocol/src/capabilities.js +3 -0
- package/packages/protocol/src/defaults.js +1 -0
- package/packages/protocol/src/errors.js +4 -0
- package/packages/protocol/src/payload-cost.js +19 -6
- package/packages/protocol/src/protocol.js +143 -7
- package/packages/protocol/src/registry.js +11 -0
- package/packages/protocol/src/summary.js +18 -10
- package/packages/protocol/src/types.js +28 -3
- package/skills/browser-bridge/SKILL.md +2 -1
- package/skills/browser-bridge/references/interaction.md +1 -0
- package/skills/browser-bridge/references/protocol.md +2 -1
- package/CHANGELOG.md +0 -55
- package/assets/banner.jpg +0 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +0 -65
- package/docs/api-reference.md +0 -157
- package/docs/cli-guide.md +0 -128
- package/docs/index.md +0 -25
- package/docs/manual-setup.md +0 -140
- package/docs/mcp-vs-cli.md +0 -258
- package/docs/publishing.md +0 -112
- package/docs/quickstart.md +0 -104
- package/docs/troubleshooting.md +0 -59
- package/docs/unpacked-extension.md +0 -72
- package/docs/usage-scenarios.md +0 -136
- package/manifest.json +0 -38
- package/packages/extension/assets/icon-128.png +0 -0
- package/packages/extension/assets/icon-16.png +0 -0
- package/packages/extension/assets/icon-32.png +0 -0
- package/packages/extension/assets/icon-48.png +0 -0
- package/packages/extension/src/background-helpers.js +0 -474
- package/packages/extension/src/background-routing.js +0 -89
- package/packages/extension/src/background.js +0 -3490
- package/packages/extension/src/content-script-helpers.js +0 -282
- package/packages/extension/src/content-script.js +0 -2043
- package/packages/extension/src/debugger-coordinator.js +0 -188
- package/packages/extension/src/sidepanel-helpers.js +0 -104
- package/packages/extension/ui/popup.html +0 -35
- package/packages/extension/ui/popup.js +0 -298
- package/packages/extension/ui/sidepanel.html +0 -102
- package/packages/extension/ui/sidepanel.js +0 -1771
- package/packages/extension/ui/ui.css +0 -1160
|
@@ -29,13 +29,20 @@ import {
|
|
|
29
29
|
SUPPORTED_VERSIONS,
|
|
30
30
|
validateBridgeRequest,
|
|
31
31
|
} from '../../protocol/src/index.js';
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
createSocketBridgeTransport,
|
|
34
|
+
formatBridgeTransport,
|
|
35
|
+
getBridgeListenTarget,
|
|
36
|
+
getBridgeTransport,
|
|
37
|
+
getSocketPath,
|
|
38
|
+
} from './config.js';
|
|
33
39
|
import { writeJsonLine } from './framing.js';
|
|
34
40
|
|
|
35
41
|
/** @typedef {import('../../protocol/src/types.js').BridgeRequest} BridgeRequest */
|
|
36
42
|
/** @typedef {import('../../protocol/src/types.js').SetupInstallParams} SetupInstallParams */
|
|
37
43
|
/** @typedef {import('../../protocol/src/types.js').SetupInstallResult} SetupInstallResult */
|
|
38
44
|
/** @typedef {import('../../protocol/src/types.js').SetupStatus} SetupStatus */
|
|
45
|
+
/** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
|
|
39
46
|
/** @typedef {import('node:net').Socket & { __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
|
|
40
47
|
/** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
|
|
41
48
|
/**
|
|
@@ -88,6 +95,14 @@ function getVersionNegotiationPayload(requestedVersion) {
|
|
|
88
95
|
};
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} socketPath
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
function isWindowsNamedPipePath(socketPath) {
|
|
103
|
+
return socketPath.startsWith('\\\\.\\pipe\\');
|
|
104
|
+
}
|
|
105
|
+
|
|
91
106
|
/**
|
|
92
107
|
* @typedef {{
|
|
93
108
|
* type?: string,
|
|
@@ -109,6 +124,7 @@ function getVersionNegotiationPayload(requestedVersion) {
|
|
|
109
124
|
export class BridgeDaemon {
|
|
110
125
|
/**
|
|
111
126
|
* @param {{
|
|
127
|
+
* transport?: BridgeTransport,
|
|
112
128
|
* socketPath?: string,
|
|
113
129
|
* listenOptions?: import('node:net').ListenOptions | null,
|
|
114
130
|
* setupStatusLoader?: () => Promise<SetupStatus>,
|
|
@@ -117,14 +133,17 @@ export class BridgeDaemon {
|
|
|
117
133
|
* }} [options={}]
|
|
118
134
|
*/
|
|
119
135
|
constructor({
|
|
120
|
-
|
|
136
|
+
transport = getBridgeTransport(),
|
|
137
|
+
socketPath = undefined,
|
|
121
138
|
listenOptions = null,
|
|
122
139
|
setupStatusLoader = collectSetupStatus,
|
|
123
140
|
setupInstaller = installSetupTarget,
|
|
124
141
|
logger = console,
|
|
125
142
|
} = {}) {
|
|
126
|
-
this.
|
|
127
|
-
this.
|
|
143
|
+
this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
144
|
+
this.socketPath =
|
|
145
|
+
this.transport.type === 'socket' ? this.transport.socketPath : getSocketPath();
|
|
146
|
+
this.listenOptions = listenOptions ?? getBridgeListenTarget(this.transport);
|
|
128
147
|
this.setupStatusLoader = setupStatusLoader;
|
|
129
148
|
this.setupInstaller = setupInstaller;
|
|
130
149
|
this.logger = logger;
|
|
@@ -138,13 +157,120 @@ export class BridgeDaemon {
|
|
|
138
157
|
this.agentSockets = new Map();
|
|
139
158
|
/** @type {Map<string, PendingEntry>} */
|
|
140
159
|
this.pendingRequests = new Map();
|
|
160
|
+
/** @type {Map<ClientSocket, Set<string>>} */
|
|
161
|
+
this.pendingRequestsByOwnerSocket = new Map();
|
|
162
|
+
/** @type {Map<ClientSocket, Set<string>>} */
|
|
163
|
+
this.pendingRequestsByTargetSocket = new Map();
|
|
141
164
|
this.pendingTimeoutMs = DEFAULT_DAEMON_PENDING_TIMEOUT_MS;
|
|
142
165
|
/** @type {Record<string, unknown>[]} */
|
|
143
166
|
this.recentLog = [];
|
|
167
|
+
/** @type {Array<{ extensionId: string, browserName: string | null, profileLabel: string | null, accessEnabled: boolean }> | null} */
|
|
168
|
+
this.connectedExtensionsCache = null;
|
|
144
169
|
/** @type {Promise<void> | null} */
|
|
145
170
|
this.stopPromise = null;
|
|
146
171
|
}
|
|
147
172
|
|
|
173
|
+
/**
|
|
174
|
+
* @returns {void}
|
|
175
|
+
*/
|
|
176
|
+
invalidateConnectedExtensionsCache() {
|
|
177
|
+
this.connectedExtensionsCache = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {Map<ClientSocket, Set<string>>} index
|
|
182
|
+
* @param {ClientSocket} socket
|
|
183
|
+
* @param {string} requestId
|
|
184
|
+
* @returns {void}
|
|
185
|
+
*/
|
|
186
|
+
addPendingRequestIndex(index, socket, requestId) {
|
|
187
|
+
const requestIds = index.get(socket);
|
|
188
|
+
if (requestIds) {
|
|
189
|
+
requestIds.add(requestId);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
index.set(socket, new Set([requestId]));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {Map<ClientSocket, Set<string>>} index
|
|
197
|
+
* @param {ClientSocket} socket
|
|
198
|
+
* @param {string} requestId
|
|
199
|
+
* @returns {void}
|
|
200
|
+
*/
|
|
201
|
+
removePendingRequestIndex(index, socket, requestId) {
|
|
202
|
+
const requestIds = index.get(socket);
|
|
203
|
+
if (!requestIds) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
requestIds.delete(requestId);
|
|
207
|
+
if (requestIds.size === 0) {
|
|
208
|
+
index.delete(socket);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {string} requestId
|
|
214
|
+
* @param {PendingEntry} pending
|
|
215
|
+
* @returns {void}
|
|
216
|
+
*/
|
|
217
|
+
trackPendingRequest(requestId, pending) {
|
|
218
|
+
this.pendingRequests.set(requestId, pending);
|
|
219
|
+
this.addPendingRequestIndex(this.pendingRequestsByOwnerSocket, pending.socket, requestId);
|
|
220
|
+
for (const targetSocket of pending.targets) {
|
|
221
|
+
this.addPendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {string} requestId
|
|
227
|
+
* @param {PendingEntry | undefined} [pending]
|
|
228
|
+
* @returns {PendingEntry | undefined}
|
|
229
|
+
*/
|
|
230
|
+
clearPendingRequest(requestId, pending = this.pendingRequests.get(requestId)) {
|
|
231
|
+
if (!pending) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
this.pendingRequests.delete(requestId);
|
|
235
|
+
this.removePendingRequestIndex(this.pendingRequestsByOwnerSocket, pending.socket, requestId);
|
|
236
|
+
for (const targetSocket of pending.targets) {
|
|
237
|
+
this.removePendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
|
|
238
|
+
}
|
|
239
|
+
return pending;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {string} requestId
|
|
244
|
+
* @param {PendingEntry} pending
|
|
245
|
+
* @param {ClientSocket} targetSocket
|
|
246
|
+
* @returns {void}
|
|
247
|
+
*/
|
|
248
|
+
removePendingTarget(requestId, pending, targetSocket) {
|
|
249
|
+
if (!pending.targets.delete(targetSocket)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.removePendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @returns {Array<{ extensionId: string, browserName: string | null, profileLabel: string | null, accessEnabled: boolean }>}
|
|
257
|
+
*/
|
|
258
|
+
getConnectedExtensionsSnapshot() {
|
|
259
|
+
if (this.connectedExtensionsCache) {
|
|
260
|
+
return this.connectedExtensionsCache;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.connectedExtensionsCache = Array.from(this.extensionSockets.entries()).map(
|
|
264
|
+
([extensionId, extSocket]) => ({
|
|
265
|
+
extensionId,
|
|
266
|
+
browserName: extSocket.__browserName ?? null,
|
|
267
|
+
profileLabel: extSocket.__profileLabel ?? null,
|
|
268
|
+
accessEnabled: extSocket.__accessEnabled ?? false,
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
return this.connectedExtensionsCache;
|
|
272
|
+
}
|
|
273
|
+
|
|
148
274
|
/**
|
|
149
275
|
* @param {ClientSocket} socket
|
|
150
276
|
* @param {DaemonMessage} message
|
|
@@ -160,6 +286,7 @@ export class BridgeDaemon {
|
|
|
160
286
|
typeof message.profileLabel === 'string' ? message.profileLabel : undefined;
|
|
161
287
|
socket.__lastActiveAt = Date.now();
|
|
162
288
|
this.extensionSockets.set(extensionId, socket);
|
|
289
|
+
this.invalidateConnectedExtensionsCache();
|
|
163
290
|
void writeJsonLine(socket, { type: 'registered', role: 'extension' });
|
|
164
291
|
return;
|
|
165
292
|
}
|
|
@@ -180,7 +307,7 @@ export class BridgeDaemon {
|
|
|
180
307
|
* @returns {Promise<BridgeDaemon>}
|
|
181
308
|
*/
|
|
182
309
|
async start() {
|
|
183
|
-
if (!this.
|
|
310
|
+
if (this.transport.type === 'socket' && !isWindowsNamedPipePath(this.socketPath)) {
|
|
184
311
|
const socketDir = path.dirname(this.socketPath);
|
|
185
312
|
await fs.promises.mkdir(socketDir, { recursive: true });
|
|
186
313
|
if (process.platform !== 'win32') {
|
|
@@ -188,7 +315,7 @@ export class BridgeDaemon {
|
|
|
188
315
|
}
|
|
189
316
|
try {
|
|
190
317
|
await fs.promises.access(this.socketPath);
|
|
191
|
-
if (await pingExistingDaemon(this.
|
|
318
|
+
if (await pingExistingDaemon(this.transport)) {
|
|
192
319
|
throw new Error(
|
|
193
320
|
`Another daemon is already running on ${this.socketPath}. Stop it before starting a new one.`
|
|
194
321
|
);
|
|
@@ -228,14 +355,10 @@ export class BridgeDaemon {
|
|
|
228
355
|
this.serverAddress = server.address();
|
|
229
356
|
resolve(undefined);
|
|
230
357
|
};
|
|
231
|
-
|
|
232
|
-
server.listen(this.listenOptions, onListen);
|
|
233
|
-
} else {
|
|
234
|
-
server.listen(this.socketPath, onListen);
|
|
235
|
-
}
|
|
358
|
+
server.listen(this.listenOptions, onListen);
|
|
236
359
|
});
|
|
237
360
|
|
|
238
|
-
if (
|
|
361
|
+
if (this.transport.type === 'socket' && process.platform !== 'win32') {
|
|
239
362
|
await fs.promises.chmod(this.socketPath, 0o600);
|
|
240
363
|
}
|
|
241
364
|
|
|
@@ -266,6 +389,8 @@ export class BridgeDaemon {
|
|
|
266
389
|
clearTimeout(pending.timeoutId);
|
|
267
390
|
}
|
|
268
391
|
this.pendingRequests.clear();
|
|
392
|
+
this.pendingRequestsByOwnerSocket.clear();
|
|
393
|
+
this.pendingRequestsByTargetSocket.clear();
|
|
269
394
|
|
|
270
395
|
for (const socket of this.agentSockets.values()) {
|
|
271
396
|
socket.destroy();
|
|
@@ -276,6 +401,7 @@ export class BridgeDaemon {
|
|
|
276
401
|
socket.destroy();
|
|
277
402
|
}
|
|
278
403
|
this.extensionSockets.clear();
|
|
404
|
+
this.invalidateConnectedExtensionsCache();
|
|
279
405
|
|
|
280
406
|
if (this.server) {
|
|
281
407
|
const server = this.server;
|
|
@@ -291,7 +417,7 @@ export class BridgeDaemon {
|
|
|
291
417
|
});
|
|
292
418
|
});
|
|
293
419
|
} finally {
|
|
294
|
-
if (!this.
|
|
420
|
+
if (this.transport.type === 'socket' && !isWindowsNamedPipePath(this.socketPath)) {
|
|
295
421
|
await fs.promises.rm(this.socketPath, { force: true });
|
|
296
422
|
}
|
|
297
423
|
}
|
|
@@ -359,6 +485,7 @@ export class BridgeDaemon {
|
|
|
359
485
|
daemon: 'ok',
|
|
360
486
|
extensionConnected: false,
|
|
361
487
|
socketPath: this.socketPath,
|
|
488
|
+
transport: formatBridgeTransport(this.transport),
|
|
362
489
|
connectedExtensions: [],
|
|
363
490
|
...getVersionNegotiationPayload(request.meta?.protocol_version),
|
|
364
491
|
});
|
|
@@ -412,24 +539,38 @@ export class BridgeDaemon {
|
|
|
412
539
|
typeof request.meta?.target_profile === 'string' ? request.meta.target_profile : null;
|
|
413
540
|
const hasExplicitTarget = Boolean(targetBrowser || targetProfile);
|
|
414
541
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
} else {
|
|
427
|
-
const mostRecent = selectMostRecentlyActiveExtension(targets);
|
|
428
|
-
if (mostRecent) {
|
|
429
|
-
targets = [mostRecent];
|
|
542
|
+
/** @type {ClientSocket[]} */
|
|
543
|
+
const targets = [];
|
|
544
|
+
/** @type {ClientSocket | null} */
|
|
545
|
+
let mostRecent = null;
|
|
546
|
+
for (const extSocket of this.extensionSockets.values()) {
|
|
547
|
+
if (targetBrowser || targetProfile) {
|
|
548
|
+
if (targetBrowser && extSocket.__browserName !== targetBrowser) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (targetProfile && extSocket.__profileLabel !== targetProfile) {
|
|
552
|
+
continue;
|
|
430
553
|
}
|
|
554
|
+
targets.push(extSocket);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (extSocket.__accessEnabled) {
|
|
559
|
+
targets.push(extSocket);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (
|
|
564
|
+
!mostRecent ||
|
|
565
|
+
(typeof extSocket.__lastActiveAt === 'number' ? extSocket.__lastActiveAt : 0) >
|
|
566
|
+
(typeof mostRecent.__lastActiveAt === 'number' ? mostRecent.__lastActiveAt : 0)
|
|
567
|
+
) {
|
|
568
|
+
mostRecent = extSocket;
|
|
431
569
|
}
|
|
432
570
|
}
|
|
571
|
+
if (!hasExplicitTarget && targets.length === 0 && mostRecent) {
|
|
572
|
+
targets.push(mostRecent);
|
|
573
|
+
}
|
|
433
574
|
|
|
434
575
|
if (targets.length === 0) {
|
|
435
576
|
const response = createFailure(
|
|
@@ -443,7 +584,7 @@ export class BridgeDaemon {
|
|
|
443
584
|
return;
|
|
444
585
|
}
|
|
445
586
|
|
|
446
|
-
this.
|
|
587
|
+
this.trackPendingRequest(request.id, {
|
|
447
588
|
socket,
|
|
448
589
|
method: request.method,
|
|
449
590
|
source: typeof request.meta?.source === 'string' ? request.meta.source : '',
|
|
@@ -451,7 +592,7 @@ export class BridgeDaemon {
|
|
|
451
592
|
timeoutId: setTimeout(() => {
|
|
452
593
|
const pending = this.pendingRequests.get(request.id);
|
|
453
594
|
if (!pending) return;
|
|
454
|
-
this.
|
|
595
|
+
this.clearPendingRequest(request.id, pending);
|
|
455
596
|
const response = createFailure(
|
|
456
597
|
request.id,
|
|
457
598
|
ERROR_CODES.TIMEOUT,
|
|
@@ -496,12 +637,18 @@ export class BridgeDaemon {
|
|
|
496
637
|
* @returns {void}
|
|
497
638
|
*/
|
|
498
639
|
handleExtensionIdentity(socket, message) {
|
|
640
|
+
let changed = false;
|
|
499
641
|
if (typeof message.browserName === 'string') {
|
|
642
|
+
changed = changed || socket.__browserName !== message.browserName;
|
|
500
643
|
socket.__browserName = message.browserName;
|
|
501
644
|
}
|
|
502
645
|
if (typeof message.profileLabel === 'string') {
|
|
646
|
+
changed = changed || socket.__profileLabel !== message.profileLabel;
|
|
503
647
|
socket.__profileLabel = message.profileLabel;
|
|
504
648
|
}
|
|
649
|
+
if (changed) {
|
|
650
|
+
this.invalidateConnectedExtensionsCache();
|
|
651
|
+
}
|
|
505
652
|
}
|
|
506
653
|
|
|
507
654
|
/**
|
|
@@ -510,7 +657,13 @@ export class BridgeDaemon {
|
|
|
510
657
|
* @returns {void}
|
|
511
658
|
*/
|
|
512
659
|
handleExtensionAccessUpdate(socket, message) {
|
|
513
|
-
|
|
660
|
+
const accessEnabled = Boolean(message.accessEnabled);
|
|
661
|
+
if (socket.__accessEnabled !== accessEnabled) {
|
|
662
|
+
socket.__accessEnabled = accessEnabled;
|
|
663
|
+
this.invalidateConnectedExtensionsCache();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
socket.__accessEnabled = accessEnabled;
|
|
514
667
|
}
|
|
515
668
|
|
|
516
669
|
/**
|
|
@@ -539,11 +692,11 @@ export class BridgeDaemon {
|
|
|
539
692
|
return;
|
|
540
693
|
}
|
|
541
694
|
|
|
542
|
-
|
|
695
|
+
this.removePendingTarget(responseMessage.id, pending, socket);
|
|
543
696
|
|
|
544
697
|
if (responseMessage.ok) {
|
|
545
698
|
clearTimeout(pending.timeoutId);
|
|
546
|
-
this.
|
|
699
|
+
this.clearPendingRequest(responseMessage.id, pending);
|
|
547
700
|
this.pushLog({
|
|
548
701
|
at: new Date().toISOString(),
|
|
549
702
|
method: responseMessage.meta?.method ?? null,
|
|
@@ -559,14 +712,8 @@ export class BridgeDaemon {
|
|
|
559
712
|
daemon: 'ok',
|
|
560
713
|
extensionConnected: true,
|
|
561
714
|
socketPath: this.socketPath,
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
extensionId: _id,
|
|
565
|
-
browserName: extSocket.__browserName ?? null,
|
|
566
|
-
profileLabel: extSocket.__profileLabel ?? null,
|
|
567
|
-
accessEnabled: extSocket.__accessEnabled ?? false,
|
|
568
|
-
})
|
|
569
|
-
),
|
|
715
|
+
transport: formatBridgeTransport(this.transport),
|
|
716
|
+
connectedExtensions: this.getConnectedExtensionsSnapshot(),
|
|
570
717
|
.../** @type {Record<string, unknown>} */ (responseMessage.result),
|
|
571
718
|
},
|
|
572
719
|
{
|
|
@@ -596,20 +743,33 @@ export class BridgeDaemon {
|
|
|
596
743
|
handleSocketClose(socket) {
|
|
597
744
|
if (socket.__extensionId) {
|
|
598
745
|
this.extensionSockets.delete(socket.__extensionId);
|
|
746
|
+
this.invalidateConnectedExtensionsCache();
|
|
599
747
|
}
|
|
600
748
|
|
|
601
749
|
if (socket.__clientId) {
|
|
602
750
|
this.agentSockets.delete(socket.__clientId);
|
|
603
751
|
}
|
|
604
752
|
|
|
605
|
-
|
|
606
|
-
|
|
753
|
+
const ownedRequestIds = this.pendingRequestsByOwnerSocket.get(socket);
|
|
754
|
+
if (ownedRequestIds) {
|
|
755
|
+
for (const id of ownedRequestIds) {
|
|
756
|
+
const pending = this.pendingRequests.get(id);
|
|
757
|
+
if (!pending) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
607
760
|
clearTimeout(pending.timeoutId);
|
|
608
|
-
this.
|
|
609
|
-
continue;
|
|
761
|
+
this.clearPendingRequest(id, pending);
|
|
610
762
|
}
|
|
611
|
-
|
|
612
|
-
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const targetRequestIds = this.pendingRequestsByTargetSocket.get(socket);
|
|
766
|
+
if (targetRequestIds) {
|
|
767
|
+
for (const id of targetRequestIds) {
|
|
768
|
+
const pending = this.pendingRequests.get(id);
|
|
769
|
+
if (!pending) {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
this.removePendingTarget(id, pending, socket);
|
|
613
773
|
void this.finishPendingRequestIfExhausted(id, pending);
|
|
614
774
|
}
|
|
615
775
|
}
|
|
@@ -629,7 +789,7 @@ export class BridgeDaemon {
|
|
|
629
789
|
}
|
|
630
790
|
|
|
631
791
|
clearTimeout(pending.timeoutId);
|
|
632
|
-
this.
|
|
792
|
+
this.clearPendingRequest(requestId, pending);
|
|
633
793
|
|
|
634
794
|
const response =
|
|
635
795
|
pending.lastErrorResponse ??
|
|
@@ -668,36 +828,25 @@ export class BridgeDaemon {
|
|
|
668
828
|
}
|
|
669
829
|
|
|
670
830
|
/**
|
|
671
|
-
*
|
|
672
|
-
* @returns {ClientSocket | null}
|
|
673
|
-
*/
|
|
674
|
-
function selectMostRecentlyActiveExtension(sockets) {
|
|
675
|
-
if (sockets.length === 0) {
|
|
676
|
-
return null;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
return sockets.reduce((best, current) => {
|
|
680
|
-
const bestAt = typeof best.__lastActiveAt === 'number' ? best.__lastActiveAt : 0;
|
|
681
|
-
const currentAt = typeof current.__lastActiveAt === 'number' ? current.__lastActiveAt : 0;
|
|
682
|
-
return currentAt > bestAt ? current : best;
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Check whether a daemon is already listening on the given socket path.
|
|
831
|
+
* Check whether a daemon is already listening on the given transport.
|
|
688
832
|
* Connects, sends a health.ping, and waits up to 500 ms for a response.
|
|
689
833
|
*
|
|
690
|
-
* @param {string}
|
|
834
|
+
* @param {BridgeTransport | string} transport
|
|
691
835
|
* @returns {Promise<boolean>}
|
|
692
836
|
*/
|
|
693
|
-
async function pingExistingDaemon(
|
|
837
|
+
export async function pingExistingDaemon(transport) {
|
|
838
|
+
const resolvedTransport =
|
|
839
|
+
typeof transport === 'string' ? createSocketBridgeTransport(transport) : transport;
|
|
694
840
|
return new Promise((resolve) => {
|
|
695
841
|
const timeout = setTimeout(() => {
|
|
696
842
|
socket.destroy();
|
|
697
843
|
resolve(false);
|
|
698
844
|
}, DAEMON_EXISTING_SOCKET_PING_TIMEOUT_MS);
|
|
699
845
|
|
|
700
|
-
const socket =
|
|
846
|
+
const socket =
|
|
847
|
+
resolvedTransport.type === 'tcp'
|
|
848
|
+
? net.createConnection({ host: resolvedTransport.host, port: resolvedTransport.port })
|
|
849
|
+
: net.createConnection(resolvedTransport.socketPath);
|
|
701
850
|
socket.once('error', () => {
|
|
702
851
|
clearTimeout(timeout);
|
|
703
852
|
resolve(false);
|
|
@@ -11,6 +11,9 @@ import { MAX_NATIVE_MESSAGE_BYTES } from '../../protocol/src/index.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export async function writeNativeMessage(stream, message) {
|
|
13
13
|
const payload = Buffer.from(JSON.stringify(message), 'utf8');
|
|
14
|
+
if (payload.length > MAX_NATIVE_MESSAGE_BYTES) {
|
|
15
|
+
throw new Error(`Native message exceeds ${MAX_NATIVE_MESSAGE_BYTES} bytes: ${payload.length}`);
|
|
16
|
+
}
|
|
14
17
|
const header = Buffer.alloc(4);
|
|
15
18
|
header.writeUInt32LE(payload.length, 0);
|
|
16
19
|
if (!stream.write(header)) {
|
|
@@ -24,34 +27,151 @@ export async function writeNativeMessage(stream, message) {
|
|
|
24
27
|
/**
|
|
25
28
|
* @param {NodeJS.ReadableStream} stream
|
|
26
29
|
* @param {(message: unknown) => void} onMessage
|
|
30
|
+
* @param {(error: Error) => void} [onProtocolError]
|
|
27
31
|
* @returns {void}
|
|
28
32
|
*/
|
|
29
|
-
export function createNativeMessageReader(stream, onMessage) {
|
|
30
|
-
|
|
33
|
+
export function createNativeMessageReader(stream, onMessage, onProtocolError) {
|
|
34
|
+
/** @type {Buffer[]} */
|
|
35
|
+
const chunks = [];
|
|
36
|
+
let bufferedBytes = 0;
|
|
37
|
+
let closed = false;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {number} length
|
|
41
|
+
* @returns {Buffer | null}
|
|
42
|
+
*/
|
|
43
|
+
function peekBytes(length) {
|
|
44
|
+
if (length === 0) {
|
|
45
|
+
return Buffer.alloc(0);
|
|
46
|
+
}
|
|
47
|
+
if (bufferedBytes < length || chunks.length === 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const firstChunk = chunks[0];
|
|
52
|
+
if (firstChunk.length >= length) {
|
|
53
|
+
return firstChunk.subarray(0, length);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const combined = Buffer.allocUnsafe(length);
|
|
57
|
+
let offset = 0;
|
|
58
|
+
for (const chunk of chunks) {
|
|
59
|
+
const copyLength = Math.min(chunk.length, length - offset);
|
|
60
|
+
chunk.copy(combined, offset, 0, copyLength);
|
|
61
|
+
offset += copyLength;
|
|
62
|
+
if (offset === length) {
|
|
63
|
+
return combined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {number} length
|
|
72
|
+
* @returns {Buffer | null}
|
|
73
|
+
*/
|
|
74
|
+
function consumeBytes(length) {
|
|
75
|
+
if (length === 0) {
|
|
76
|
+
return Buffer.alloc(0);
|
|
77
|
+
}
|
|
78
|
+
if (bufferedBytes < length || chunks.length === 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const firstChunk = chunks[0];
|
|
83
|
+
if (firstChunk.length === length) {
|
|
84
|
+
chunks.shift();
|
|
85
|
+
bufferedBytes -= length;
|
|
86
|
+
return firstChunk;
|
|
87
|
+
}
|
|
88
|
+
if (firstChunk.length > length) {
|
|
89
|
+
const consumed = firstChunk.subarray(0, length);
|
|
90
|
+
chunks[0] = firstChunk.subarray(length);
|
|
91
|
+
bufferedBytes -= length;
|
|
92
|
+
return consumed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const combined = Buffer.allocUnsafe(length);
|
|
96
|
+
let offset = 0;
|
|
97
|
+
let remaining = length;
|
|
98
|
+
while (remaining > 0 && chunks.length > 0) {
|
|
99
|
+
const chunk = chunks[0];
|
|
100
|
+
const copyLength = Math.min(chunk.length, remaining);
|
|
101
|
+
chunk.copy(combined, offset, 0, copyLength);
|
|
102
|
+
offset += copyLength;
|
|
103
|
+
remaining -= copyLength;
|
|
104
|
+
if (copyLength === chunk.length) {
|
|
105
|
+
chunks.shift();
|
|
106
|
+
} else {
|
|
107
|
+
chunks[0] = chunk.subarray(copyLength);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
bufferedBytes -= length;
|
|
112
|
+
return combined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {Error} error
|
|
117
|
+
* @returns {void}
|
|
118
|
+
*/
|
|
119
|
+
function closeReader(error) {
|
|
120
|
+
if (closed) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
closed = true;
|
|
124
|
+
stream.removeListener('data', handleData);
|
|
125
|
+
onProtocolError?.(error);
|
|
126
|
+
const destroy = /** @type {{ destroy?: (() => void) | undefined }} */ (stream).destroy;
|
|
127
|
+
if (typeof destroy === 'function') {
|
|
128
|
+
destroy.call(stream);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
31
131
|
|
|
32
132
|
/** @param {Buffer} chunk */
|
|
33
|
-
|
|
34
|
-
|
|
133
|
+
function handleData(chunk) {
|
|
134
|
+
if (closed || chunk.length === 0) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
chunks.push(chunk);
|
|
139
|
+
bufferedBytes += chunk.length;
|
|
35
140
|
|
|
36
|
-
while (
|
|
37
|
-
const
|
|
141
|
+
while (bufferedBytes >= 4) {
|
|
142
|
+
const header = peekBytes(4);
|
|
143
|
+
if (!header) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const length = header.readUInt32LE(0);
|
|
38
148
|
if (length > MAX_NATIVE_MESSAGE_BYTES) {
|
|
39
|
-
|
|
149
|
+
closeReader(
|
|
150
|
+
new Error(`Native message exceeds ${MAX_NATIVE_MESSAGE_BYTES} bytes: ${length}`)
|
|
151
|
+
);
|
|
40
152
|
return;
|
|
41
153
|
}
|
|
42
|
-
|
|
154
|
+
|
|
155
|
+
const frameLength = 4 + length;
|
|
156
|
+
if (bufferedBytes < frameLength) {
|
|
43
157
|
return;
|
|
44
158
|
}
|
|
45
159
|
|
|
46
|
-
const
|
|
47
|
-
|
|
160
|
+
const frame = consumeBytes(frameLength);
|
|
161
|
+
if (!frame) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const payload = frame.subarray(4);
|
|
48
166
|
try {
|
|
49
167
|
onMessage(JSON.parse(payload.toString('utf8')));
|
|
50
168
|
} catch {
|
|
51
169
|
// Malformed JSON payload - skip it.
|
|
52
170
|
}
|
|
53
171
|
}
|
|
54
|
-
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
stream.on('data', handleData);
|
|
55
175
|
}
|
|
56
176
|
|
|
57
177
|
/**
|