feat(protocol): expose structured host connection targets

This commit is contained in:
Mohamed Boudra
2026-11-02 04:56:27 +01:00
parent efc64a5443
commit 4992b8d3da
3 changed files with 67 additions and 1 deletions
+28
View File
@@ -1,5 +1,33 @@
import { expect, test } from "vitest";
import { parseHostTarget } from "./host-target.js";
import { serializeSshTransportUri } from "./ssh-transport.js";
import { serializeSocketHostTarget } from "./host-target.js";
test("SSH and local socket targets round trip through their serializers", () => {
const ssh = parseHostTarget("ssh://deploy@build-box:2222?daemonPort=7777");
if (ssh.kind !== "ssh") throw new Error("expected SSH");
expect(parseHostTarget(serializeSshTransportUri(ssh.ssh))).toEqual(ssh);
for (const [type, path] of [
["unix", "/tmp/paseo.sock"],
["pipe", "\\\\.\\pipe\\paseo"],
] as const) {
const target = parseHostTarget(`${type}://${path}`);
expect(parseHostTarget(serializeSocketHostTarget(type, path))).toEqual(target);
}
});
test.each([
["example.com:6767", { type: "directTcp", endpoint: "example.com:6767", useTls: false }],
[
"tcp://example.com:6767?ssl=true",
{ type: "directTcp", endpoint: "example.com:6767", useTls: true },
],
["wss://example.com/ws", { type: "directTcp", endpoint: "example.com:443", useTls: true }],
["unix:///tmp/paseo.sock", { type: "directSocket", path: "/tmp/paseo.sock" }],
["pipe://mypipe", { type: "directPipe", path: "mypipe" }],
])("%s exposes its structured connection", (input, connection) => {
expect(parseHostTarget(input)).toMatchObject({ kind: "socket", connection });
});
test.each([
["127.0.0.1:6767", "tcp://127.0.0.1:6767"],
+26 -1
View File
@@ -14,12 +14,23 @@ export type HostTarget =
| {
kind: "socket";
url: string;
connection:
| { type: "directTcp"; endpoint: string; useTls: boolean }
| { type: "directSocket"; path: string }
| { type: "directPipe"; path: string }
| { type: "relay"; offer: ConnectionOffer };
socketPath?: string;
password?: string;
offer?: ConnectionOffer;
description: string;
};
/** Serialize a local socket target in the shared host grammar. */
export function serializeSocketHostTarget(type: "unix" | "pipe", path: string): string {
if (!path || path === "~") throw new Error("Invalid socket path");
return `${type}://${path}`;
}
/** The shared saved-host and CLI grammar. Errors never echo credential-bearing input. */
export function parseHostTarget(input: string): HostTarget {
try {
@@ -57,6 +68,9 @@ function parse(input: string): HostTarget {
const pipe = input.startsWith("pipe://") || input.startsWith("\\\\.\\pipe\\");
return {
kind: "socket",
connection: pipe
? { type: "directPipe", path: socketPath }
: { type: "directSocket", path: socketPath },
socketPath,
url: pipe ? "ws://localhost/ws" : `ws+unix://${socketPath}:/ws`,
description: `${pipe ? "pipe" : "unix"}://${socketPath}`,
@@ -67,6 +81,7 @@ function parse(input: string): HostTarget {
const endpoint = validatedEndpoint(/^\d+$/.test(input) ? `127.0.0.1:${input}` : input);
return {
kind: "socket",
connection: { type: "directTcp", endpoint, useTls: false },
url: buildDaemonWebSocketUrl(endpoint, { useTls: false }),
description: `tcp://${endpoint}`,
};
@@ -75,6 +90,7 @@ function relayTarget(offer: ConnectionOffer, password?: string): HostTarget {
const endpoint = validatedEndpoint(offer.relay.endpoint);
return {
kind: "socket",
connection: { type: "relay", offer },
url: buildRelayWebSocketUrl({
endpoint,
serverId: offer.serverId,
@@ -97,6 +113,7 @@ function tcpTarget(input: string): HostTarget {
);
return {
kind: "socket",
connection: { type: "directTcp", endpoint, useTls: parts.useTls },
url: buildDaemonWebSocketUrl(endpoint, { useTls: parts.useTls }),
password: parts.password,
description: `tcp://${endpoint}`,
@@ -109,7 +126,15 @@ function websocketTarget(input: string): HostTarget {
// Unknown query parameters may themselves contain credentials; discovery shows only the origin.
if (url.pathname === "/") url.pathname = "/ws";
url.searchParams.delete("password");
return { kind: "socket", url: url.toString(), password, description: url.origin };
const useTls = url.protocol === "wss:";
const endpoint = `${url.hostname}:${url.port || (useTls ? "443" : "80")}`;
return {
kind: "socket",
connection: { type: "directTcp", endpoint, useTls },
url: url.toString(),
password,
description: url.origin,
};
}
function sshTarget(input: string): HostTarget {
+13
View File
@@ -60,6 +60,19 @@ export function parseSshTransportUri(value: string): SshTransportTarget {
};
}
/** Serialize an SSH target without daemon credentials. */
export function serializeSshTransportUri(target: SshTransportTarget): string {
const host = validateSshHost(target.host);
const separator = host.lastIndexOf("@");
const username = separator < 0 ? "" : `${encodeURIComponent(host.slice(0, separator))}@`;
const hostname = host.slice(separator + 1);
const authority = hostname.includes(":") ? `[${hostname}]` : hostname;
const port = target.sshPort === undefined ? "" : `:${validatePort(target.sshPort, "SSH port")}`;
const daemonPort = validatePort(target.daemonPort, "Daemon port");
const query = daemonPort === DEFAULT_SSH_DAEMON_PORT ? "" : `?daemonPort=${daemonPort}`;
return `ssh://${username}${authority}${port}${query}`;
}
export function buildSshTunnelArgs(target: SshTransportTarget): string[] {
const host = validateSshHost(target.host);
const daemonPort = validatePort(target.daemonPort, "Daemon port");