Add peerAPIURL field to IPNNetMapNode (shared by self and all peers).
Add localAPI(method, path, body?) method to IPN class and interface,
returning {status, body}. Bump tailscale submodule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add TypeScript types (WhoIsResponse, PingType, PingResult, DNSQueryResult,
ExitNodeSuggestion) and validated IPN class methods for the four new Go
bindings. Bump tailscale submodule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add TLSCertKeyPair type and TypeScript wrappers for getCert, listenTLS, and
setFunnel. Add Http example component that provisions a TLS cert, opens HTTP
and HTTPS listeners, serves "Hewwo, world!", and enables Tailscale Funnel on
port 443. Bump tailscale submodule to include the matching Go changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces `npm run build --workspaces --if-present` with `turbo run build`,
which resolves build order from each package's package.json dependencies
and parallelises independent packages automatically.
- turbo.json: declares build task with `^build` topological dependency
- package.json: switches build script to turbo, adds packageManager field
(required by turbo v2), adds turbo to devDependencies
- .gitignore: add .turbo/ cache directory
- tailscale: bump submodule for tsconfig types-pinning fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vanilla webpack 5 app with:
- babel-loader + @babel/preset-typescript for TS/TSX compilation
- @pmmmwh/react-refresh-webpack-plugin for React Fast Refresh in dev
- sass-loader + css-loader + style-loader for SCSS support
- HtmlWebpackPlugin templating
- Dev server on port 3000 (npm run dev), production build (npm run build)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Aligns the TypeScript type with the Go-side json tag fix so
waitingFiles() now returns {name, size} instead of {Name, Size}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Picks up the notify-callback nil-Prefs panic fix and the restored
incoming-file progress notifications in taildrop.
Adds the done flag to IPNIncomingFile so consumers can distinguish the
final completion notification from in-progress updates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Expose a new optional IPNRunOptions callback:
- notifyOutgoingFiles(files: IPNOutgoingFile[]): called with the full
set of outgoing transfers whenever progress changes. Each entry carries
id, peerID, name, started (Unix ms), declaredSize, sent bytes,
finished, and succeeded. Entries remain present after completion so
callers can observe success/failure before discarding them.
Also exports IPNOutgoingFile from the package index.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expose two new optional callbacks on IPNRunOptions:
- notifyFilesWaiting(): called when a received file is staged and ready
to retrieve via IPN.waitingFiles(). Use as a push alternative to
polling.
- notifyIncomingFiles(files: IPNIncomingFile[]): called with the current
snapshot of in-progress inbound transfers whenever progress updates.
Empty array means all transfers have finished. IPNIncomingFile carries
name, started (Unix ms timestamp), declaredSize (-1 if unknown), and
received bytes, suitable for a progress bar.
Also exports IPNIncomingFile from the package index.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add TypeScript bindings for the Taildrop file transfer feature and exit
node selection to the @webnet/tsconnect package.
Taildrop:
- IPNConfig.fileOps: pluggable JS storage backend (IPNFileOps interface)
- IPN.listFileTargets(): list peers that can receive files
- IPN.sendFile(stableNodeID, filename, data): send a file to a peer
- IPN.waitingFiles(): list received files staged for pickup
- IPN.openWaitingFile(name): read a received file as Uint8Array
- IPN.deleteWaitingFile(name): delete a received file
- IPNFileOps void op callbacks simplified to cb(err?: string)
- IPNFileOps result op callbacks use cb(result: T | null, err?: string)
Exit node:
- IPNNetMapPeerNode.exitNodeOption, .stableNodeID
- IPNRunOptions.notifyExitNode callback
- IPN.setExitNode(stableNodeID): select or clear exit node
- IPN.setExitNodeEnabled(enabled): toggle most-recent exit node
Submodule also includes fixes for three Taildrop bugs: DirectFileMode
being incorrectly set for WASM (blocking WaitingFiles/OpenFile/Delete),
waitingFiles() returning null instead of [] when empty, and the
jsFileOps void callback convention change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire up the exit node feature across the Go→JS bridge and the
TypeScript wrapper layer:
- Bump tailscale submodule to feat/tsconnect-exit-node (wasm bridge
changes: exitNodeOption/stableNodeID on peers, notifyExitNode
callback, setExitNode/setExitNodeEnabled methods).
- Add exitNodeOption and stableNodeID fields to IPNNetMapPeerNode.
- Add notifyExitNode to IPNCallbacks and IPNRunOptions.
- Add setExitNode/setExitNodeEnabled to the IPN interface and class.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch to module/moduleResolution: NodeNext and verbatimModuleSyntax
- Drop DOM lib, add @types/node, bump target to ES2022
- Add test script (node --experimental-strip-types --test) and typecheck script
- Add mockTransport helper and smoke tests in connection.test.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add packages/http with package.json, tsconfig.json, and empty index.ts
- Add @webnet/http as a dependency of test-app
- Expose http namespace on window for console testing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Import @webnet/tsconnect/cacert.pem as a URL and expose cacertURL plus a
fetch-and-cache loadCACerts() helper on window. The example snippet in
index.html now shows how to pass the bundle into IPN.dialTLS for hosts that
the baked-in LE roots don't cover.
Vite's default server.fs.deny blocks *.{crt,pem} to guard TLS private keys;
the shipped bundle is a public trust store, so vite.config.ts overrides the
deny list to drop that pattern while keeping the .env guards. (The 403 error
reports this as an allow-list failure, which is misleading — both paths go
through the same handler in isFileLoadingAllowed.)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the curl.se distribution of Mozilla's NSS trust store under ca/cacert.pem
(145 certs, 226 KB raw / 119 KB brotli) and expose it alongside main.wasm as
./cacert.pem, ./cacert.pem.br, and ./cacert.pem.gz subpath exports. build.sh
now copies the bundle into dist/ and precompresses it with gzip -9 and
brotli -q 11. scripts/update-ca-bundle.sh refreshes the committed PEM from
curl.se with SHA-256 verification; it's decoupled from the regular build so
the trust store can be bumped on its own cadence.
Consumers can hand the fetched PEM straight to IPN.dialTLS via
TLSDialOptions.caCerts to talk to hosts with non-LE certs that the baked-in
wasm roots don't cover.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop -fast-compression from the build so precompress uses brotli.BestCompression
instead of BestSpeed: main.wasm.br goes from 9.79 MB to 5.97 MB (-39%) with no
runtime change. Expose main.wasm.br and main.wasm.gz as subpath exports
alongside main.wasm so consumers can address the precompressed artifacts
directly; serving them with the right Content-Encoding is left to the reverse
proxy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The raw objects returned from the wasm bridge are untyped bags of
js.FuncOf handlers. Calling them with the wrong types, on a closed
handle, or before the IPN is running, panics the Go runtime and
takes down the entire wasm instance for the lifetime of the page —
one bad call and the user has to reload.
Add src/ipn.ts with IPN / Conn / PacketConn / TCPListener classes
that wrap the raw handles and:
- Throw NotRunningError if an IPN method is called before run().
- Throw ClosedError if read/write/accept is called on a closed
Conn/PacketConn/TCPListener, without dispatching into Go.
- Type-check string/Uint8Array arguments in JS before they reach
js.CopyBytesToGo, which would otherwise crash.
- Range-check network literals for dial/listen/listenICMP.
- Expose addr / localAddr / remoteAddr / state / running / closed
as getters rather than methods. Addresses are cached in the
constructor so reading them never dispatches into Go, eliminating
the "call .localAddr() on a closed conn" crash.
- Parse the netmap JSON inside notifyNetMap before handing it off,
and make all run() callbacks optional (state is also a getter).
initIPN now returns a factory that produces IPN class instances
directly; index.ts re-exports the classes and hides the raw types,
so the public surface is the validated one.
The test-app inline example is updated to use getters (conn.remoteAddr,
ln.addr, ipn.state) instead of method calls.
Type the new TCP listening surface from the tailscale submodule:
- listen() is now overloaded: tcp/tcp4/tcp6 returns a TCPListener,
udp/udp4/udp6 returns a PacketConn. The network parameter is a
literal union instead of a bare string so callers get completion
and typo protection.
- TCPListener extends AsyncIterable<Conn>, so consumers can write
`for await (const conn of listener)`.
- dial() drops "udp" from its accepted networks. Connected UDP
through a stream-shaped Conn was confusing; UDP belongs in
listen(), and ICMP in listenICMP(). The Go side still accepts
whatever string for callers who really want to bypass the type.
Bumps the TS lib/target to ES2018 for AsyncIterable, bumps the
tailscale submodule, and updates the test-app inline example to
demonstrate `for await` over a TCP listener.
Type the new ipn.dialTLS bridge from the tailscale submodule. The
default verification path uses the baked-in LetsEncrypt roots that
ship with the existing tsconnect wasm — sufficient for any tailnet
HTTPS endpoint provisioned via `tailscale cert`. Callers can supply
PEM roots, override SNI, or skip verification via TLSDialOptions.
Bumps the tailscale submodule and updates the test-app inline
example to demonstrate dialTLS.
Adds a minimal @webnet/test-app package that boots a Vite dev server,
imports initIPN from @webnet/tsconnect and the bundled main.wasm as a
URL asset, and hangs both on window so you can drive the full IPN
lifecycle from devtools:
const newIPN = await initIPN(wasmURL)
const ipn = newIPN({ authKey, hostname })
ipn.run({ ... })
ipn.login()
const conn = await ipn.dial("tcp", "peer:22")
const pc = await ipn.listenICMP("icmp4")
The index.html doubles as usage documentation for the wrapper API.
Add TypeScript declarations for the new networking methods landed on
the tailscale submodule: ipn.dial (TCP/UDP), ipn.listen (UDP) and
ipn.listenICMP. Export the Conn and PacketConn interfaces that wrap
the underlying Go net.Conn / net.PacketConn bridges so callers can
type their handles.
Bumps the tailscale submodule to pick up the matching wasm changes
and the nethttpomithttp2 build tag fix.
Set up npm workspaces monorepo with the tailscale/tailscale repo as a
git submodule. Adds packages/tsconnect which builds the tsconnect wasm
via tailscale's Go toolchain and compiles a TypeScript wrapper that
initializes the wasm, captures newIPN from the global scope, removes it
to avoid pollution, and re-exports it with proper module-scoped types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>