@browserbridge/bbx 1.7.2 → 1.7.4
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/package.json +10 -9
- package/packages/agent-client/src/cli.js +8 -2
- package/packages/agent-client/src/mcp-config.js +55 -55
- package/packages/agent-client/src/runtime.js +20 -1
- package/packages/mcp-server/src/guidance.js +4 -4
- package/packages/mcp-server/src/handlers-capture.js +221 -30
- package/packages/mcp-server/src/handlers-navigation.js +10 -0
- package/packages/mcp-server/src/handlers-page.js +154 -67
- package/packages/mcp-server/src/handlers-utils.js +27 -68
- package/packages/mcp-server/src/handlers.js +2 -0
- package/packages/mcp-server/src/server.js +38 -13
- package/packages/native-host/src/daemon-process.js +16 -8
- package/packages/native-host/src/daemon.js +201 -79
- package/packages/protocol/src/capabilities.js +1 -1
- package/packages/protocol/src/defaults.js +17 -0
- package/packages/protocol/src/protocol.js +60 -5
- package/packages/protocol/src/registry.js +15 -15
- package/packages/protocol/src/summary.js +61 -30
- package/packages/protocol/src/types.ts +11 -0
- package/skills/browser-bridge/SKILL.md +15 -11
- package/skills/browser-bridge/references/interaction.md +11 -8
- package/skills/browser-bridge/references/protocol.md +9 -5
- package/skills/browser-bridge/references/token-efficiency.md +5 -4
- package/skills/browser-bridge/references/ui-workflows.md +11 -9
|
@@ -20,12 +20,14 @@ import {
|
|
|
20
20
|
createFailure,
|
|
21
21
|
createSuccess,
|
|
22
22
|
DAEMON_EXISTING_SOCKET_PING_TIMEOUT_MS,
|
|
23
|
+
DAEMON_PENDING_TIMEOUT_MARGIN_MS,
|
|
23
24
|
DAEMON_RECENT_LOG_LIMIT,
|
|
24
25
|
DEFAULT_DAEMON_PENDING_TIMEOUT_MS,
|
|
25
26
|
DEFAULT_LOG_TAIL_LIMIT,
|
|
26
27
|
ERROR_CODES,
|
|
27
28
|
getProtocolVersion,
|
|
28
29
|
getSupportedProtocolVersions,
|
|
30
|
+
MAX_DAEMON_PENDING_TIMEOUT_MS,
|
|
29
31
|
parseJsonLines,
|
|
30
32
|
setProtocolPackageVersion,
|
|
31
33
|
validateBridgeRequest,
|
|
@@ -54,7 +56,8 @@ setProtocolPackageVersion(DAEMON_VERSION);
|
|
|
54
56
|
/** @typedef {import('../../protocol/src/types.js').SetupStatus} SetupStatus */
|
|
55
57
|
/** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
|
|
56
58
|
/** @typedef {import('./daemon-logger.js').DaemonLoggerLike} DaemonLoggerLike */
|
|
57
|
-
/** @typedef {
|
|
59
|
+
/** @typedef {'agent' | 'extension'} SocketRole */
|
|
60
|
+
/** @typedef {import('node:net').Socket & { readonly __role?: SocketRole, __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
|
|
58
61
|
/** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, protocolVersion?: string, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
|
|
59
62
|
/**
|
|
60
63
|
* @typedef {{
|
|
@@ -327,6 +330,17 @@ export class BridgeDaemon {
|
|
|
327
330
|
* @returns {void}
|
|
328
331
|
*/
|
|
329
332
|
registerSocket(socket, message) {
|
|
333
|
+
if (socket.__role) {
|
|
334
|
+
void writeJsonLine(socket, {
|
|
335
|
+
type: 'registration_failed',
|
|
336
|
+
error: {
|
|
337
|
+
code: ERROR_CODES.INVALID_REQUEST,
|
|
338
|
+
message: `Socket is already registered as ${socket.__role}.`,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
330
344
|
if (this.isAuthRequired() && normalizeBridgeAuthToken(message.authToken) !== this.authToken) {
|
|
331
345
|
this.logger.error('socket registration rejected', { role: message.role ?? null });
|
|
332
346
|
void writeJsonLine(socket, {
|
|
@@ -339,8 +353,24 @@ export class BridgeDaemon {
|
|
|
339
353
|
return;
|
|
340
354
|
}
|
|
341
355
|
|
|
356
|
+
if (message.role !== 'extension' && message.role !== 'agent') {
|
|
357
|
+
void writeJsonLine(socket, {
|
|
358
|
+
type: 'registration_failed',
|
|
359
|
+
error: {
|
|
360
|
+
code: ERROR_CODES.INVALID_REQUEST,
|
|
361
|
+
message: 'Socket role must be "agent" or "extension".',
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
Object.defineProperty(socket, '__role', {
|
|
368
|
+
value: message.role,
|
|
369
|
+
enumerable: false,
|
|
370
|
+
configurable: false,
|
|
371
|
+
writable: false,
|
|
372
|
+
});
|
|
342
373
|
if (message.role === 'extension') {
|
|
343
|
-
socket.__authenticated = true;
|
|
344
374
|
const extensionId = randomUUID();
|
|
345
375
|
socket.__extensionId = extensionId;
|
|
346
376
|
socket.__browserName =
|
|
@@ -360,7 +390,6 @@ export class BridgeDaemon {
|
|
|
360
390
|
}
|
|
361
391
|
|
|
362
392
|
if (message.role === 'agent') {
|
|
363
|
-
socket.__authenticated = true;
|
|
364
393
|
const clientId = message.clientId || randomUUID();
|
|
365
394
|
this.agentSockets.set(clientId, socket);
|
|
366
395
|
socket.__clientId = clientId;
|
|
@@ -524,36 +553,61 @@ export class BridgeDaemon {
|
|
|
524
553
|
return this.registerSocket(socket, message);
|
|
525
554
|
}
|
|
526
555
|
|
|
527
|
-
if (
|
|
528
|
-
return this.
|
|
556
|
+
if (!socket.__role) {
|
|
557
|
+
return this.rejectUnregisteredMessage(socket, message);
|
|
529
558
|
}
|
|
530
559
|
|
|
531
560
|
if (message?.type === 'log') {
|
|
561
|
+
if (socket.__role !== 'extension') {
|
|
562
|
+
return this.rejectMessageForRole(socket, message);
|
|
563
|
+
}
|
|
532
564
|
this.pushLog(message.entry ?? {});
|
|
533
565
|
return;
|
|
534
566
|
}
|
|
535
567
|
|
|
536
568
|
if (message?.type === 'extension.response') {
|
|
569
|
+
if (socket.__role !== 'extension') {
|
|
570
|
+
return this.rejectMessageForRole(socket, message);
|
|
571
|
+
}
|
|
537
572
|
return this.handleExtensionResponse(socket, message);
|
|
538
573
|
}
|
|
539
574
|
|
|
540
575
|
if (message?.type === 'extension.identity') {
|
|
576
|
+
if (socket.__role !== 'extension') {
|
|
577
|
+
return this.rejectMessageForRole(socket, message);
|
|
578
|
+
}
|
|
541
579
|
return this.handleExtensionIdentity(socket, message);
|
|
542
580
|
}
|
|
543
581
|
|
|
544
582
|
if (message?.type === 'extension.access_update') {
|
|
583
|
+
if (socket.__role !== 'extension') {
|
|
584
|
+
return this.rejectMessageForRole(socket, message);
|
|
585
|
+
}
|
|
545
586
|
return this.handleExtensionAccessUpdate(socket, message);
|
|
546
587
|
}
|
|
547
588
|
|
|
548
589
|
if (message?.type === 'extension.activity') {
|
|
590
|
+
if (socket.__role !== 'extension') {
|
|
591
|
+
return this.rejectMessageForRole(socket, message);
|
|
592
|
+
}
|
|
549
593
|
return this.handleExtensionActivity(socket, message);
|
|
550
594
|
}
|
|
551
595
|
|
|
552
596
|
if (message?.type === 'extension.setup_status.request') {
|
|
597
|
+
if (socket.__role !== 'extension') {
|
|
598
|
+
return this.rejectMessageForRole(socket, message);
|
|
599
|
+
}
|
|
553
600
|
return this.handleExtensionSetupStatus(socket, message);
|
|
554
601
|
}
|
|
555
602
|
|
|
556
603
|
if (message?.type === 'agent.request') {
|
|
604
|
+
const isExtensionSetupRequest =
|
|
605
|
+
socket.__role === 'extension' &&
|
|
606
|
+
(message.request?.method === 'setup.get_status' ||
|
|
607
|
+
message.request?.method === 'setup.install');
|
|
608
|
+
if (socket.__role !== 'agent' && !isExtensionSetupRequest) {
|
|
609
|
+
return this.rejectMessageForRole(socket, message);
|
|
610
|
+
}
|
|
557
611
|
return this.handleAgentRequest(socket, message);
|
|
558
612
|
}
|
|
559
613
|
|
|
@@ -571,7 +625,8 @@ export class BridgeDaemon {
|
|
|
571
625
|
* @param {DaemonMessage} message
|
|
572
626
|
* @returns {Promise<void>}
|
|
573
627
|
*/
|
|
574
|
-
async
|
|
628
|
+
async rejectUnregisteredMessage(socket, message) {
|
|
629
|
+
const authRequired = this.isAuthRequired();
|
|
575
630
|
if (message?.type === 'agent.request') {
|
|
576
631
|
const candidate =
|
|
577
632
|
message.request && typeof message.request === 'object'
|
|
@@ -579,8 +634,10 @@ export class BridgeDaemon {
|
|
|
579
634
|
: {};
|
|
580
635
|
const response = createFailure(
|
|
581
636
|
typeof candidate.id === 'string' && candidate.id.trim() ? candidate.id : 'unauthenticated',
|
|
582
|
-
ERROR_CODES.ACCESS_DENIED,
|
|
583
|
-
|
|
637
|
+
authRequired ? ERROR_CODES.ACCESS_DENIED : ERROR_CODES.INVALID_REQUEST,
|
|
638
|
+
authRequired
|
|
639
|
+
? 'Register with the daemon auth token before sending bridge requests.'
|
|
640
|
+
: 'Register the socket before sending bridge requests.',
|
|
584
641
|
null,
|
|
585
642
|
typeof candidate.method === 'string' ? { method: candidate.method } : {}
|
|
586
643
|
);
|
|
@@ -591,8 +648,25 @@ export class BridgeDaemon {
|
|
|
591
648
|
await writeJsonLine(socket, {
|
|
592
649
|
type: 'error',
|
|
593
650
|
error: {
|
|
594
|
-
code: ERROR_CODES.ACCESS_DENIED,
|
|
595
|
-
message:
|
|
651
|
+
code: authRequired ? ERROR_CODES.ACCESS_DENIED : ERROR_CODES.INVALID_REQUEST,
|
|
652
|
+
message: authRequired
|
|
653
|
+
? 'Register with the daemon auth token before sending bridge messages.'
|
|
654
|
+
: 'Register the socket before sending bridge messages.',
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* @param {ClientSocket} socket
|
|
661
|
+
* @param {DaemonMessage} message
|
|
662
|
+
* @returns {Promise<void>}
|
|
663
|
+
*/
|
|
664
|
+
async rejectMessageForRole(socket, message) {
|
|
665
|
+
await writeJsonLine(socket, {
|
|
666
|
+
type: 'error',
|
|
667
|
+
error: {
|
|
668
|
+
code: ERROR_CODES.INVALID_REQUEST,
|
|
669
|
+
message: `Message type ${JSON.stringify(message.type ?? null)} is not allowed for ${socket.__role} sockets.`,
|
|
596
670
|
},
|
|
597
671
|
});
|
|
598
672
|
}
|
|
@@ -690,6 +764,17 @@ export class BridgeDaemon {
|
|
|
690
764
|
}
|
|
691
765
|
|
|
692
766
|
if (request.method === 'setup.install') {
|
|
767
|
+
if (socket.__role === 'agent' && !this.isLocalSocket(socket)) {
|
|
768
|
+
const response = createFailure(
|
|
769
|
+
request.id,
|
|
770
|
+
ERROR_CODES.ACCESS_DENIED,
|
|
771
|
+
'setup.install is restricted to local Browser Bridge clients.',
|
|
772
|
+
null,
|
|
773
|
+
{ method: request.method }
|
|
774
|
+
);
|
|
775
|
+
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
693
778
|
try {
|
|
694
779
|
const response = createSuccess(
|
|
695
780
|
request.id,
|
|
@@ -718,40 +803,9 @@ export class BridgeDaemon {
|
|
|
718
803
|
typeof request.meta?.target_profile === 'string' ? request.meta.target_profile : null;
|
|
719
804
|
const hasExplicitTarget = Boolean(targetBrowser || targetProfile);
|
|
720
805
|
|
|
721
|
-
|
|
722
|
-
const targets = [];
|
|
723
|
-
/** @type {ClientSocket | null} */
|
|
724
|
-
let mostRecent = null;
|
|
725
|
-
for (const extSocket of this.extensionSockets.values()) {
|
|
726
|
-
if (targetBrowser || targetProfile) {
|
|
727
|
-
if (targetBrowser && extSocket.__browserName !== targetBrowser) {
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
if (targetProfile && extSocket.__profileLabel !== targetProfile) {
|
|
731
|
-
continue;
|
|
732
|
-
}
|
|
733
|
-
targets.push(extSocket);
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
if (extSocket.__accessEnabled) {
|
|
738
|
-
targets.push(extSocket);
|
|
739
|
-
continue;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
if (
|
|
743
|
-
!mostRecent ||
|
|
744
|
-
(typeof extSocket.__lastActiveAt === 'number' ? extSocket.__lastActiveAt : 0) >
|
|
745
|
-
(typeof mostRecent.__lastActiveAt === 'number' ? mostRecent.__lastActiveAt : 0)
|
|
746
|
-
) {
|
|
747
|
-
mostRecent = extSocket;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
if (!hasExplicitTarget && targets.length === 0 && mostRecent) {
|
|
751
|
-
targets.push(mostRecent);
|
|
752
|
-
}
|
|
806
|
+
const target = this.selectExtensionTarget(targetBrowser, targetProfile);
|
|
753
807
|
|
|
754
|
-
if (
|
|
808
|
+
if (!target) {
|
|
755
809
|
const response = createFailure(
|
|
756
810
|
request.id,
|
|
757
811
|
ERROR_CODES.EXTENSION_DISCONNECTED,
|
|
@@ -768,7 +822,7 @@ export class BridgeDaemon {
|
|
|
768
822
|
method: request.method,
|
|
769
823
|
protocolVersion: request.meta?.protocol_version,
|
|
770
824
|
source: typeof request.meta?.source === 'string' ? request.meta.source : '',
|
|
771
|
-
targets: new Set(
|
|
825
|
+
targets: new Set([target]),
|
|
772
826
|
timeoutId: setTimeout(() => {
|
|
773
827
|
const pending = this.pendingRequests.get(request.id);
|
|
774
828
|
if (!pending) return;
|
|
@@ -789,38 +843,101 @@ export class BridgeDaemon {
|
|
|
789
843
|
message: error instanceof Error ? error.message : String(error),
|
|
790
844
|
});
|
|
791
845
|
});
|
|
792
|
-
}, this.
|
|
846
|
+
}, this.getPendingTimeoutMs(request)),
|
|
793
847
|
});
|
|
794
848
|
this.logger.info('request routed', {
|
|
795
849
|
requestId: request.id,
|
|
796
850
|
method: request.method,
|
|
797
851
|
clientId: socket.__clientId ?? null,
|
|
798
|
-
targetCount:
|
|
852
|
+
targetCount: 1,
|
|
799
853
|
});
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
854
|
+
try {
|
|
855
|
+
await writeJsonLine(target, { type: 'extension.request', request });
|
|
856
|
+
} catch (error) {
|
|
857
|
+
this.logger.error('request route failed', {
|
|
858
|
+
requestId: request.id,
|
|
859
|
+
method: request.method,
|
|
860
|
+
message: error instanceof Error ? error.message : String(error),
|
|
861
|
+
});
|
|
862
|
+
const pending = this.pendingRequests.get(request.id);
|
|
863
|
+
if (!pending) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
this.removePendingTarget(request.id, pending, target);
|
|
867
|
+
if (target.__extensionId && this.extensionSockets.get(target.__extensionId) === target) {
|
|
868
|
+
this.extensionSockets.delete(target.__extensionId);
|
|
869
|
+
this.invalidateConnectedExtensionsCache();
|
|
870
|
+
}
|
|
871
|
+
target.destroy(error instanceof Error ? error : undefined);
|
|
872
|
+
await this.finishPendingRequestIfExhausted(request.id, pending);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* @param {ClientSocket} socket
|
|
878
|
+
* @returns {boolean}
|
|
879
|
+
*/
|
|
880
|
+
isLocalSocket(socket) {
|
|
881
|
+
if (this.transport.type === 'socket') {
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
const address = socket.remoteAddress ?? '';
|
|
885
|
+
return (
|
|
886
|
+
address === '127.0.0.1' ||
|
|
887
|
+
address === '::1' ||
|
|
888
|
+
address === 'localhost' ||
|
|
889
|
+
address === '::ffff:127.0.0.1'
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Select from a snapshot so routing neither mutates nor observes mutations to
|
|
895
|
+
* the live extension map while candidates are ordered.
|
|
896
|
+
*
|
|
897
|
+
* @param {string | null} targetBrowser
|
|
898
|
+
* @param {string | null} targetProfile
|
|
899
|
+
* @returns {ClientSocket | null}
|
|
900
|
+
*/
|
|
901
|
+
selectExtensionTarget(targetBrowser, targetProfile) {
|
|
902
|
+
const candidates = Array.from(this.extensionSockets.entries()).filter(([, extSocket]) => {
|
|
903
|
+
return (
|
|
904
|
+
(!targetBrowser || extSocket.__browserName === targetBrowser) &&
|
|
905
|
+
(!targetProfile || extSocket.__profileLabel === targetProfile)
|
|
906
|
+
);
|
|
907
|
+
});
|
|
908
|
+
candidates.sort(([leftId, left], [rightId, right]) => {
|
|
909
|
+
const accessDelta =
|
|
910
|
+
Number(Boolean(right.__accessEnabled)) - Number(Boolean(left.__accessEnabled));
|
|
911
|
+
if (accessDelta !== 0) {
|
|
912
|
+
return accessDelta;
|
|
913
|
+
}
|
|
914
|
+
const activityDelta =
|
|
915
|
+
(typeof right.__lastActiveAt === 'number' ? right.__lastActiveAt : 0) -
|
|
916
|
+
(typeof left.__lastActiveAt === 'number' ? left.__lastActiveAt : 0);
|
|
917
|
+
if (activityDelta !== 0) {
|
|
918
|
+
return activityDelta;
|
|
919
|
+
}
|
|
920
|
+
return leftId < rightId ? -1 : leftId > rightId ? 1 : 0;
|
|
921
|
+
});
|
|
922
|
+
return candidates[0]?.[1] ?? null;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* @param {BridgeRequest} request
|
|
927
|
+
* @returns {number}
|
|
928
|
+
*/
|
|
929
|
+
getPendingTimeoutMs(request) {
|
|
930
|
+
const configuredBase = Math.min(
|
|
931
|
+
MAX_DAEMON_PENDING_TIMEOUT_MS,
|
|
932
|
+
Math.max(1, this.pendingTimeoutMs)
|
|
933
|
+
);
|
|
934
|
+
const operationTimeout = request.params.timeoutMs;
|
|
935
|
+
if (typeof operationTimeout !== 'number' || !Number.isFinite(operationTimeout)) {
|
|
936
|
+
return configuredBase;
|
|
937
|
+
}
|
|
938
|
+
return Math.min(
|
|
939
|
+
MAX_DAEMON_PENDING_TIMEOUT_MS,
|
|
940
|
+
Math.max(configuredBase, operationTimeout + DAEMON_PENDING_TIMEOUT_MARGIN_MS)
|
|
824
941
|
);
|
|
825
942
|
}
|
|
826
943
|
|
|
@@ -922,7 +1039,7 @@ export class BridgeDaemon {
|
|
|
922
1039
|
}
|
|
923
1040
|
|
|
924
1041
|
const pending = this.pendingRequests.get(responseMessage.id);
|
|
925
|
-
if (!pending) {
|
|
1042
|
+
if (!pending || !pending.targets.has(socket)) {
|
|
926
1043
|
return;
|
|
927
1044
|
}
|
|
928
1045
|
|
|
@@ -968,7 +1085,7 @@ export class BridgeDaemon {
|
|
|
968
1085
|
return;
|
|
969
1086
|
}
|
|
970
1087
|
|
|
971
|
-
//
|
|
1088
|
+
// A routed request has one target, so its error is final.
|
|
972
1089
|
pending.lastErrorResponse = responseMessage;
|
|
973
1090
|
|
|
974
1091
|
await this.finishPendingRequestIfExhausted(responseMessage.id, pending);
|
|
@@ -981,8 +1098,10 @@ export class BridgeDaemon {
|
|
|
981
1098
|
handleSocketClose(socket) {
|
|
982
1099
|
if (socket.__extensionId) {
|
|
983
1100
|
this.logger.info('extension disconnected', { extensionId: socket.__extensionId });
|
|
984
|
-
this.extensionSockets.
|
|
985
|
-
|
|
1101
|
+
if (this.extensionSockets.get(socket.__extensionId) === socket) {
|
|
1102
|
+
this.extensionSockets.delete(socket.__extensionId);
|
|
1103
|
+
this.invalidateConnectedExtensionsCache();
|
|
1104
|
+
}
|
|
986
1105
|
}
|
|
987
1106
|
|
|
988
1107
|
if (socket.__clientId) {
|
|
@@ -1151,11 +1270,14 @@ export async function pingExistingDaemon(transport) {
|
|
|
1151
1270
|
});
|
|
1152
1271
|
|
|
1153
1272
|
socket.once('connect', () => {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1273
|
+
socket.write(
|
|
1274
|
+
`${JSON.stringify({
|
|
1275
|
+
type: 'register',
|
|
1276
|
+
role: 'agent',
|
|
1277
|
+
clientId: 'ping_probe',
|
|
1278
|
+
...(authToken ? { authToken } : {}),
|
|
1279
|
+
})}\n`
|
|
1280
|
+
);
|
|
1159
1281
|
socket.write(
|
|
1160
1282
|
`${JSON.stringify({
|
|
1161
1283
|
type: 'agent.request',
|
|
@@ -64,7 +64,7 @@ export const METHOD_CAPABILITIES = Object.freeze({
|
|
|
64
64
|
'page.get_storage': CAPABILITIES.PAGE_READ,
|
|
65
65
|
'page.get_text': CAPABILITIES.PAGE_READ,
|
|
66
66
|
'page.get_network': CAPABILITIES.NETWORK_READ,
|
|
67
|
-
// Interception mutates traffic (block/forge responses)
|
|
67
|
+
// Interception mutates traffic (block/forge responses) - distinct from
|
|
68
68
|
// passive network reads so hosts can gate it independently.
|
|
69
69
|
'network.intercept.add': CAPABILITIES.NETWORK_INTERCEPT,
|
|
70
70
|
'network.intercept.remove': CAPABILITIES.NETWORK_INTERCEPT,
|
|
@@ -30,9 +30,21 @@ export const MAX_JSON_LINE_BYTES = MAX_NATIVE_MESSAGE_BYTES;
|
|
|
30
30
|
/** Default timeout for a bridge request awaiting an extension response (ms). */
|
|
31
31
|
export const DEFAULT_DAEMON_PENDING_TIMEOUT_MS = 30_000;
|
|
32
32
|
|
|
33
|
+
/** Transport allowance added after an operation's own normalized timeout. */
|
|
34
|
+
export const DAEMON_PENDING_TIMEOUT_MARGIN_MS = 2_000;
|
|
35
|
+
|
|
36
|
+
/** Upper bound for a daemon request awaiting an extension response. */
|
|
37
|
+
export const MAX_DAEMON_PENDING_TIMEOUT_MS = 122_000;
|
|
38
|
+
|
|
33
39
|
/** Default timeout for a BridgeClient request (ms). */
|
|
34
40
|
export const DEFAULT_CLIENT_REQUEST_TIMEOUT_MS = 8_000;
|
|
35
41
|
|
|
42
|
+
/** Transport allowance added after an operation timeout on the client side. */
|
|
43
|
+
export const CLIENT_REQUEST_TIMEOUT_MARGIN_MS = 4_000;
|
|
44
|
+
|
|
45
|
+
/** Upper bound for a client request, kept beyond the daemon's maximum deadline. */
|
|
46
|
+
export const MAX_CLIENT_REQUEST_TIMEOUT_MS = 124_000;
|
|
47
|
+
|
|
36
48
|
/** Maximum number of recent log entries retained by the daemon. */
|
|
37
49
|
export const DAEMON_RECENT_LOG_LIMIT = 200;
|
|
38
50
|
|
|
@@ -64,6 +76,11 @@ export const DEBUGGER_BACKED_METHODS = new Set([
|
|
|
64
76
|
'performance.get_metrics',
|
|
65
77
|
'screenshot.capture_element',
|
|
66
78
|
'screenshot.capture_region',
|
|
79
|
+
'screenshot.capture_full_page',
|
|
80
|
+
'network.intercept.add',
|
|
81
|
+
'network.intercept.remove',
|
|
82
|
+
'network.intercept.list',
|
|
83
|
+
'network.intercept.clear',
|
|
67
84
|
'cdp.get_document',
|
|
68
85
|
'cdp.get_dom_snapshot',
|
|
69
86
|
'cdp.get_box_model',
|
|
@@ -271,7 +271,23 @@ export function validateBridgeRequest(request) {
|
|
|
271
271
|
) {
|
|
272
272
|
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'Request params must be an object.');
|
|
273
273
|
}
|
|
274
|
-
const
|
|
274
|
+
const hasTabId = Object.hasOwn(candidate, 'tab_id');
|
|
275
|
+
const candidateTabId = candidate.tab_id;
|
|
276
|
+
/** @type {number | null} */
|
|
277
|
+
let tabId = null;
|
|
278
|
+
if (hasTabId && candidateTabId !== null) {
|
|
279
|
+
if (
|
|
280
|
+
typeof candidateTabId !== 'number' ||
|
|
281
|
+
!Number.isSafeInteger(candidateTabId) ||
|
|
282
|
+
candidateTabId <= 0
|
|
283
|
+
) {
|
|
284
|
+
throw new BridgeError(
|
|
285
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
286
|
+
'Request tab_id must be null or a positive safe integer.'
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
tabId = candidateTabId;
|
|
290
|
+
}
|
|
275
291
|
|
|
276
292
|
const method = /** @type {BridgeMethod} */ (candidate.method);
|
|
277
293
|
const params =
|
|
@@ -282,7 +298,7 @@ export function validateBridgeRequest(request) {
|
|
|
282
298
|
return {
|
|
283
299
|
id: candidate.id,
|
|
284
300
|
method,
|
|
285
|
-
tab_id:
|
|
301
|
+
tab_id: tabId,
|
|
286
302
|
params: normalizeRequestParams(method, params),
|
|
287
303
|
meta: {
|
|
288
304
|
...meta,
|
|
@@ -345,6 +361,7 @@ function normalizeRequestParams(method, params) {
|
|
|
345
361
|
case 'input.click':
|
|
346
362
|
case 'input.focus':
|
|
347
363
|
case 'input.type':
|
|
364
|
+
case 'input.fill':
|
|
348
365
|
case 'input.press_key':
|
|
349
366
|
return normalizeInputAction(params);
|
|
350
367
|
case 'cdp.dispatch_key_event':
|
|
@@ -375,16 +392,45 @@ function normalizeRequestParams(method, params) {
|
|
|
375
392
|
}
|
|
376
393
|
}
|
|
377
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Return the browser-operation timeout for methods that wait asynchronously.
|
|
397
|
+
* This lets clients and the daemon keep transport deadlines outside the
|
|
398
|
+
* normalized operation deadline, including when callers omit timeoutMs.
|
|
399
|
+
*
|
|
400
|
+
* @param {BridgeMethod} method
|
|
401
|
+
* @param {Record<string, unknown>} [params={}]
|
|
402
|
+
* @returns {number | null}
|
|
403
|
+
*/
|
|
404
|
+
export function getBridgeOperationTimeoutMs(method, params = {}) {
|
|
405
|
+
switch (method) {
|
|
406
|
+
case 'navigation.navigate':
|
|
407
|
+
case 'navigation.reload':
|
|
408
|
+
case 'navigation.go_back':
|
|
409
|
+
case 'navigation.go_forward':
|
|
410
|
+
return normalizeNavigationAction(params).timeoutMs;
|
|
411
|
+
case 'page.wait_for_load_state':
|
|
412
|
+
return normalizeWaitForLoadStateParams(params).timeoutMs;
|
|
413
|
+
case 'page.evaluate':
|
|
414
|
+
return normalizeEvaluateParams(params).timeoutMs;
|
|
415
|
+
case 'dom.wait_for':
|
|
416
|
+
return clampInt(params.timeoutMs, 100, 30_000, 5_000);
|
|
417
|
+
default:
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
378
422
|
/**
|
|
379
423
|
* @param {DomQueryParams} [params={}]
|
|
380
424
|
* @returns {NormalizedDomQuery}
|
|
381
425
|
*/
|
|
382
426
|
export function normalizeDomQuery(params = {}) {
|
|
427
|
+
const nestedBudget =
|
|
428
|
+
params.budget && typeof params.budget === 'object' ? params.budget : undefined;
|
|
383
429
|
return {
|
|
384
430
|
selector:
|
|
385
431
|
typeof params.selector === 'string' && params.selector.trim() ? params.selector : 'body',
|
|
386
432
|
withinRef: typeof params.withinRef === 'string' ? params.withinRef : null,
|
|
387
|
-
budget: applyBudget(params),
|
|
433
|
+
budget: applyBudget({ ...nestedBudget, ...params }),
|
|
388
434
|
};
|
|
389
435
|
}
|
|
390
436
|
|
|
@@ -663,9 +709,18 @@ export function normalizeConsoleParams(params = {}) {
|
|
|
663
709
|
*/
|
|
664
710
|
export function normalizeWaitForParams(params = {}) {
|
|
665
711
|
const validStates = ['attached', 'detached', 'visible', 'hidden'];
|
|
712
|
+
const selector =
|
|
713
|
+
typeof params.selector === 'string' && params.selector.trim() ? params.selector : '';
|
|
714
|
+
const text = typeof params.text === 'string' && params.text.trim() ? params.text : null;
|
|
715
|
+
if (!selector && text === null) {
|
|
716
|
+
throw new BridgeError(
|
|
717
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
718
|
+
'selector or text is required for dom.wait_for.'
|
|
719
|
+
);
|
|
720
|
+
}
|
|
666
721
|
return {
|
|
667
|
-
selector:
|
|
668
|
-
text
|
|
722
|
+
selector: selector || '*',
|
|
723
|
+
text,
|
|
669
724
|
state: validStates.includes(String(params.state ?? ''))
|
|
670
725
|
? /** @type {'attached' | 'detached' | 'visible' | 'hidden'} */ (String(params.state))
|
|
671
726
|
: 'attached',
|