feat(tsconnect): Node.js support — WasmSource, fs shim, safesocket fix, test suite #33
No Reviewers
Labels
Clear labels
Agentic
Component/CI
Component/Funnel
Component/React
Component/State
Component/Taildrive
Component/Taildrop
Component/Tailscale
Component/Tailshare
Component/Transport
Component/tsconnect
Component/VFS
Component/WebRTC
Component/Worker
Human
Protocol/FTP
Protocol/HTTP
Protocol/SFTP
Protocol/SMB
Protocol/SSH
Protocol/WebDAV
Protocol/WebSocket
Security
Agent
claude-fable-5
1
Work done by Claude Fable 5
Agent
claude-opus-4-8
2
Work done by Claude Opus 4.8
Agent
claude-opus-5
Work done by Claude Opus 5
Agent
claude-sonnet-4-6
3
Work done by Claude Sonnet 4.6
Agent
claude-sonnet-5
Work done by Claude Sonnet 5
Agent
gpt-5.5
7
Work done by GPT 5.5
Agent
gpt-5.6-luna
6
Work done by GPT 5.6 Luna
Agent
gpt-5.6-sol
4
Work done by GPT 5.6 Sol
Agent
gpt-5.6-terra
5
Work done by GPT 5.6 Terra
Opened by an agent
Work on the CI tooling
Work on the Tailscale Funnel or certificate system
Work on a React binding
Work on a state store (eg Redux)
Work on the taildrive system
Work on the taildrop system
Work on the Tailscale fork
Work on the Tailshare app
Work on the transport system
Work on the tsconnect packages
Work on the VFS system
Work on the WebRTC system
Work on the worker system
Opened by a human
Kind
Bug
Bug work
Kind
Enhancement
Enhancement work
Kind
Feature
Feature work
Kind
Maintenance
Maintenance work
Priority
P0
1
Critical work that must be done right now
Priority
P1
2
Urgent work
Priority
P2
3
Medium priority work
Priority
P3
4
Low priority work
Priority
P4
5
Lowest priority work, wishlist-tier
Work on the FTP protocol
Work on the HTTP protocol
Work on the SFTP protocol
Work on the SMB protocol
Work on the SSH protocol
Work on the WebDAV protocol
Work on the WebSocket protocol
Security work
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Tailshare roadmap
Done
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: webnet/webnet#33
Reference in New Issue
Block a user
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.
What this PR does
Extends
@webnet/tsconnectwith full Node.js support, covering three areas.1.
WasmSource— multi-environment WASM loading (initIPN)initIPNpreviously only accepted a URL string and calledfetch(), which does not work withfile://paths in Node.js. The parameter type is now aWasmSourceunion:string | URL→fetch()+instantiateStreaming(browser / CDN)ArrayBuffer | ArrayBufferView→instantiate(Node.jsfs.readFile, Bun)Response→instantiateStreaming(Cloudflare Workers module binding)ReadableStream<Uint8Array>→ wrappedResponse+instantiateStreaming2.
wasm_exec.js/ Node.js environmentwasm_exec.jsinstalls ENOSYS stubs forglobalThis.fswhen it is falsy. In Node.js,globalThis.fsis undefined, so the stubs are installed — and this is the correct behaviour. tsconnect's WASM routes all network calls through JavaScript'sfetch()and WebSocket APIs, which use Node.js's own DNS resolver. The Go net package's attempt to read/etc/resolv.confshould remain a no-op via the ENOSYS stub. An earlier iteration of this branch setglobalThis.fsto Node.js's realfs, which caused Go to pick up nameservers from the host'sresolv.confand try them directly — breaking in environments where those servers are unreachable.wasm_exec.jsis now imported directly inindex.tsandGois extracted fromglobalThisinline. A separateenv-node.ts/env-web.tssplit with a#envpackage.jsonimports condition was explored, but both files were identical so the indirection was removed.3.
IPN.shutdown()A
shutdown()method onIPN(TypeScript) andjsIPN(Go/WASM) stops theLocalBackend, closes the safesocket listener, and signalsmain()to return so the Go runtime exits cleanly. The TypeScript side awaits thego.run()Promise as the authoritative exit signal, avoiding a race where Go deletes_instbefore a callback-based resolve could fire. Go-side nil guards were added forlbandlnsoshutdown()is safe even whenrun()was never called.4. Two Go-side bug fixes in the
tailscalesubmodulesafesocket_js.go: hardcoded"Tailscale-IPN"memconn address causedlog.Fatalon a secondnewIPN()call in the same WASM process. An atomic counter now gives each instance a unique name.wasm_js.go:listen()rejected the standard":port"(any-interface) address form;netstack.ListenTCPrequires an explicit bind address. Now normalises:port→0.0.0.0:port.5. Test suite
InMemoryFileOpsandInMemoryState(no WASM needed)/localapi/v0/status, and two-node TCP dial/listengetFactory()singleton (WASM compiled once) and a top-levelbefore()/after()pair —shutdown()kills the entire Go WASM runtime so per-test teardown is not possible--test-force-exitkeeps the runner from hanging on tsx's open handles after Go exits--test-timeout=180000to accommodate the time nodes need to reach Running statetest:coveragescript added for consistency with other packagesRunning the integration tests
Or set them in
.env.localat the repo root (gitignored).WIP: feat(tsconnect): accept ArrayBuffer/Uint8Array/Response/ReadableStream in initIPNto feat(tsconnect): Node.js support — WasmSource, fs shim, safesocket fix, test suiteca10c7a026to1a660613160501c2fe64toa9d26d965ba9d26d965bto04ff2aa76004ff2aa760toa502735fd3