@browserbridge/bbx 1.6.0 → 1.6.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/package.json +1 -1
- package/packages/agent-client/src/cli.js +264 -0
- package/packages/agent-client/src/command-registry.js +4 -0
- package/packages/agent-client/src/remotes.js +202 -0
- package/packages/mcp-server/src/handlers-capture.js +201 -198
- package/packages/mcp-server/src/handlers-navigation.js +81 -9
- package/packages/mcp-server/src/handlers-page.js +110 -88
- package/packages/mcp-server/src/handlers-utils.js +55 -27
- package/packages/mcp-server/src/handlers.js +52 -10
- package/packages/mcp-server/src/server.js +23 -6
- package/packages/native-host/src/auth-token.js +56 -1
- package/packages/native-host/src/config.js +122 -5
package/package.json
CHANGED
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
|
|
9
10
|
import {
|
|
10
11
|
applyWindowsTcpTransportDefaults,
|
|
12
|
+
getProxyConfigPath,
|
|
13
|
+
readProxyConfig,
|
|
11
14
|
SUPPORTED_BROWSERS,
|
|
12
15
|
} from '../../native-host/src/config.js';
|
|
16
|
+
import { writeBridgeAuthToken } from '../../native-host/src/auth-token.js';
|
|
13
17
|
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
14
18
|
import { uninstallNativeManifest } from '../../native-host/src/install-manifest.js';
|
|
15
19
|
import {
|
|
@@ -47,6 +51,15 @@ import {
|
|
|
47
51
|
removeMcpConfig,
|
|
48
52
|
} from './mcp-config.js';
|
|
49
53
|
import { getDoctorReport, requestBridge, resolveRef } from './runtime.js';
|
|
54
|
+
import {
|
|
55
|
+
addRemoteDestination,
|
|
56
|
+
createBridgeClientForDestination,
|
|
57
|
+
listBridgeDestinations,
|
|
58
|
+
normalizeDestinationId,
|
|
59
|
+
parseRemoteEndpoint,
|
|
60
|
+
readRemoteConfig,
|
|
61
|
+
removeRemoteDestination,
|
|
62
|
+
} from './remotes.js';
|
|
50
63
|
import { collectSetupStatus } from './setup-status.js';
|
|
51
64
|
import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
|
|
52
65
|
|
|
@@ -382,6 +395,16 @@ if (command === 'mcp') {
|
|
|
382
395
|
process.exit(1);
|
|
383
396
|
}
|
|
384
397
|
|
|
398
|
+
if (command === 'proxy') {
|
|
399
|
+
await handleProxyCommand(rest);
|
|
400
|
+
process.exit(0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (command === 'remote') {
|
|
404
|
+
await handleRemoteCommand(rest);
|
|
405
|
+
process.exit(0);
|
|
406
|
+
}
|
|
407
|
+
|
|
385
408
|
const clientTimeoutMs = getClientTimeoutOverride();
|
|
386
409
|
applyWindowsTcpTransportDefaults();
|
|
387
410
|
const client = new BridgeClient(
|
|
@@ -828,6 +851,247 @@ async function main() {
|
|
|
828
851
|
}
|
|
829
852
|
}
|
|
830
853
|
|
|
854
|
+
/**
|
|
855
|
+
* @param {string[]} args
|
|
856
|
+
* @returns {Promise<void>}
|
|
857
|
+
*/
|
|
858
|
+
async function handleProxyCommand(args) {
|
|
859
|
+
const [subcommand, ...restArgs] = args;
|
|
860
|
+
if (subcommand === 'enable') {
|
|
861
|
+
const options = parseProxyEnableArgs(restArgs);
|
|
862
|
+
const token = options.token ?? randomUUID();
|
|
863
|
+
await writeBridgeAuthToken(token);
|
|
864
|
+
const configPath = getProxyConfigPath();
|
|
865
|
+
await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
|
|
866
|
+
await fs.promises.writeFile(
|
|
867
|
+
configPath,
|
|
868
|
+
`${JSON.stringify(
|
|
869
|
+
{
|
|
870
|
+
enabled: true,
|
|
871
|
+
port: options.port,
|
|
872
|
+
bindHost: options.bindHost,
|
|
873
|
+
token,
|
|
874
|
+
},
|
|
875
|
+
null,
|
|
876
|
+
2
|
|
877
|
+
)}\n`,
|
|
878
|
+
{ encoding: 'utf8', mode: 0o600 }
|
|
879
|
+
);
|
|
880
|
+
const result = await restartBridgeDaemon();
|
|
881
|
+
const exampleHost = getProxyExampleHost(options.bindHost);
|
|
882
|
+
process.stdout.write(
|
|
883
|
+
[
|
|
884
|
+
`Browser Bridge proxy enabled on ${options.bindHost}:${options.port}.`,
|
|
885
|
+
'',
|
|
886
|
+
`Token: ${token}`,
|
|
887
|
+
`Config: ${configPath}`,
|
|
888
|
+
`Daemon: ${result.previouslyRunning ? 'restarted' : 'started'} (pid ${result.pid})`,
|
|
889
|
+
'',
|
|
890
|
+
'On your dev machine, add this remote:',
|
|
891
|
+
'',
|
|
892
|
+
`bbx remote add <name> <host:port> --token ${token}`,
|
|
893
|
+
'',
|
|
894
|
+
'Example:',
|
|
895
|
+
'',
|
|
896
|
+
`bbx remote add remote-bbx ${exampleHost}:${options.port} --token ${token}`,
|
|
897
|
+
'',
|
|
898
|
+
].join('\n')
|
|
899
|
+
);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (subcommand === 'disable') {
|
|
904
|
+
const configPath = getProxyConfigPath();
|
|
905
|
+
await fs.promises.rm(configPath, { force: true });
|
|
906
|
+
const result = await restartBridgeDaemon();
|
|
907
|
+
process.stdout.write(
|
|
908
|
+
`Browser Bridge proxy disabled. Daemon ${result.previouslyRunning ? 'restarted' : 'started'}.\n`
|
|
909
|
+
);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (subcommand === 'status') {
|
|
914
|
+
const config = readProxyConfig();
|
|
915
|
+
if (!config) {
|
|
916
|
+
process.stdout.write('Browser Bridge proxy is disabled.\n');
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
process.stdout.write(
|
|
920
|
+
[
|
|
921
|
+
`Browser Bridge proxy is enabled on ${config.bindHost}:${config.port}.`,
|
|
922
|
+
`Config: ${getProxyConfigPath()}`,
|
|
923
|
+
'',
|
|
924
|
+
].join('\n')
|
|
925
|
+
);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
throw new Error('Usage: bbx proxy <enable|disable|status>');
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* @param {string[]} args
|
|
934
|
+
* @returns {{ port: number, bindHost: string, token?: string }}
|
|
935
|
+
*/
|
|
936
|
+
function parseProxyEnableArgs(args) {
|
|
937
|
+
/** @type {{ port: number, bindHost: string, token?: string }} */
|
|
938
|
+
const options = { port: 9223, bindHost: '0.0.0.0' };
|
|
939
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
940
|
+
const arg = args[index];
|
|
941
|
+
if (arg === '--port') {
|
|
942
|
+
options.port = parseIntArg(args[++index] || '', 'port');
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
if (arg === '--bind-host') {
|
|
946
|
+
options.bindHost = args[++index] || '';
|
|
947
|
+
if (!options.bindHost.trim()) {
|
|
948
|
+
throw new Error('--bind-host requires a value.');
|
|
949
|
+
}
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (arg === '--token') {
|
|
953
|
+
options.token = args[++index] || '';
|
|
954
|
+
if (!options.token.trim()) {
|
|
955
|
+
throw new Error('--token requires a value.');
|
|
956
|
+
}
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
throw new Error(`Unknown proxy enable option "${arg}".`);
|
|
960
|
+
}
|
|
961
|
+
if (options.port < 1 || options.port > 65535) {
|
|
962
|
+
throw new Error('port must be an integer between 1 and 65535.');
|
|
963
|
+
}
|
|
964
|
+
return options;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* @param {string} bindHost
|
|
969
|
+
* @returns {string}
|
|
970
|
+
*/
|
|
971
|
+
function getProxyExampleHost(bindHost) {
|
|
972
|
+
if (bindHost !== '0.0.0.0' && bindHost !== '::') {
|
|
973
|
+
return bindHost;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
for (const entries of Object.values(os.networkInterfaces())) {
|
|
977
|
+
for (const entry of entries ?? []) {
|
|
978
|
+
if (!entry.internal && entry.family === 'IPv4') {
|
|
979
|
+
return entry.address;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return '<host>';
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* @param {string[]} args
|
|
989
|
+
* @returns {Promise<void>}
|
|
990
|
+
*/
|
|
991
|
+
async function handleRemoteCommand(args) {
|
|
992
|
+
const [subcommand, ...restArgs] = args;
|
|
993
|
+
if (subcommand === 'add') {
|
|
994
|
+
const [name, endpoint, ...optionArgs] = restArgs;
|
|
995
|
+
if (!name || !endpoint) {
|
|
996
|
+
throw new Error('Usage: bbx remote add <name> <host:port> --token <token>');
|
|
997
|
+
}
|
|
998
|
+
const token = parseRemoteTokenOption(optionArgs);
|
|
999
|
+
const { host, port } = parseRemoteEndpoint(endpoint);
|
|
1000
|
+
const remote = await addRemoteDestination({
|
|
1001
|
+
id: normalizeDestinationId(name),
|
|
1002
|
+
host,
|
|
1003
|
+
port,
|
|
1004
|
+
token,
|
|
1005
|
+
});
|
|
1006
|
+
process.stdout.write(
|
|
1007
|
+
`Remote destination "${remote.id}" saved (${remote.host}:${remote.port}).\n`
|
|
1008
|
+
);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (subcommand === 'remove') {
|
|
1013
|
+
const [name] = restArgs;
|
|
1014
|
+
if (!name) {
|
|
1015
|
+
throw new Error('Usage: bbx remote remove <name>');
|
|
1016
|
+
}
|
|
1017
|
+
const removed = await removeRemoteDestination(name);
|
|
1018
|
+
process.stdout.write(
|
|
1019
|
+
removed
|
|
1020
|
+
? `Remote destination "${name}" removed.\n`
|
|
1021
|
+
: `Remote destination "${name}" was not configured.\n`
|
|
1022
|
+
);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (subcommand === 'list') {
|
|
1027
|
+
const config = await readRemoteConfig();
|
|
1028
|
+
if (config.remotes.length === 0) {
|
|
1029
|
+
process.stdout.write('No remote destinations configured.\n');
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
process.stdout.write(
|
|
1033
|
+
`${config.remotes.map((remote) => `${remote.id}\t${remote.host}:${remote.port}`).join('\n')}\n`
|
|
1034
|
+
);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (subcommand === 'test') {
|
|
1039
|
+
const [name] = restArgs;
|
|
1040
|
+
if (!name) {
|
|
1041
|
+
throw new Error('Usage: bbx remote test <name>');
|
|
1042
|
+
}
|
|
1043
|
+
const remoteClient = await createBridgeClientForDestination(name);
|
|
1044
|
+
try {
|
|
1045
|
+
await remoteClient.connect();
|
|
1046
|
+
const response = await remoteClient.request({ method: 'health.ping' });
|
|
1047
|
+
process.stdout.write(
|
|
1048
|
+
response.ok
|
|
1049
|
+
? `Remote destination "${name}" is reachable.\n`
|
|
1050
|
+
: `Remote destination "${name}" failed: ${response.error.message}\n`
|
|
1051
|
+
);
|
|
1052
|
+
} finally {
|
|
1053
|
+
await remoteClient.close();
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (subcommand === 'destinations') {
|
|
1059
|
+
const destinations = await listBridgeDestinations();
|
|
1060
|
+
process.stdout.write(
|
|
1061
|
+
`${destinations
|
|
1062
|
+
.map((destination) =>
|
|
1063
|
+
destination.local
|
|
1064
|
+
? `${destination.id}\tlocal`
|
|
1065
|
+
: `${destination.id}\t${destination.host}:${destination.port}`
|
|
1066
|
+
)
|
|
1067
|
+
.join('\n')}\n`
|
|
1068
|
+
);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
throw new Error('Usage: bbx remote <add|remove|list|test|destinations>');
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* @param {string[]} args
|
|
1077
|
+
* @returns {string}
|
|
1078
|
+
*/
|
|
1079
|
+
function parseRemoteTokenOption(args) {
|
|
1080
|
+
let token = '';
|
|
1081
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1082
|
+
const arg = args[index];
|
|
1083
|
+
if (arg === '--token') {
|
|
1084
|
+
token = args[++index] || '';
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
throw new Error(`Unknown remote add option "${arg}".`);
|
|
1088
|
+
}
|
|
1089
|
+
if (!token.trim()) {
|
|
1090
|
+
throw new Error('Usage: bbx remote add <name> <host:port> --token <token>');
|
|
1091
|
+
}
|
|
1092
|
+
return token.trim();
|
|
1093
|
+
}
|
|
1094
|
+
|
|
831
1095
|
/**
|
|
832
1096
|
* @param {unknown} error
|
|
833
1097
|
* @returns {string}
|
|
@@ -257,6 +257,10 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
257
257
|
'bbx status Check bridge connection',
|
|
258
258
|
'bbx doctor Diagnose install, daemon, extension, and access readiness',
|
|
259
259
|
'bbx restart Restart the local Browser Bridge daemon',
|
|
260
|
+
'bbx proxy enable [--port 9223] [--bind-host 0.0.0.0] Enable opt-in TCP proxy mode and print a token',
|
|
261
|
+
'bbx proxy disable Disable proxy mode and restart the daemon',
|
|
262
|
+
'bbx remote add <name> <host:port> --token <token> Add a remote Browser Bridge destination (example: remote-bbx 192.168.56.20:9223)',
|
|
263
|
+
'bbx remote list List remote destinations',
|
|
260
264
|
'bbx access-request Request Browser Bridge access for the focused window',
|
|
261
265
|
'bbx logs Recent bridge logs',
|
|
262
266
|
'bbx tabs List available tabs',
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { createTcpBridgeTransport, getBridgeDir } from '../../native-host/src/config.js';
|
|
7
|
+
import { normalizeBridgeAuthToken } from '../../native-host/src/auth-token.js';
|
|
8
|
+
import { BridgeClient } from './client.js';
|
|
9
|
+
|
|
10
|
+
const REMOTES_FILENAME = 'remotes.json';
|
|
11
|
+
const LOCAL_DESTINATION_ID = 'local';
|
|
12
|
+
export const DEFAULT_REMOTE_PORT = 9223;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{ id: string, host: string, port: number, token: string }} RemoteDestination
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{ remotes: RemoteDestination[] }} RemoteConfig
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function getRemoteConfigPath(env = process.env) {
|
|
27
|
+
return path.join(getBridgeDir(env), REMOTES_FILENAME);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} id
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeDestinationId(id) {
|
|
35
|
+
const normalized = id.trim();
|
|
36
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/u.test(normalized) || normalized === LOCAL_DESTINATION_ID) {
|
|
37
|
+
throw new Error('Remote name must be 1-64 letters, numbers, dots, dashes, or underscores.');
|
|
38
|
+
}
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} endpoint
|
|
44
|
+
* @returns {{ host: string, port: number }}
|
|
45
|
+
*/
|
|
46
|
+
export function parseRemoteEndpoint(endpoint) {
|
|
47
|
+
const trimmed = endpoint.trim();
|
|
48
|
+
const match = /^(.+):(\d{1,5})$/u.exec(trimmed);
|
|
49
|
+
const host = (match ? match[1] : trimmed).trim().replace(/^\[|\]$/gu, '');
|
|
50
|
+
const port = match ? Number.parseInt(match[2], 10) : DEFAULT_REMOTE_PORT;
|
|
51
|
+
if (!host || /[\s/]/u.test(host)) {
|
|
52
|
+
throw new Error('Remote host must be a hostname or IP address.');
|
|
53
|
+
}
|
|
54
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
55
|
+
throw new Error('Remote port must be an integer between 1 and 65535.');
|
|
56
|
+
}
|
|
57
|
+
return { host, port };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {unknown} value
|
|
62
|
+
* @returns {RemoteDestination | null}
|
|
63
|
+
*/
|
|
64
|
+
function normalizeRemote(value) {
|
|
65
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const record = /** @type {Record<string, unknown>} */ (value);
|
|
69
|
+
if (
|
|
70
|
+
typeof record.id !== 'string' ||
|
|
71
|
+
typeof record.host !== 'string' ||
|
|
72
|
+
typeof record.port !== 'number' ||
|
|
73
|
+
typeof record.token !== 'string'
|
|
74
|
+
) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const token = normalizeBridgeAuthToken(record.token);
|
|
78
|
+
if (!token) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const id = normalizeDestinationId(record.id);
|
|
83
|
+
const { host, port } = parseRemoteEndpoint(`${record.host}:${record.port}`);
|
|
84
|
+
return { id, host, port, token };
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {{ configPath?: string, readFile?: typeof fs.promises.readFile }} [options={}]
|
|
92
|
+
* @returns {Promise<RemoteConfig>}
|
|
93
|
+
*/
|
|
94
|
+
export async function readRemoteConfig(options = {}) {
|
|
95
|
+
const configPath = options.configPath ?? getRemoteConfigPath();
|
|
96
|
+
const readFile = options.readFile ?? fs.promises.readFile.bind(fs.promises);
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(await readFile(configPath, 'utf8'));
|
|
99
|
+
/** @type {RemoteDestination[]} */
|
|
100
|
+
const remotes = [];
|
|
101
|
+
if (Array.isArray(parsed?.remotes)) {
|
|
102
|
+
for (const entry of parsed.remotes) {
|
|
103
|
+
const remote = normalizeRemote(entry);
|
|
104
|
+
if (remote) {
|
|
105
|
+
remotes.push(remote);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { remotes };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (
|
|
112
|
+
error &&
|
|
113
|
+
typeof error === 'object' &&
|
|
114
|
+
/** @type {{ code?: unknown }} */ (error).code === 'ENOENT'
|
|
115
|
+
) {
|
|
116
|
+
return { remotes: [] };
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {RemoteConfig} config
|
|
124
|
+
* @param {{ configPath?: string, writeFile?: typeof fs.promises.writeFile, mkdir?: typeof fs.promises.mkdir }} [options={}]
|
|
125
|
+
* @returns {Promise<void>}
|
|
126
|
+
*/
|
|
127
|
+
export async function writeRemoteConfig(config, options = {}) {
|
|
128
|
+
const configPath = options.configPath ?? getRemoteConfigPath();
|
|
129
|
+
const writeFile = options.writeFile ?? fs.promises.writeFile.bind(fs.promises);
|
|
130
|
+
const mkdir = options.mkdir ?? fs.promises.mkdir.bind(fs.promises);
|
|
131
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
132
|
+
await writeFile(configPath, `${JSON.stringify({ remotes: config.remotes }, null, 2)}\n`, {
|
|
133
|
+
encoding: 'utf8',
|
|
134
|
+
mode: 0o600,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {RemoteDestination} remote
|
|
140
|
+
* @returns {Promise<RemoteDestination>}
|
|
141
|
+
*/
|
|
142
|
+
export async function addRemoteDestination(remote) {
|
|
143
|
+
const config = await readRemoteConfig();
|
|
144
|
+
const existing = config.remotes.findIndex((entry) => entry.id === remote.id);
|
|
145
|
+
if (existing === -1) {
|
|
146
|
+
config.remotes.push(remote);
|
|
147
|
+
} else {
|
|
148
|
+
config.remotes[existing] = remote;
|
|
149
|
+
}
|
|
150
|
+
await writeRemoteConfig(config);
|
|
151
|
+
return remote;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {string} id
|
|
156
|
+
* @returns {Promise<boolean>}
|
|
157
|
+
*/
|
|
158
|
+
export async function removeRemoteDestination(id) {
|
|
159
|
+
const normalizedId = normalizeDestinationId(id);
|
|
160
|
+
const config = await readRemoteConfig();
|
|
161
|
+
const next = config.remotes.filter((remote) => remote.id !== normalizedId);
|
|
162
|
+
await writeRemoteConfig({ remotes: next });
|
|
163
|
+
return next.length !== config.remotes.length;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @returns {Promise<Array<{ id: string, local: boolean, host: string | null, port: number | null }>>}
|
|
168
|
+
*/
|
|
169
|
+
export async function listBridgeDestinations() {
|
|
170
|
+
const config = await readRemoteConfig();
|
|
171
|
+
return [
|
|
172
|
+
{ id: LOCAL_DESTINATION_ID, local: true, host: null, port: null },
|
|
173
|
+
...config.remotes.map((remote) => ({
|
|
174
|
+
id: remote.id,
|
|
175
|
+
local: false,
|
|
176
|
+
host: remote.host,
|
|
177
|
+
port: remote.port,
|
|
178
|
+
})),
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {string | null | undefined} destinationId
|
|
184
|
+
* @param {{ defaultTimeoutMs?: number }} [options={}]
|
|
185
|
+
* @returns {Promise<BridgeClient>}
|
|
186
|
+
*/
|
|
187
|
+
export async function createBridgeClientForDestination(destinationId, options = {}) {
|
|
188
|
+
if (!destinationId || destinationId === LOCAL_DESTINATION_ID) {
|
|
189
|
+
return new BridgeClient(options);
|
|
190
|
+
}
|
|
191
|
+
const config = await readRemoteConfig();
|
|
192
|
+
const remote = config.remotes.find((entry) => entry.id === destinationId);
|
|
193
|
+
if (!remote) {
|
|
194
|
+
throw new Error(`Unknown Browser Bridge destination "${destinationId}".`);
|
|
195
|
+
}
|
|
196
|
+
return new BridgeClient({
|
|
197
|
+
...options,
|
|
198
|
+
transport: createTcpBridgeTransport(remote.port, remote.host),
|
|
199
|
+
authToken: remote.token,
|
|
200
|
+
restartDaemonOnVersionMismatch: false,
|
|
201
|
+
});
|
|
202
|
+
}
|