feat(ssh): bump restty and follow upstream tailscale ssh

This commit is contained in:
Aarnav Tale
2026-08-27 14:59:47 -07:00
parent b9aa99c45e
commit 29afac60f6
31 changed files with 1653 additions and 1814 deletions
+41 -54
View File
@@ -1,56 +1,21 @@
const WASM_MODULE_URL = `${__PREFIX__}/hp_ssh.wasm`;
const WASM_HELPER_URL = `${__PREFIX__}/wasm_exec.js`;
declare global {
type HeadplaneSSHFactory = (config: HeadplaneSSHConfig) => HeadplaneSSH;
var __hp_ssh_resolve: ((factory: HeadplaneSSHFactory) => void) | undefined;
var Go: {
new (): {
importObject: WebAssembly.Imports;
run(instance: WebAssembly.Instance): Promise<void>;
argv?: string[];
env?: Record<string, string>;
exit?: (code: number) => void;
};
};
}
interface HeadplaneSSHConfig {
export interface TailnetConfig {
controlURL: string;
preAuthKey: string;
authKey: string;
hostname: string;
onReady: () => void;
onError?: (message: string) => void;
onPanic: (error: string) => void;
}
export interface HeadplaneSSH {
openTunnel(config: TunnelConfig): TunnelSession;
}
interface TunnelConfig {
ipAddress: string;
username: string;
timeout?: number;
onData: (data: string) => void;
onConnect: () => void;
onDisconnect: () => void;
}
export interface TunnelSession {
writeInput(data: string): void;
resize(cols: number, rows: number): void;
close(): void;
}
let resolvedFactory: Promise<HeadplaneSSHFactory> | null = null;
let goHelper: Promise<void> | null = null;
function loadGoHelper(): Promise<void> {
if (typeof globalThis.Go !== "undefined") {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
goHelper ??= new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = WASM_HELPER_URL;
script.crossOrigin = "anonymous";
@@ -58,25 +23,47 @@ function loadGoHelper(): Promise<void> {
script.onerror = () => reject(new Error("Failed to load Go WASM helper"));
document.head.appendChild(script);
});
return goHelper;
}
/**
* One-shot function that loads the Go WASM binary and returns the SSH factory.
* Automatically loads the Go JS helper if it hasn't been loaded yet.
* Boots the Tailscale WASM node and resolves once it has joined the Tailnet.
* Rejects if the pre-auth key is refused or the Go runtime panics.
*/
export async function loadHeadplaneWASM(): Promise<HeadplaneSSHFactory> {
if (!resolvedFactory) {
await loadGoHelper();
export async function connectTailnet(config: TailnetConfig): Promise<IPN> {
await loadGoHelper();
const go = new Go();
const result = await WebAssembly.instantiateStreaming(fetch(WASM_MODULE_URL), go.importObject);
const go = new Go();
const module = await WebAssembly.instantiateStreaming(fetch(WASM_MODULE_URL), go.importObject);
resolvedFactory = new Promise<HeadplaneSSHFactory>((resolve) => {
globalThis.__hp_ssh_resolve = resolve;
// The Go process parks on a channel forever, so returning means it died.
go.run(module.instance).then(() => config.onPanic("Unexpected shutdown"));
const ipn = newIPN({
controlURL: config.controlURL,
authKey: config.authKey,
hostname: config.hostname,
});
let loginStarted = false;
return new Promise((resolve, reject) => {
ipn.run({
notifyState: (state) => {
if (state === "Running") resolve(ipn);
// The backend parks at NeedsLogin until login starts. With an auth key
// set this consumes it rather than opening an interactive flow.
if (state === "NeedsLogin" && !loginStarted) {
loginStarted = true;
ipn.login();
}
},
notifyNetMap: () => {},
// Only reached when the auth key was refused and the node wants a human.
notifyBrowseToURL: () => reject(new Error("Headscale rejected the pre-auth key")),
notifyPanicRecover: (error) => reject(new Error(error)),
});
go.run(result.instance);
}
return resolvedFactory;
});
}