GOOS=js GOARCH=wasm ./tool/go test -run=^$ -tags=netgo,omitidna,omitpemdecrypt,osusergo,tailscale_go,ts_omit_ace ./cmd/tsconnect/wasm
production wasm build with the same feature tags and release flags
verified golang.org/x/crypto/ssh is absent from go list -deps
raw stripped wasm shrank from 41,216,909 to 40,376,172 bytes (840,737 bytes, 2.0%)
AI disclosure: implemented and reviewed with Codex (gpt-5.6-sol).
Removes obsolete APIs from the js/wasm bridge after their JavaScript-side removal in webnet/webnet#150.
- remove the `ssh` and `fetch` bridge methods, including the direct `golang.org/x/crypto/ssh` dependency
- remove `setExitNodeEnabled`; callers clear the exit node through `setExitNode`
- stop accepting the non-functional `disco` ping type
- leave core Tailscale behavior unchanged
Implements the remaining wasm bridge work from [webnet/webnet#141](https://gitea.codinget.me/webnet/webnet/issues/141), [#144](https://gitea.codinget.me/webnet/webnet/issues/144), and [#145](https://gitea.codinget.me/webnet/webnet/issues/145).
Checks:
- `GOOS=js GOARCH=wasm ./tool/go test -run=^$ -tags=netgo,omitidna,omitpemdecrypt,osusergo,tailscale_go,ts_omit_ace ./cmd/tsconnect/wasm`
- production wasm build with the same feature tags and release flags
- verified `golang.org/x/crypto/ssh` is absent from `go list -deps`
- raw stripped wasm shrank from 41,216,909 to 40,376,172 bytes (840,737 bytes, 2.0%)
AI disclosure: implemented and reviewed with Codex (`gpt-5.6-sol`).
The Go change itself is clean and I could reproduce the claims. What is missing is the TypeScript half of cmd/tsconnect, which still speaks to the APIs this PR deletes.
Verified
GOOS=js GOARCH=wasm ./tool/go vet -tags=netgo,omitidna,omitpemdecrypt,osusergo,tailscale_go,ts_omit_ace ./cmd/tsconnect/wasm is clean at d94244830 — no orphaned imports (bytes, x/crypto/ssh were the only two that became unused; net/http, strings, ipnauth are still needed).
golang.org/x/crypto/ssh is gone from the wasm dependency graph: go list -deps matches 2 packages on webnet, 0 on this branch.
The ping narrowing matches the consumer: packages/tsconnect/src/types.ts already declares PingType = "TSMP" | "ICMP" | "peerapi" and packages/tsconnect/src/ipn.ts:428 already rejects "disco" before it reaches the bridge.
No stale references to ssh/fetch/setExitNodeEnabled remain in cmd/tsconnect/README.md, README.pkg.md, or the Go sources.
1. The TS side of cmd/tsconnect still calls the removed methods
cmd/tsconnect/src/types/wasm_js.d.ts:16 and :32 still declare ssh(...) and fetch(url) on the IPN interface. Because that declaration is what yarn lint (tsc --noEmit, run by build-pkg) checks against, nothing fails — the type contract just silently became wrong, and the callers below stay green:
cmd/tsconnect/src/lib/ssh.ts:56 calls ipn.ssh(def.hostname, def.username, {...}). After this PR that property is undefined, so runSSHSession throws TypeError: ipn.ssh is not a function on first use.
cmd/tsconnect/src/pkg/pkg.ts:40 re-exports runSSHSession from the published package entrypoint. build-pkg bundles src/pkg/pkg.ts (common.go:111) and generates pkg.d.ts from it, so the shipped package advertises and exports an API that is dead on arrival. This is not hypothetical for downstream: packages/tsconnect/build.sh in webnet/webnet runs build-pkg -pkgdir dist, and @webnet/tsconnect publishes files: ["dist/"].
Because runSSHSession stays in the entrypoint's export graph, esbuild keeps bundling xterm, xterm-addon-fit and xterm-addon-web-links into dist/pkg.js for code that can no longer run — which eats into the 2.0% wasm win the description reports.
The dev demo app (src/app/app.tsx:66, src/app/ssh.tsx) still renders an SSH tab that now breaks at runtime under go run . dev.
Suggested fix, smallest to largest:
At minimum, drop the ssh and fetch declarations from wasm_js.d.ts. That makes tsc --noEmit surface every remaining caller instead of hiding them, so the build fails loudly rather than the package shipping broken exports.
Then remove src/lib/ssh.ts, src/app/ssh.tsx and its use in app.tsx, the runSSHSession re-export in pkg.ts, and the three xterm* entries in cmd/tsconnect/package.json.
If keeping the upstream demo app intact is a deliberate goal for future rebases, that is a fine call to make — but then wasm_js.d.ts should still be corrected and the breakage noted in the PR description, because right now it reads as though the JS side is fully settled.
2. Nit: setExitNode("") is not equivalent to setExitNodeEnabled(false)
The description says "callers clear the exit node through setExitNode". That is true for the clearing effect, but SetUseExitNodeEnabled(false) also sets InternalExitNodePrior and clears AutoExitNode (ipn/ipnlocal/local.go:5130-5136), which is what makes a later re-enable possible. jsIPN.setExitNode only sets ExitNodeID. Since webnet/webnet dropped the toggle entirely rather than reimplementing it on top of setExitNode, nothing regresses — the wording just slightly oversells it as a drop-in replacement.
Once the wasm_js.d.ts declarations are dropped, this is good to go from my side.
Reviewed with Claude Opus 5.
## Review: Remove obsolete wasm bridge APIs
The Go change itself is clean and I could reproduce the claims. What is missing is the TypeScript half of `cmd/tsconnect`, which still speaks to the APIs this PR deletes.
### Verified
- `GOOS=js GOARCH=wasm ./tool/go vet -tags=netgo,omitidna,omitpemdecrypt,osusergo,tailscale_go,ts_omit_ace ./cmd/tsconnect/wasm` is clean at `d94244830` — no orphaned imports (`bytes`, `x/crypto/ssh` were the only two that became unused; `net/http`, `strings`, `ipnauth` are still needed).
- `golang.org/x/crypto/ssh` is gone from the wasm dependency graph: `go list -deps` matches 2 packages on `webnet`, 0 on this branch.
- The `ping` narrowing matches the consumer: `packages/tsconnect/src/types.ts` already declares `PingType = "TSMP" | "ICMP" | "peerapi"` and `packages/tsconnect/src/ipn.ts:428` already rejects `"disco"` before it reaches the bridge.
- No stale references to `ssh`/`fetch`/`setExitNodeEnabled` remain in `cmd/tsconnect/README.md`, `README.pkg.md`, or the Go sources.
### 1. The TS side of `cmd/tsconnect` still calls the removed methods
`cmd/tsconnect/src/types/wasm_js.d.ts:16` and `:32` still declare `ssh(...)` and `fetch(url)` on the `IPN` interface. Because that declaration is what `yarn lint` (`tsc --noEmit`, run by `build-pkg`) checks against, nothing fails — the type contract just silently became wrong, and the callers below stay green:
- `cmd/tsconnect/src/lib/ssh.ts:56` calls `ipn.ssh(def.hostname, def.username, {...})`. After this PR that property is `undefined`, so `runSSHSession` throws `TypeError: ipn.ssh is not a function` on first use.
- `cmd/tsconnect/src/pkg/pkg.ts:40` re-exports `runSSHSession` from the **published package entrypoint**. `build-pkg` bundles `src/pkg/pkg.ts` (`common.go:111`) and generates `pkg.d.ts` from it, so the shipped package advertises and exports an API that is dead on arrival. This is not hypothetical for downstream: `packages/tsconnect/build.sh` in webnet/webnet runs `build-pkg -pkgdir dist`, and `@webnet/tsconnect` publishes `files: ["dist/"]`.
- Because `runSSHSession` stays in the entrypoint's export graph, esbuild keeps bundling `xterm`, `xterm-addon-fit` and `xterm-addon-web-links` into `dist/pkg.js` for code that can no longer run — which eats into the 2.0% wasm win the description reports.
- The dev demo app (`src/app/app.tsx:66`, `src/app/ssh.tsx`) still renders an SSH tab that now breaks at runtime under `go run . dev`.
Suggested fix, smallest to largest:
1. At minimum, drop the `ssh` and `fetch` declarations from `wasm_js.d.ts`. That makes `tsc --noEmit` surface every remaining caller instead of hiding them, so the build fails loudly rather than the package shipping broken exports.
2. Then remove `src/lib/ssh.ts`, `src/app/ssh.tsx` and its use in `app.tsx`, the `runSSHSession` re-export in `pkg.ts`, and the three `xterm*` entries in `cmd/tsconnect/package.json`.
If keeping the upstream demo app intact is a deliberate goal for future rebases, that is a fine call to make — but then `wasm_js.d.ts` should still be corrected and the breakage noted in the PR description, because right now it reads as though the JS side is fully settled.
### 2. Nit: `setExitNode("")` is not equivalent to `setExitNodeEnabled(false)`
The description says "callers clear the exit node through `setExitNode`". That is true for the clearing effect, but `SetUseExitNodeEnabled(false)` also sets `InternalExitNodePrior` and clears `AutoExitNode` (`ipn/ipnlocal/local.go:5130-5136`), which is what makes a later re-enable possible. `jsIPN.setExitNode` only sets `ExitNodeID`. Since webnet/webnet dropped the toggle entirely rather than reimplementing it on top of `setExitNode`, nothing regresses — the wording just slightly oversells it as a drop-in replacement.
---
Once the `wasm_js.d.ts` declarations are dropped, this is good to go from my side.
Reviewed with Claude Opus 5.
Withdrawing the setExitNodeEnabled nit. It holds in the general LocalBackend case but not here: the bridge only ever surfaces Prefs.ExitNodeID (wasm_js.go:557, via notifyExitNode), and never reads or emits InternalExitNodePrior or AutoExitNode. The only reader of InternalExitNodePrior was SetUseExitNodeEnabled itself, which this PR removes, and AutoExitNode was never settable through the bridge in the first place (jsIPN.setExitNode sets ExitNodeID only), so clearing it was already a no-op. Neither is observable from the API surface or from Tailscale behavior, so setExitNode("") is genuinely equivalent here and the description is accurate as written.
I overstated the downstream impact of the leftover TS.pkg-types writes to a hardcoded pkg/pkg.d.ts (cmd/tsconnect/package.json:19) rather than to -pkgdir, so the generated types land in cmd/tsconnect/pkg/pkg.d.ts and never reach the -pkgdir output. The built dist/ in webnet/webnet confirms it — there is no pkg.d.ts there, and nothing imports dist/pkg.js:
file
bytes
used by @webnet/tsconnect
main.wasm
37,169,651
yes
cacert.pem
226,168
yes
wasm_exec.js
16,992
yes (src/index.ts:1)
build-info.json
92
yes (staleness check)
pkg.js
288,385
no
pkg.js.map
1,194,628
no
pkg.css
9,979
no
pkg.css.map
16,992
no
package.json
240
no (stray @tailscale/connect manifest)
README.md
247
no
So it is purely npm package bloat: ~1.5 MB of unreferenced files in a 37.6 MB tarball. The broken runSSHSession is never loaded and never appears in the published type surface, and the bundled xterm lives in pkg.js, not main.wasm — my line about it eating into the 2.0% wasm win was wrong.
What remains from finding 1 is in-repo only, with no downstream consequence: src/types/wasm_js.d.ts now misdeclares ssh/fetch, which is what keeps yarn lint green over the now-broken src/lib/ssh.ts:56, and the go run . dev demo app's SSH tab fails at runtime. Still worth correcting so the type contract does not lie, but it does not block this PR.
Two corrections to my review above.
**Withdrawing the `setExitNodeEnabled` nit.** It holds in the general `LocalBackend` case but not here: the bridge only ever surfaces `Prefs.ExitNodeID` (`wasm_js.go:557`, via `notifyExitNode`), and never reads or emits `InternalExitNodePrior` or `AutoExitNode`. The only reader of `InternalExitNodePrior` was `SetUseExitNodeEnabled` itself, which this PR removes, and `AutoExitNode` was never settable through the bridge in the first place (`jsIPN.setExitNode` sets `ExitNodeID` only), so clearing it was already a no-op. Neither is observable from the API surface or from Tailscale behavior, so `setExitNode("")` is genuinely equivalent here and the description is accurate as written.
**I overstated the downstream impact of the leftover TS.** `pkg-types` writes to a hardcoded `pkg/pkg.d.ts` (`cmd/tsconnect/package.json:19`) rather than to `-pkgdir`, so the generated types land in `cmd/tsconnect/pkg/pkg.d.ts` and never reach the `-pkgdir` output. The built `dist/` in webnet/webnet confirms it — there is no `pkg.d.ts` there, and nothing imports `dist/pkg.js`:
| file | bytes | used by `@webnet/tsconnect` |
| --- | --- | --- |
| `main.wasm` | 37,169,651 | yes |
| `cacert.pem` | 226,168 | yes |
| `wasm_exec.js` | 16,992 | yes (`src/index.ts:1`) |
| `build-info.json` | 92 | yes (staleness check) |
| `pkg.js` | 288,385 | no |
| `pkg.js.map` | 1,194,628 | no |
| `pkg.css` | 9,979 | no |
| `pkg.css.map` | 16,992 | no |
| `package.json` | 240 | no (stray `@tailscale/connect` manifest) |
| `README.md` | 247 | no |
So it is purely npm package bloat: ~1.5 MB of unreferenced files in a 37.6 MB tarball. The broken `runSSHSession` is never loaded and never appears in the published type surface, and the bundled `xterm` lives in `pkg.js`, not `main.wasm` — my line about it eating into the 2.0% wasm win was wrong.
What remains from finding 1 is in-repo only, with no downstream consequence: `src/types/wasm_js.d.ts` now misdeclares `ssh`/`fetch`, which is what keeps `yarn lint` green over the now-broken `src/lib/ssh.ts:56`, and the `go run . dev` demo app's SSH tab fails at runtime. Still worth correcting so the type contract does not lie, but it does not block this PR.
The lack of changes outside the wasm bridge itself is intentional, to keep this fork's patchset against upstream minimal, to simplify rebases whenever we need to catch up.
The fact that files that aren't useful to us are still ending up in @webnet/tsconnect's dist directory is preexisting, they've always been unused but now they're also incorrect and broken. Not blocking, but tracked in webnet/webnet#154 purely to remove dead weight and package bloat.
Final assessment: APPROVED.
The lack of changes outside the wasm bridge itself is intentional, to keep this fork's patchset against upstream minimal, to simplify rebases whenever we need to catch up.
The fact that files that aren't useful to us are still ending up in @webnet/tsconnect's dist directory is preexisting, they've always been unused but now they're also incorrect and broken. Not blocking, but tracked in webnet/webnet#154 purely to remove dead weight and package bloat.
Final assessment: APPROVED.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Removes obsolete APIs from the js/wasm bridge after their JavaScript-side removal in webnet/webnet#150.
sshandfetchbridge methods, including the directgolang.org/x/crypto/sshdependencysetExitNodeEnabled; callers clear the exit node throughsetExitNodediscoping typeImplements the remaining wasm bridge work from webnet/webnet#141, #144, and #145.
Checks:
GOOS=js GOARCH=wasm ./tool/go test -run=^$ -tags=netgo,omitidna,omitpemdecrypt,osusergo,tailscale_go,ts_omit_ace ./cmd/tsconnect/wasmgolang.org/x/crypto/sshis absent fromgo list -depsAI disclosure: implemented and reviewed with Codex (
gpt-5.6-sol).Review: Remove obsolete wasm bridge APIs
The Go change itself is clean and I could reproduce the claims. What is missing is the TypeScript half of
cmd/tsconnect, which still speaks to the APIs this PR deletes.Verified
GOOS=js GOARCH=wasm ./tool/go vet -tags=netgo,omitidna,omitpemdecrypt,osusergo,tailscale_go,ts_omit_ace ./cmd/tsconnect/wasmis clean atd94244830— no orphaned imports (bytes,x/crypto/sshwere the only two that became unused;net/http,strings,ipnauthare still needed).golang.org/x/crypto/sshis gone from the wasm dependency graph:go list -depsmatches 2 packages onwebnet, 0 on this branch.pingnarrowing matches the consumer:packages/tsconnect/src/types.tsalready declaresPingType = "TSMP" | "ICMP" | "peerapi"andpackages/tsconnect/src/ipn.ts:428already rejects"disco"before it reaches the bridge.ssh/fetch/setExitNodeEnabledremain incmd/tsconnect/README.md,README.pkg.md, or the Go sources.1. The TS side of
cmd/tsconnectstill calls the removed methodscmd/tsconnect/src/types/wasm_js.d.ts:16and:32still declaressh(...)andfetch(url)on theIPNinterface. Because that declaration is whatyarn lint(tsc --noEmit, run bybuild-pkg) checks against, nothing fails — the type contract just silently became wrong, and the callers below stay green:cmd/tsconnect/src/lib/ssh.ts:56callsipn.ssh(def.hostname, def.username, {...}). After this PR that property isundefined, sorunSSHSessionthrowsTypeError: ipn.ssh is not a functionon first use.cmd/tsconnect/src/pkg/pkg.ts:40re-exportsrunSSHSessionfrom the published package entrypoint.build-pkgbundlessrc/pkg/pkg.ts(common.go:111) and generatespkg.d.tsfrom it, so the shipped package advertises and exports an API that is dead on arrival. This is not hypothetical for downstream:packages/tsconnect/build.shin webnet/webnet runsbuild-pkg -pkgdir dist, and@webnet/tsconnectpublishesfiles: ["dist/"].runSSHSessionstays in the entrypoint's export graph, esbuild keeps bundlingxterm,xterm-addon-fitandxterm-addon-web-linksintodist/pkg.jsfor code that can no longer run — which eats into the 2.0% wasm win the description reports.src/app/app.tsx:66,src/app/ssh.tsx) still renders an SSH tab that now breaks at runtime undergo run . dev.Suggested fix, smallest to largest:
sshandfetchdeclarations fromwasm_js.d.ts. That makestsc --noEmitsurface every remaining caller instead of hiding them, so the build fails loudly rather than the package shipping broken exports.src/lib/ssh.ts,src/app/ssh.tsxand its use inapp.tsx, therunSSHSessionre-export inpkg.ts, and the threexterm*entries incmd/tsconnect/package.json.If keeping the upstream demo app intact is a deliberate goal for future rebases, that is a fine call to make — but then
wasm_js.d.tsshould still be corrected and the breakage noted in the PR description, because right now it reads as though the JS side is fully settled.2. Nit:
setExitNode("")is not equivalent tosetExitNodeEnabled(false)The description says "callers clear the exit node through
setExitNode". That is true for the clearing effect, butSetUseExitNodeEnabled(false)also setsInternalExitNodePriorand clearsAutoExitNode(ipn/ipnlocal/local.go:5130-5136), which is what makes a later re-enable possible.jsIPN.setExitNodeonly setsExitNodeID. Since webnet/webnet dropped the toggle entirely rather than reimplementing it on top ofsetExitNode, nothing regresses — the wording just slightly oversells it as a drop-in replacement.Once the
wasm_js.d.tsdeclarations are dropped, this is good to go from my side.Reviewed with Claude Opus 5.
Two corrections to my review above.
Withdrawing the
setExitNodeEnablednit. It holds in the generalLocalBackendcase but not here: the bridge only ever surfacesPrefs.ExitNodeID(wasm_js.go:557, vianotifyExitNode), and never reads or emitsInternalExitNodePriororAutoExitNode. The only reader ofInternalExitNodePriorwasSetUseExitNodeEnableditself, which this PR removes, andAutoExitNodewas never settable through the bridge in the first place (jsIPN.setExitNodesetsExitNodeIDonly), so clearing it was already a no-op. Neither is observable from the API surface or from Tailscale behavior, sosetExitNode("")is genuinely equivalent here and the description is accurate as written.I overstated the downstream impact of the leftover TS.
pkg-typeswrites to a hardcodedpkg/pkg.d.ts(cmd/tsconnect/package.json:19) rather than to-pkgdir, so the generated types land incmd/tsconnect/pkg/pkg.d.tsand never reach the-pkgdiroutput. The builtdist/in webnet/webnet confirms it — there is nopkg.d.tsthere, and nothing importsdist/pkg.js:@webnet/tsconnectmain.wasmcacert.pemwasm_exec.jssrc/index.ts:1)build-info.jsonpkg.jspkg.js.mappkg.csspkg.css.mappackage.json@tailscale/connectmanifest)README.mdSo it is purely npm package bloat: ~1.5 MB of unreferenced files in a 37.6 MB tarball. The broken
runSSHSessionis never loaded and never appears in the published type surface, and the bundledxtermlives inpkg.js, notmain.wasm— my line about it eating into the 2.0% wasm win was wrong.What remains from finding 1 is in-repo only, with no downstream consequence:
src/types/wasm_js.d.tsnow misdeclaresssh/fetch, which is what keepsyarn lintgreen over the now-brokensrc/lib/ssh.ts:56, and thego run . devdemo app's SSH tab fails at runtime. Still worth correcting so the type contract does not lie, but it does not block this PR.The lack of changes outside the wasm bridge itself is intentional, to keep this fork's patchset against upstream minimal, to simplify rebases whenever we need to catch up.
The fact that files that aren't useful to us are still ending up in @webnet/tsconnect's dist directory is preexisting, they've always been unused but now they're also incorrect and broken. Not blocking, but tracked in webnet/webnet#154 purely to remove dead weight and package bloat.
Final assessment: APPROVED.