chore: remove deprecated APIs: setExitNodeEnabled, ping(disco), fetch, ssh #150

Merged
codinget merged 1 commits from chore/remove-deprecated-apis into main 2026-07-30 00:16:43 +02:00
12 changed files with 15 additions and 488 deletions
@@ -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>(
+1 -84
View File
@@ -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])
}
-1
View File
@@ -3,7 +3,6 @@ export {
WorkerConn,
WorkerTCPListener,
WorkerPacketConn,
WorkerSSHSession,
workerDialer,
} from "./client.js"
export type {
+1 -17
View File
@@ -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"))
}
-16
View File
@@ -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 -147
View File
@@ -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 = {
+1 -75
View File
@@ -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")
+1 -2
View File
@@ -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,
+1 -21
View File
@@ -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>
}
+1 -1
View File
@@ -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 }
+6 -42
View File
@@ -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>
}
+2 -32
View File
@@ -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>