surf-cli 2.8.0 → 2.10.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.
Files changed (47) hide show
  1. package/README.md +146 -8
  2. package/native/abort.cjs +65 -0
  3. package/native/activity-journal.cjs +55 -0
  4. package/native/ai-queue.cjs +64 -0
  5. package/native/aistudio-build.cjs +21 -13
  6. package/native/aistudio-client.cjs +40 -20
  7. package/native/browser-lock.cjs +2 -2
  8. package/native/chatgpt-client.cjs +49 -31
  9. package/native/cli.cjs +352 -482
  10. package/native/client-transport.cjs +168 -0
  11. package/native/do-executor.cjs +68 -510
  12. package/native/do-parser.cjs +8 -249
  13. package/native/doctor.cjs +55 -5
  14. package/native/endpoint.cjs +174 -0
  15. package/native/file-transfer.cjs +734 -0
  16. package/native/gemini-client.cjs +156 -71
  17. package/native/grok-client.cjs +98 -89
  18. package/native/host-helpers.cjs +43 -26
  19. package/native/host-sessions.cjs +287 -0
  20. package/native/host.cjs +998 -620
  21. package/native/listener.cjs +20 -0
  22. package/native/mcp-server.cjs +60 -65
  23. package/native/network-export.cjs +116 -0
  24. package/native/network-store.cjs +38 -58
  25. package/native/perplexity-client.cjs +46 -17
  26. package/native/playbook-authoring.cjs +44 -0
  27. package/native/playbook-cli.cjs +157 -0
  28. package/native/playbook-client.cjs +259 -0
  29. package/native/playbook-receipts.cjs +109 -0
  30. package/native/playbook-records.cjs +208 -0
  31. package/native/playbook-runtime.cjs +177 -0
  32. package/native/playbooks.cjs +235 -0
  33. package/native/private-state.cjs +156 -0
  34. package/native/redaction.cjs +104 -0
  35. package/native/remote-auth.cjs +279 -0
  36. package/native/remote-transport.cjs +337 -0
  37. package/native/request-pending.cjs +148 -0
  38. package/native/socket-path.cjs +1 -1
  39. package/native/workflow-definition.cjs +368 -0
  40. package/native/workflow-runtime.cjs +225 -0
  41. package/package.json +9 -6
  42. package/playbooks/page/ops/read.json +22 -0
  43. package/playbooks/page/playbook.json +7 -0
  44. package/scripts/install-native-host.cjs +36 -5
  45. package/skills/README.md +11 -5
  46. package/skills/deep-x-research/SKILL.md +106 -0
  47. package/skills/surf/SKILL.md +72 -5
@@ -1,250 +1,9 @@
1
- /**
2
- * Parser for surf `do` workflow commands
3
- *
4
- * Parses newline-separated commands into structured step arrays:
5
- *
6
- * Input:
7
- * 'go "https://example.com"
8
- * click e5
9
- * screenshot'
10
- *
11
- * Output:
12
- * [
13
- * { cmd: 'navigate', args: { url: 'https://example.com' } },
14
- * { cmd: 'click', args: { ref: 'e5' } },
15
- * { cmd: 'screenshot', args: {} }
16
- * ]
17
- */
18
-
19
- // Aliases mapping (matches cli.cjs)
20
- const ALIASES = {
21
- snap: "screenshot",
22
- read: "page.read",
23
- find: "search",
24
- go: "navigate",
25
- net: "network",
26
- "network.dump": "network.get",
27
- };
28
-
29
- // Primary argument mapping for positional args (matches cli.cjs)
30
- const PRIMARY_ARG_MAP = {
31
- ai: "query",
32
- gemini: "query",
33
- chatgpt: "query",
34
- perplexity: "query",
35
- grok: "query",
36
- navigate: "url",
37
- go: "url",
38
- js: "code",
39
- javascript_tool: "code",
40
- key: "key",
41
- wait: "duration",
42
- health: "url",
43
- new_tab: "url",
44
- "tab.new": "url",
45
- switch_tab: "tab_id",
46
- "tab.switch": "id",
47
- close_tab: "tab_id",
48
- "tab.close": "id",
49
- "tab.name": "name",
50
- "tab.unname": "name",
51
- scroll_to_position: "position",
52
- type: "text",
53
- smart_type: "text",
54
- "emulate.network": "preset",
55
- "emulate.cpu": "rate",
56
- search: "term",
57
- find: "term",
58
- "wait.element": "selector",
59
- "wait.url": "pattern",
60
- zoom: "level",
61
- "history.search": "query",
62
- "network.get": "id",
63
- "network.body": "id",
64
- "network.curl": "id",
65
- "network.path": "id",
66
- "window.new": "url",
67
- "window.focus": "id",
68
- "window.close": "id",
69
- "locate.role": "role",
70
- "locate.text": "text",
71
- "locate.label": "label",
72
- "emulate.device": "device",
73
- "frame.js": "code",
74
- "element.styles": "selector",
75
- "select": "selector",
76
- };
77
-
78
- /**
79
- * Tokenize a command line, respecting single and double quotes
80
- * @param {string} line - Single line to tokenize
81
- * @returns {string[]} - Array of tokens
82
- */
83
- function tokenize(line) {
84
- const tokens = [];
85
- let current = '';
86
- let inQuote = null;
87
-
88
- for (let i = 0; i < line.length; i++) {
89
- const ch = line[i];
90
-
91
- if (inQuote) {
92
- if (ch === inQuote) {
93
- // End of quoted string
94
- inQuote = null;
95
- } else {
96
- current += ch;
97
- }
98
- } else if (ch === '"' || ch === "'") {
99
- // Start of quoted string
100
- inQuote = ch;
101
- } else if (ch === ' ' || ch === '\t') {
102
- // Whitespace separator
103
- if (current) {
104
- tokens.push(current);
105
- current = '';
106
- }
107
- } else {
108
- current += ch;
109
- }
110
- }
111
-
112
- // Don't forget last token
113
- if (current) {
114
- tokens.push(current);
115
- }
116
-
117
- return tokens;
118
- }
119
-
120
- /**
121
- * Parse a single command line into a step object
122
- * @param {string} line - Single command line
123
- * @returns {{ cmd: string, args: object } | null}
124
- */
125
- function parseCommandLine(line) {
126
- const tokens = tokenize(line);
127
- if (tokens.length === 0) return null;
128
-
129
- // Get command and apply alias
130
- let cmd = tokens[0];
131
- cmd = ALIASES[cmd] || cmd;
132
-
133
- const args = {};
134
- let i = 1;
135
-
136
- // Handle first positional argument based on command type
137
- if (i < tokens.length && !tokens[i].startsWith('--')) {
138
- const firstArg = tokens[i];
139
-
140
- // Special handling for click command
141
- if (cmd === 'click') {
142
- if (/^e\d+$/.test(firstArg)) {
143
- // Element reference: e5 -> ref
144
- args.ref = firstArg;
145
- i++;
146
- } else if (/^\d+$/.test(firstArg) && tokens[i + 1] && /^\d+$/.test(tokens[i + 1])) {
147
- // Coordinates: 100 200 -> x, y
148
- args.x = parseInt(firstArg, 10);
149
- args.y = parseInt(tokens[i + 1], 10);
150
- i += 2;
151
- }
152
- } else if (cmd === 'select') {
153
- // Select takes selector + one or more values: select e5 "US" or select e5 "opt1" "opt2"
154
- args.selector = firstArg;
155
- i++;
156
- // Collect remaining positional args as values
157
- const values = [];
158
- while (i < tokens.length && !tokens[i].startsWith('--')) {
159
- values.push(tokens[i]);
160
- i++;
161
- }
162
- // Host expects 'values' (always), matching CLI behavior
163
- if (values.length === 1) {
164
- args.values = values[0]; // Single value as string (host will wrap in array)
165
- } else if (values.length > 1) {
166
- args.values = values; // Multiple values as array
167
- }
168
- } else if (cmd === 'scroll') {
169
- if (firstArg === 'top' || firstArg === 'bottom') {
170
- cmd = `scroll.${firstArg}`;
171
- i++;
172
- } else if (['up', 'down', 'left', 'right'].includes(firstArg)) {
173
- args.direction = firstArg;
174
- i++;
175
- if (i < tokens.length && /^-?\d+$/.test(tokens[i])) {
176
- args.scroll_pixels = parseInt(tokens[i], 10);
177
- i++;
178
- }
179
- }
180
- } else {
181
- // Use PRIMARY_ARG_MAP for other commands
182
- const primaryKey = PRIMARY_ARG_MAP[cmd];
183
- if (primaryKey) {
184
- args[primaryKey] = firstArg;
185
- i++;
186
- }
187
- }
188
- }
189
-
190
- // Parse --flag value pairs
191
- while (i < tokens.length) {
192
- const token = tokens[i];
193
- if (token.startsWith('--')) {
194
- const key = token.slice(2);
195
- const next = tokens[i + 1];
196
- if (next && !next.startsWith('--')) {
197
- // Flag with value
198
- let val = next;
199
- // Type coercion
200
- if (val === "true") val = true;
201
- else if (val === "false") val = false;
202
- else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
203
- else if (/^-?\d+\.\d+$/.test(val)) val = parseFloat(val);
204
- args[key] = val;
205
- i += 2;
206
- } else {
207
- // Boolean flag
208
- args[key] = true;
209
- i++;
210
- }
211
- } else {
212
- // Skip unrecognized positional (shouldn't happen normally)
213
- i++;
214
- }
215
- }
216
-
217
- return { cmd, args };
218
- }
219
-
220
- /**
221
- * Parse a workflow string into step array
222
- * Supports pipe-separated (inline) or newline-separated (file) commands
223
- * @param {string} input - Workflow string
224
- * @returns {Array<{ cmd: string, args: object }>}
225
- */
226
- function parseDoCommands(input) {
227
- // Determine separator: use pipe if present, otherwise newlines
228
- // Pipe is preferred for inline: 'go "url" | click e5 | screenshot'
229
- // Newlines for files or heredocs
230
- const hasPipe = input.includes('|');
231
- const separator = hasPipe ? '|' : '\n';
232
-
233
- // Also handle literal \n for backwards compatibility
234
- const normalized = hasPipe ? input : input.replace(/\\n/g, '\n');
235
-
236
- return normalized
237
- .split(separator)
238
- .map(line => line.trim())
239
- .filter(line => line && !line.startsWith('#'))
240
- .map(line => parseCommandLine(line))
241
- .filter(step => step !== null);
242
- }
243
-
244
- module.exports = {
245
- parseDoCommands,
246
- parseCommandLine,
247
- tokenize,
248
- ALIASES,
249
- PRIMARY_ARG_MAP
1
+ const definition = require("./workflow-definition.cjs");
2
+
3
+ module.exports = {
4
+ ALIASES: definition.ALIASES,
5
+ PRIMARY_ARG_MAP: definition.PRIMARY_ARG_MAP,
6
+ parseCommandLine: definition.parseCommandLine,
7
+ parseDoCommands: definition.parseDoCommands,
8
+ tokenize: definition.tokenize,
250
9
  };
package/native/doctor.cjs CHANGED
@@ -3,6 +3,7 @@ const net = require("net");
3
3
  const os = require("os");
4
4
  const path = require("path");
5
5
  const { execFileSync } = require("child_process");
6
+ const { connectEndpoint, selectEndpoint } = require("./endpoint.cjs");
6
7
 
7
8
  const HOST_NAME = "surf.browser.host";
8
9
 
@@ -402,6 +403,16 @@ function buildRecommendations(report) {
402
403
  return Array.from(new Set(recommendations));
403
404
  }
404
405
 
406
+ function remoteRecommendations(endpoint, code) {
407
+ const base = [`Confirm Surf is listening on ${endpoint.display}; the remote host listener must allow this Tailnet connection.`];
408
+ if (code === "ENOTFOUND") return [...base, "Check the Tailnet DNS name, then run `tailscale status` and `tailscale ping <host>`."].map((item) => item.replace("<host>", endpoint.host));
409
+ if (code === "ETIMEDOUT") return [...base, `Run \`tailscale ping ${endpoint.host}\`; check restrictive Tailnet ACLs/grants and host firewall rules.`];
410
+ if (code === "ECONNREFUSED") return [...base, "Verify the host process is running and bound to the requested TCP port; check restrictive Tailnet ACLs/grants."];
411
+ if (code === "ENETUNREACH" || code === "EHOSTUNREACH") return [...base, `Run \`tailscale status\` and \`tailscale ping ${endpoint.host}\`; check Tailnet routing plus restrictive ACLs/grants.`];
412
+ if (code === "EAUTH") return [...base, "Verify the client credential path, pinned host identity, and that the labeled client has not been revoked."].map((item) => item.replace("<host>", endpoint.host));
413
+ return base;
414
+ }
415
+
405
416
  async function runDoctor(rawOptions = {}, deps = {}) {
406
417
  const env = deps.env || process.env;
407
418
  const platform = deps.platform || process.platform;
@@ -413,6 +424,41 @@ async function runDoctor(rawOptions = {}, deps = {}) {
413
424
  socket: rawOptions.socket || env.SURF_SOCKET || defaultSocketPath(platform),
414
425
  connectTimeoutMs: rawOptions.connectTimeoutMs ?? 750,
415
426
  };
427
+ const selectedEndpoint = rawOptions.endpoint || selectEndpoint([], env).endpoint;
428
+ if (selectedEndpoint.kind === "remote") {
429
+ const endpoint = selectedEndpoint;
430
+ const connection = await (deps.connectEndpoint || ((target, timeoutMs) => new Promise((resolve) => {
431
+ let settled = false;
432
+ let socket;
433
+ const finish = (result) => {
434
+ if (settled) return;
435
+ settled = true;
436
+ clearTimeout(timeout);
437
+ socket?.destroy();
438
+ resolve(result);
439
+ };
440
+ const timeout = setTimeout(() => finish({
441
+ ok: false,
442
+ code: socket?.connected ? "EAUTH" : "ETIMEDOUT",
443
+ message: socket?.connected ? "remote authentication timed out" : `timed out after ${timeoutMs}ms`,
444
+ }), timeoutMs);
445
+ socket = connectEndpoint(target, () => finish({ ok: true, message: "authenticated" }));
446
+ socket.once("error", (error) => finish({ ok: false, code: error.code, message: error.message || String(error) }));
447
+ })))(endpoint, options.connectTimeoutMs);
448
+ const checks = [{ id: "remote-endpoint", status: "info", message: `Remote endpoint: ${endpoint.display}`, endpoint: endpoint.display }, {
449
+ id: "remote-connect", status: connection.ok ? "pass" : "fail",
450
+ message: connection.ok ? `Connected to remote endpoint ${endpoint.display}` : `Could not connect to remote endpoint ${endpoint.display}: ${connection.message}`,
451
+ code: connection.code,
452
+ }, {
453
+ id: "remote-auth", status: connection.ok ? "pass" : connection.code === "EAUTH" ? "fail" : "info",
454
+ message: connection.ok ? "Remote credential authenticated and server identity verified" : connection.code === "EAUTH" ? "Remote credential rejected, revoked, or server identity mismatch" : "Remote authentication was not reached",
455
+ code: connection.code,
456
+ }];
457
+ const summary = summarize(checks);
458
+ const report = { ok: connection.ok, summary, environment: { platform, remoteEndpoint: endpoint.display, endpointKind: "remote", browsers: [] }, manifests: [], checks };
459
+ report.recommendations = connection.ok ? [] : remoteRecommendations(endpoint, connection.code);
460
+ return report;
461
+ }
416
462
  const effectiveTarget = resolveEffectiveTarget(options, { platform, runningInWsl });
417
463
  const browsers = resolveBrowsers(options.browser);
418
464
  const context = {
@@ -502,9 +548,13 @@ function formatCheck(check) {
502
548
  function formatDoctorReport(report) {
503
549
  const lines = ["Surf doctor", ""];
504
550
  lines.push(`Platform: ${report.environment.platform}${report.environment.runningInWsl ? " (WSL2 detected)" : ""}`);
505
- lines.push(`Target: ${report.environment.effectiveTarget === "wsl-windows" ? "Windows browser from WSL2" : report.environment.effectiveTarget}`);
506
- lines.push(`Socket: ${report.environment.socketPath}`);
507
- lines.push(`Browsers: ${report.environment.browsers.join(", ")}`);
551
+ if (report.environment.endpointKind === "remote") {
552
+ lines.push(`Remote endpoint: ${report.environment.remoteEndpoint}`);
553
+ } else {
554
+ lines.push(`Target: ${report.environment.effectiveTarget === "wsl-windows" ? "Windows browser from WSL2" : report.environment.effectiveTarget}`);
555
+ }
556
+ if (report.environment.socketPath) lines.push(`Socket: ${report.environment.socketPath}`);
557
+ if (report.environment.browsers.length) lines.push(`Browsers: ${report.environment.browsers.join(", ")}`);
508
558
  lines.push("");
509
559
 
510
560
  for (const check of report.checks.filter((item) => item.status !== "info")) {
@@ -542,7 +592,7 @@ Examples:
542
592
  `;
543
593
  }
544
594
 
545
- async function runDoctorCli(rawArgs) {
595
+ async function runDoctorCli(rawArgs, endpoint) {
546
596
  let options;
547
597
  try {
548
598
  options = parseDoctorArgs(rawArgs);
@@ -558,7 +608,7 @@ async function runDoctorCli(rawArgs) {
558
608
  }
559
609
 
560
610
  try {
561
- const report = await runDoctor(options);
611
+ const report = await runDoctor({ ...options, endpoint });
562
612
  if (options.json) {
563
613
  console.log(JSON.stringify(report, null, 2));
564
614
  } else {
@@ -0,0 +1,174 @@
1
+ const net = require("net");
2
+ const { DEFAULT_SOCKET_PATH } = require("./socket-path.cjs");
3
+ const { authenticateClient } = require("./remote-transport.cjs");
4
+
5
+ function parseRemoteEndpoint(value) {
6
+ if (typeof value !== "string" || !value) throw new Error("--remote requires host:port");
7
+ let host;
8
+ let portText;
9
+ if (value.startsWith("[")) {
10
+ const match = value.match(/^\[([^\]]+)\]:(\d+)$/);
11
+ if (!match || net.isIP(match?.[1]) !== 6) throw new Error("remote endpoint must use a bracketed IPv6 address and port");
12
+ [, host, portText] = match;
13
+ host = new URL(`http://[${host}]`).hostname.slice(1, -1);
14
+ } else {
15
+ const match = value.match(/^([^:]+):(\d+)$/);
16
+ if (!match) throw new Error("remote endpoint must be host:port (IPv6 must be bracketed)");
17
+ [, host, portText] = match;
18
+ if (host.includes("/") || host.includes("@") || host.includes(":") || host === "*" || host.includes("*")) throw new Error("remote endpoint host is invalid");
19
+ if ((/^\d+(?:\.\d+){3}$/.test(host) && net.isIP(host) !== 4) || (net.isIP(host) !== 4 && !/^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(host))) {
20
+ throw new Error("remote endpoint host is invalid");
21
+ }
22
+ host = host.toLowerCase();
23
+ }
24
+ const port = Number(portText);
25
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("remote endpoint port must be between 1 and 65535");
26
+ if ((net.isIP(host) === 4 && host === "0.0.0.0") || (net.isIP(host) === 6 && /^0*:?0*$/.test(host.replace(/:/g, "")))) {
27
+ throw new Error("remote endpoint host must not be unspecified");
28
+ }
29
+ const display = net.isIP(host) === 6 ? `[${host}]:${port}` : `${host}:${port}`;
30
+ return { kind: "remote", host, port, display, key: `tcp:${display}`, connectionOptions: { host, port } };
31
+ }
32
+
33
+ function selectEndpoint(args, env) {
34
+ const selectedEnv = env === undefined ? process.env : env;
35
+ const remoteIndexes = [];
36
+ const credentialIndexes = [];
37
+ for (let i = 0; i < args.length; i++) {
38
+ if (args[i] === "--remote") remoteIndexes.push(i);
39
+ if (args[i] === "--remote-credential") credentialIndexes.push(i);
40
+ }
41
+ if (remoteIndexes.length > 1) throw new Error("--remote may only be specified once");
42
+ if (credentialIndexes.length > 1) throw new Error("--remote-credential may only be specified once");
43
+ let cliRemote;
44
+ let cliCredential;
45
+ const strippedArgs = [...args];
46
+ if (remoteIndexes.length) {
47
+ const index = remoteIndexes[0];
48
+ cliRemote = args[index + 1];
49
+ if (!cliRemote || cliRemote.startsWith("--")) throw new Error("--remote requires host:port");
50
+ strippedArgs.splice(index, 2);
51
+ }
52
+ if (credentialIndexes.length) {
53
+ const index = credentialIndexes[0];
54
+ cliCredential = args[index + 1];
55
+ if (!cliCredential || cliCredential.startsWith("--")) throw new Error("--remote-credential requires a file path");
56
+ const adjustedIndex = index - (remoteIndexes.length && index > remoteIndexes[0] ? 2 : 0);
57
+ strippedArgs.splice(adjustedIndex, 2);
58
+ }
59
+ const remoteValue = cliRemote || selectedEnv.SURF_REMOTE;
60
+ if (remoteValue) {
61
+ const credentialPath = cliCredential || selectedEnv.SURF_REMOTE_CREDENTIAL;
62
+ if (!credentialPath) throw new Error("remote endpoint requires --remote-credential <path> or SURF_REMOTE_CREDENTIAL");
63
+ return { args: strippedArgs, endpoint: { ...parseRemoteEndpoint(remoteValue), credentialPath } };
64
+ }
65
+ if (cliCredential) throw new Error("--remote-credential requires a remote endpoint");
66
+ const socketPath = selectedEnv.SURF_SOCKET || DEFAULT_SOCKET_PATH;
67
+ return { args: strippedArgs, endpoint: { kind: "local", path: socketPath, display: socketPath, key: `unix:${socketPath}`, connectionOptions: socketPath } };
68
+ }
69
+
70
+ function createRemoteSocket(endpoint) {
71
+ const rawSocket = net.createConnection(endpoint.connectionOptions, () => {});
72
+ let ready = false;
73
+ let connected = false;
74
+ let destroyed = false;
75
+ const pending = new Map();
76
+ const queue = (event, listener, once) => {
77
+ if (ready) {
78
+ once ? rawSocket.once(event, listener) : rawSocket.on(event, listener);
79
+ return;
80
+ }
81
+ const listeners = pending.get(event) || [];
82
+ listeners.push({ listener, once });
83
+ pending.set(event, listeners);
84
+ };
85
+ const flush = () => {
86
+ ready = true;
87
+ for (const [event, listeners] of pending) {
88
+ for (const { listener, once } of listeners) {
89
+ once ? rawSocket.once(event, listener) : rawSocket.on(event, listener);
90
+ }
91
+ }
92
+ pending.clear();
93
+ };
94
+ const proxy = {
95
+ on(event, listener) { queue(event, listener, false); return proxy; },
96
+ once(event, listener) { queue(event, listener, true); return proxy; },
97
+ removeListener(event, listener) {
98
+ if (ready) rawSocket.removeListener(event, listener);
99
+ else pending.set(event, (pending.get(event) || []).filter((entry) => entry.listener !== listener));
100
+ return proxy;
101
+ },
102
+ write(...args) { return rawSocket.write(...args); },
103
+ end(...args) { return rawSocket.end(...args); },
104
+ destroy(...args) { destroyed = true; return rawSocket.destroy(...args); },
105
+ setTimeout(...args) { rawSocket.setTimeout(...args); return proxy; },
106
+ get authenticated() { return ready; },
107
+ get connected() { return connected; },
108
+ };
109
+ rawSocket.once("connect", () => { connected = true; });
110
+ rawSocket.on("error", (error) => {
111
+ if (ready || destroyed) return;
112
+ ready = true;
113
+ const listeners = pending.get("error") || [];
114
+ pending.delete("error");
115
+ for (const { listener } of listeners) listener(error);
116
+ if (proxy.__pendingErrors) proxy.__pendingErrors.length = 0;
117
+ flush();
118
+ });
119
+ rawSocket.on("close", () => {
120
+ if (ready) return;
121
+ ready = true;
122
+ const error = new Error("remote authentication connection closed");
123
+ for (const { listener } of pending.get("error") || []) listener(error);
124
+ for (const { listener } of pending.get("close") || []) listener();
125
+ pending.clear();
126
+ if (proxy.__pendingErrors) proxy.__pendingErrors.length = 0;
127
+ });
128
+ return { rawSocket, proxy, flush };
129
+ }
130
+
131
+ function connectEndpoint(endpoint, onConnect) {
132
+ if (endpoint.kind === "local") {
133
+ return net.createConnection(endpoint.connectionOptions, onConnect || (() => {}));
134
+ }
135
+ const { rawSocket, proxy, flush } = createRemoteSocket(endpoint);
136
+ rawSocket.once("connect", () => {
137
+ authenticateClient(rawSocket, endpoint.credentialPath)
138
+ .then(() => {
139
+ flush();
140
+ if (onConnect) onConnect(proxy);
141
+ })
142
+ .catch((error) => {
143
+ error.code = error.code || "EAUTH";
144
+ const listeners = proxy.__pendingErrors || [];
145
+ for (const listener of listeners) {
146
+ proxy.removeListener("error", listener);
147
+ listener(error);
148
+ }
149
+ proxy.__pendingErrors.length = 0;
150
+ flush();
151
+ proxy.destroy();
152
+ });
153
+ });
154
+ proxy.__pendingErrors = [];
155
+ const originalOn = proxy.on;
156
+ const originalOnce = proxy.once;
157
+ proxy.on = (event, listener) => {
158
+ if (event === "error" && !proxy.authenticated) proxy.__pendingErrors.push(listener);
159
+ return originalOn(event, listener);
160
+ };
161
+ proxy.once = (event, listener) => {
162
+ if (event === "error" && !proxy.authenticated) proxy.__pendingErrors.push(listener);
163
+ return originalOnce(event, listener);
164
+ };
165
+ return proxy;
166
+ }
167
+
168
+ function formatEndpointError(error, endpoint, formatSocketError) {
169
+ if (endpoint.kind === "local") return formatSocketError(error);
170
+ const message = error?.message || String(error);
171
+ return `Remote endpoint connection failed (${endpoint.display}): ${message}`;
172
+ }
173
+
174
+ module.exports = { parseRemoteEndpoint, selectEndpoint, connectEndpoint, formatEndpointError };