surf-cli 2.8.0 → 2.9.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 (42) hide show
  1. package/README.md +98 -4
  2. package/dist/content/index.js +116 -0
  3. package/dist/content/index.js.map +1 -0
  4. package/dist/manifest.json +2 -11
  5. package/dist/options/options.js +3 -3
  6. package/dist/options/options.js.map +1 -1
  7. package/dist/service-worker/index.js +261 -61
  8. package/dist/service-worker/index.js.map +1 -1
  9. package/native/abort.cjs +65 -0
  10. package/native/ai-queue.cjs +64 -0
  11. package/native/aistudio-build.cjs +21 -13
  12. package/native/aistudio-client.cjs +40 -20
  13. package/native/browser-lock.cjs +2 -2
  14. package/native/chatgpt-client.cjs +47 -31
  15. package/native/cli.cjs +300 -204
  16. package/native/client-transport.cjs +168 -0
  17. package/native/do-executor.cjs +25 -44
  18. package/native/doctor.cjs +55 -5
  19. package/native/endpoint.cjs +174 -0
  20. package/native/file-transfer.cjs +734 -0
  21. package/native/gemini-client.cjs +156 -71
  22. package/native/grok-client.cjs +98 -89
  23. package/native/host-helpers.cjs +37 -12
  24. package/native/host-sessions.cjs +283 -0
  25. package/native/host.cjs +800 -620
  26. package/native/listener.cjs +20 -0
  27. package/native/mcp-server.cjs +60 -65
  28. package/native/network-export.cjs +113 -0
  29. package/native/perplexity-client.cjs +46 -17
  30. package/native/remote-auth.cjs +279 -0
  31. package/native/remote-transport.cjs +337 -0
  32. package/native/request-pending.cjs +148 -0
  33. package/native/socket-path.cjs +1 -1
  34. package/package.json +8 -6
  35. package/scripts/install-native-host.cjs +36 -5
  36. package/skills/README.md +11 -5
  37. package/skills/deep-x-research/SKILL.md +106 -0
  38. package/skills/surf/SKILL.md +31 -4
  39. package/dist/content/accessibility-tree.js +0 -11
  40. package/dist/content/accessibility-tree.js.map +0 -1
  41. package/dist/content/visual-indicator.js +0 -111
  42. package/dist/content/visual-indicator.js.map +0 -1
@@ -0,0 +1,168 @@
1
+ const { connectEndpoint } = require("./endpoint.cjs");
2
+ const { createFrameParser, createSocketWriter } = require("./remote-transport.cjs");
3
+ const { createClientTransferController } = require("./file-transfer.cjs");
4
+
5
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
6
+
7
+ async function openClientTransport(endpoint, { requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = {}) {
8
+ let socket;
9
+ let ready;
10
+ let readyReject;
11
+ let readyErrorListener;
12
+ ready = new Promise((resolve, reject) => {
13
+ readyReject = reject;
14
+ readyErrorListener = (error) => reject(error);
15
+ socket = connectEndpoint(endpoint, () => {
16
+ socket.removeListener("error", readyErrorListener);
17
+ resolve();
18
+ });
19
+ socket.once("error", readyErrorListener);
20
+ });
21
+ await ready;
22
+ const pending = new Map();
23
+ const writer = createSocketWriter(socket, { onOverflow: ({ error }) => socket.destroy(error) });
24
+ const transfers = endpoint.kind === "remote"
25
+ ? createClientTransferController({ writer, onActivity: () => {} })
26
+ : null;
27
+ let transferChain = Promise.resolve();
28
+ let transferCleanupPromise = null;
29
+ const cleanupTransfers = (error) => {
30
+ if (!transfers) return Promise.resolve();
31
+ transferCleanupPromise ||= transfers.cleanup(error);
32
+ return transferCleanupPromise;
33
+ };
34
+ const rejectPending = (error) => {
35
+ for (const entry of pending.values()) {
36
+ clearTimeout(entry.timer);
37
+ entry.reject(error);
38
+ }
39
+ pending.clear();
40
+ };
41
+ const parser = createFrameParser({
42
+ onFrame(message) {
43
+ if (transfers && message.type && message.type.startsWith("transfer_")) {
44
+ transferChain = transferChain.then(() => transfers.handle(message)).catch((error) => {
45
+ rejectPending(error);
46
+ return writer.send({ type: "transfer_error", version: 1, transferId: message.transferId, error: error.message }).finally(() => socket.destroy(error)).then(() => { throw error; });
47
+ });
48
+ transferChain.catch(() => undefined);
49
+ return;
50
+ }
51
+ if (message.type === "extension_disconnected") {
52
+ transferChain = transferChain.then(() => {
53
+ const error = new Error(message.message || "Surf extension disconnected");
54
+ rejectPending(error);
55
+ socket.end();
56
+ });
57
+ transferChain.catch(() => undefined);
58
+ return;
59
+ }
60
+ if (message.id === undefined || !pending.has(message.id)) return;
61
+ transferChain = transferChain.then(() => {
62
+ const entry = pending.get(message.id);
63
+ if (!entry) return;
64
+ pending.delete(message.id);
65
+ clearTimeout(entry.timer);
66
+ entry.resolve(message);
67
+ }).catch((error) => {
68
+ rejectPending(error);
69
+ socket.destroy(error);
70
+ });
71
+ transferChain.catch(() => undefined);
72
+ },
73
+ onError(error) {
74
+ rejectPending(error);
75
+ readyReject?.(error);
76
+ socket.destroy();
77
+ },
78
+ });
79
+ socket.on("data", (chunk) => parser.push(chunk));
80
+ let closed = false;
81
+ socket.on("close", () => {
82
+ parser.close();
83
+ const error = new Error("connection closed");
84
+ rejectPending(error);
85
+ writer.close(error);
86
+ cleanupTransfers(error);
87
+ closed = true;
88
+ });
89
+ return {
90
+ socket,
91
+ transfers,
92
+ async request(message, timeoutMs = requestTimeoutMs, transferPlan = {}) {
93
+ if (closed) throw new Error("client transport is closed");
94
+ if (message.id === undefined || message.id === null) throw new Error("request id is required");
95
+ if (pending.has(message.id)) throw new Error("duplicate request id");
96
+ const downloadIds = (transferPlan.downloads || []).map((download) => download.transferId);
97
+ const timeoutError = new Error("request timed out");
98
+ let rejectTimeout;
99
+ const timeout = new Promise((_, reject) => { rejectTimeout = reject; });
100
+ timeout.catch(() => undefined);
101
+ const timer = setTimeout(() => {
102
+ closed = true;
103
+ rejectPending(timeoutError);
104
+ cleanupTransfers(timeoutError).catch(() => {});
105
+ writer.close(timeoutError);
106
+ socket.destroy();
107
+ rejectTimeout(timeoutError);
108
+ }, timeoutMs);
109
+ const execute = async () => {
110
+ try {
111
+ if (transfers) {
112
+ for (const download of transferPlan.downloads || []) await transfers.expectDownload(download);
113
+ const uploadDescriptors = [];
114
+ for (const upload of transferPlan.uploads || []) {
115
+ const result = await transfers.upload(upload.path, upload);
116
+ uploadDescriptors.push({ transferId: result.transferId, field: upload.field, original: upload.original, kind: "upload" });
117
+ }
118
+ message = { ...message, _surfTransfers: { uploads: uploadDescriptors, downloads: (transferPlan.downloads || []).map(({ transferId, field, original }) => ({ transferId, field, original, kind: "download" })) }, _surfPaths: transferPlan.pathRefs || [] };
119
+ }
120
+ const response = new Promise((resolve, reject) => {
121
+ pending.set(message.id, { resolve, reject, timer });
122
+ });
123
+ response.catch(() => undefined);
124
+ try {
125
+ await writer.send(message);
126
+ } catch (error) {
127
+ const entry = pending.get(message.id);
128
+ if (entry) { pending.delete(message.id); clearTimeout(entry.timer); entry.reject(error); }
129
+ await transfers?.cancelDownloads(downloadIds);
130
+ await response.catch(() => undefined);
131
+ throw error;
132
+ }
133
+ const result = await response;
134
+ if (result.error || (transfers && downloadIds.some((id) => transfers.hasDownload(id)))) {
135
+ await transfers?.cancelDownloads(downloadIds);
136
+ if (!result.error) throw new Error("request completed without completing output download");
137
+ }
138
+ return result;
139
+ } catch (error) {
140
+ await transfers?.cancelDownloads(downloadIds);
141
+ throw error;
142
+ }
143
+ };
144
+ try {
145
+ return await Promise.race([execute(), timeout]);
146
+ } finally {
147
+ clearTimeout(timer);
148
+ }
149
+ },
150
+ async closeAsync() {
151
+ if (closed) return transferCleanupPromise;
152
+ closed = true;
153
+ const error = new Error("client transport is closed");
154
+ parser.close();
155
+ rejectPending(error);
156
+ writer.close(error);
157
+ const cleanup = cleanupTransfers(error);
158
+ socket.end();
159
+ await cleanup;
160
+ return cleanup;
161
+ },
162
+ close() {
163
+ return this.closeAsync();
164
+ },
165
+ };
166
+ }
167
+
168
+ module.exports = { DEFAULT_REQUEST_TIMEOUT_MS, openClientTransport };
@@ -10,8 +10,10 @@
10
10
  * Follows the same socket communication pattern as --script mode in cli.cjs.
11
11
  */
12
12
 
13
- const net = require("net");
14
- const { SOCKET_PATH, formatSocketError } = require("./socket-path.cjs");
13
+ const { selectEndpoint } = require("./endpoint.cjs");
14
+ const { openClientTransport } = require("./client-transport.cjs");
15
+ const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
16
+ const { prepareRemoteTool, validateLocalToolPaths } = require("./file-transfer.cjs");
15
17
 
16
18
  // Maximum iterations for loops (safety cap)
17
19
  const MAX_LOOP_ITERATIONS = 100;
@@ -71,48 +73,27 @@ function getAutoWaitCommand(cmd) {
71
73
  * @returns {Promise<object>} - Response from host
72
74
  */
73
75
  function sendDoRequest(toolName, toolArgs, context = {}) {
74
- return new Promise((resolve, reject) => {
75
- const sock = net.createConnection(SOCKET_PATH, () => {
76
- const req = {
77
- type: "tool_request",
78
- method: "execute_tool",
79
- params: { tool: toolName, args: toolArgs },
80
- id: "do-" + Date.now() + "-" + Math.random(),
81
- };
82
- if (context.tabId) req.tabId = context.tabId;
83
- if (context.windowId) req.windowId = context.windowId;
84
- sock.write(JSON.stringify(req) + "\n");
85
- });
86
-
87
- let buf = "";
88
- sock.on("data", (d) => {
89
- buf += d.toString();
90
- const lines = buf.split("\n");
91
- buf = lines.pop();
92
- for (const line of lines) {
93
- if (!line.trim()) continue;
94
- try {
95
- const resp = JSON.parse(line);
96
- sock.end();
97
- resolve(resp);
98
- } catch {
99
- sock.end();
100
- reject(new Error("Invalid JSON response"));
101
- }
102
- }
103
- });
104
-
105
- sock.on("error", (e) => {
106
- reject(new Error(formatSocketError(e)));
107
- });
108
-
109
- const timeoutId = setTimeout(() => {
110
- sock.destroy();
111
- reject(new Error("Request timeout"));
112
- }, 30000);
113
-
114
- sock.on("close", () => clearTimeout(timeoutId));
115
- });
76
+ const request = {
77
+ type: "tool_request",
78
+ method: "execute_tool",
79
+ params: { tool: toolName, args: toolArgs },
80
+ id: "do-" + Date.now() + "-" + Math.random(),
81
+ };
82
+ if (context.tabId) request.tabId = context.tabId;
83
+ if (context.windowId) request.windowId = context.windowId;
84
+ const requestTimeoutMs = context.timeoutMs || resolveRequestDeadlineMs(toolName, toolArgs);
85
+ const endpoint = context.endpoint || selectEndpoint([]).endpoint;
86
+ const prepared = endpoint.kind === "remote" ? prepareRemoteTool(toolName, toolArgs) : (() => { const args = validateLocalToolPaths(toolName, toolArgs); return { args, uploads: [], downloads: [] }; })();
87
+ request.params.args = prepared.args;
88
+ if (context.transport) return context.transport.request(request, requestTimeoutMs, prepared);
89
+ return (async () => {
90
+ const transport = await openClientTransport(endpoint);
91
+ try {
92
+ return await transport.request(request, requestTimeoutMs, prepared);
93
+ } finally {
94
+ await transport.close();
95
+ }
96
+ })();
116
97
  }
117
98
 
118
99
  /**
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 };