@browserbridge/bbx 1.0.1 → 1.2.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 +122 -45
- package/packages/agent-client/src/client.js +134 -8
- package/packages/agent-client/src/command-registry.js +4 -1
- 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-capture.js +279 -0
- package/packages/mcp-server/src/handlers-dom.js +196 -0
- package/packages/mcp-server/src/handlers-navigation.js +79 -0
- package/packages/mcp-server/src/handlers-page.js +365 -0
- package/packages/mcp-server/src/handlers-utils.js +296 -0
- package/packages/mcp-server/src/handlers.js +63 -1159
- package/packages/mcp-server/src/server.js +13 -3
- package/packages/native-host/bin/bridge-daemon.js +34 -4
- package/packages/native-host/bin/install-manifest.js +32 -2
- package/packages/native-host/bin/postinstall.js +16 -0
- package/packages/native-host/src/config.js +131 -6
- package/packages/native-host/src/daemon-logger.js +157 -0
- package/packages/native-host/src/daemon-process.js +422 -0
- package/packages/native-host/src/daemon.js +322 -77
- 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 +4 -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 +13 -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,24 @@ 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';
|
|
39
|
+
import { normalizeDaemonLogger } from './daemon-logger.js';
|
|
33
40
|
import { writeJsonLine } from './framing.js';
|
|
34
41
|
|
|
42
|
+
const DAEMON_VERSION = loadDaemonVersion();
|
|
43
|
+
|
|
35
44
|
/** @typedef {import('../../protocol/src/types.js').BridgeRequest} BridgeRequest */
|
|
36
45
|
/** @typedef {import('../../protocol/src/types.js').SetupInstallParams} SetupInstallParams */
|
|
37
46
|
/** @typedef {import('../../protocol/src/types.js').SetupInstallResult} SetupInstallResult */
|
|
38
47
|
/** @typedef {import('../../protocol/src/types.js').SetupStatus} SetupStatus */
|
|
48
|
+
/** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
|
|
49
|
+
/** @typedef {import('./daemon-logger.js').DaemonLoggerLike} DaemonLoggerLike */
|
|
39
50
|
/** @typedef {import('node:net').Socket & { __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
|
|
40
51
|
/** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
|
|
41
52
|
/**
|
|
@@ -88,6 +99,27 @@ function getVersionNegotiationPayload(requestedVersion) {
|
|
|
88
99
|
};
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
/**
|
|
103
|
+
* @returns {string | null}
|
|
104
|
+
*/
|
|
105
|
+
function loadDaemonVersion() {
|
|
106
|
+
try {
|
|
107
|
+
const raw = fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8');
|
|
108
|
+
const parsed = JSON.parse(raw);
|
|
109
|
+
return parsed && typeof parsed.version === 'string' ? parsed.version : null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} socketPath
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
export function isWindowsNamedPipePath(socketPath) {
|
|
120
|
+
return socketPath.startsWith('\\\\.\\pipe\\');
|
|
121
|
+
}
|
|
122
|
+
|
|
91
123
|
/**
|
|
92
124
|
* @typedef {{
|
|
93
125
|
* type?: string,
|
|
@@ -109,25 +141,30 @@ function getVersionNegotiationPayload(requestedVersion) {
|
|
|
109
141
|
export class BridgeDaemon {
|
|
110
142
|
/**
|
|
111
143
|
* @param {{
|
|
144
|
+
* transport?: BridgeTransport,
|
|
112
145
|
* socketPath?: string,
|
|
113
146
|
* listenOptions?: import('node:net').ListenOptions | null,
|
|
114
147
|
* setupStatusLoader?: () => Promise<SetupStatus>,
|
|
115
148
|
* setupInstaller?: (params: Record<string, unknown>) => Promise<SetupInstallResult>,
|
|
116
|
-
* logger?: Pick<Console, 'log' | 'error'>
|
|
149
|
+
* logger?: DaemonLoggerLike | Pick<Console, 'log' | 'error'>
|
|
117
150
|
* }} [options={}]
|
|
118
151
|
*/
|
|
119
152
|
constructor({
|
|
120
|
-
|
|
153
|
+
transport = getBridgeTransport(),
|
|
154
|
+
socketPath = undefined,
|
|
121
155
|
listenOptions = null,
|
|
122
156
|
setupStatusLoader = collectSetupStatus,
|
|
123
157
|
setupInstaller = installSetupTarget,
|
|
124
|
-
logger =
|
|
158
|
+
logger = undefined,
|
|
125
159
|
} = {}) {
|
|
126
|
-
this.
|
|
127
|
-
this.
|
|
160
|
+
this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
161
|
+
this.socketPath =
|
|
162
|
+
this.transport.type === 'socket' ? this.transport.socketPath : getSocketPath();
|
|
163
|
+
this.listenOptions = listenOptions ?? getBridgeListenTarget(this.transport);
|
|
128
164
|
this.setupStatusLoader = setupStatusLoader;
|
|
129
165
|
this.setupInstaller = setupInstaller;
|
|
130
|
-
|
|
166
|
+
/** @type {DaemonLoggerLike} */
|
|
167
|
+
this.logger = normalizeDaemonLogger(logger);
|
|
131
168
|
/** @type {net.Server | null} */
|
|
132
169
|
this.server = null;
|
|
133
170
|
/** @type {net.AddressInfo | string | null} */
|
|
@@ -138,11 +175,130 @@ export class BridgeDaemon {
|
|
|
138
175
|
this.agentSockets = new Map();
|
|
139
176
|
/** @type {Map<string, PendingEntry>} */
|
|
140
177
|
this.pendingRequests = new Map();
|
|
178
|
+
/** @type {Map<ClientSocket, Set<string>>} */
|
|
179
|
+
this.pendingRequestsByOwnerSocket = new Map();
|
|
180
|
+
/** @type {Map<ClientSocket, Set<string>>} */
|
|
181
|
+
this.pendingRequestsByTargetSocket = new Map();
|
|
141
182
|
this.pendingTimeoutMs = DEFAULT_DAEMON_PENDING_TIMEOUT_MS;
|
|
142
183
|
/** @type {Record<string, unknown>[]} */
|
|
143
184
|
this.recentLog = [];
|
|
185
|
+
/** @type {Array<{ extensionId: string, browserName: string | null, profileLabel: string | null, accessEnabled: boolean }> | null} */
|
|
186
|
+
this.connectedExtensionsCache = null;
|
|
144
187
|
/** @type {Promise<void> | null} */
|
|
145
188
|
this.stopPromise = null;
|
|
189
|
+
/** @type {number} */
|
|
190
|
+
this.startedAt = 0;
|
|
191
|
+
/** @type {number} */
|
|
192
|
+
this.requestsProcessed = 0;
|
|
193
|
+
/** @type {number} */
|
|
194
|
+
this.requestsFailed = 0;
|
|
195
|
+
/** @type {number} */
|
|
196
|
+
this.totalResponseTimeMs = 0;
|
|
197
|
+
/** @type {Map<string, number>} */
|
|
198
|
+
this.requestStartTimes = new Map();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @returns {void}
|
|
203
|
+
*/
|
|
204
|
+
invalidateConnectedExtensionsCache() {
|
|
205
|
+
this.connectedExtensionsCache = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param {Map<ClientSocket, Set<string>>} index
|
|
210
|
+
* @param {ClientSocket} socket
|
|
211
|
+
* @param {string} requestId
|
|
212
|
+
* @returns {void}
|
|
213
|
+
*/
|
|
214
|
+
addPendingRequestIndex(index, socket, requestId) {
|
|
215
|
+
const requestIds = index.get(socket);
|
|
216
|
+
if (requestIds) {
|
|
217
|
+
requestIds.add(requestId);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
index.set(socket, new Set([requestId]));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {Map<ClientSocket, Set<string>>} index
|
|
225
|
+
* @param {ClientSocket} socket
|
|
226
|
+
* @param {string} requestId
|
|
227
|
+
* @returns {void}
|
|
228
|
+
*/
|
|
229
|
+
removePendingRequestIndex(index, socket, requestId) {
|
|
230
|
+
const requestIds = index.get(socket);
|
|
231
|
+
if (!requestIds) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
requestIds.delete(requestId);
|
|
235
|
+
if (requestIds.size === 0) {
|
|
236
|
+
index.delete(socket);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {string} requestId
|
|
242
|
+
* @param {PendingEntry} pending
|
|
243
|
+
* @returns {void}
|
|
244
|
+
*/
|
|
245
|
+
trackPendingRequest(requestId, pending) {
|
|
246
|
+
this.pendingRequests.set(requestId, pending);
|
|
247
|
+
this.requestStartTimes.set(requestId, Date.now());
|
|
248
|
+
this.addPendingRequestIndex(this.pendingRequestsByOwnerSocket, pending.socket, requestId);
|
|
249
|
+
for (const targetSocket of pending.targets) {
|
|
250
|
+
this.addPendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {string} requestId
|
|
256
|
+
* @param {PendingEntry | undefined} [pending]
|
|
257
|
+
* @returns {PendingEntry | undefined}
|
|
258
|
+
*/
|
|
259
|
+
clearPendingRequest(requestId, pending = this.pendingRequests.get(requestId)) {
|
|
260
|
+
if (!pending) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
this.pendingRequests.delete(requestId);
|
|
264
|
+
this.requestStartTimes.delete(requestId);
|
|
265
|
+
this.removePendingRequestIndex(this.pendingRequestsByOwnerSocket, pending.socket, requestId);
|
|
266
|
+
for (const targetSocket of pending.targets) {
|
|
267
|
+
this.removePendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
|
|
268
|
+
}
|
|
269
|
+
return pending;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {string} requestId
|
|
274
|
+
* @param {PendingEntry} pending
|
|
275
|
+
* @param {ClientSocket} targetSocket
|
|
276
|
+
* @returns {void}
|
|
277
|
+
*/
|
|
278
|
+
removePendingTarget(requestId, pending, targetSocket) {
|
|
279
|
+
if (!pending.targets.delete(targetSocket)) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.removePendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @returns {Array<{ extensionId: string, browserName: string | null, profileLabel: string | null, accessEnabled: boolean }>}
|
|
287
|
+
*/
|
|
288
|
+
getConnectedExtensionsSnapshot() {
|
|
289
|
+
if (this.connectedExtensionsCache) {
|
|
290
|
+
return this.connectedExtensionsCache;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.connectedExtensionsCache = Array.from(this.extensionSockets.entries()).map(
|
|
294
|
+
([extensionId, extSocket]) => ({
|
|
295
|
+
extensionId,
|
|
296
|
+
browserName: extSocket.__browserName ?? null,
|
|
297
|
+
profileLabel: extSocket.__profileLabel ?? null,
|
|
298
|
+
accessEnabled: extSocket.__accessEnabled ?? false,
|
|
299
|
+
})
|
|
300
|
+
);
|
|
301
|
+
return this.connectedExtensionsCache;
|
|
146
302
|
}
|
|
147
303
|
|
|
148
304
|
/**
|
|
@@ -160,6 +316,12 @@ export class BridgeDaemon {
|
|
|
160
316
|
typeof message.profileLabel === 'string' ? message.profileLabel : undefined;
|
|
161
317
|
socket.__lastActiveAt = Date.now();
|
|
162
318
|
this.extensionSockets.set(extensionId, socket);
|
|
319
|
+
this.invalidateConnectedExtensionsCache();
|
|
320
|
+
this.logger.info('extension registered', {
|
|
321
|
+
extensionId,
|
|
322
|
+
browserName: socket.__browserName ?? null,
|
|
323
|
+
profileLabel: socket.__profileLabel ?? null,
|
|
324
|
+
});
|
|
163
325
|
void writeJsonLine(socket, { type: 'registered', role: 'extension' });
|
|
164
326
|
return;
|
|
165
327
|
}
|
|
@@ -168,6 +330,7 @@ export class BridgeDaemon {
|
|
|
168
330
|
const clientId = message.clientId || randomUUID();
|
|
169
331
|
this.agentSockets.set(clientId, socket);
|
|
170
332
|
socket.__clientId = clientId;
|
|
333
|
+
this.logger.info('agent registered', { clientId });
|
|
171
334
|
void writeJsonLine(socket, {
|
|
172
335
|
type: 'registered',
|
|
173
336
|
role: 'agent',
|
|
@@ -180,7 +343,7 @@ export class BridgeDaemon {
|
|
|
180
343
|
* @returns {Promise<BridgeDaemon>}
|
|
181
344
|
*/
|
|
182
345
|
async start() {
|
|
183
|
-
if (!this.
|
|
346
|
+
if (this.transport.type === 'socket' && !isWindowsNamedPipePath(this.socketPath)) {
|
|
184
347
|
const socketDir = path.dirname(this.socketPath);
|
|
185
348
|
await fs.promises.mkdir(socketDir, { recursive: true });
|
|
186
349
|
if (process.platform !== 'win32') {
|
|
@@ -188,12 +351,14 @@ export class BridgeDaemon {
|
|
|
188
351
|
}
|
|
189
352
|
try {
|
|
190
353
|
await fs.promises.access(this.socketPath);
|
|
191
|
-
if (await pingExistingDaemon(this.
|
|
354
|
+
if (await pingExistingDaemon(this.transport)) {
|
|
192
355
|
throw new Error(
|
|
193
356
|
`Another daemon is already running on ${this.socketPath}. Stop it before starting a new one.`
|
|
194
357
|
);
|
|
195
358
|
}
|
|
196
|
-
this.logger.
|
|
359
|
+
this.logger.info('Removing stale socket from previous run', {
|
|
360
|
+
socketPath: this.socketPath,
|
|
361
|
+
});
|
|
197
362
|
} catch (error) {
|
|
198
363
|
if (error instanceof Error && error.message.startsWith('Another daemon')) {
|
|
199
364
|
throw error;
|
|
@@ -206,15 +371,14 @@ export class BridgeDaemon {
|
|
|
206
371
|
this.server = net.createServer((socket) => {
|
|
207
372
|
const typedSocket = /** @type {ClientSocket} */ (socket);
|
|
208
373
|
typedSocket.on('error', (err) => {
|
|
209
|
-
this.logger.error
|
|
374
|
+
this.logger.error('socket error', { message: err.message });
|
|
210
375
|
});
|
|
211
376
|
parseJsonLines(typedSocket, (raw) => {
|
|
212
377
|
const message = /** @type {DaemonMessage} */ (raw);
|
|
213
378
|
void this.handleClientMessage(typedSocket, message).catch((err) => {
|
|
214
|
-
this.logger.error
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
);
|
|
379
|
+
this.logger.error('handler error', {
|
|
380
|
+
message: err instanceof Error ? err.message : String(err),
|
|
381
|
+
});
|
|
218
382
|
});
|
|
219
383
|
});
|
|
220
384
|
typedSocket.on('close', () => this.handleSocketClose(typedSocket));
|
|
@@ -228,17 +392,20 @@ export class BridgeDaemon {
|
|
|
228
392
|
this.serverAddress = server.address();
|
|
229
393
|
resolve(undefined);
|
|
230
394
|
};
|
|
231
|
-
|
|
232
|
-
server.listen(this.listenOptions, onListen);
|
|
233
|
-
} else {
|
|
234
|
-
server.listen(this.socketPath, onListen);
|
|
235
|
-
}
|
|
395
|
+
server.listen(this.listenOptions, onListen);
|
|
236
396
|
});
|
|
237
397
|
|
|
238
|
-
if (
|
|
398
|
+
if (this.transport.type === 'socket' && process.platform !== 'win32') {
|
|
239
399
|
await fs.promises.chmod(this.socketPath, 0o600);
|
|
240
400
|
}
|
|
241
401
|
|
|
402
|
+
this.logger.info('Daemon listening', {
|
|
403
|
+
transport: formatBridgeTransport(this.transport),
|
|
404
|
+
socketPath: this.socketPath ?? null,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
this.startedAt = Date.now();
|
|
408
|
+
|
|
242
409
|
return this;
|
|
243
410
|
}
|
|
244
411
|
|
|
@@ -266,6 +433,8 @@ export class BridgeDaemon {
|
|
|
266
433
|
clearTimeout(pending.timeoutId);
|
|
267
434
|
}
|
|
268
435
|
this.pendingRequests.clear();
|
|
436
|
+
this.pendingRequestsByOwnerSocket.clear();
|
|
437
|
+
this.pendingRequestsByTargetSocket.clear();
|
|
269
438
|
|
|
270
439
|
for (const socket of this.agentSockets.values()) {
|
|
271
440
|
socket.destroy();
|
|
@@ -276,6 +445,7 @@ export class BridgeDaemon {
|
|
|
276
445
|
socket.destroy();
|
|
277
446
|
}
|
|
278
447
|
this.extensionSockets.clear();
|
|
448
|
+
this.invalidateConnectedExtensionsCache();
|
|
279
449
|
|
|
280
450
|
if (this.server) {
|
|
281
451
|
const server = this.server;
|
|
@@ -291,7 +461,7 @@ export class BridgeDaemon {
|
|
|
291
461
|
});
|
|
292
462
|
});
|
|
293
463
|
} finally {
|
|
294
|
-
if (!this.
|
|
464
|
+
if (this.transport.type === 'socket' && !isWindowsNamedPipePath(this.socketPath)) {
|
|
295
465
|
await fs.promises.rm(this.socketPath, { force: true });
|
|
296
466
|
}
|
|
297
467
|
}
|
|
@@ -357,9 +527,12 @@ export class BridgeDaemon {
|
|
|
357
527
|
if (this.extensionSockets.size === 0) {
|
|
358
528
|
const response = createSuccess(request.id, {
|
|
359
529
|
daemon: 'ok',
|
|
530
|
+
daemonVersion: DAEMON_VERSION,
|
|
360
531
|
extensionConnected: false,
|
|
361
532
|
socketPath: this.socketPath,
|
|
533
|
+
transport: formatBridgeTransport(this.transport),
|
|
362
534
|
connectedExtensions: [],
|
|
535
|
+
daemon_supported_versions: SUPPORTED_VERSIONS,
|
|
363
536
|
...getVersionNegotiationPayload(request.meta?.protocol_version),
|
|
364
537
|
});
|
|
365
538
|
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
@@ -375,6 +548,26 @@ export class BridgeDaemon {
|
|
|
375
548
|
return;
|
|
376
549
|
}
|
|
377
550
|
|
|
551
|
+
if (request.method === 'daemon.metrics') {
|
|
552
|
+
const now = Date.now();
|
|
553
|
+
const uptimeMs = this.startedAt > 0 ? now - this.startedAt : 0;
|
|
554
|
+
const avgResponseTimeMs =
|
|
555
|
+
this.requestsProcessed > 0
|
|
556
|
+
? Math.round(this.totalResponseTimeMs / this.requestsProcessed)
|
|
557
|
+
: 0;
|
|
558
|
+
const response = createSuccess(request.id, {
|
|
559
|
+
uptimeMs,
|
|
560
|
+
activeAgents: this.agentSockets.size,
|
|
561
|
+
activeExtensions: this.extensionSockets.size,
|
|
562
|
+
pendingRequests: this.pendingRequests.size,
|
|
563
|
+
requestsProcessed: this.requestsProcessed,
|
|
564
|
+
requestsFailed: this.requestsFailed,
|
|
565
|
+
avgResponseTimeMs,
|
|
566
|
+
});
|
|
567
|
+
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
378
571
|
if (request.method === 'setup.get_status') {
|
|
379
572
|
const response = createSuccess(request.id, await this.setupStatusLoader(), {
|
|
380
573
|
method: request.method,
|
|
@@ -412,24 +605,38 @@ export class BridgeDaemon {
|
|
|
412
605
|
typeof request.meta?.target_profile === 'string' ? request.meta.target_profile : null;
|
|
413
606
|
const hasExplicitTarget = Boolean(targetBrowser || targetProfile);
|
|
414
607
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
} else {
|
|
427
|
-
const mostRecent = selectMostRecentlyActiveExtension(targets);
|
|
428
|
-
if (mostRecent) {
|
|
429
|
-
targets = [mostRecent];
|
|
608
|
+
/** @type {ClientSocket[]} */
|
|
609
|
+
const targets = [];
|
|
610
|
+
/** @type {ClientSocket | null} */
|
|
611
|
+
let mostRecent = null;
|
|
612
|
+
for (const extSocket of this.extensionSockets.values()) {
|
|
613
|
+
if (targetBrowser || targetProfile) {
|
|
614
|
+
if (targetBrowser && extSocket.__browserName !== targetBrowser) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (targetProfile && extSocket.__profileLabel !== targetProfile) {
|
|
618
|
+
continue;
|
|
430
619
|
}
|
|
620
|
+
targets.push(extSocket);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (extSocket.__accessEnabled) {
|
|
625
|
+
targets.push(extSocket);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (
|
|
630
|
+
!mostRecent ||
|
|
631
|
+
(typeof extSocket.__lastActiveAt === 'number' ? extSocket.__lastActiveAt : 0) >
|
|
632
|
+
(typeof mostRecent.__lastActiveAt === 'number' ? mostRecent.__lastActiveAt : 0)
|
|
633
|
+
) {
|
|
634
|
+
mostRecent = extSocket;
|
|
431
635
|
}
|
|
432
636
|
}
|
|
637
|
+
if (!hasExplicitTarget && targets.length === 0 && mostRecent) {
|
|
638
|
+
targets.push(mostRecent);
|
|
639
|
+
}
|
|
433
640
|
|
|
434
641
|
if (targets.length === 0) {
|
|
435
642
|
const response = createFailure(
|
|
@@ -443,7 +650,7 @@ export class BridgeDaemon {
|
|
|
443
650
|
return;
|
|
444
651
|
}
|
|
445
652
|
|
|
446
|
-
this.
|
|
653
|
+
this.trackPendingRequest(request.id, {
|
|
447
654
|
socket,
|
|
448
655
|
method: request.method,
|
|
449
656
|
source: typeof request.meta?.source === 'string' ? request.meta.source : '',
|
|
@@ -451,7 +658,8 @@ export class BridgeDaemon {
|
|
|
451
658
|
timeoutId: setTimeout(() => {
|
|
452
659
|
const pending = this.pendingRequests.get(request.id);
|
|
453
660
|
if (!pending) return;
|
|
454
|
-
this.
|
|
661
|
+
this.clearPendingRequest(request.id, pending);
|
|
662
|
+
this.recordRequestCompletion(request.id, false);
|
|
455
663
|
const response = createFailure(
|
|
456
664
|
request.id,
|
|
457
665
|
ERROR_CODES.TIMEOUT,
|
|
@@ -463,6 +671,12 @@ export class BridgeDaemon {
|
|
|
463
671
|
});
|
|
464
672
|
}, this.pendingTimeoutMs),
|
|
465
673
|
});
|
|
674
|
+
this.logger.info('request routed', {
|
|
675
|
+
requestId: request.id,
|
|
676
|
+
method: request.method,
|
|
677
|
+
clientId: socket.__clientId ?? null,
|
|
678
|
+
targetCount: targets.length,
|
|
679
|
+
});
|
|
466
680
|
const broadcastPayload = { type: 'extension.request', request };
|
|
467
681
|
await Promise.all(targets.map((extSocket) => writeJsonLine(extSocket, broadcastPayload)));
|
|
468
682
|
}
|
|
@@ -496,12 +710,18 @@ export class BridgeDaemon {
|
|
|
496
710
|
* @returns {void}
|
|
497
711
|
*/
|
|
498
712
|
handleExtensionIdentity(socket, message) {
|
|
713
|
+
let changed = false;
|
|
499
714
|
if (typeof message.browserName === 'string') {
|
|
715
|
+
changed = changed || socket.__browserName !== message.browserName;
|
|
500
716
|
socket.__browserName = message.browserName;
|
|
501
717
|
}
|
|
502
718
|
if (typeof message.profileLabel === 'string') {
|
|
719
|
+
changed = changed || socket.__profileLabel !== message.profileLabel;
|
|
503
720
|
socket.__profileLabel = message.profileLabel;
|
|
504
721
|
}
|
|
722
|
+
if (changed) {
|
|
723
|
+
this.invalidateConnectedExtensionsCache();
|
|
724
|
+
}
|
|
505
725
|
}
|
|
506
726
|
|
|
507
727
|
/**
|
|
@@ -510,7 +730,13 @@ export class BridgeDaemon {
|
|
|
510
730
|
* @returns {void}
|
|
511
731
|
*/
|
|
512
732
|
handleExtensionAccessUpdate(socket, message) {
|
|
513
|
-
|
|
733
|
+
const accessEnabled = Boolean(message.accessEnabled);
|
|
734
|
+
if (socket.__accessEnabled !== accessEnabled) {
|
|
735
|
+
socket.__accessEnabled = accessEnabled;
|
|
736
|
+
this.invalidateConnectedExtensionsCache();
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
socket.__accessEnabled = accessEnabled;
|
|
514
740
|
}
|
|
515
741
|
|
|
516
742
|
/**
|
|
@@ -539,11 +765,12 @@ export class BridgeDaemon {
|
|
|
539
765
|
return;
|
|
540
766
|
}
|
|
541
767
|
|
|
542
|
-
|
|
768
|
+
this.removePendingTarget(responseMessage.id, pending, socket);
|
|
543
769
|
|
|
544
770
|
if (responseMessage.ok) {
|
|
545
771
|
clearTimeout(pending.timeoutId);
|
|
546
|
-
this.
|
|
772
|
+
this.clearPendingRequest(responseMessage.id, pending);
|
|
773
|
+
this.recordRequestCompletion(responseMessage.id, true);
|
|
547
774
|
this.pushLog({
|
|
548
775
|
at: new Date().toISOString(),
|
|
549
776
|
method: responseMessage.meta?.method ?? null,
|
|
@@ -559,15 +786,11 @@ export class BridgeDaemon {
|
|
|
559
786
|
daemon: 'ok',
|
|
560
787
|
extensionConnected: true,
|
|
561
788
|
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
|
-
),
|
|
789
|
+
transport: formatBridgeTransport(this.transport),
|
|
790
|
+
connectedExtensions: this.getConnectedExtensionsSnapshot(),
|
|
570
791
|
.../** @type {Record<string, unknown>} */ (responseMessage.result),
|
|
792
|
+
daemonVersion: DAEMON_VERSION,
|
|
793
|
+
daemon_supported_versions: SUPPORTED_VERSIONS,
|
|
571
794
|
},
|
|
572
795
|
{
|
|
573
796
|
...responseMessage.meta,
|
|
@@ -595,21 +818,37 @@ export class BridgeDaemon {
|
|
|
595
818
|
*/
|
|
596
819
|
handleSocketClose(socket) {
|
|
597
820
|
if (socket.__extensionId) {
|
|
821
|
+
this.logger.info('extension disconnected', { extensionId: socket.__extensionId });
|
|
598
822
|
this.extensionSockets.delete(socket.__extensionId);
|
|
823
|
+
this.invalidateConnectedExtensionsCache();
|
|
599
824
|
}
|
|
600
825
|
|
|
601
826
|
if (socket.__clientId) {
|
|
827
|
+
this.logger.info('agent disconnected', { clientId: socket.__clientId });
|
|
602
828
|
this.agentSockets.delete(socket.__clientId);
|
|
603
829
|
}
|
|
604
830
|
|
|
605
|
-
|
|
606
|
-
|
|
831
|
+
const ownedRequestIds = this.pendingRequestsByOwnerSocket.get(socket);
|
|
832
|
+
if (ownedRequestIds) {
|
|
833
|
+
for (const id of ownedRequestIds) {
|
|
834
|
+
const pending = this.pendingRequests.get(id);
|
|
835
|
+
if (!pending) {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
607
838
|
clearTimeout(pending.timeoutId);
|
|
608
|
-
this.
|
|
609
|
-
|
|
839
|
+
this.clearPendingRequest(id, pending);
|
|
840
|
+
this.recordRequestCompletion(id, false);
|
|
610
841
|
}
|
|
611
|
-
|
|
612
|
-
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const targetRequestIds = this.pendingRequestsByTargetSocket.get(socket);
|
|
845
|
+
if (targetRequestIds) {
|
|
846
|
+
for (const id of targetRequestIds) {
|
|
847
|
+
const pending = this.pendingRequests.get(id);
|
|
848
|
+
if (!pending) {
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
this.removePendingTarget(id, pending, socket);
|
|
613
852
|
void this.finishPendingRequestIfExhausted(id, pending);
|
|
614
853
|
}
|
|
615
854
|
}
|
|
@@ -629,7 +868,8 @@ export class BridgeDaemon {
|
|
|
629
868
|
}
|
|
630
869
|
|
|
631
870
|
clearTimeout(pending.timeoutId);
|
|
632
|
-
this.
|
|
871
|
+
this.clearPendingRequest(requestId, pending);
|
|
872
|
+
this.recordRequestCompletion(requestId, false);
|
|
633
873
|
|
|
634
874
|
const response =
|
|
635
875
|
pending.lastErrorResponse ??
|
|
@@ -655,6 +895,22 @@ export class BridgeDaemon {
|
|
|
655
895
|
});
|
|
656
896
|
}
|
|
657
897
|
|
|
898
|
+
/**
|
|
899
|
+
* @param {string} requestId
|
|
900
|
+
* @param {boolean} ok
|
|
901
|
+
* @returns {void}
|
|
902
|
+
*/
|
|
903
|
+
recordRequestCompletion(requestId, ok) {
|
|
904
|
+
const startedAt = this.requestStartTimes.get(requestId);
|
|
905
|
+
this.requestsProcessed += 1;
|
|
906
|
+
if (!ok) {
|
|
907
|
+
this.requestsFailed += 1;
|
|
908
|
+
}
|
|
909
|
+
if (typeof startedAt === 'number') {
|
|
910
|
+
this.totalResponseTimeMs += Date.now() - startedAt;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
658
914
|
/**
|
|
659
915
|
* @param {Record<string, unknown>} entry
|
|
660
916
|
* @returns {void}
|
|
@@ -668,36 +924,25 @@ export class BridgeDaemon {
|
|
|
668
924
|
}
|
|
669
925
|
|
|
670
926
|
/**
|
|
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.
|
|
927
|
+
* Check whether a daemon is already listening on the given transport.
|
|
688
928
|
* Connects, sends a health.ping, and waits up to 500 ms for a response.
|
|
689
929
|
*
|
|
690
|
-
* @param {string}
|
|
930
|
+
* @param {BridgeTransport | string} transport
|
|
691
931
|
* @returns {Promise<boolean>}
|
|
692
932
|
*/
|
|
693
|
-
async function pingExistingDaemon(
|
|
933
|
+
export async function pingExistingDaemon(transport) {
|
|
934
|
+
const resolvedTransport =
|
|
935
|
+
typeof transport === 'string' ? createSocketBridgeTransport(transport) : transport;
|
|
694
936
|
return new Promise((resolve) => {
|
|
695
937
|
const timeout = setTimeout(() => {
|
|
696
938
|
socket.destroy();
|
|
697
939
|
resolve(false);
|
|
698
940
|
}, DAEMON_EXISTING_SOCKET_PING_TIMEOUT_MS);
|
|
699
941
|
|
|
700
|
-
const socket =
|
|
942
|
+
const socket =
|
|
943
|
+
resolvedTransport.type === 'tcp'
|
|
944
|
+
? net.createConnection({ host: resolvedTransport.host, port: resolvedTransport.port })
|
|
945
|
+
: net.createConnection(resolvedTransport.socketPath);
|
|
701
946
|
socket.once('error', () => {
|
|
702
947
|
clearTimeout(timeout);
|
|
703
948
|
resolve(false);
|