@browserbridge/bbx 1.11.0 → 1.11.2
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/SECURITY.md +14 -2
- package/package.json +2 -2
- package/packages/agent-client/src/client.js +110 -37
- package/packages/agent-client/src/runtime.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +1 -1
- package/packages/mcp-server/src/handlers-navigation.js +14 -21
- package/packages/mcp-server/src/handlers-page.js +16 -20
- package/packages/mcp-server/src/handlers-utils.js +188 -44
- package/packages/mcp-server/src/server.js +2 -1
- package/packages/native-host/bin/postinstall.js +7 -5
- package/packages/native-host/src/auth-token.js +138 -14
- package/packages/native-host/src/daemon-process.js +5 -2
- package/packages/native-host/src/daemon.js +258 -63
- package/packages/native-host/src/framing.js +107 -17
- package/packages/native-host/src/install-manifest.js +3 -9
- package/packages/native-host/src/native-host.js +7 -3
- package/packages/protocol/src/defaults.js +6 -0
- package/packages/protocol/src/protocol.js +62 -3
- package/packages/protocol/src/types.ts +2 -0
|
@@ -44,7 +44,12 @@ import {
|
|
|
44
44
|
getSocketPath,
|
|
45
45
|
} from './config.js';
|
|
46
46
|
import { ArtifactStore } from './artifact-store.js';
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
bridgeAuthTokensEqual,
|
|
49
|
+
ensureBridgeAuthToken,
|
|
50
|
+
ensureBridgeExtensionAuthToken,
|
|
51
|
+
readBridgeAuthToken,
|
|
52
|
+
} from './auth-token.js';
|
|
48
53
|
import { normalizeDaemonLogger } from './daemon-logger.js';
|
|
49
54
|
import { DomBaselineOwnerRegistry, isValidBaselineId } from './dom-baseline-owners.js';
|
|
50
55
|
import { writeJsonLine } from './framing.js';
|
|
@@ -59,7 +64,7 @@ setProtocolPackageVersion(DAEMON_VERSION);
|
|
|
59
64
|
/** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
|
|
60
65
|
/** @typedef {import('./daemon-logger.js').DaemonLoggerLike} DaemonLoggerLike */
|
|
61
66
|
/** @typedef {'agent' | 'extension'} SocketRole */
|
|
62
|
-
/** @typedef {import('node:net').Socket & { readonly __role?: SocketRole, __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __browserExtensionId?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
|
|
67
|
+
/** @typedef {import('node:net').Socket & { readonly __role?: SocketRole, __localControl?: boolean, __registrationTimeoutId?: NodeJS.Timeout, __invalidLineCount?: number, __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __browserExtensionId?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
|
|
63
68
|
/** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, mcpEra?: string, method?: string, protocolVersion?: string, baselineId?: string | null, automaticMcpRetry?: boolean, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
|
|
64
69
|
/**
|
|
65
70
|
* @typedef {{
|
|
@@ -123,6 +128,14 @@ const DIAGNOSTIC_METHODS = new Set([
|
|
|
123
128
|
'setup.get_status',
|
|
124
129
|
'setup.install',
|
|
125
130
|
]);
|
|
131
|
+
export const DAEMON_REGISTRATION_TIMEOUT_MS = 5_000;
|
|
132
|
+
export const MAX_DAEMON_CONNECTIONS = 64;
|
|
133
|
+
export const MAX_DAEMON_PENDING_REQUESTS = 128;
|
|
134
|
+
export const MAX_DAEMON_PENDING_REQUESTS_PER_CLIENT = 16;
|
|
135
|
+
export const MAX_DAEMON_ACTIVE_HANDLERS_PER_CLIENT = 32;
|
|
136
|
+
export const MAX_DAEMON_INVALID_LINES = 3;
|
|
137
|
+
const MAX_CLIENT_ID_LENGTH = 128;
|
|
138
|
+
const MAX_ROUTING_LABEL_LENGTH = 128;
|
|
126
139
|
|
|
127
140
|
/**
|
|
128
141
|
* @param {unknown} value
|
|
@@ -134,6 +147,42 @@ function asHealthRecord(value) {
|
|
|
134
147
|
: null;
|
|
135
148
|
}
|
|
136
149
|
|
|
150
|
+
/**
|
|
151
|
+
* @param {unknown} value
|
|
152
|
+
* @returns {string | undefined}
|
|
153
|
+
*/
|
|
154
|
+
function normalizeRoutingLabel(value) {
|
|
155
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_ROUTING_LABEL_LENGTH) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
return hasAsciiControlCharacters(value) ? undefined : value;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** @param {string} value @returns {boolean} */
|
|
162
|
+
function hasAsciiControlCharacters(value) {
|
|
163
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
164
|
+
const code = value.charCodeAt(index);
|
|
165
|
+
if (code < 32 || code === 127) return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {unknown} value
|
|
172
|
+
* @returns {string | null}
|
|
173
|
+
*/
|
|
174
|
+
function normalizeClientId(value) {
|
|
175
|
+
if (value == null || value === '') return randomUUID();
|
|
176
|
+
if (
|
|
177
|
+
typeof value !== 'string' ||
|
|
178
|
+
value.length > MAX_CLIENT_ID_LENGTH ||
|
|
179
|
+
!/^[A-Za-z0-9_.:-]+$/u.test(value)
|
|
180
|
+
) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
|
|
137
186
|
/**
|
|
138
187
|
* @param {unknown} value
|
|
139
188
|
* @returns {number | undefined}
|
|
@@ -383,6 +432,7 @@ export class BridgeDaemon {
|
|
|
383
432
|
* setupInstaller?: (params: Record<string, unknown>) => Promise<SetupInstallResult>,
|
|
384
433
|
* logger?: DaemonLoggerLike | Pick<Console, 'log' | 'error'>,
|
|
385
434
|
* authToken?: string | null,
|
|
435
|
+
* extensionAuthToken?: string | null,
|
|
386
436
|
* artifactStore?: ArtifactStore
|
|
387
437
|
* }} [options={}]
|
|
388
438
|
*/
|
|
@@ -394,6 +444,7 @@ export class BridgeDaemon {
|
|
|
394
444
|
setupInstaller = installSetupTarget,
|
|
395
445
|
logger = undefined,
|
|
396
446
|
authToken = undefined,
|
|
447
|
+
extensionAuthToken = undefined,
|
|
397
448
|
artifactStore = new ArtifactStore(getArtifactStorePath()),
|
|
398
449
|
} = {}) {
|
|
399
450
|
this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
@@ -412,6 +463,11 @@ export class BridgeDaemon {
|
|
|
412
463
|
this.extensionSockets = new Map();
|
|
413
464
|
/** @type {Map<string, ClientSocket>} */
|
|
414
465
|
this.agentSockets = new Map();
|
|
466
|
+
/** @type {Set<ClientSocket>} */
|
|
467
|
+
this.clientSockets = new Set();
|
|
468
|
+
/** @type {Map<ClientSocket, number>} */
|
|
469
|
+
this.activeHandlers = new Map();
|
|
470
|
+
this.activeHandlerCount = 0;
|
|
415
471
|
/** @type {Map<string, PendingEntry>} */
|
|
416
472
|
this.pendingRequests = new Map();
|
|
417
473
|
/** @type {Map<ClientSocket, Set<string>>} */
|
|
@@ -437,6 +493,8 @@ export class BridgeDaemon {
|
|
|
437
493
|
this.requestStartTimes = new Map();
|
|
438
494
|
/** @type {string | null | undefined} */
|
|
439
495
|
this.authToken = authToken;
|
|
496
|
+
/** @type {string | null | undefined} */
|
|
497
|
+
this.extensionAuthToken = extensionAuthToken;
|
|
440
498
|
this.artifactStore = artifactStore;
|
|
441
499
|
this.artifactStoreInitialized = false;
|
|
442
500
|
this.domBaselines = new DomBaselineOwnerRegistry({
|
|
@@ -516,7 +574,7 @@ export class BridgeDaemon {
|
|
|
516
574
|
pending = this.pendingRequests.get(requestId),
|
|
517
575
|
preserveArtifacts = false
|
|
518
576
|
) {
|
|
519
|
-
if (!pending) {
|
|
577
|
+
if (!pending || this.pendingRequests.get(requestId) !== pending) {
|
|
520
578
|
return undefined;
|
|
521
579
|
}
|
|
522
580
|
clearTimeout(pending.timeoutId);
|
|
@@ -569,6 +627,7 @@ export class BridgeDaemon {
|
|
|
569
627
|
* @returns {void}
|
|
570
628
|
*/
|
|
571
629
|
registerSocket(socket, message) {
|
|
630
|
+
this.clientSockets.add(socket);
|
|
572
631
|
if (socket.__role) {
|
|
573
632
|
void writeJsonLine(socket, {
|
|
574
633
|
type: 'registration_failed',
|
|
@@ -576,30 +635,38 @@ export class BridgeDaemon {
|
|
|
576
635
|
code: ERROR_CODES.INVALID_REQUEST,
|
|
577
636
|
message: `Socket is already registered as ${socket.__role}.`,
|
|
578
637
|
},
|
|
579
|
-
});
|
|
638
|
+
}).catch(() => socket.destroy());
|
|
580
639
|
return;
|
|
581
640
|
}
|
|
582
641
|
|
|
583
|
-
if (
|
|
584
|
-
this.logger.error('socket registration rejected', { role: message.role ?? null });
|
|
642
|
+
if (message.role !== 'extension' && message.role !== 'agent') {
|
|
585
643
|
void writeJsonLine(socket, {
|
|
586
644
|
type: 'registration_failed',
|
|
587
645
|
error: {
|
|
588
|
-
code: ERROR_CODES.
|
|
589
|
-
message: '
|
|
646
|
+
code: ERROR_CODES.INVALID_REQUEST,
|
|
647
|
+
message: 'Socket role must be "agent" or "extension".',
|
|
590
648
|
},
|
|
591
|
-
}).
|
|
649
|
+
}).catch(() => socket.destroy());
|
|
592
650
|
return;
|
|
593
651
|
}
|
|
594
652
|
|
|
595
|
-
|
|
653
|
+
const expectedToken =
|
|
654
|
+
message.role === 'extension'
|
|
655
|
+
? this.extensionAuthToken
|
|
656
|
+
: this.isAuthRequired()
|
|
657
|
+
? this.authToken
|
|
658
|
+
: null;
|
|
659
|
+
if (expectedToken && !bridgeAuthTokensEqual(message.authToken, expectedToken)) {
|
|
660
|
+
this.logger.error('socket registration rejected', { role: message.role ?? null });
|
|
596
661
|
void writeJsonLine(socket, {
|
|
597
662
|
type: 'registration_failed',
|
|
598
663
|
error: {
|
|
599
|
-
code: ERROR_CODES.
|
|
600
|
-
message: '
|
|
664
|
+
code: ERROR_CODES.ACCESS_DENIED,
|
|
665
|
+
message: 'Bridge daemon authentication failed.',
|
|
601
666
|
},
|
|
602
|
-
})
|
|
667
|
+
})
|
|
668
|
+
.catch(() => {})
|
|
669
|
+
.finally(() => socket.destroy());
|
|
603
670
|
return;
|
|
604
671
|
}
|
|
605
672
|
|
|
@@ -609,13 +676,16 @@ export class BridgeDaemon {
|
|
|
609
676
|
configurable: false,
|
|
610
677
|
writable: false,
|
|
611
678
|
});
|
|
679
|
+
if (socket.__registrationTimeoutId) {
|
|
680
|
+
clearTimeout(socket.__registrationTimeoutId);
|
|
681
|
+
socket.__registrationTimeoutId = undefined;
|
|
682
|
+
}
|
|
612
683
|
if (message.role === 'extension') {
|
|
684
|
+
socket.__localControl = true;
|
|
613
685
|
const extensionId = randomUUID();
|
|
614
686
|
socket.__extensionId = extensionId;
|
|
615
|
-
socket.__browserName =
|
|
616
|
-
|
|
617
|
-
socket.__profileLabel =
|
|
618
|
-
typeof message.profileLabel === 'string' ? message.profileLabel : undefined;
|
|
687
|
+
socket.__browserName = normalizeRoutingLabel(message.browserName);
|
|
688
|
+
socket.__profileLabel = normalizeRoutingLabel(message.profileLabel);
|
|
619
689
|
socket.__browserExtensionId = normalizeBrowserExtensionId(message.browserExtensionId);
|
|
620
690
|
socket.__lastActiveAt = Date.now();
|
|
621
691
|
this.extensionSockets.set(extensionId, socket);
|
|
@@ -625,12 +695,27 @@ export class BridgeDaemon {
|
|
|
625
695
|
browserName: socket.__browserName ?? null,
|
|
626
696
|
profileLabel: socket.__profileLabel ?? null,
|
|
627
697
|
});
|
|
628
|
-
void writeJsonLine(socket, { type: 'registered', role: 'extension' })
|
|
698
|
+
void writeJsonLine(socket, { type: 'registered', role: 'extension' }).catch(() =>
|
|
699
|
+
socket.destroy()
|
|
700
|
+
);
|
|
629
701
|
return;
|
|
630
702
|
}
|
|
631
703
|
|
|
632
704
|
if (message.role === 'agent') {
|
|
633
|
-
|
|
705
|
+
socket.__localControl = this.transport.type === 'socket';
|
|
706
|
+
const clientId = normalizeClientId(message.clientId);
|
|
707
|
+
if (!clientId) {
|
|
708
|
+
void writeJsonLine(socket, {
|
|
709
|
+
type: 'registration_failed',
|
|
710
|
+
error: {
|
|
711
|
+
code: ERROR_CODES.INVALID_REQUEST,
|
|
712
|
+
message: 'Agent clientId is invalid.',
|
|
713
|
+
},
|
|
714
|
+
})
|
|
715
|
+
.catch(() => {})
|
|
716
|
+
.finally(() => socket.destroy());
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
634
719
|
this.agentSockets.set(clientId, socket);
|
|
635
720
|
socket.__clientId = clientId;
|
|
636
721
|
this.logger.info('agent registered', { clientId });
|
|
@@ -638,7 +723,7 @@ export class BridgeDaemon {
|
|
|
638
723
|
type: 'registered',
|
|
639
724
|
role: 'agent',
|
|
640
725
|
clientId,
|
|
641
|
-
});
|
|
726
|
+
}).catch(() => socket.destroy());
|
|
642
727
|
return;
|
|
643
728
|
}
|
|
644
729
|
}
|
|
@@ -649,6 +734,10 @@ export class BridgeDaemon {
|
|
|
649
734
|
if (this.authToken === undefined) {
|
|
650
735
|
this.authToken = this.transport.type === 'tcp' ? await ensureBridgeAuthToken() : null;
|
|
651
736
|
}
|
|
737
|
+
if (this.extensionAuthToken === undefined) {
|
|
738
|
+
this.extensionAuthToken =
|
|
739
|
+
this.transport.type === 'tcp' ? await ensureBridgeExtensionAuthToken() : null;
|
|
740
|
+
}
|
|
652
741
|
|
|
653
742
|
if (this.transport.type === 'socket' && !isWindowsNamedPipePath(this.socketPath)) {
|
|
654
743
|
const socketDir = path.dirname(this.socketPath);
|
|
@@ -658,7 +747,7 @@ export class BridgeDaemon {
|
|
|
658
747
|
}
|
|
659
748
|
try {
|
|
660
749
|
await fs.promises.access(this.socketPath);
|
|
661
|
-
if (await
|
|
750
|
+
if (await hasLiveListener(this.transport)) {
|
|
662
751
|
throw new Error(
|
|
663
752
|
`Another daemon is already running on ${this.socketPath}. Stop it before starting a new one.`
|
|
664
753
|
);
|
|
@@ -666,38 +755,79 @@ export class BridgeDaemon {
|
|
|
666
755
|
this.logger.info('Removing stale socket from previous run', {
|
|
667
756
|
socketPath: this.socketPath,
|
|
668
757
|
});
|
|
758
|
+
await fs.promises.rm(this.socketPath, { force: true });
|
|
669
759
|
} catch (error) {
|
|
670
|
-
if (error
|
|
760
|
+
if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'ENOENT')
|
|
671
761
|
throw error;
|
|
672
|
-
}
|
|
673
762
|
// Socket does not exist - normal startup.
|
|
674
763
|
}
|
|
675
|
-
await fs.promises.rm(this.socketPath, { force: true });
|
|
676
764
|
}
|
|
677
765
|
|
|
678
766
|
this.server = net.createServer((socket) => {
|
|
679
767
|
const typedSocket = /** @type {ClientSocket} */ (socket);
|
|
768
|
+
this.clientSockets.add(typedSocket);
|
|
769
|
+
typedSocket.__invalidLineCount = 0;
|
|
770
|
+
typedSocket.__registrationTimeoutId = setTimeout(() => {
|
|
771
|
+
if (!typedSocket.__role) {
|
|
772
|
+
this.logger.warn('unregistered socket timed out');
|
|
773
|
+
typedSocket.destroy();
|
|
774
|
+
}
|
|
775
|
+
}, DAEMON_REGISTRATION_TIMEOUT_MS);
|
|
776
|
+
typedSocket.__registrationTimeoutId.unref?.();
|
|
680
777
|
typedSocket.on('error', (err) => {
|
|
681
778
|
this.logger.error('socket error', { message: err.message });
|
|
682
779
|
});
|
|
683
780
|
parseJsonLines(
|
|
684
781
|
typedSocket,
|
|
685
782
|
(raw) => {
|
|
783
|
+
if (typedSocket.destroyed) return;
|
|
686
784
|
const message = /** @type {DaemonMessage} */ (raw);
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
785
|
+
const admitted =
|
|
786
|
+
message?.type === 'agent.request' || message?.type === 'extension.setup_status.request';
|
|
787
|
+
const active = this.activeHandlers.get(typedSocket) ?? 0;
|
|
788
|
+
if (
|
|
789
|
+
admitted &&
|
|
790
|
+
(active >= MAX_DAEMON_ACTIVE_HANDLERS_PER_CLIENT ||
|
|
791
|
+
this.activeHandlerCount >= MAX_DAEMON_PENDING_REQUESTS)
|
|
792
|
+
) {
|
|
793
|
+
typedSocket.destroy();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (admitted) {
|
|
797
|
+
this.activeHandlers.set(typedSocket, active + 1);
|
|
798
|
+
this.activeHandlerCount += 1;
|
|
799
|
+
}
|
|
800
|
+
void this.handleClientMessage(typedSocket, message)
|
|
801
|
+
.catch((err) => {
|
|
802
|
+
this.logger.error('handler error', {
|
|
803
|
+
message: err instanceof Error ? err.message : String(err),
|
|
804
|
+
});
|
|
805
|
+
})
|
|
806
|
+
.finally(() => {
|
|
807
|
+
if (admitted) {
|
|
808
|
+
const remaining = (this.activeHandlers.get(typedSocket) ?? 1) - 1;
|
|
809
|
+
if (remaining) this.activeHandlers.set(typedSocket, remaining);
|
|
810
|
+
else this.activeHandlers.delete(typedSocket);
|
|
811
|
+
this.activeHandlerCount -= 1;
|
|
812
|
+
}
|
|
690
813
|
});
|
|
691
|
-
});
|
|
692
814
|
},
|
|
693
815
|
{
|
|
694
816
|
onProtocolError: (error) => {
|
|
695
817
|
this.logger.error('socket protocol error', { message: error.message });
|
|
696
818
|
},
|
|
819
|
+
onInvalidLine: () => {
|
|
820
|
+
typedSocket.__invalidLineCount = (typedSocket.__invalidLineCount ?? 0) + 1;
|
|
821
|
+
if (typedSocket.__invalidLineCount >= MAX_DAEMON_INVALID_LINES) {
|
|
822
|
+
this.logger.warn('socket sent too many malformed JSON lines');
|
|
823
|
+
typedSocket.destroy();
|
|
824
|
+
}
|
|
825
|
+
},
|
|
697
826
|
}
|
|
698
827
|
);
|
|
699
828
|
typedSocket.on('close', () => this.handleSocketClose(typedSocket));
|
|
700
829
|
});
|
|
830
|
+
this.server.maxConnections = MAX_DAEMON_CONNECTIONS;
|
|
701
831
|
|
|
702
832
|
const server = this.server;
|
|
703
833
|
await new Promise((resolve, reject) => {
|
|
@@ -753,6 +883,10 @@ export class BridgeDaemon {
|
|
|
753
883
|
this.pendingRequests.clear();
|
|
754
884
|
this.pendingRequestsByOwnerSocket.clear();
|
|
755
885
|
this.pendingRequestsByTargetSocket.clear();
|
|
886
|
+
this.requestStartTimes.clear();
|
|
887
|
+
|
|
888
|
+
for (const socket of this.clientSockets) socket.destroy();
|
|
889
|
+
this.clientSockets.clear();
|
|
756
890
|
|
|
757
891
|
for (const socket of this.agentSockets.values()) {
|
|
758
892
|
socket.destroy();
|
|
@@ -772,6 +906,7 @@ export class BridgeDaemon {
|
|
|
772
906
|
|
|
773
907
|
if (this.server) {
|
|
774
908
|
const server = this.server;
|
|
909
|
+
const ownedListener = server.listening;
|
|
775
910
|
this.server = null;
|
|
776
911
|
try {
|
|
777
912
|
await new Promise((resolve, reject) => {
|
|
@@ -784,7 +919,11 @@ export class BridgeDaemon {
|
|
|
784
919
|
});
|
|
785
920
|
});
|
|
786
921
|
} finally {
|
|
787
|
-
if (
|
|
922
|
+
if (
|
|
923
|
+
ownedListener &&
|
|
924
|
+
this.transport.type === 'socket' &&
|
|
925
|
+
!isWindowsNamedPipePath(this.socketPath)
|
|
926
|
+
) {
|
|
788
927
|
await fs.promises.rm(this.socketPath, { force: true });
|
|
789
928
|
}
|
|
790
929
|
}
|
|
@@ -1076,7 +1215,7 @@ export class BridgeDaemon {
|
|
|
1076
1215
|
}
|
|
1077
1216
|
|
|
1078
1217
|
if (request.method === 'setup.install') {
|
|
1079
|
-
if (socket.
|
|
1218
|
+
if (!socket.__localControl) {
|
|
1080
1219
|
const response = createFailure(
|
|
1081
1220
|
request.id,
|
|
1082
1221
|
ERROR_CODES.ACCESS_DENIED,
|
|
@@ -1115,6 +1254,26 @@ export class BridgeDaemon {
|
|
|
1115
1254
|
typeof request.meta?.target_profile === 'string' ? request.meta.target_profile : null;
|
|
1116
1255
|
const hasExplicitTarget = Boolean(targetBrowser || targetProfile);
|
|
1117
1256
|
|
|
1257
|
+
const ownerPendingCount = this.pendingRequestsByOwnerSocket.get(socket)?.size ?? 0;
|
|
1258
|
+
if (
|
|
1259
|
+
this.pendingRequests.size >= MAX_DAEMON_PENDING_REQUESTS ||
|
|
1260
|
+
ownerPendingCount >= MAX_DAEMON_PENDING_REQUESTS_PER_CLIENT
|
|
1261
|
+
) {
|
|
1262
|
+
const response = createFailure(
|
|
1263
|
+
request.id,
|
|
1264
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1265
|
+
'Daemon pending-request limit reached. Wait for existing requests to finish.',
|
|
1266
|
+
{
|
|
1267
|
+
maxPendingRequests: MAX_DAEMON_PENDING_REQUESTS,
|
|
1268
|
+
maxPendingRequestsPerClient: MAX_DAEMON_PENDING_REQUESTS_PER_CLIENT,
|
|
1269
|
+
},
|
|
1270
|
+
{ method: request.method }
|
|
1271
|
+
);
|
|
1272
|
+
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
1273
|
+
this.recordDaemonRequestOutcome(request, false);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1118
1277
|
const baselineId =
|
|
1119
1278
|
typeof request.params.baselineId === 'string' ? request.params.baselineId : null;
|
|
1120
1279
|
const usesBaselineOwner =
|
|
@@ -1209,7 +1368,7 @@ export class BridgeDaemon {
|
|
|
1209
1368
|
message: error instanceof Error ? error.message : String(error),
|
|
1210
1369
|
});
|
|
1211
1370
|
const pending = this.pendingRequests.get(request.id);
|
|
1212
|
-
if (!pending) {
|
|
1371
|
+
if (!pending || !pending.targets.has(target)) {
|
|
1213
1372
|
return;
|
|
1214
1373
|
}
|
|
1215
1374
|
this.removePendingTarget(request.id, pending, target);
|
|
@@ -1222,23 +1381,6 @@ export class BridgeDaemon {
|
|
|
1222
1381
|
}
|
|
1223
1382
|
}
|
|
1224
1383
|
|
|
1225
|
-
/**
|
|
1226
|
-
* @param {ClientSocket} socket
|
|
1227
|
-
* @returns {boolean}
|
|
1228
|
-
*/
|
|
1229
|
-
isLocalSocket(socket) {
|
|
1230
|
-
if (this.transport.type === 'socket') {
|
|
1231
|
-
return true;
|
|
1232
|
-
}
|
|
1233
|
-
const address = socket.remoteAddress ?? '';
|
|
1234
|
-
return (
|
|
1235
|
-
address === '127.0.0.1' ||
|
|
1236
|
-
address === '::1' ||
|
|
1237
|
-
address === 'localhost' ||
|
|
1238
|
-
address === '::ffff:127.0.0.1'
|
|
1239
|
-
);
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
1384
|
/**
|
|
1243
1385
|
* Select from a snapshot so routing neither mutates nor observes mutations to
|
|
1244
1386
|
* the live extension map while candidates are ordered.
|
|
@@ -1348,13 +1490,15 @@ export class BridgeDaemon {
|
|
|
1348
1490
|
*/
|
|
1349
1491
|
handleExtensionIdentity(socket, message) {
|
|
1350
1492
|
let changed = false;
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
socket.__browserName
|
|
1493
|
+
const browserName = normalizeRoutingLabel(message.browserName);
|
|
1494
|
+
if (browserName) {
|
|
1495
|
+
changed = changed || socket.__browserName !== browserName;
|
|
1496
|
+
socket.__browserName = browserName;
|
|
1354
1497
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
socket.__profileLabel
|
|
1498
|
+
const profileLabel = normalizeRoutingLabel(message.profileLabel);
|
|
1499
|
+
if (profileLabel) {
|
|
1500
|
+
changed = changed || socket.__profileLabel !== profileLabel;
|
|
1501
|
+
socket.__profileLabel = profileLabel;
|
|
1358
1502
|
}
|
|
1359
1503
|
if (typeof message.browserExtensionId === 'string') {
|
|
1360
1504
|
const browserExtensionId = normalizeBrowserExtensionId(message.browserExtensionId);
|
|
@@ -1399,6 +1543,20 @@ export class BridgeDaemon {
|
|
|
1399
1543
|
const ownerId = pending.socket.__clientId;
|
|
1400
1544
|
if (!ownerId) return;
|
|
1401
1545
|
try {
|
|
1546
|
+
if (
|
|
1547
|
+
message.type === 'extension.artifact.chunk' ||
|
|
1548
|
+
message.type === 'extension.artifact.commit'
|
|
1549
|
+
) {
|
|
1550
|
+
const transfer = this.artifactStore.transfers.get(String(message.artifactId ?? ''));
|
|
1551
|
+
if (
|
|
1552
|
+
!transfer ||
|
|
1553
|
+
transfer.requestId !== requestId ||
|
|
1554
|
+
transfer.ownerId !== ownerId ||
|
|
1555
|
+
transfer.extensionId !== socket.__extensionId
|
|
1556
|
+
) {
|
|
1557
|
+
throw new Error('Artifact transfer does not belong to this request and extension.');
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1402
1560
|
if (message.type === 'extension.artifact.begin') {
|
|
1403
1561
|
this.artifactStore.begin({
|
|
1404
1562
|
artifactId: String(artifact.artifactId ?? ''),
|
|
@@ -1457,12 +1615,11 @@ export class BridgeDaemon {
|
|
|
1457
1615
|
|
|
1458
1616
|
/**
|
|
1459
1617
|
* @param {ClientSocket} socket
|
|
1460
|
-
* @param {DaemonMessage}
|
|
1618
|
+
* @param {DaemonMessage} _message
|
|
1461
1619
|
* @returns {void}
|
|
1462
1620
|
*/
|
|
1463
|
-
handleExtensionActivity(socket,
|
|
1464
|
-
socket.__lastActiveAt =
|
|
1465
|
-
typeof message.at === 'number' && Number.isFinite(message.at) ? message.at : Date.now();
|
|
1621
|
+
handleExtensionActivity(socket, _message) {
|
|
1622
|
+
socket.__lastActiveAt = Date.now();
|
|
1466
1623
|
}
|
|
1467
1624
|
|
|
1468
1625
|
/**
|
|
@@ -1495,6 +1652,7 @@ export class BridgeDaemon {
|
|
|
1495
1652
|
}
|
|
1496
1653
|
|
|
1497
1654
|
this.removePendingTarget(responseMessage.id, pending, socket);
|
|
1655
|
+
clearTimeout(pending.timeoutId);
|
|
1498
1656
|
|
|
1499
1657
|
if (responseMessage.ok) {
|
|
1500
1658
|
const result =
|
|
@@ -1540,7 +1698,7 @@ export class BridgeDaemon {
|
|
|
1540
1698
|
}
|
|
1541
1699
|
}
|
|
1542
1700
|
if (!this.domBaselines.register(baselineId, socket, result.expiresAt)) {
|
|
1543
|
-
await this.releaseOrphanDomBaseline(socket, responseMessage);
|
|
1701
|
+
await this.releaseOrphanDomBaseline(socket, responseMessage).catch(() => {});
|
|
1544
1702
|
pending.lastErrorResponse = createFailure(
|
|
1545
1703
|
responseMessage.id,
|
|
1546
1704
|
ERROR_CODES.DOM_BASELINE_INVALIDATED,
|
|
@@ -1592,11 +1750,12 @@ export class BridgeDaemon {
|
|
|
1592
1750
|
if (baselineId) this.domBaselines.delete(baselineId);
|
|
1593
1751
|
await this.releaseOrphanDomBaseline(socket, responseMessage).catch(() => {});
|
|
1594
1752
|
}
|
|
1595
|
-
this.clearPendingRequest(responseMessage.id, pending)
|
|
1596
|
-
|
|
1753
|
+
if (this.clearPendingRequest(responseMessage.id, pending)) {
|
|
1754
|
+
this.recordRequestCompletion(responseMessage.id, false, pending);
|
|
1755
|
+
}
|
|
1597
1756
|
throw error;
|
|
1598
1757
|
}
|
|
1599
|
-
this.clearPendingRequest(responseMessage.id, pending, true);
|
|
1758
|
+
if (!this.clearPendingRequest(responseMessage.id, pending, true)) return;
|
|
1600
1759
|
this.recordRequestCompletion(responseMessage.id, true, pending);
|
|
1601
1760
|
this.pushLog({
|
|
1602
1761
|
at: new Date().toISOString(),
|
|
@@ -1623,6 +1782,11 @@ export class BridgeDaemon {
|
|
|
1623
1782
|
* @returns {void}
|
|
1624
1783
|
*/
|
|
1625
1784
|
handleSocketClose(socket) {
|
|
1785
|
+
this.clientSockets.delete(socket);
|
|
1786
|
+
if (socket.__registrationTimeoutId) {
|
|
1787
|
+
clearTimeout(socket.__registrationTimeoutId);
|
|
1788
|
+
socket.__registrationTimeoutId = undefined;
|
|
1789
|
+
}
|
|
1626
1790
|
if (socket.__extensionId) {
|
|
1627
1791
|
this.artifactStore.deleteByExtension(socket.__extensionId);
|
|
1628
1792
|
this.domBaselines.clearForSocket(socket);
|
|
@@ -1681,7 +1845,7 @@ export class BridgeDaemon {
|
|
|
1681
1845
|
* @returns {Promise<void>}
|
|
1682
1846
|
*/
|
|
1683
1847
|
async finishPendingRequestIfExhausted(requestId, pending) {
|
|
1684
|
-
if (pending.targets.size > 0 ||
|
|
1848
|
+
if (pending.targets.size > 0 || this.pendingRequests.get(requestId) !== pending) {
|
|
1685
1849
|
return;
|
|
1686
1850
|
}
|
|
1687
1851
|
|
|
@@ -1875,6 +2039,37 @@ export async function pingExistingDaemon(transport) {
|
|
|
1875
2039
|
});
|
|
1876
2040
|
}
|
|
1877
2041
|
|
|
2042
|
+
/**
|
|
2043
|
+
* Only a refused or missing endpoint proves a Unix socket is stale.
|
|
2044
|
+
* @param {BridgeTransport} transport
|
|
2045
|
+
* @returns {Promise<boolean>}
|
|
2046
|
+
*/
|
|
2047
|
+
export function hasLiveListener(transport) {
|
|
2048
|
+
return new Promise((resolve, reject) => {
|
|
2049
|
+
const socket =
|
|
2050
|
+
transport.type === 'tcp'
|
|
2051
|
+
? net.createConnection({ host: transport.host, port: transport.port })
|
|
2052
|
+
: net.createConnection(transport.socketPath);
|
|
2053
|
+
const timer = setTimeout(() => finish(true), DAEMON_EXISTING_SOCKET_PING_TIMEOUT_MS);
|
|
2054
|
+
/** @param {boolean} live @param {Error} [error] */
|
|
2055
|
+
function finish(live, error) {
|
|
2056
|
+
clearTimeout(timer);
|
|
2057
|
+
socket.destroy();
|
|
2058
|
+
if (error) reject(error);
|
|
2059
|
+
else resolve(live);
|
|
2060
|
+
}
|
|
2061
|
+
socket.once('connect', () => finish(true));
|
|
2062
|
+
socket.once('error', (error) => {
|
|
2063
|
+
if (
|
|
2064
|
+
'code' in error &&
|
|
2065
|
+
(error.code === 'ENOENT' || error.code === 'ECONNREFUSED' || error.code === 'ENOTSOCK')
|
|
2066
|
+
)
|
|
2067
|
+
finish(false);
|
|
2068
|
+
else finish(true, error);
|
|
2069
|
+
});
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
|
|
1878
2073
|
/**
|
|
1879
2074
|
* @param {Record<string, unknown>} params
|
|
1880
2075
|
* @returns {SetupInstallParams & { action: 'install' | 'uninstall', kind: 'mcp' | 'skill', target: string }}
|