chore: remove deprecated APIs: setExitNodeEnabled, ping(disco), fetch, ssh #150
@@ -47,7 +47,6 @@ export function DebugStuff() {
|
||||
</div>
|
||||
{ipn && (
|
||||
<div className={styles.row}>
|
||||
<FetchDebug />
|
||||
<SuggestExitNodeDebug />
|
||||
</div>
|
||||
)}
|
||||
@@ -399,55 +398,6 @@ function WaitingFileDebug({ id }: { id: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function FetchDebug() {
|
||||
const ipc = use(IpnContext)
|
||||
const [result, setResult] = useState<{
|
||||
status: number
|
||||
statusText: string
|
||||
text: string | null
|
||||
} | null>(null)
|
||||
const [running, setRunning] = useState(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input type="text" defaultValue="" placeholder="URL" />{" "}
|
||||
<button
|
||||
disabled={!ipc || running}
|
||||
onClick={async (e) => {
|
||||
try {
|
||||
if (running) return
|
||||
if (!ipc) return
|
||||
|
||||
setRunning(true)
|
||||
const input = (
|
||||
e.target as HTMLButtonElement
|
||||
).parentElement!.querySelector<HTMLInputElement>('[type="text"]')!
|
||||
const result = await ipc.fetch(input.value)
|
||||
setResult({ status: result.status, statusText: result.statusText, text: null })
|
||||
const text = await result.text()
|
||||
setResult({ status: result.status, statusText: result.statusText, text })
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Fetch (tailscale)
|
||||
</button>{" "}
|
||||
<button disabled={!result} onClick={() => setResult(null)}>
|
||||
Clear output
|
||||
</button>
|
||||
{running && <> (running)</>}
|
||||
<br />
|
||||
{result && (
|
||||
<>
|
||||
{result.status} {result.statusText}
|
||||
<pre>{result.text}</pre>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SuggestExitNodeDebug() {
|
||||
const ipn = use(IpnContext)
|
||||
const [suggested, setSuggested] = useState<Awaited<ReturnType<IPN["suggestExitNode"]>> | null>(
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
IPNFileTarget,
|
||||
IPNIncomingFile,
|
||||
IPNOutgoingFile,
|
||||
IPNSSHSession,
|
||||
IPNService,
|
||||
IPNState,
|
||||
IPNWaitingFile,
|
||||
@@ -20,7 +19,6 @@ import type {
|
||||
DrivePermissions,
|
||||
IpnClient,
|
||||
IpnPacketConn,
|
||||
IpnSSHTermConfig,
|
||||
TLSCertKeyPair,
|
||||
WhoIsResponse,
|
||||
PingType,
|
||||
@@ -38,7 +36,6 @@ import type {
|
||||
PacketConnW2C,
|
||||
DriveW2C,
|
||||
DriveC2W,
|
||||
SSHMsg,
|
||||
WorkerConfig,
|
||||
FileOpsLimits,
|
||||
TransferToken,
|
||||
@@ -440,45 +437,6 @@ export class WorkerPacketConn implements IpnPacketConn, StateTransferable<Transf
|
||||
}
|
||||
}
|
||||
|
||||
// ── WorkerSSHSession ──────────────────────────────────────────────────────────
|
||||
|
||||
export class WorkerSSHSession implements IPNSSHSession {
|
||||
#port: MessagePort
|
||||
#cbs: Partial<IpnSSHTermConfig>
|
||||
#closed = false
|
||||
|
||||
constructor(port: MessagePort, cbs: Partial<IpnSSHTermConfig> = {}) {
|
||||
this.#port = port
|
||||
this.#cbs = cbs
|
||||
port.onmessage = (e: MessageEvent) => this.#onMessage(e.data as SSHMsg)
|
||||
port.start()
|
||||
}
|
||||
|
||||
#onMessage(msg: SSHMsg): void {
|
||||
const cbs = this.#cbs
|
||||
if (msg.type === "write") cbs.writeFn?.(msg.data)
|
||||
else if (msg.type === "writeError") cbs.writeErrorFn?.(msg.err)
|
||||
else if (msg.type === "setReadFn") {
|
||||
cbs.setReadFn?.((data) => this.#port.postMessage({ type: "readData", data } satisfies SSHMsg))
|
||||
} else if (msg.type === "progress") cbs.onConnectionProgress?.(msg.msg)
|
||||
else if (msg.type === "connected") cbs.onConnected?.()
|
||||
else if (msg.type === "done") cbs.onDone?.()
|
||||
}
|
||||
|
||||
resize(rows: number, cols: number): boolean {
|
||||
this.#port.postMessage({ type: "resize", rows, cols } satisfies SSHMsg)
|
||||
return true
|
||||
}
|
||||
|
||||
close(): boolean {
|
||||
if (this.#closed) return false
|
||||
this.#closed = true
|
||||
this.#port.postMessage({ type: "close" } satisfies SSHMsg)
|
||||
this.#port.close()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ── WorkerDriveHandler ────────────────────────────────────────────────────────
|
||||
|
||||
function connectDriveHandler(port: MessagePort, fn: RawDriveHandler): void {
|
||||
@@ -781,14 +739,6 @@ export class IpnWorkerClient implements IpnClientHandle, TransferHost {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "sshResult": {
|
||||
const p = this.#pending.get(msg.id)
|
||||
if (p) {
|
||||
this.#pending.delete(msg.id)
|
||||
p.resolve(msg.port)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -887,14 +837,10 @@ export class IpnWorkerClient implements IpnClientHandle, TransferHost {
|
||||
})
|
||||
}
|
||||
|
||||
async setExitNode(stableNodeID: string): Promise<void> {
|
||||
async setExitNode(stableNodeID: string | null): Promise<void> {
|
||||
await this.#call("setExitNode", [stableNodeID])
|
||||
}
|
||||
|
||||
async setExitNodeEnabled(enabled: boolean): Promise<void> {
|
||||
await this.#call("setExitNodeEnabled", [enabled])
|
||||
}
|
||||
|
||||
async listFileTargets(): Promise<IPNFileTarget[]> {
|
||||
return this.#call("listFileTargets", [])
|
||||
}
|
||||
@@ -973,20 +919,6 @@ export class IpnWorkerClient implements IpnClientHandle, TransferHost {
|
||||
await this.#call("setFunnel", [hostname, port, enabled])
|
||||
}
|
||||
|
||||
async fetch(
|
||||
url: string,
|
||||
): Promise<{ status: number; statusText: string; text(): Promise<string> }> {
|
||||
const res = await this.#call<{ status: number; statusText: string; body: string }>("fetch", [
|
||||
url,
|
||||
])
|
||||
const body = res.body
|
||||
return {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
text: () => Promise.resolve(body),
|
||||
}
|
||||
}
|
||||
|
||||
async whoIs(addr: string, proto?: string): Promise<WhoIsResponse | null> {
|
||||
return this.#call("whoIs", [addr, proto])
|
||||
}
|
||||
@@ -1028,21 +960,6 @@ export class IpnWorkerClient implements IpnClientHandle, TransferHost {
|
||||
await this.#call("setFileOpsConfig", [config])
|
||||
}
|
||||
|
||||
async ssh(
|
||||
host: string,
|
||||
username: string,
|
||||
termConfig: IpnSSHTermConfig,
|
||||
): Promise<WorkerSSHSession> {
|
||||
const port = await this.#call<MessagePort>("ssh", [
|
||||
host,
|
||||
username,
|
||||
termConfig.rows,
|
||||
termConfig.cols,
|
||||
termConfig.timeoutSeconds,
|
||||
])
|
||||
return new WorkerSSHSession(port, termConfig)
|
||||
}
|
||||
|
||||
detachResource(resourceId: number): Promise<TransferToken> {
|
||||
return this.#call("detachResource", [resourceId])
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ export {
|
||||
WorkerConn,
|
||||
WorkerTCPListener,
|
||||
WorkerPacketConn,
|
||||
WorkerSSHSession,
|
||||
workerDialer,
|
||||
} from "./client.js"
|
||||
export type {
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
IPNFileTarget,
|
||||
IPNIncomingFile,
|
||||
IPNOutgoingFile,
|
||||
IPNSSHSession,
|
||||
IPNService,
|
||||
IPNState,
|
||||
IPNWaitingFile,
|
||||
@@ -21,7 +20,6 @@ import type {
|
||||
PingResult,
|
||||
DNSQueryResult,
|
||||
IpnPacketConn,
|
||||
IpnSSHTermConfig,
|
||||
} from "@webnet/tsconnect"
|
||||
import type { RawTransport } from "@webnet/transport"
|
||||
import type { IpnClientHandle, IpnWorkerClientStore } from "./client.js"
|
||||
@@ -132,14 +130,10 @@ export class IpnMainThreadHandle implements IpnClientHandle {
|
||||
this.#ipn.logout()
|
||||
}
|
||||
|
||||
async setExitNode(stableNodeID: string): Promise<void> {
|
||||
async setExitNode(stableNodeID: string | null): Promise<void> {
|
||||
await this.#ipn.setExitNode(stableNodeID)
|
||||
}
|
||||
|
||||
async setExitNodeEnabled(enabled: boolean): Promise<void> {
|
||||
await this.#ipn.setExitNodeEnabled(enabled)
|
||||
}
|
||||
|
||||
async listFileTargets(): Promise<IPNFileTarget[]> {
|
||||
return this.#ipn.listFileTargets()
|
||||
}
|
||||
@@ -195,12 +189,6 @@ export class IpnMainThreadHandle implements IpnClientHandle {
|
||||
await this.#ipn.setFunnel(hostname, port, enabled)
|
||||
}
|
||||
|
||||
async fetch(
|
||||
url: string,
|
||||
): Promise<{ status: number; statusText: string; text(): Promise<string> }> {
|
||||
return this.#ipn.fetch(url)
|
||||
}
|
||||
|
||||
async whoIs(addr: string, proto?: string): Promise<WhoIsResponse | null> {
|
||||
return this.#ipn.whoIs(addr, proto)
|
||||
}
|
||||
@@ -244,10 +232,6 @@ export class IpnMainThreadHandle implements IpnClientHandle {
|
||||
if (config.maxFileSize !== undefined) this.#fsaFileOps.maxFileSize = config.maxFileSize
|
||||
}
|
||||
|
||||
async ssh(host: string, username: string, termConfig: IpnSSHTermConfig): Promise<IPNSSHSession> {
|
||||
return this.#ipn.ssh(host, username, termConfig)
|
||||
}
|
||||
|
||||
sendTransfer(): Promise<{ delivered: boolean }> {
|
||||
return Promise.reject(new Error("transfer not supported on main-thread"))
|
||||
}
|
||||
|
||||
@@ -69,7 +69,6 @@ export type W2C =
|
||||
| { type: "listener"; id: number; addr: string; port: MessagePort; resourceId: number }
|
||||
| { type: "packetconn"; id: number; localAddr: string; port: MessagePort; resourceId: number }
|
||||
| { type: "drivePort"; id: number; port: MessagePort }
|
||||
| { type: "sshResult"; id: number; port: MessagePort }
|
||||
| {
|
||||
type: "transfer"
|
||||
channel: string
|
||||
@@ -153,21 +152,6 @@ export type DriveC2W =
|
||||
| { type: "write"; reqId: number; chunk: Uint8Array }
|
||||
| { type: "end"; reqId: number }
|
||||
|
||||
// ── SSH sub-protocol (single bidirectional port) ─────────────────────────────
|
||||
|
||||
export type SSHMsg =
|
||||
// worker → client
|
||||
| { type: "write"; data: string }
|
||||
| { type: "writeError"; err: string }
|
||||
| { type: "setReadFn" }
|
||||
| { type: "progress"; msg: string }
|
||||
| { type: "connected" }
|
||||
| { type: "done" }
|
||||
// client → worker
|
||||
| { type: "readData"; data: string }
|
||||
| { type: "resize"; rows: number; cols: number }
|
||||
| { type: "close" }
|
||||
|
||||
// ── Stream sub-protocol (ReadableStream fallback for Safari) ─────────────────
|
||||
|
||||
export type StreamMsg =
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { test, suite } from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import {
|
||||
WorkerConn,
|
||||
WorkerTCPListener,
|
||||
WorkerPacketConn,
|
||||
WorkerSSHSession,
|
||||
IpnWorkerClient,
|
||||
} from "./client.js"
|
||||
import { WorkerConn, WorkerTCPListener, WorkerPacketConn, IpnWorkerClient } from "./client.js"
|
||||
import type { TransferHost } from "./client.js"
|
||||
import type { TransferToken } from "./protocol.js"
|
||||
import { pumpStreamToPort, portToReadableStream } from "./protocol.js"
|
||||
@@ -591,146 +585,6 @@ suite("WorkerPacketConn", () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ── WorkerSSHSession ──────────────────────────────────────────────────────────
|
||||
|
||||
suite("WorkerSSHSession", () => {
|
||||
test("resize() sends resize message to worker", async () => {
|
||||
const { port1, port2 } = new MessageChannel()
|
||||
const messages: unknown[] = []
|
||||
port2.onmessage = (e: MessageEvent) => messages.push(e.data)
|
||||
port2.start()
|
||||
|
||||
const session = new WorkerSSHSession(port1)
|
||||
const result = session.resize(24, 80)
|
||||
assert.equal(result, true)
|
||||
await waitFor(() => messages.some((m: unknown) => (m as { type: string }).type === "resize"))
|
||||
const resize = messages.find((m: unknown) => (m as { type: string }).type === "resize") as
|
||||
| { rows: number; cols: number }
|
||||
| undefined
|
||||
assert.ok(resize, "expected resize message")
|
||||
assert.equal(resize!.rows, 24)
|
||||
assert.equal(resize!.cols, 80)
|
||||
port1.close()
|
||||
port2.close()
|
||||
})
|
||||
|
||||
test("close() sends close message to worker", async () => {
|
||||
const { port1, port2 } = new MessageChannel()
|
||||
const messages: unknown[] = []
|
||||
port2.onmessage = (e: MessageEvent) => messages.push(e.data)
|
||||
port2.start()
|
||||
|
||||
const session = new WorkerSSHSession(port1)
|
||||
const result = session.close()
|
||||
assert.equal(result, true)
|
||||
await waitFor(() => messages.some((m: unknown) => (m as { type: string }).type === "close"))
|
||||
port2.close()
|
||||
})
|
||||
|
||||
test("close() is idempotent — returns false on second call", () => {
|
||||
const { port1, port2 } = new MessageChannel()
|
||||
const session = new WorkerSSHSession(port1)
|
||||
assert.equal(session.close(), true)
|
||||
assert.equal(session.close(), false)
|
||||
port2.close()
|
||||
})
|
||||
|
||||
test("write callback fires when worker sends write", async () => {
|
||||
const { port1, port2 } = new MessageChannel()
|
||||
const received: string[] = []
|
||||
const _session = new WorkerSSHSession(port1, { writeFn: (d) => received.push(d) })
|
||||
|
||||
port2.postMessage({ type: "write", data: "hello" })
|
||||
await waitFor(() => received.length > 0)
|
||||
assert.deepEqual(received, ["hello"])
|
||||
port2.close()
|
||||
port1.close()
|
||||
})
|
||||
|
||||
test("writeError callback fires on writeError message", async () => {
|
||||
const { port1, port2 } = new MessageChannel()
|
||||
const errors: string[] = []
|
||||
const _session = new WorkerSSHSession(port1, { writeErrorFn: (e) => errors.push(e) })
|
||||
|
||||
port2.postMessage({ type: "writeError", err: "ssh error" })
|
||||
await waitFor(() => errors.length > 0)
|
||||
assert.deepEqual(errors, ["ssh error"])
|
||||
port2.close()
|
||||
port1.close()
|
||||
})
|
||||
|
||||
test("onConnectionProgress fires on progress message", async () => {
|
||||
const { port1, port2 } = new MessageChannel()
|
||||
const msgs: string[] = []
|
||||
const _session = new WorkerSSHSession(port1, { onConnectionProgress: (m) => msgs.push(m) })
|
||||
|
||||
port2.postMessage({ type: "progress", msg: "connecting..." })
|
||||
await waitFor(() => msgs.length > 0)
|
||||
assert.deepEqual(msgs, ["connecting..."])
|
||||
port2.close()
|
||||
port1.close()
|
||||
})
|
||||
|
||||
test("onConnected fires on connected message", async () => {
|
||||
const { port1, port2 } = new MessageChannel()
|
||||
let connected = false
|
||||
const _session = new WorkerSSHSession(port1, {
|
||||
onConnected: () => {
|
||||
connected = true
|
||||
},
|
||||
})
|
||||
|
||||
port2.postMessage({ type: "connected" })
|
||||
await waitFor(() => connected)
|
||||
assert.equal(connected, true)
|
||||
port2.close()
|
||||
port1.close()
|
||||
})
|
||||
|
||||
test("onDone fires on done message", async () => {
|
||||
const { port1, port2 } = new MessageChannel()
|
||||
let done = false
|
||||
const _session = new WorkerSSHSession(port1, {
|
||||
onDone: () => {
|
||||
done = true
|
||||
},
|
||||
})
|
||||
|
||||
port2.postMessage({ type: "done" })
|
||||
await waitFor(() => done)
|
||||
assert.equal(done, true)
|
||||
port2.close()
|
||||
port1.close()
|
||||
})
|
||||
|
||||
test("setReadFn message wires up a readData sender", async () => {
|
||||
const { port1, port2 } = new MessageChannel()
|
||||
let capturedReadFn: ((data: string) => void) | null = null
|
||||
const messages: unknown[] = []
|
||||
|
||||
port2.onmessage = (e: MessageEvent) => messages.push(e.data)
|
||||
port2.start()
|
||||
|
||||
const _session = new WorkerSSHSession(port1, {
|
||||
setReadFn: (fn) => {
|
||||
capturedReadFn = fn
|
||||
},
|
||||
})
|
||||
|
||||
port2.postMessage({ type: "setReadFn" })
|
||||
await waitFor(() => capturedReadFn !== null)
|
||||
assert.ok(capturedReadFn, "setReadFn should have been called")
|
||||
;(capturedReadFn as (data: string) => void)("user input")
|
||||
await waitFor(() => messages.some((m: unknown) => (m as { type: string }).type === "readData"))
|
||||
const readData = messages.find((m: unknown) => (m as { type: string }).type === "readData") as
|
||||
| { data: string }
|
||||
| undefined
|
||||
assert.ok(readData, "expected readData message")
|
||||
assert.equal(readData!.data, "user input")
|
||||
port2.close()
|
||||
})
|
||||
})
|
||||
|
||||
// ── State transfer: proxies ───────────────────────────────────────────────────
|
||||
|
||||
const connToken: TransferToken = {
|
||||
|
||||
@@ -19,7 +19,6 @@ import type {
|
||||
PacketConnW2C,
|
||||
DriveW2C,
|
||||
DriveC2W,
|
||||
SSHMsg,
|
||||
WorkerConfig,
|
||||
FileOpsLimits,
|
||||
} from "./protocol.js"
|
||||
@@ -354,57 +353,6 @@ function bridgeDriveHandler(): { port: MessagePort; handler: RawDriveHandler } {
|
||||
return { port: port2, handler }
|
||||
}
|
||||
|
||||
// ── Worker-side SSH bridge ────────────────────────────────────────────────────
|
||||
|
||||
function bridgeSSH(
|
||||
host: string,
|
||||
username: string,
|
||||
rows: number,
|
||||
cols: number,
|
||||
timeoutSeconds?: number,
|
||||
): { port: MessagePort } {
|
||||
const ch = new MessageChannel()
|
||||
const { port1, port2 } = ch
|
||||
|
||||
let readFn: ((data: string) => void) | null = null
|
||||
|
||||
const session = ipn!.ssh(host, username, {
|
||||
writeFn: (data) => port1.postMessage({ type: "write", data } satisfies SSHMsg),
|
||||
writeErrorFn: (err) => port1.postMessage({ type: "writeError", err } satisfies SSHMsg),
|
||||
setReadFn: (fn) => {
|
||||
readFn = fn
|
||||
port1.postMessage({ type: "setReadFn" } satisfies SSHMsg)
|
||||
},
|
||||
rows,
|
||||
cols,
|
||||
timeoutSeconds,
|
||||
onConnectionProgress: (msg) => port1.postMessage({ type: "progress", msg } satisfies SSHMsg),
|
||||
onConnected: () => port1.postMessage({ type: "connected" } satisfies SSHMsg),
|
||||
onDone: () => port1.postMessage({ type: "done" } satisfies SSHMsg),
|
||||
})
|
||||
|
||||
// session is a Promise<IPNSSHSession> since IPN.ssh() is now async.
|
||||
// We need to handle the case where it hasn't resolved yet.
|
||||
let resolvedSession: { resize(r: number, c: number): boolean; close(): boolean } | null = null
|
||||
Promise.resolve(session).then((s) => {
|
||||
resolvedSession = s
|
||||
})
|
||||
|
||||
port1.onmessage = (e: MessageEvent) => {
|
||||
const msg = e.data as SSHMsg
|
||||
if (msg.type === "readData") {
|
||||
readFn?.(msg.data)
|
||||
} else if (msg.type === "resize") {
|
||||
resolvedSession?.resize(msg.rows, msg.cols)
|
||||
} else if (msg.type === "close") {
|
||||
resolvedSession?.close()
|
||||
}
|
||||
}
|
||||
port1.start()
|
||||
|
||||
return { port: port2 }
|
||||
}
|
||||
|
||||
// ── Message handler ───────────────────────────────────────────────────────────
|
||||
|
||||
async function handleCall(
|
||||
@@ -438,11 +386,7 @@ async function handleCall(
|
||||
ok(undefined)
|
||||
break
|
||||
case "setExitNode":
|
||||
await ipn.setExitNode(args[0] as string)
|
||||
ok(undefined)
|
||||
break
|
||||
case "setExitNodeEnabled":
|
||||
await ipn.setExitNodeEnabled(args[0] as boolean)
|
||||
await ipn.setExitNode(args[0] as string | null)
|
||||
ok(undefined)
|
||||
break
|
||||
case "listFileTargets":
|
||||
@@ -577,11 +521,6 @@ async function handleCall(
|
||||
await ipn.setFunnel(args[0] as string, args[1] as number, args[2] as boolean)
|
||||
ok(undefined)
|
||||
break
|
||||
case "fetch": {
|
||||
const res = await ipn.fetch(args[0] as string)
|
||||
ok({ status: res.status, statusText: res.statusText, body: await res.text() })
|
||||
break
|
||||
}
|
||||
case "whoIs":
|
||||
ok(await ipn.whoIs(args[0] as string, args[1] as string | undefined))
|
||||
break
|
||||
@@ -618,19 +557,6 @@ async function handleCall(
|
||||
send(port, msg, [drivePort])
|
||||
break
|
||||
}
|
||||
case "ssh": {
|
||||
const [host, username, rows, cols, timeoutSeconds] = args as [
|
||||
string,
|
||||
string,
|
||||
number,
|
||||
number,
|
||||
number | undefined,
|
||||
]
|
||||
const { port: sshPort } = bridgeSSH(host, username, rows, cols, timeoutSeconds)
|
||||
const msg: W2C = { type: "sshResult", id, port: sshPort }
|
||||
send(port, msg, [sshPort])
|
||||
break
|
||||
}
|
||||
case "setFileOpsConfig": {
|
||||
if (!fileOps) {
|
||||
fail("fileOps not configured")
|
||||
|
||||
@@ -28,14 +28,13 @@ export {
|
||||
WebStorageState,
|
||||
getIceServers,
|
||||
} from "./helpers.js"
|
||||
export type { IpnClient, IpnPacketConn, IpnSSHTermConfig } from "./interface.js"
|
||||
export type { IpnClient, IpnPacketConn } from "./interface.js"
|
||||
|
||||
export type {
|
||||
TLSDialOptions,
|
||||
IPNConfig,
|
||||
IPNCallbacks,
|
||||
IPNState,
|
||||
IPNSSHSession,
|
||||
IPNStateStorage,
|
||||
IPNNetMap,
|
||||
IPNNetMapNode,
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
IPNDrivePeer,
|
||||
IPNFileTarget,
|
||||
IPNService,
|
||||
IPNSSHSession,
|
||||
IPNState,
|
||||
IPNWaitingFile,
|
||||
PingResult,
|
||||
@@ -17,18 +16,6 @@ import type {
|
||||
} from "./types.js"
|
||||
import type { IPNRunOptions } from "./ipn.js"
|
||||
|
||||
export type IpnSSHTermConfig = {
|
||||
writeFn: (data: string) => void
|
||||
writeErrorFn: (err: string) => void
|
||||
setReadFn: (readFn: (data: string) => void) => void
|
||||
rows: number
|
||||
cols: number
|
||||
timeoutSeconds?: number
|
||||
onConnectionProgress: (message: string) => void
|
||||
onConnected: () => void
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
export interface IpnPacketConn {
|
||||
get closed(): boolean
|
||||
close(): void | Promise<void>
|
||||
@@ -45,8 +32,7 @@ export interface IpnClient {
|
||||
login(): void
|
||||
logout(): void
|
||||
shutdown(): Promise<void>
|
||||
setExitNode(stableNodeID: string): Promise<void>
|
||||
setExitNodeEnabled(enabled: boolean): Promise<void>
|
||||
setExitNode(stableNodeID: string | null): Promise<void>
|
||||
listFileTargets(): Promise<IPNFileTarget[]>
|
||||
sendFile(
|
||||
stableNodeID: string,
|
||||
@@ -72,7 +58,6 @@ export interface IpnClient {
|
||||
keyPEM: string,
|
||||
): Promise<RawListener & AsyncIterable<RawTransport>>
|
||||
setFunnel(hostname: string, port: number, enabled: boolean): Promise<void>
|
||||
fetch(url: string): Promise<{ status: number; statusText: string; text(): Promise<string> }>
|
||||
whoIs(addr: string, proto?: string): Promise<WhoIsResponse | null>
|
||||
queryDNS(name: string, type?: number): Promise<DNSQueryResult>
|
||||
ping(ip: string, type?: PingType, size?: number): Promise<PingResult>
|
||||
@@ -81,9 +66,4 @@ export interface IpnClient {
|
||||
localAPI(method: string, path: string, body?: string): Promise<{ status: number; body: string }>
|
||||
listDrivePeers(): Promise<IPNDrivePeer[]>
|
||||
serveDrive(fn: RawDriveHandler): void | Promise<void>
|
||||
ssh(
|
||||
host: string,
|
||||
username: string,
|
||||
termConfig: IpnSSHTermConfig,
|
||||
): IPNSSHSession | Promise<IPNSSHSession>
|
||||
}
|
||||
|
||||
@@ -930,7 +930,7 @@ suite(
|
||||
})
|
||||
assert.equal(selected, EXIT_NODE_ID)
|
||||
|
||||
await ipn.setExitNode("")
|
||||
await ipn.setExitNode(null)
|
||||
const cleared = await poll("exit node cleared", 15_000, async () => {
|
||||
const { body } = await ipn.localAPI("GET", "/localapi/v0/prefs")
|
||||
const prefs = JSON.parse(body) as { ExitNodeID?: string }
|
||||
|
||||
@@ -23,7 +23,6 @@ import type {
|
||||
IPNNetMap,
|
||||
IPNOutgoingFile,
|
||||
IPNService,
|
||||
IPNSSHSession,
|
||||
IPNState,
|
||||
IPNWaitingFile,
|
||||
PacketConn as RawPacketConn,
|
||||
@@ -35,7 +34,7 @@ import type {
|
||||
TLSDialOptions,
|
||||
WhoIsResponse,
|
||||
} from "./types.js"
|
||||
import type { IpnClient, IpnPacketConn as IIpnPacketConn, IpnSSHTermConfig } from "./interface.js"
|
||||
import type { IpnClient, IpnPacketConn as IIpnPacketConn } from "./interface.js"
|
||||
|
||||
/**
|
||||
* Options for {@link IPN.run}. All callbacks are optional; the current state
|
||||
@@ -247,26 +246,13 @@ export class IPN implements IpnClient {
|
||||
* Select an exit node by stable node ID. Pass an empty string to clear the
|
||||
* exit node. The target peer must have `exitNodeOption: true` in the netmap.
|
||||
*/
|
||||
async setExitNode(stableNodeID: string): Promise<void> {
|
||||
async setExitNode(stableNodeID: string | null): Promise<void> {
|
||||
this.#assertRunning("setExitNode")
|
||||
if (stableNodeID === null) stableNodeID = ""
|
||||
assertString(stableNodeID, "stableNodeID", "IPN.setExitNode")
|
||||
await this.#raw.setExitNode(stableNodeID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the most recently used exit node on or off. Useful for a simple
|
||||
* on/off switch without needing to re-specify the node ID.
|
||||
*/
|
||||
async setExitNodeEnabled(enabled: boolean): Promise<void> {
|
||||
this.#assertRunning("setExitNodeEnabled")
|
||||
if (typeof enabled !== "boolean") {
|
||||
throw new TypeError(
|
||||
`IPN.setExitNodeEnabled: expected enabled to be a boolean, got ${typeOf(enabled)}`,
|
||||
)
|
||||
}
|
||||
await this.#raw.setExitNodeEnabled(enabled)
|
||||
}
|
||||
|
||||
/** List peers that can receive Taildrop files. Requires fileOps in the IPN config. */
|
||||
async listFileTargets(): Promise<IPNFileTarget[]> {
|
||||
this.#assertRunning("listFileTargets")
|
||||
@@ -416,31 +402,6 @@ export class IPN implements IpnClient {
|
||||
await this.#raw.setFunnel(hostname, port, enabled)
|
||||
}
|
||||
|
||||
/** Thin pass-through for the Go-backed fetch helper. */
|
||||
async fetch(url: string): Promise<{
|
||||
status: number
|
||||
statusText: string
|
||||
text(): Promise<string>
|
||||
}> {
|
||||
this.#assertRunning("fetch")
|
||||
assertString(url, "url", "IPN.fetch")
|
||||
return this.#raw.fetch(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin pass-through for the Tailscale SSH terminal session. The termConfig
|
||||
* object is handed directly to Go; all its callbacks must be present.
|
||||
*/
|
||||
async ssh(host: string, username: string, termConfig: IpnSSHTermConfig): Promise<IPNSSHSession> {
|
||||
this.#assertRunning("ssh")
|
||||
assertString(host, "host", "IPN.ssh")
|
||||
assertString(username, "username", "IPN.ssh")
|
||||
if (termConfig === null || typeof termConfig !== "object") {
|
||||
throw new TypeError(`IPN.ssh: expected termConfig to be an object, got ${typeOf(termConfig)}`)
|
||||
}
|
||||
return this.#raw.ssh(host, username, termConfig)
|
||||
}
|
||||
|
||||
async whoIs(addr: string, proto?: string): Promise<WhoIsResponse | null> {
|
||||
this.#assertRunning("whoIs")
|
||||
assertString(addr, "addr", "IPN.whoIs")
|
||||
@@ -464,6 +425,9 @@ export class IPN implements IpnClient {
|
||||
if (typeof size !== "number" || !Number.isInteger(size) || size < 0) {
|
||||
throw new RangeError(`IPN.ping: size must be a non-negative integer, got ${size}`)
|
||||
}
|
||||
if ((type as string) === "disco") {
|
||||
throw new TypeError("disco ping is unsupported on wasm targets")
|
||||
}
|
||||
return this.#raw.ping(ip, type, size) as Promise<PingResult>
|
||||
}
|
||||
|
||||
|
||||
@@ -201,11 +201,6 @@ export type IPNNetMap = {
|
||||
lockedOut: boolean
|
||||
}
|
||||
|
||||
export interface IPNSSHSession {
|
||||
resize(rows: number, cols: number): boolean
|
||||
close(): boolean
|
||||
}
|
||||
|
||||
/** A stream-oriented connection (TCP or connected UDP) through the Tailscale network. */
|
||||
export interface Conn {
|
||||
/** Read up to 64 KiB from the connection. */
|
||||
@@ -348,7 +343,7 @@ export type WhoIsResponse = {
|
||||
}
|
||||
|
||||
/** Ping type passed to {@link IPN.ping}. */
|
||||
export type PingType = "disco" | "TSMP" | "ICMP" | "peerapi"
|
||||
export type PingType = "TSMP" | "ICMP" | "peerapi"
|
||||
|
||||
/** Result of a {@link IPN.ping} call. */
|
||||
export type PingResult = {
|
||||
@@ -395,11 +390,6 @@ export interface IPN {
|
||||
* exit node. The chosen node must have `exitNodeOption: true` in the netmap.
|
||||
*/
|
||||
setExitNode(stableNodeID: string): Promise<void>
|
||||
/**
|
||||
* Toggle the most recently used exit node on or off. Useful for a simple
|
||||
* on/off switch when the desired exit node is already known.
|
||||
*/
|
||||
setExitNodeEnabled(enabled: boolean): Promise<void>
|
||||
/** List peers that can receive Taildrop files. Requires fileOps in the IPN config. */
|
||||
listFileTargets(): Promise<IPNFileTarget[]>
|
||||
/**
|
||||
@@ -420,26 +410,6 @@ export interface IPN {
|
||||
openWaitingFile(name: string): Promise<ReadableStream<Uint8Array>>
|
||||
/** Delete a received file by name. */
|
||||
deleteWaitingFile(name: string): Promise<void>
|
||||
ssh(
|
||||
host: string,
|
||||
username: string,
|
||||
termConfig: {
|
||||
writeFn: (data: string) => void
|
||||
writeErrorFn: (err: string) => void
|
||||
setReadFn: (readFn: (data: string) => void) => void
|
||||
rows: number
|
||||
cols: number
|
||||
timeoutSeconds?: number
|
||||
onConnectionProgress: (message: string) => void
|
||||
onConnected: () => void
|
||||
onDone: () => void
|
||||
},
|
||||
): IPNSSHSession
|
||||
fetch(url: string): Promise<{
|
||||
status: number
|
||||
statusText: string
|
||||
text: () => Promise<string>
|
||||
}>
|
||||
|
||||
/**
|
||||
* Dial a TCP connection to a Tailscale peer.
|
||||
@@ -531,7 +501,7 @@ export interface IPN {
|
||||
* Ping a tailnet peer and return latency and path information.
|
||||
*
|
||||
* @param ip - IP address to ping (tailnet IP of the target node)
|
||||
* @param type - ping type: `"TSMP"` (default), `"disco"`, `"ICMP"`, or `"peerapi"`
|
||||
* @param type - ping type: `"TSMP"` (default), `"ICMP"`, or `"peerapi"`
|
||||
* @param size - payload size in bytes (default 0)
|
||||
*/
|
||||
ping(ip: string, type?: PingType, size?: number): Promise<PingResult>
|
||||
|
||||
Reference in New Issue
Block a user