@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.
@@ -6,9 +6,13 @@ import path from 'node:path';
6
6
 
7
7
  import { getBridgeDir } from './config.js';
8
8
 
9
+ export const BRIDGE_AUTH_TOKEN_ENV = 'BBX_AUTH_TOKEN';
10
+ export const BRIDGE_AUTH_TOKEN_FILE_ENV = 'BBX_AUTH_TOKEN_FILE';
9
11
  const TOKEN_FILENAME = 'daemon.auth';
10
12
  const TOKEN_BYTES = 32;
11
13
  const TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,256}$/u;
14
+ const UUID_TOKEN_PATTERN =
15
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
12
16
 
13
17
  /**
14
18
  * @param {NodeJS.ProcessEnv} [env=process.env]
@@ -27,7 +31,32 @@ export function normalizeBridgeAuthToken(value) {
27
31
  return null;
28
32
  }
29
33
  const token = value.trim();
30
- return TOKEN_PATTERN.test(token) ? token : null;
34
+ return TOKEN_PATTERN.test(token) || UUID_TOKEN_PATTERN.test(token) ? token : null;
35
+ }
36
+
37
+ /**
38
+ * @param {{ env?: NodeJS.ProcessEnv, readFile?: typeof fs.promises.readFile }} [options={}]
39
+ * @returns {Promise<string | null>}
40
+ */
41
+ export async function readBridgeAuthTokenOverride(options = {}) {
42
+ const env = options.env ?? process.env;
43
+ const readFile = options.readFile ?? fs.promises.readFile.bind(fs.promises);
44
+ const explicitToken = normalizeBridgeAuthToken(env[BRIDGE_AUTH_TOKEN_ENV]);
45
+ if (explicitToken) {
46
+ return explicitToken;
47
+ }
48
+ const tokenFile = env[BRIDGE_AUTH_TOKEN_FILE_ENV];
49
+ if (!tokenFile) {
50
+ return null;
51
+ }
52
+ try {
53
+ return normalizeBridgeAuthToken(await readFile(tokenFile, 'utf8'));
54
+ } catch (error) {
55
+ if (isMissingFileError(error)) {
56
+ return null;
57
+ }
58
+ throw error;
59
+ }
31
60
  }
32
61
 
33
62
  /**
@@ -35,6 +64,10 @@ export function normalizeBridgeAuthToken(value) {
35
64
  * @returns {Promise<string | null>}
36
65
  */
37
66
  export async function readBridgeAuthToken(options = {}) {
67
+ const override = await readBridgeAuthTokenOverride(options);
68
+ if (override) {
69
+ return override;
70
+ }
38
71
  const tokenPath = options.tokenPath ?? getBridgeAuthTokenPath();
39
72
  const readFile = options.readFile ?? fs.promises.readFile.bind(fs.promises);
40
73
  try {
@@ -47,6 +80,28 @@ export async function readBridgeAuthToken(options = {}) {
47
80
  }
48
81
  }
49
82
 
83
+ /**
84
+ * @param {string} token
85
+ * @param {{ tokenPath?: string, writeFile?: typeof fs.promises.writeFile, mkdir?: typeof fs.promises.mkdir, chmod?: typeof fs.promises.chmod }} [options={}]
86
+ * @returns {Promise<string>}
87
+ */
88
+ export async function writeBridgeAuthToken(token, options = {}) {
89
+ const normalized = normalizeBridgeAuthToken(token);
90
+ if (!normalized) {
91
+ throw new Error('Bridge auth token must be a UUID or 32-256 URL-safe characters.');
92
+ }
93
+ const tokenPath = options.tokenPath ?? getBridgeAuthTokenPath();
94
+ const writeFile = options.writeFile ?? fs.promises.writeFile.bind(fs.promises);
95
+ const mkdir = options.mkdir ?? fs.promises.mkdir.bind(fs.promises);
96
+ const chmod = options.chmod ?? fs.promises.chmod.bind(fs.promises);
97
+ await mkdir(path.dirname(tokenPath), { recursive: true });
98
+ await writeFile(tokenPath, `${normalized}\n`, { encoding: 'utf8', mode: 0o600 });
99
+ if (process.platform !== 'win32') {
100
+ await chmod(tokenPath, 0o600).catch(() => {});
101
+ }
102
+ return normalized;
103
+ }
104
+
50
105
  /**
51
106
  * @param {{
52
107
  * tokenPath?: string,
@@ -7,9 +7,21 @@ import path from 'node:path';
7
7
  export const APP_NAME = 'com.browserbridge.browser_bridge';
8
8
  export const BRIDGE_HOME_ENV = 'BROWSER_BRIDGE_HOME';
9
9
  export const BRIDGE_TCP_PORT_ENV = 'BBX_TCP_PORT';
10
+ export const BRIDGE_TCP_HOST_ENV = 'BBX_TCP_HOST';
11
+ export const BRIDGE_TCP_BIND_HOST_ENV = 'BBX_TCP_BIND_HOST';
10
12
  export const DEFAULT_WINDOWS_TCP_PORT = 9223;
13
+ export const PROXY_CONFIG_FILENAME = 'proxy.json';
11
14
 
12
- /** @typedef {{ type: 'socket', socketPath: string, label: string } | { type: 'tcp', host: string, port: number, label: string }} BridgeTransport */
15
+ /** @typedef {{ type: 'socket', socketPath: string, label: string } | { type: 'tcp', host: string, port: number, bindHost?: string, label: string }} BridgeTransport */
16
+
17
+ /**
18
+ * @typedef {{
19
+ * enabled: boolean,
20
+ * port: number,
21
+ * bindHost: string,
22
+ * token?: string,
23
+ * }} BridgeProxyConfig
24
+ */
13
25
 
14
26
  /**
15
27
  * The official Chrome Web Store extension ID used when callers do not provide
@@ -63,6 +75,72 @@ export function getSocketPath(env = process.env) {
63
75
  return path.join(getBridgeDir(env), 'bridge.sock');
64
76
  }
65
77
 
78
+ /**
79
+ * @param {NodeJS.ProcessEnv} [env=process.env]
80
+ * @returns {string}
81
+ */
82
+ export function getProxyConfigPath(env = process.env) {
83
+ return path.join(getBridgeDir(env), PROXY_CONFIG_FILENAME);
84
+ }
85
+
86
+ /**
87
+ * @param {unknown} value
88
+ * @returns {string | null}
89
+ */
90
+ function normalizeHost(value) {
91
+ if (typeof value !== 'string') {
92
+ return null;
93
+ }
94
+ const host = value.trim();
95
+ if (!host || /[\s/]/u.test(host)) {
96
+ return null;
97
+ }
98
+ return host;
99
+ }
100
+
101
+ /**
102
+ * @param {unknown} value
103
+ * @returns {number | null}
104
+ */
105
+ function normalizePort(value) {
106
+ if (typeof value !== 'number' && typeof value !== 'string') {
107
+ return null;
108
+ }
109
+ const raw = String(value).trim();
110
+ const port = Number.parseInt(raw, 10);
111
+ if (!Number.isInteger(port) || String(port) !== raw || port < 1 || port > 65535) {
112
+ return null;
113
+ }
114
+ return port;
115
+ }
116
+
117
+ /**
118
+ * @param {NodeJS.ProcessEnv} [env=process.env]
119
+ * @returns {BridgeProxyConfig | null}
120
+ */
121
+ export function readProxyConfig(env = process.env) {
122
+ try {
123
+ const parsed = JSON.parse(fs.readFileSync(getProxyConfigPath(env), 'utf8'));
124
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
125
+ return null;
126
+ }
127
+ const record = /** @type {Record<string, unknown>} */ (parsed);
128
+ const port = normalizePort(record.port);
129
+ const bindHost = normalizeHost(record.bindHost) ?? '0.0.0.0';
130
+ if (record.enabled !== true || port === null) {
131
+ return null;
132
+ }
133
+ return {
134
+ enabled: true,
135
+ port,
136
+ bindHost,
137
+ ...(typeof record.token === 'string' ? { token: record.token } : {}),
138
+ };
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+
66
144
  /**
67
145
  * @param {string} socketPath
68
146
  * @returns {BridgeTransport}
@@ -80,12 +158,13 @@ export function createSocketBridgeTransport(socketPath) {
80
158
  * @param {string} [host='127.0.0.1']
81
159
  * @returns {BridgeTransport}
82
160
  */
83
- export function createTcpBridgeTransport(port, host = '127.0.0.1') {
161
+ export function createTcpBridgeTransport(port, host = '127.0.0.1', bindHost = host) {
84
162
  return {
85
163
  type: 'tcp',
86
164
  host,
87
165
  port,
88
- label: `${host}:${port}`,
166
+ ...(bindHost !== host ? { bindHost } : {}),
167
+ label: bindHost !== host ? `${host}:${port} (bind ${bindHost})` : `${host}:${port}`,
89
168
  };
90
169
  }
91
170
 
@@ -109,6 +188,38 @@ export function getBridgeTcpPort(env = process.env) {
109
188
  return port;
110
189
  }
111
190
 
191
+ /**
192
+ * @param {NodeJS.ProcessEnv} [env=process.env]
193
+ * @returns {string}
194
+ */
195
+ export function getBridgeTcpHost(env = process.env) {
196
+ const raw = env[BRIDGE_TCP_HOST_ENV];
197
+ if (raw == null || raw === '') {
198
+ return '127.0.0.1';
199
+ }
200
+ const host = normalizeHost(raw);
201
+ if (!host) {
202
+ throw new Error(`${BRIDGE_TCP_HOST_ENV} must be a hostname or IP address.`);
203
+ }
204
+ return host;
205
+ }
206
+
207
+ /**
208
+ * @param {NodeJS.ProcessEnv} [env=process.env]
209
+ * @returns {string | null}
210
+ */
211
+ export function getBridgeTcpBindHost(env = process.env) {
212
+ const raw = env[BRIDGE_TCP_BIND_HOST_ENV];
213
+ if (raw == null || raw === '') {
214
+ return null;
215
+ }
216
+ const host = normalizeHost(raw);
217
+ if (!host) {
218
+ throw new Error(`${BRIDGE_TCP_BIND_HOST_ENV} must be a hostname or IP address.`);
219
+ }
220
+ return host;
221
+ }
222
+
112
223
  /**
113
224
  * Align Windows CLI/daemon entrypoints with the installed native-host launcher,
114
225
  * which uses TCP by default. Preserve explicit overrides and custom bridge-home
@@ -139,7 +250,13 @@ export function applyWindowsTcpTransportDefaults(env = process.env) {
139
250
  export function getBridgeTransport(env = process.env) {
140
251
  const tcpPort = getBridgeTcpPort(env);
141
252
  if (tcpPort !== null) {
142
- return createTcpBridgeTransport(tcpPort);
253
+ const host = getBridgeTcpHost(env);
254
+ return createTcpBridgeTransport(tcpPort, host, getBridgeTcpBindHost(env) ?? host);
255
+ }
256
+
257
+ const proxyConfig = readProxyConfig(env);
258
+ if (proxyConfig) {
259
+ return createTcpBridgeTransport(proxyConfig.port, '127.0.0.1', proxyConfig.bindHost);
143
260
  }
144
261
 
145
262
  return createSocketBridgeTransport(getSocketPath(env));
@@ -151,7 +268,7 @@ export function getBridgeTransport(env = process.env) {
151
268
  */
152
269
  export function getBridgeListenTarget(transport = getBridgeTransport()) {
153
270
  if (transport.type === 'tcp') {
154
- return { host: transport.host, port: transport.port };
271
+ return { host: transport.bindHost ?? transport.host, port: transport.port };
155
272
  }
156
273
  return transport.socketPath;
157
274
  }