Files
webnet/AI_CHANGES.md
T
codingetandClaude 80defc2868
CI / format (pull_request) Successful in 3m13s
CI / lint (pull_request) Successful in 3m13s
CI / install (pull_request) Successful in 7m47s
CI / typetest (pull_request) Successful in 2m25s
CI / typecheck (pull_request) Successful in 2m36s
CI / node-tests (pull_request) Successful in 2m41s
CI / browser-tests (pull_request) Successful in 4m18s
test(drive): declare PROPPATCH over a minimal filesystem unsupported
Opting out skipped the test entirely, leaving the path with no coverage. The
tri-state capability with a forbidden alias keeps the assertions that the call
rejects and the properties are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:01:33 +00:00

97 KiB
Raw Blame History

AI disclosure

This file records all work in this repository that was assisted or authored by an AI agent. It is append-only; merge conflicts are resolved by keeping all lines (see .gitattributes).

  • @webnet/tsconnectlistDrivePeers honours online and peerAPI reachability (issue #143): Claude Code (Claude Opus 5) fixed listDrivePeers in the tailscale fork (cmd/tsconnect/wasm/drive.go), which returned every peer carrying PeerCapabilityTaildriveSharer — in practice most of the tailnet, since the cap is an ACL grant usually given to a whole group or tag. It now applies the same conjunction as LocalBackend.driveRemotesFromPeers: online, non-empty peerAPI base, and the sharer cap. The peerAPI URL was already computed and discarded. The TypeScript doc comments on IPN.listDrivePeers and IPNDrivePeer were corrected to say the result is peers allowed to share, not peers that expose a share, and the headscale integration test now asserts no returned peer is offline or unreachable. Deciding whether to add an opt-in stricter mode that queries each peer's Taildrive endpoint (N peerAPI round-trips) is left open in the issue.

  • @webnet/tsconnect — no late "Go program has already exited" after shutdown() (issue #147): Claude Code (Claude Opus 5) fixed the wasm runtime teardown at its source. Go's wasm_exec.js carries the runtime's pending work across program exit: the scheduler's setTimeout stays armed, and every callback Go handed to JS (DERP WebSocket event listeners, fetch continuations) stays registered; all of them route through _resume, which throws "Go program has already exited" once the instance is gone. A live probe against headscale confirmed all three shapes after a clean shutdown() — a Timeout._onTimeout uncaught error, two WebSocket close-listener uncaught errors, and one unhandled rejection. initIPN now wraps the Go instance it creates: runtime.wasmExit clears the pending scheduler timeouts, and _resume returns quietly once the runtime has exited (nothing can be resumed after the instance is dropped). Because a quiet _resume also means a call into a dead Go handle resolves with undefined instead of throwing — which would spin a read() loop rather than break it — Conn, PacketConn and TCPListener now report themselves closed once their runtime has exited, so such calls raise ClosedError and close() becomes a no-op. An intermediate attempt to hand back a rejected promise instead was rejected after testing: node:test attributes the rejection to the creating hook and fails the run just as the original bug did. No Go-side change was needed — the backend shutdown itself is already clean; the leak is entirely in the JS glue.

  • @webnet/tsconnect / @webnet/tsconnect-worker — reverted the #147 shutdown workarounds: Claude Code (Claude Opus 5) removed the three workarounds the late-_resume bug forced, now that it is fixed: both *.shutdown.probe.ts child processes are gone (the shutdown tests run in-process again, each on its own initIPN runtime), the shared @webnet/tsconnect integration suite shuts its runtime down in after() again, and the *.probe.ts tsconfig excludes both packages carried are removed.

  • CI — scheduled tsconnect CA bundle refresh (issue #142): gpt-5.6-sol added a monthly, manually dispatchable Gitea Actions workflow that checksum-verifies the current curl.se Mozilla CA bundle, exits without changes when it is current, and otherwise force-updates one automation branch and creates or refreshes its pull request. The generated PR body includes curl.se's Mozilla data date and fingerprint-based added/removed certificate common-name counts and lists.

  • @webnet/tsconnect-worker — isolated Go/WASM connection tests: gpt-5.6-sol fixed the flaky main-thread connection tests by moving their real Go runtimes into a dedicated child process that owns the runtime's known late scheduler errors, covering connection fallback, shutdown, and repeated disconnect without leaking live runtimes into normal or coverage test processes.

  • @webnet/tsconnect — integration coverage for the untested bridge surface (PR #146): Claude Code (Claude Opus 5) extended the headscale integration suite from a TCP listen/dial pair to real two-node coverage of UDP datagrams, listenICMP (including a hand-built ICMP echo request and its reply), listenTLS/dialTLS against a run-time-generated self-signed CA plus the wrong-CA rejection case, upgradeTls on a live connection, Taildrop end-to-end (listFileTargets, sendFile, waitingFiles, openWaitingFile, deleteWaitingFile), serveDrive/listDrivePeers over a real netmap, suggestExitNode, and setExitNode. serveDrive, listDrivePeers and upgradeTls previously had tests only against mock raw objects that never loaded the wasm. Taildrop registers its own node pair from a new optional TSCONNECT_TEST_USER_AUTH_KEY, because canPutFile requires an untagged peer and the shared key registers tag:test; setExitNode is gated on TSCONNECT_TEST_EXIT_NODE_ID, and both are documented in .env.example. shutdown() runs in a child process (src/shutdown.probe.ts, excluded from the browser build via a new *.probe.ts tsconfig exclude) and the shared suite no longer tears the runtime down, because an exiting Go runtime leaves scheduler timeouts that throw afterwards and node:test claims the exception first (issue #147). Writing the tests surfaced two pre-existing gaps, worked around rather than fixed: ":port" is normalised only for TCP listen, so listen("udp4", ":0") and listenTLS(":0", …) fail with ParseAddrPort(":0"): no IP; and a UDP socket bound to 0.0.0.0 cannot reach the tailnet. Verified against a tailscale build from the rebased fork branch across three consecutive green runs.

  • Tailshare and example-app frontend typechecking (issue #122): gpt-5.6-terra added Turbo-discoverable TypeScript checks to both frontend packages, updated their library targets and declarations for current dependency typings, removed stale tsconnect source includes, and corrected the example HTTP demo for the current listener and server APIs so both applications typecheck cleanly.

  • Tailshare and example-app frontend typechecking — review follow-up: gpt-5.6-terra preserved the example HTTP demo's original GET / behavior while migrating it to the current server API, following an independent Claude review.

  • Repository package-boundary enforcement (issue #107): gpt-5.6-sol removed all cross-package _internals imports and cross-package re-exports, replaced wildcard barrels with explicit reviewed exports, moved shared length-prefixed wire encoding into the utilities package, relocated shared path-helper coverage to VFS, preserved SSH malformed-auth and SFTP cleanup regressions, and added tested ESLint enforcement, package-boundary documentation, and SSH public-surface typetests.

  • @webnet/tsconnect-worker — deterministic async test cleanup: gpt-5.6-sol fixed a flaky Go/WASM test teardown by awaiting the runtime shutdown started by disconnect(), replaced nearby fixed-delay MessagePort waits with direct call assertions and an ordered delivery barrier, and converted TTL tests to mocked timers so they wait for every relevant expiration rather than whichever equal-duration timer happens to fire first.

  • @webnet/sftp — stable keyless server identity: GPT-5.6 Luna fixed SFTPServer to lazily generate and cache one ephemeral host key per server instance, and added a regression test covering sequential connections and host-key fingerprints.

  • @webnet/vfs — canonical path helpers: Codex (GPT-5.6 Luna) added public POSIX VFS path utilities for normalization, resolution, parents, basenames, and joining. The helpers clamp traversal at the VFS root, preserve backslashes and Unicode names, and are covered by focused edge-case tests. MemoryVFS, FsaVFS, FTP, and SFTP now reuse the shared implementation; NodeVFS retains its separate traversal-rejection guard.

  • @webnet/vfs, @webnet/drive, @webnet/ftp, @webnet/sftp — conformance over a minimal backing filesystem (issues #181, #177): Claude Code (Claude Opus 5) added withoutOptional and unsupportedOptional to @webnet/vfs/conformance, replacing the copies in the fallback tests and the WebDAV tests, and gave each protocol package a second testAsyncVFSConformance run whose server is handed a filesystem reduced to the required operations. The client under test is unchanged, so the suite's existing assertions — inclusive range ends, byte-for-byte comparisons, copy replacing rather than merging — now apply to the servers' fallback paths, which were never entered when every server was backed by a full MemoryVFS. That run reproduces #177 before it is fixed: the WebDAV server's Range fallback discarded range.start and streamed from byte zero while advertising the requested window, so a resumed download or a media seek silently stored the wrong bytes at the wrong offset; readFileRangeFallback now windows the stream and removes the branch, and the unit test that only asserted a 206 status asserts the body and the framing headers. The three remaining differences the second run exposed are declared rather than shimmed, because each is a legitimate protocol answer from a server that cannot do the work: WebDAV PROPPATCH answers 403 (dead properties cannot be built from the required operations), and FTP RNFR/RNTO and SFTP RENAME answer 502 and SSH_FX_OP_UNSUPPORTED, which reach the client as unsupported. All three are declared with the tri-state capability rather than skipped, so the suite still checks that the call rejects and changes nothing; the WebDAV run aliases forbidden to unsupported for that, since 403 is the answer the protocol gives here. A Claude Sonnet 5 review caught that this run originally opted out of setProps altogether, which left PROPPATCH over a minimal filesystem with no coverage at all — the gap #181 exists to close. Whether those three should shim instead is the decision #179 exists to make. @webnet/smb2 has no server over an AsyncVFS — its test server is a wire-protocol mock over its own store — so nothing there to reduce.

This project was set up with the assistance of Claude Code (Anthropic). The following were written by Claude Code:

  • @webnet/http-static — VFS-backed static HTTP handler: GPT-5.6 Luna added a new workspace package exposing createStaticHandler, with request/context path resolution, prefix and suffix lookup, index files, HTML/JSON directory listings, fallback paths, metadata and conditional responses, HEAD support, and single-byte range streaming. Added focused unit coverage and package build/test/typecheck scaffolding.

  • @webnet/http-static — review fixes: GPT-5.6 Luna addressed review findings by rejecting requests outside a configured prefix, returning 416 for ranges on empty files, normalizing method matching, and honoring If-None-Match: *.

  • @webnet/http-static — precedence test vectors: GPT-5.6 Luna added a seeded MemoryVFS matrix covering every requested path across default/custom/disabled indexes, suffix probing, and fallback configurations.

  • @webnet/http-static — HTTP-relative directory listings: GPT-5.6 Luna updated generated JSON paths and HTML links to use the request URL pathname, including configured HTTP prefixes, and added prefixed listing coverage.

  • The tailscale submodule fork and its Go-side tsconnect patches (the webnet branch)

  • The packages/tsconnect TypeScript SDK

  • The packages/test-app Vite test application

  • The repo tooling setup (ESLint, Prettier, lint-staged, commitlint, dpdm, TypeScript 6)

  • AGENTS.md — the agent guidelines file (worktree/submodule rules, commit-trailer convention, PR finalisation checklist)

The following were hand-written:

  • The packages/http HTTP/1.1 server library (HTTP parsing, server/client connection management, chunked transfer encoding, router/middleware system, response serialisation)

The following were hand-written but with substantial Claude Code contributions:

  • packages/http — low-level transport layer: reviewed and significantly reworked by Claude Code, fixing multiple bugs (errcallback not cleared after a successful read, ServerConnection draining the body after closing the connection, PooledDialer throwing instead of queuing waiters, shouldClose() doing a case-sensitive header comparison) and adding transport primitives (halfClose, readEnded, whenClosed, remoteAddr, localAddr)
  • packages/http — timeout support: the headersTimeout, keepAliveTimeout, and bodyTimeout options across both client and server were implemented by Claude Code
  • packages/http — parse error messages: improved by Claude Code to include the offending values
  • packages/http — package and build scaffolding: initial package.json, tsconfig.json, and build configuration were set up by Claude Code

The test suite for packages/http was mostly generated by Claude Code, which also identified and fixed several further bugs: a body-size-limit error being swallowed on the final body chunk, a stale idle-connection entry left behind by rejectConnection, and a read() call hanging indefinitely after a socket close.

  • packages/httphijack() on server and client responses: the hijack() method on ServerResponse and ClientResponse, the ReadBuffer.drain() helper, and the prependTransport() utility were implemented by Claude Code

  • packages/http — 1xx informational response support: implemented by Claude Code. Server side: automatic 100 Continue (sent lazily when the handler reads the body) and res.sendInformational() for 103 Early Hints etc. Client side: default skip mode, interim: "collect" to capture 1xx into res.informational[], conn.requestStream() async generator that yields each interim response and the final one as they arrive, and fetchStream() / f.stream() to expose the same streaming behaviour through the fetch API with proper connection pool management.

  • packages/http — WebSocket support: implemented by Claude Code. upgradeWebSocket(req, res) for server-side handshake; connectWebSocket(dialer, url, options?) to open a new WebSocket connection, or connectWebSocket(res, key) to promote an existing fetch()/fetchStream() 101 response — both return a WebSocketConnection async iterable. Frame codec (read/write), masking, fragmented-message reassembly, ping/pong, and the close handshake are all implemented from scratch using the Web Crypto API (crypto.subtle.digest for SHA-1, crypto.getRandomValues for mask keys) with no external dependencies. Two fixed bugs in fetch.ts were required for pool safety: a case-insensitive Connection: upgrade check and immediate pool ejection on 101 to prevent a microtask race before hijack. Exported as three tree-shakeable entry points: @webnet/http/websocket (combined), @webnet/http/websocket/client, and @webnet/http/websocket/server.

  • packages/http — 3xx redirect support: implemented by Claude Code. redirect.mode ("manual" / "same-connection" / "same-origin" / "follow"), redirect.max, redirect.filter (string array / Set / callback), redirect.credentials ("keep" / "strip-cross-origin" / "strip"), redirect.body ("resubmit" / "strip-non-resubmit" / "strip"), and redirect.collect to gather followed 3xx into res.redirects[]. In streaming mode all redirects and interims are yielded as they arrive; a drain() method was added to ClientConnection to support clean connection hand-off between redirect hops.

  • packages/http — lint fix and WebSocket test coverage: Claude Code fixed a prefer-const lint error in ServerConnection by refactoring HijackFn to accept res as a parameter (eliminating a forward-reference let res! pattern), updated the ESLint config to recognise _-prefixed variables as intentionally unused (varsIgnorePattern), and added test coverage for extended-length WebSocket frames (2-byte and 8-byte), multi-chunk ReadBuffer slicing, empty close frames, unsolicited PONG frames, invalid port URLs, and the fetchStream() 101 early-break path.

  • packages/http — typed context extension via next(extra): the Router<T> generic, the Next<TAdd> conditional type, and the context-passing chain were designed and implemented by Claude Code

  • Monorepo restructuring: the following packages were created or substantially reworked by Claude Code — @webnet/transport (transport abstractions, buffer utilities, node and loopback implementations extracted from @webnet/http); @webnet/websocket (WebSocket support extracted from @webnet/http); @webnet/tsconnect-redux (Redux state bindings extracted from @webnet/tsconnect); @webnet/tsconnect-react (React hooks and context extracted from @webnet/tsconnect). @webnet/http's Server class was refactored to take a Handler argument instead of extending Router; Router was moved to a ./router sub-export. @webnet/tsconnect's Conn and TCPListener were updated to formally implement RawTransport and RawListener; IPNDialer implementing RawDialer was added.

  • @webnet/tsconnect — drop pre-compression of assets: Claude Code removed the gzip/brotli pre-compression of main.wasm (from the tsconnect fork's build-pkg) and cacert.pem (from packages/tsconnect/build.sh), dropping the corresponding .br/.gz package exports and adding scripts/asset-sizes.sh (exposed as npm run asset-sizes) to let consumers measure raw, gzip, and brotli sizes of the built assets.

  • packages/xml: a thin XML parse/serialize package with conditional exports — browser builds use the native DOMParser, Node.js builds use @xmldom/xmldom. Authored by Claude Code.

  • packages/drive: WebDAV client and server with an async VFS abstraction. Includes MemoryVFS, NodeVFS (node:fs), FsaVFS (browser File System Access API, with an OPFS factory method), createDAVHandler() (server-side HTTP handler compatible with @webnet/http), and DAVClient (which implements AsyncVFS so it can be used as a backing store for another server instance). Full WebDAV Level 2 locking support (LockStore interface, InMemoryLockStore, LOCK/UNLOCK methods, If: header enforcement, lockdiscovery/supportedlock properties, and client-side lock()/unlock()/refreshLock() with optional lockToken on all mutating methods). Authored by Claude Code.

  • @webnet/tsconnect — streaming Taildrop: Claude Code removed all full-file buffering from the Taildrop send and receive paths. IPN.sendFile now accepts a ReadableStream<Uint8Array> + declaredSize; IPN.openWaitingFile returns Promise<ReadableStream<Uint8Array>>. On the Go/WASM side, a new jsStreamReader (io.ReadCloser) pulls chunks from a JS ReadableStreamDefaultReader via awaited .read() Promises (channel+js.FuncOf pattern), and jsReadableStream wraps a Go io.ReadCloser in a pull-based JS ReadableStream. UserIPNFileOps.openReader now returns a ReadableStream instead of a Uint8Array. A new FsaFileOps class (with FsaFileOps.createFromOpfs()) provides an OPFS-backed UserIPNFileOps where received chunks land directly on disk and downloads stream back through Go without buffering. InMemoryFileOps.openReader was updated to emit stored chunks one-by-one via a ReadableStream.

  • @webnet/tsconnectIPN.shutdown(): Claude Code added a shutdown() method to IPN (TypeScript) and jsIPN (Go/WASM). Calling it stops the LocalBackend, closes the safesocket listener to unblock srv.Run, and signals main() to return so the Go runtime exits. The TypeScript side awaits the go.run() Promise (captured in initIPN and threaded into each IPN) as the authoritative "Go runtime has exited" signal, avoiding a race where Go deletes _inst before a callback-based resolve could fire. Go-side nil guards were added for lb and ln so shutdown() is safe to call even when run() was never invoked.

  • @webnet/tsconnect — multi-environment WASM loading and Node.js fixes: Claude Code investigated Worker/SharedWorker and Node.js/Bun compatibility. The Go WASM and wasm_exec.js are already Worker-compatible (js.Global() maps to the Worker's globalThis; wasm_exec.js stubs fs/process/path when absent). Three issues were found and fixed for Node.js:

    1. initIPN accepted only a URL string and called fetch(), which does not support file:// URLs in Node.js. Claude Code added a WasmSource union type (string | URL | ArrayBuffer | ArrayBufferView | Response | ReadableStream<Uint8Array>) dispatching to WebAssembly.instantiate for binary sources and WebAssembly.instantiateStreaming for URL/Response/ReadableStream inputs.
    2. wasm_exec.js installs ENOSYS stubs for globalThis.fs when it is falsy. In Node.js globalThis.fs is undefined, so the stubs are installed — and this is the correct behaviour: tsconnect's WASM routes all network calls through JavaScript's fetch() and WebSocket APIs (which use Node.js's own DNS resolver), so the Go net package's /etc/resolv.conf read should remain a no-op. An earlier iteration set globalThis.fs to Node.js's real fs, which caused Go to read the host's /etc/resolv.conf directly and attempt to use those nameservers, breaking in environments where they are unreachable (e.g. Tailscale-managed entries without Tailscale running). wasm_exec.js is now imported directly in index.ts and Go is extracted from globalThis inline — a separate env-node.ts/env-web.ts split was explored but both files were identical, so the indirection was removed.
    3. In the tailscale submodule, two Go-side bugs were fixed: safesocket_js.go used a hardcoded memconn address "Tailscale-IPN", so a second newIPN() call in the same WASM process would log.Fatal; an atomic counter now gives each instance a unique address. And wasm_js.go's listen() rejected the standard ":0" (any-interface) address form that netstack does not accept; it now normalises :port to 0.0.0.0:port. Claude Code also wrote the full automated test suite for @webnet/tsconnect: unit tests for InMemoryFileOps and InMemoryState (no WASM required), and integration tests that spin up real Tailscale nodes against a headscale control server, verifying initIPN WASM loading, /localapi/v0/status, and two-node TCP dial/listen.
  • @webnet/tsconnectgetIceServers(): Claude Code implemented getIceServers(ipn), which fetches the tailnet's DERPMap via LocalAPI and converts it to an RTCIceServer[] list for use with new RTCPeerConnection({ iceServers }). DERP servers run an integrated RFC 5389-compliant STUN server on UDP 3478 by default; since tsconnect in the browser cannot use raw UDP (all traffic goes through DERP over WebSocket), WebRTC is the only way to establish a direct peer-to-peer connection. Also added DERPMap, DERPRegion, and DERPNode types. Unit and integration tests included.

  • @webnet/tsconnect — service advertisement: Claude Code added SetExplicitServices to LocalBackend in the tailscale fork (bypassing the OS portlist gate), wired a setServices(services) WASM binding, and added a services field to the netmap JSON for both self and peers (stripping internal peerapi entries). On the TypeScript side: IPNService type, services: IPNService[] on IPNNetMapNode, and IPN.setServices(). In @webnet/tsconnect-redux: getPeersByService, getPeersByServiceDescription (both memoized with createSelector), and getSelfServices (referentially stable empty-array fallback). Claude Code also wrote integration tests for setServices() and fixed two bugs found while making them reliable: SetExplicitServices previously only sent a "lite" map update that discarded the control server's response, so notifyNetMap never fired with the new services — fixed by adding Auto.RestartMap() to force a fresh streaming netmap; and userServicesFromView returned a nil slice when a node advertised no services, which serialized as null instead of [] in the netmap JSON.

  • @webnet/tsconnect — taildrive WebDAV bridge and listDrivePeers(): Claude Code implemented jsFileSystemForRemote in the tailscale fork (cmd/tsconnect/wasm/drive.go), a drive.FileSystemForRemote backed by a JS callback: request bodies stream to JS chunk-by-chunk via readBodyChunk(), response bodies stream back via write()/end() with http.Flusher.Flush() after each chunk, so large transfers never buffer in memory. sys.DriveForRemote is set at newIPN time; setDriveHandler (wired to IPN.serveDrive(fn)) registers the actual handler later, since the Tailscale auth/permission-parsing (DriveSharingEnabled, ParsePermissions) happens entirely Go-side before the JS handler is invoked. Also added listDrivePeers (wired to IPN.listDrivePeers()), mirroring native driveRemotesFromPeers: returns peers carrying PeerCapabilityTaildriveSharer, gated on DriveAccessEnabled(). On the TypeScript side: DriveSharePermission, DrivePermissions, JsDriveRequest, JsDriveResponse, RawDriveHandler, IPNDrivePeer types. Claude Code also created the new @webnet/taildrive package (./server sub-export) with bridgeDriveHandler(handler), which adapts an @webnet/http Handler (e.g. from @webnet/drive's createDAVHandler) to the Go bridge's raw request/response callback shape — kept in a separate package from @webnet/tsconnect and @webnet/drive to avoid a circular dependency between the two. Unit tests cover bridgeDriveHandler's request/response translation (headers, all Body union variants, hasBody detection) and IPN.serveDrive/listDrivePeers argument validation and JSON handling against a fake RawIPN, so they run without a WASM build. A true end-to-end test against a live control plane is blocked on Headscale ACL support for nodeAttrs/grants/Taildrive, which only lands in Headscale v0.29.0 (not yet released stably at the time of writing).

  • packages/httpReadableStream body support: implemented by Claude Code. Renamed the AsyncIterable-returning method to iter() and added a new stream() returning a ReadableStream<Uint8Array> with BYOB (byte stream) support where available. WritableHttp.body now accepts ReadableStream<Uint8Array>, written as chunked transfer encoding with BYOB reads and periodic flushes every 2^16 bytes. The iterableToStream()/streamToIterable() helpers in packages/drive were removed as they are no longer needed. @webnet/taildrive's bridgeDriveHandler (predating this rename) was updated to match: ctx.req.stream() now returns a real BYOB-capable ReadableStream<Uint8Array> instead of an AsyncIterable, a ctx.req.iter() method was added for the old semantics, and response bodies that are a ReadableStream are now forwarded correctly instead of falling through to JSON serialization.

  • @webnet/transportNodeListener bug fixes: two bugs fixed by Claude Code (Claude Sonnet 4.6): double invocation of the error callback (fired both directly from close() and from the server's "close" event), and a connection leak when accept() was rejected while a late-arriving connection triggered the still-registered once("connection") handler.

  • @webnet/transport — WebRTC DataChannel transport: Claude Code (Claude Sonnet 4.6) implemented @webnet/transport/webrtc — a RawTransport over an RTCDataChannel. Designed to coexist on a shared RTCPeerConnection alongside other channels (multiplexed file transfers, audio/video). Write-side backpressure defers write() promises via bufferedamountlow when bufferedAmount exceeds the high-watermark. Receive-side backpressure for slow local readers (e.g. OPFS writes) removes the message listener so the browser's internal SCTP buffer fills and signals flow control back to the sender; the listener is re-added once the read queue drains. All WebRTC types are defined as local structural interfaces so the package needs no DOM lib. 114 tests at 100% coverage.

  • @webnet/tsconnect-worker: new package authored by Claude Code (Claude Sonnet 4.6). Wraps the IPN and tsconnect-redux Redux store in a SharedWorker, exposing every IPN method to clients via message-port RPC with per-object MessageChannels for Conn, TCPListener, PacketConn, drive handler, and SSH sessions. Key features: IndexedDBState for sync IPN state storage (unavailable localStorage workaround — loads entire store into a Map at startup, serves reads/writes synchronously, fires async IDB writes on mutation); Web Locks API for client liveness detection (worker cleans up all resources opened by a disconnected client: connections, listeners, packet connections, drive handler); Redux action broadcast to all clients on every dispatch; full state snapshot sent to each new client on connect; graceful IPN shutdown when the last client leaves. Two tsconfigs: DOM lib for IpnWorkerClient and proxy classes (WorkerConn, WorkerTCPListener, WorkerPacketConn, WorkerSSHSession); WebWorker lib for the worker entry point. Round-2 improvements (Claude Sonnet 4.6): IndexedDBState moved to @webnet/tsconnect/helpers and exported from the package; buildIpnStore now accepts an optional preloadedState, and the worker sends the full Redux state snapshot (preloadState message) instead of synthetic actions on client connect; WorkerConn formally implements RawTransport, WorkerTCPListener implements RawListener, WorkerPacketConn implements IpnPacketConn; a new IpnClient interface added to @webnet/tsconnect (with IpnSSHTermConfig and IpnPacketConn) implemented by both IPN and IpnWorkerClient to prevent method drift; IPN.ssh() made async; WorkerSSHSession.resize()/close() made fire-and-forget (sync) to implement IPNSSHSession; ReadableStream transfer fallback via MessageChannel sub-protocol for Safari compatibility on both sendFile and openWaitingFile; stateStorage: "memory" option added to WorkerConfig for ephemeral (non-persisted) IPN state. @webnet/tsconnect-react updated by Claude Code (Claude Sonnet 4.6): IpnContext widened from IPN to IpnClient; useBuildIpnWorker(worker, config, runParams?, workerOptions?) hook added to connect to a SharedWorker and return an IpnWorkerClient, with StrictMode-safe cleanup via disconnect(); disconnect() method added to IpnWorkerClient to release the Web Lock without stopping the worker; workerOptions parameter added to support both classic (webpack-bundled) and module workers. example-app updated by Claude Code (Claude Sonnet 4.6): SharedWorker demo added — src/worker.ts is the webpack worker entry, IpnProvider runs useBuildIpnWorker when SharedWorker is available (falling back to main-thread useBuildIpn), switches the Redux Provider to the worker client's embedded store on connect; the Debug UI shows a "use SharedWorker" checkbox (checked by default when available, with appropriate state/taildrop labels for each mode). Round-3 bug fixes (Claude Sonnet 4.6): liveness detection now fires for all clients including those that connect during init (unified onHello handler in onconnect; registerClient always receives lockName and acquires the lock immediately); dead bodyCallbacks field removed from drive pending map; drive cleanup only installs the no-op handler when no other client still has drive registered; wrapDispatch as any replaced with targeted two-step cast; IndexedDBState.setState logs write failures via tx.onerror; pumpStreamToPort fire-and-forget made explicit with .catch(() => {}).

  • AGENTS.md / CLAUDE.md and AI_CHANGES.md: PR review instructions (use tea, interactive vs autonomous modes) and AI disclosure consolidation into AI_CHANGES.md authored by Claude Code (Claude Sonnet 4.6).

  • @webnet/vfs — VFS package extraction: Claude Code (Claude Sonnet 4.6) split the VFS abstraction (AsyncVFS, Stat, VFSError, VFSErrorCode) and its three implementations (MemoryVFS, NodeVFS, FsaVFS) out of @webnet/drive into a new standalone @webnet/vfs package, following the same pattern as the earlier @webnet/transport split from @webnet/http. @webnet/drive and @webnet/test-app now import directly from @webnet/vfs. The drive package's ./vfs/* sub-exports were removed. The NodeVFS test suite moved with the implementation.

  • @webnet/tsconnectFsaFileOps limits and change notification: Claude Code (Claude Sonnet 4.6) added maxFiles/maxTotalSize/maxFileSize limit getters/setters, fileCount/totalSize/openFiles getters, and onChange/offChange change-notification to FsaFileOps, mirroring the existing InMemoryFileOps API. File sizes are tracked in memory via a #fileSizes map that is updated on every openWriter/write/remove/rename; createFromOpfs now scans the directory at startup so pre-existing files count toward limits. The constructor was updated to accept an optional { maxFiles, maxTotalSize, maxFileSize, initialSizes } options bag. Follow-up fixes (also Claude Sonnet 4.6): moved #fileSizes.set in openWriter to after all async FSA ops so a failure doesn't leave a phantom entry; fixed rename to stat and track previously-untracked files under their new name rather than letting them become invisible; documented the stat/totalSize lag during active writes.

  • @webnet/tsconnect - InMemoryFileOps limits and change handlers reviewed by Claude Code (Sonnet 4.6)

  • Browser testing infrastructure: Claude Code (Claude Sonnet 4.6) added browser integration testing using Playwright as a library within the existing node:test runner. Key design decisions: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 in .npmrc prevents binary downloads on npm ci (explicit npx playwright install in CI only); browser test files use the *.browser.ts extension to avoid matching the existing src/**/*.test.ts glob so the regular test suite is unaffected; @webnet/browser-test-utils is a new private package that exports forBrowsers() (registers test suites for Chromium and Firefox, handles browser lifecycle) and serveDirectory() (minimal HTTP server using path.resolve/path.relative to guard against path traversal). test:browser per-package scripts added; test:browser:coverage intentionally omitted since c8 only sees Node orchestration code, not browser-side code inside page.evaluate(). Browser tests use page.evaluate() with dynamic import() to load built package modules into real browser contexts. Integration tests written for: DataChannelTransport (real RTCPeerConnection loopback, send/receive and close-propagation); FsaVFS (OPFS round-trip, stat, readdir, delete); IndexedDBState (multi-instance persistence, empty-DB initialisation); FsaFileOps (write/read/stat/remove/rename/listFiles cycle). CI pipeline added at .gitea/workflows/test-browser.yml.

  • @webnet/browser-test-utils__name polyfill: Claude Code (Claude Sonnet 4.6) fixed a ReferenceError: __name is not defined crash in all *.browser.ts tests. tsx hardcodes keepNames: true in its internal esbuild options, which injects a __name helper at module scope and wraps named function/const assignments with __name(fn, "name"). Playwright's page.evaluate() serializes callbacks via .toString(), capturing only the function body — so the module-level helper is absent in the browser context. Fixed by calling page.addInitScript() in forBrowsers's newPage to inject a matching polyfill (Object.defineProperty(target, "name", ...)) into every page before any evaluate() runs.

  • ControlledFileOps — observable file-ops binding: Claude Code (Claude Sonnet 4.6) added a ControlledFileOps interface to @webnet/tsconnect that extends UserIPNFileOps with readable metrics (fileCount, openFiles, totalSize), configurable limits (maxFiles, maxTotalSize, maxFileSize), and an onChange(handler) => unsubscribe subscription — already satisfied structurally by InMemoryFileOps and FsaFileOps. Added a fileOps Redux slice to @webnet/tsconnect-redux (stores FileOpsState | null; null when file ops are not configured) with setFileOpsState action, selectFileOps selector, and a getFileOpsState convenience selector. Added bindFileOpsToStore(fileOps, store) to @webnet/tsconnect-redux's binding module: takes an initial snapshot and re-dispatches on every onChange call; the returned unsubscribe is stored by the worker for lifecycle safety. In @webnet/tsconnect-worker: the FsaFileOps instance was hoisted from init() to module scope; bindFileOpsToStore is called after the store is created; a new setFileOpsConfig({ maxFiles?, maxTotalSize?, maxFileSize? }) method on IpnWorkerClient proxies to a new worker-side handleCall case that mutates the live instance and throws back any limit-violation errors. All state updates propagate via the existing wrapDispatch broadcast, so every connected client's store reflects the current file-ops state and limits in real time. Unit tests for bindFileOpsToStore added to @webnet/tsconnect-redux.

  • ControlledFileOps — observable file-ops binding: Claude Code (Claude Sonnet 4.6) added a ControlledFileOps interface to @webnet/tsconnect that extends UserIPNFileOps with readable metrics (fileCount, openFiles, totalSize), configurable limits (maxFiles, maxTotalSize, maxFileSize), and an onChange(handler) => unsubscribe subscription — already satisfied structurally by InMemoryFileOps and FsaFileOps. Added a fileOps Redux slice to @webnet/tsconnect-redux (stores FileOpsState | null; null when file ops are not configured) with setFileOpsState action, selectFileOps selector, and a getFileOpsState convenience selector. Added bindFileOpsToStore(fileOps, store) to @webnet/tsconnect-redux's binding module: takes an initial snapshot and re-dispatches on every onChange call. In @webnet/tsconnect-worker: the FsaFileOps instance was hoisted from init() to module scope; bindFileOpsToStore is called after the store is created; a new setFileOpsConfig({ maxFiles?, maxTotalSize?, maxFileSize? }) method on IpnWorkerClient proxies to a new worker-side handleCall case that mutates the live instance and throws back any limit-violation errors. All state updates propagate via the existing wrapDispatch broadcast, so every connected client's store reflects the current file-ops state and limits in real time.

  • @webnet/tsconnect-redux — typecheck fix: Claude Code (Claude Sonnet 4.6) added @types/node to devDependencies and "types": ["node"] to tsconfig.json so that the node:test/node:assert/strict imports in binding.test.ts resolve during tsc --noEmit.

  • @webnet/tsconnect-worker — main-thread fallback and connectWithFallback: Claude Code (Claude Sonnet 4.6) added a full main-thread fallback path for environments that do not support SharedWorker (e.g. Chrome on Android before 2025). Key additions:

    • IpnClientHandle interface: extends IpnClient, adding connectionMode, store, state, running, fileOps, run(), disconnect(). Both IpnWorkerClient and the new IpnMainThreadHandle implement it.
    • IpnMainThreadHandle: wraps an IPN instance with a Redux store (wired via runWithStore at construction so future state changes fire user callbacks set later in run()). Proxies all IpnClient methods directly. disconnect() shuts down IPN and releases the lock.
    • connectMainThread(config): initialises IPN in the main thread. For IndexedDB-backed state, acquires a web lock keyed to the DB name ("tsconnect-idb:<dbName>"); rejects immediately if held. In-memory state skips the lock entirely.
    • connectWithFallback(workerUrlOrFactory, config, opts?): tries the SharedWorker path first; falls back to connectMainThread if SharedWorker is undefined, opts.disableSharedWorker is true, or the worker path throws.
    • useBuildIpnWorker updated in @webnet/tsconnect-react: now uses connectWithFallback and returns IpnClientHandle | null instead of IpnWorkerClient | null.
    • Tests: 6 Node tests (all pass — WASM loads, IpnMainThreadHandle is returned, run() fires callbacks, disconnect() is idempotent, connectWithFallback falls back in Node); 6 browser tests in Chromium and Firefox (SharedWorker path → connectionMode=worker; disableSharedWorker: trueconnectionMode=main-thread; IDB lock contention → connectMainThread rejects immediately). Browser tests use esbuild (hoisted transitive dep) to bundle the package inline in the test's before() hook. WorkerConfig.wasmUrl widened to string | ArrayBuffer | ArrayBufferView so tests can pass raw WASM bytes (Node.js fetch() does not support file:// URLs). Node tests restructured to share a single WASM+IPN instance per suite via before()/after() hooks, preventing uncaught Go goroutine errors from sequentially shutting down multiple WASM runtimes. Code review addressed: IpnClientHandle.disconnect() JSDoc clarifies the main-thread-vs-worker semantics difference; connectWithFallback logs a console.warn for diagnosed fallbacks; lock release uses a closure rather than an unbound method reference.
  • CI — lint, format, typecheck, typetest, and build workflows: Claude Code (Claude Sonnet 4.6) added .gitea/workflows/checks.yml with five jobs (lint, format, typecheck, typetest, build) following the same structure as the existing test-node.yml and test-browser.yml workflows. Also added a @webnet/tsconnect#typecheck package-level override in turbo.json so that package's typecheck task depends on its own build (required because src/index.ts imports ../dist/wasm_exec.js, a WASM artifact absent in a fresh environment).

  • @webnet/tsconnect-worker — FileOps limits at init: Claude Code (Claude Sonnet 4.6) added fileOpsMaxFiles, fileOpsMaxTotalSize, and fileOpsMaxFileSize fields to WorkerConfig in protocol.ts, and wired them through to FsaFileOps.createFromOpfs() in worker.ts. Limits can also be changed after startup via the existing setFileOpsConfig() method on IpnWorkerClient.

  • @webnet/tsconnect-worker — FileOps limits at init: Claude Code (Claude Sonnet 4.6) added fileOpsMaxFiles, fileOpsMaxTotalSize, and fileOpsMaxFileSize fields to WorkerConfig in protocol.ts, and wired them through to FsaFileOps.createFromOpfs() in worker.ts. Limits can also be changed after startup via the existing setFileOpsConfig() method on IpnWorkerClient. A FileOpsLimits named type was extracted to protocol.ts and shared between the worker-side handler and the client-side setFileOpsConfig signature to prevent future drift.

  • AGENTS.md / CLAUDE.md — PR workflow improvements: Claude Code (Claude Sonnet 4.6) added guidance for always basing PRs on origin/main (fetch/pull before branching), watching CI results after push, spawning an autonomous code-review agent (Sonnet-class) once CI passes, and sending a push notification to the user when work settles or needs unblocking.

  • Test coverage gap-fill: Claude Code (Claude Sonnet 4.6) added unit tests to four previously uncovered packages. packages/xml: 17 tests for el(), text(), and stringify() (namespace prefixing, escaping, self-closing vs. open/close tags, all namespaces hoisted to root), plus test/test:coverage scripts. packages/tsconnect-redux: 60 tests covering all ~25 exported selectors (state, login, exitNode, self, lockedOut, suggestedExitNode, fileTargets, peer lookups by id/name/service/description, self-services, outgoing/incoming/waiting file selectors, fileOps state). packages/vfs: 44 direct MemoryVFS unit tests (stat, readdir, writeFile, readFileRange, mkdir, delete recursive, copy, move, setProps; all VFSError codes). packages/tsconnect-worker: 38 tests for WorkerConn, WorkerTCPListener, WorkerPacketConn, WorkerSSHSession, pumpStreamToPort, and portToReadableStream using MessageChannel pairs (no browser APIs required); added @types/node devDep and "types":["node"] to tsconfig. HTTP redirect tests were found to already exist in packages/http/src/client/fetch.test.ts (lines 5851203).

  • AGENTS.md / CLAUDE.md — PR labeling and auto-finalise: Claude Code (Claude Sonnet 4.6) added guidance to apply the org-level Agentic and Agent/<model-line> labels (which already exist at org level; Agent/* are exclusive) to every agent-opened PR, and to auto-finalise PRs without waiting for the user when the work is trivial or fully specified upfront.

  • @webnet/react and @webnet/utils — new packages: Claude Code (Claude Sonnet 4.6) extracted the useClient and useLocalStorage hooks from packages/tailshare into a new standalone @webnet/react package, and extracted the download, upload, readBlob, and fmtSize utilities into a new @webnet/utils package. Both packages are plain TypeScript with no dependencies beyond React (peer dep for @webnet/react) and are built with tsc.

  • @webnet/react and @webnet/utils — integration into example-app and test-app: Claude Code (Claude Sonnet 4.6) wired @webnet/react and @webnet/utils into the consumer apps. In example-app: replaced the local useClient duplicate with the package version; added useLocalStorage to persist the SharedWorker toggle (ipn:useWorker) across page reloads; replaced the local fmtSize with @webnet/utils; simplified the WaitingFileDebug download handler to use download() from @webnet/utils. Two bugs were fixed in @webnet/utils/download during integration: a typo (suggegestedNamesuggestedName) in the showSaveFilePicker call, and synchronous URL.revokeObjectURL reverted to a deferred setTimeout for Safari compatibility. In test-app: added @webnet/utils as a dependency and exposed it on window.utils for console testing.

  • @webnet/tsconnect-worker — flaky closed-state test fix: Claude Code (Claude Sonnet 4.6) replaced racy setTimeout(r, 10) waits in four "rejects immediately when closed" tests (read, write, accept, readFrom) with a deterministic while (!x.closed) await setImmediate() poll. The 10 ms sleep was not always sufficient under CI load, causing the "closed" message to arrive after the method under test created a pending promise, yielding "packet conn closed" instead of the expected "already closed" error.

  • CI — consolidate workflows and eliminate redundant install/build: Claude Code (Claude Sonnet 5) merged checks.yml, test-node.yml, and test-browser.yml into a single .gitea/workflows/ci.yml. Previously all 7 jobs independently repeated checkout + tailscale submodule clone + npm ci, and 5 of them independently rebuilt the whole workspace via Turbo. Live smoke tests against this Gitea instance found actions/cache (and setup-node's built-in cache: npm, same API) times out against the built-in cache proxy, and actions/upload-artifact/download-artifact v4 refuse to run at all (GHES detection), but v3 of the artifact actions work correctly. Given that, a new install job now does the real work once (submodule clone, npm ci, build) and hands node_modules plus Turbo's local cache off to typecheck/typetest/node-tests/browser-tests (all needs: install) via actions/upload-artifact@v3/download-artifact@v3, instead of each job reinstalling and rebuilding from scratch. lint/format never touched the submodule or build output, so they dropped that step and stay independent for fast feedback. The shared checkout+submodule+setup-node preamble was factored into a composite action, .gitea/actions/setup/action.yml (composite actions require the repo to already be checked out, so actions/checkout stays a separate first step in every job rather than being absorbed into the composite action). A concurrency group cancels superseded runs on the same ref. One real bug was found and fixed during this work: packages/xml and packages/vfs pin a newer local TypeScript than the workspace root, installed by npm as nested packages/{xml,vfs}/node_modules/typescript; the first version of the install job's node_modules archive only captured the root node_modules, so downstream jobs silently typechecked those two packages against the wrong TypeScript version and produced spurious errors — fixed by archiving packages/*/node_modules alongside the root. Cross-run caching (reusing a previous run's install/build) is not available until the Gitea instance's cache backend is fixed server-side; that's a separate, out-of-scope follow-up.

  • @webnet/tsconnect-worker — remaining flaky message-wait fixes: Claude Code (Claude Sonnet 5) found the same fixed-delay race pattern still present in the rest of worker.test.ts (the WorkerSSHSession.resize()/close() tests called out as flaky in CI, plus the other close()-message, callback-firing, and pumpStreamToPort tests), where a flat setTimeout(r, 10) was used to wait for a postMessage to be delivered before asserting on it. Added a generic waitFor(predicate, timeoutMs?) helper that polls via setImmediate and replaced every such fixed sleep with a poll on the actual condition (message present in the captured array, or callback fired). Following an autonomous review, the four remaining ad hoc while (!x.closed) await setImmediate() spin-loops from the earlier b917cd8 fix were also consolidated onto the same waitFor helper for consistency.

  • AGENTS.md / CLAUDE.mdtea mergeability and symlink clarifications: Claude Code (Claude Fable 5) documented that CLAUDE.md is a symlink to AGENTS.md, that tea reports mergeable: false while the WIP: title prefix is present (so mergeability is only meaningful at finalisation), and that the conflicting-files section header in tea output is always printed — only files listed under it indicate actual conflicts.

  • AGENTS.md / CLAUDE.md — exact model attribution: Codex (GPT-5.6-Sol) documented that agents must use the exact running model for commits, PRs, reviews, and Agent/* labels; prefer harness-provided identity, fall back to Codex CLI session metadata when needed, and never guess or reuse another session's model identity.

  • AGENTS.md / CLAUDE.md — project structure: Codex (GPT-5.6 Luna) documented that this repository uses npm workspaces and package-lock.json, with Turbo as the task runner and tailscale/ as a git submodule rather than an npm workspace; agents should not treat it as a pnpm repository.

  • CI — require built WASM test artifacts: Codex (GPT-5.6 Terra) made test and test:coverage depend on each package's own build, archives/restores workspace build outputs for every downstream CI job, and turns missing tsconnect WASM artifacts into CI-only test failures while preserving local skips.

  • @webnet/ftp — new package (FTP/FTPS client and server): Claude Code (Claude Fable 5, with Claude Sonnet 5 subagents on the common protocol layer) authored a new @webnet/ftp package: an FTP + implicit-FTPS server exposing any AsyncVFS over the RawListener transport interface, and an FTPClient implements AsyncVFS (mirroring DAVClient) over a RawDialer. Server command set targets FileZilla/KDE kio/classic ftp compatibility: USER/PASS (pluggable authenticate callback returning a per-user VFS), FEAT/OPTS UTF8/SYST/TYPE/MODE/STRU/PBSZ/PROT, PWD/CWD/CDUP, PASV/EPSV passive data channels via a caller-injected dataListen factory (TLS listeners = FTPS data channels; PORT/EPRT and AUTH TLS are 502 stubs pending active-mode and transport TLS-upgrade support), LIST/NLST (unix ls format), MLSD/MLST, STAT, SIZE/MDTM, REST/RETR/STOR streaming directly between the data connection and VFS web streams, DELE/RMD/MKD, RNFR/RNTO, ABOR. Client prefers EPSV/MLSD and falls back to PASV and unix-ls LIST parsing (for vsftpd, which lacks MLSD); operations are serialized on a single control connection with a mutex held across each verb+data-stream sequence; readFileRange maps to REST plus client-side truncation; copy is omitted (no server-side copy in FTP). Error translation both ways via vfsErrorToReply/replyToVFSError tables. 125 tests: unit tests for the codec/listing/address/time/path helpers and loopback integration suites (full AsyncVFS conformance, LIST/PASV fallback paths, raw protocol-level server assertions).

  • Development apps — explicit remote-host mode: Codex (GPT-5.6 Terra) added a safe local dev mode and an explicit dev:host mode to the Vite test app and webpack example app. The hosted mode binds to all interfaces and permits forwarded hosts only when WEBNET_DEV_HOST=1; workspace documentation covers trusted-network exposure and WebSocket proxying for HMR.

  • @webnet/ftp — real-server integration coverage: Codex (GPT-5.5) added an opt-in integration suite for a real FTP server. It is skipped unless FTP_TEST_HOST, FTP_TEST_USER, and FTP_TEST_PASS are configured, with optional FTP_TEST_PORT and FTP_TEST_DIR; it verifies upload, listing, download, rename, and recursive cleanup.

  • AGENTS.md — scoped pre-commit checks and CI-based PR finalisation: Codex (GPT-5.6 Luna) documented running linting, formatting, and typechecking scoped to affected packages or files before commits, relying on CI for global checks during PR finalisation, and monitoring CI after pushes.

  • AGENTS.md — issue labeling: Codex (GPT-5.6 Luna) extended the required Agentic and model-specific Agent/* labels from pull requests to issues, and documented the org-level Human label for human-opened work.

  • Explicit TLS upgrade (STARTTLS / AUTH TLS) on RawTransport: Claude Code (Claude Fable 5) added an optional in-place upgradeTls(options?: TlsUpgradeOptions) method and an optional isTls getter to RawTransport in @webnet/transport, for protocols that negotiate in plaintext and then upgrade the existing connection (SMTP STARTTLS, FTPS AUTH TLS). TlsUpgradeOptions is a union of client mode (serverName/insecureSkipVerify/caCerts; serverName required unless skipping verification, since an upgraded connection only knows its peer IP) and server mode (isServer: true with certPem/keyPem). All implementations require a quiescent transport (no pending read, no buffered data) and close the connection on handshake failure. NodeTransport swaps its socket for a node:tls TLSSocket wrapping the same socket (listener re-wiring extracted into #attach/#detach; a peer aborting the handshake surfaces as a plain close on the server side, handled explicitly); a checked-in self-signed localhost cert fixture backs a real bidirectional STARTTLS test suite. @webnet/tsconnect's Conn swaps its raw Go handle via a new upgradeTLS wasm bridge method (added to wrapConn in the tailscale submodule, with the TLS client-config construction factored out of dialTLS into tlsClientConfigFromJS; server mode uses tls.X509KeyPair + tls.Server like listenTLS) and guards against in-flight reads/writes with an operation counter; dialTLS-created conns now report isTls: true. @webnet/tsconnect-worker gained upgradeTls/upgraded/upgradeError messages in the per-conn protocol, an isTls field on the conn/accepted messages, and matching WorkerConn support. Verified end-to-end with a live STARTTLS handshake against smtp.gmail.com:587. Three AI review rounds hardened the feature: an autonomous Sonnet review found that reads/writes weren't blocked while an upgrade handshake was in flight (fixed with an upgrading guard in all three implementations); a neutral Fable 5 review found the server-side pipelined-ClientHello race (a fast peer's handshake bytes landing in the internal buffer before upgradeTls is called are now unshifted back into the stream) and the worker upgradeError conflating validation failures with handshake failures (the reply now carries a closed flag); a GPT-5.6 review found that pre-handshake configuration failures leaked the Go conn (the wrapper marked it closed while Go left it open — Go now closes on every error path) and that a synchronous invalid-PEM throw could permanently strand the node transport in upgrading state (TLS socket construction moved inside the failure path). Final semantics, documented on RawTransport: synchronous option-validation rejections leave the transport usable; any failure after the upgrade starts closes it.

  • Cross-tab state and ownership transfer (@webnet/transport, @webnet/tsconnect-worker, @webnet/http, @webnet/drive): designed and orchestrated by Claude Code (Claude Fable 5), implemented by Claude Opus 4.8 and Claude Sonnet 4.5 subagents. Adds a mechanism to move live resources (tcp/tls conns, listeners, udp packet conns, plus arbitrary JSON/byte/transferable payloads) from one SharedWorker client tab to another without reopening connections, e.g. to pop a protocol panel out into its own window that survives the originator closing. @webnet/transport gains the StateTransferable interface (transferState() detaches and returns a structured-cloneable state) and isStateTransferable. @webnet/tsconnect-worker: worker-side resources are tracked per client under stable resource ids and can be detached into a TTL'd pending-transfer registry (transfers.ts) that survives the owning client's death (IPN shutdown is deferred while transfers are pending) and re-bridged to the claiming client; clients register an app-supplied clientKey in hello, and IpnWorkerClient gains a broker API (sendTransfer/onTransfer, convenience transfer/onAdopt, claim, transferSupported, workerDialer); envelopes addressed to a not-yet-connected key are queued in the worker and flushed on registration (Fable 5 fixed a handshake bug found by the browser tests where flushed envelopes arriving before ready were dropped). @webnet/http: ClientConnection.canExport()/exportTransport() and ConnectionPool.exportIdle()/seed() move idle keep-alive connections (with any buffered prefix bytes) between pools. @webnet/drive: DAVClient implements StateTransferable<DavTransferState> and DAVClient.adopt() rebuilds a client from claimed transports, degrading gracefully to re-dialing when claims fail. Covered by Node unit tests (registry, proxies, broker, pool, DAV) and a two-tab Playwright suite proving a transferred listener survives originator-tab death.

  • Cross-tab state and ownership transfer (@webnet/transport, @webnet/tsconnect-worker, @webnet/http, @webnet/drive): designed and orchestrated by Claude Code (Claude Fable 5), implemented by Claude Opus 4.8 and Claude Sonnet 4.5 subagents. Adds a mechanism to move live resources (tcp/tls conns, listeners, udp packet conns, plus arbitrary JSON/byte/transferable payloads) from one SharedWorker client tab to another without reopening connections, e.g. to pop a protocol panel out into its own window that survives the originator closing. @webnet/transport gains the StateTransferable interface (transferState() detaches and returns a structured-cloneable state) and isStateTransferable. @webnet/tsconnect-worker: worker-side resources are tracked per client under stable resource ids and can be detached into a TTL'd pending-transfer registry (transfers.ts) that survives the owning client's death (IPN shutdown is deferred while transfers are pending) and re-bridged to the claiming client; clients register an app-supplied clientKey in hello, and IpnWorkerClient gains a broker API (sendTransfer/onTransfer, convenience transfer/onAdopt, claim, transferSupported, workerDialer); envelopes addressed to a not-yet-connected key are queued in the worker and flushed on registration (Fable 5 fixed a handshake bug found by the browser tests where flushed envelopes arriving before ready were dropped). @webnet/http: ClientConnection.canExport()/exportTransport() and ConnectionPool.exportIdle()/seed() move idle keep-alive connections (with any buffered prefix bytes) between pools. @webnet/drive: DAVClient implements StateTransferable<DavTransferState> and DAVClient.adopt() rebuilds a client from claimed transports, degrading gracefully to re-dialing when claims fail. Covered by Node unit tests (registry, proxies, broker, pool, DAV) and a two-tab Playwright suite proving a transferred listener survives originator-tab death. Following review (GPT 5.6-Terra and an autonomous Claude Sonnet 4.5 pass), Fable 5 fixed an ownership-crossing bug (results of in-flight reads/accepts at detach time are now parked in a per-resource generation-tagged backlog and delivered to the claiming owner in order, instead of leaking on the old client entry) and five smaller findings (nested-token collection in transfer(), handshake replay ordering, DAV export error path, pool slot pruning, main-thread stub rejection semantics). After the branch was rebased onto the TLS-upgrade work, Fable 5 integrated the two features: transferred conns keep their TLS state across ownership transfer (isTls on claim replies and conn ResourceMeta), upgradeTls is serialized through the per-resource channel lock, and transferState refuses while an upgrade is in flight.

  • @webnet/ftp — explicit FTPS (AUTH TLS): Codex (GPT-5.6-Sol) added an "explicit" client security mode with verified control/data upgrades, configurable CA roots and an explicit insecure override that still sends the configured hostname for SNI. The server now handles AUTH TLS, advertises it through FEAT, supports optional or required TLS-before-login policy, and applies PROT P to passive data transports. Passive listeners arm acceptance when PASV/EPSV starts so Node transports cannot lose early connections, while TLS handshakes remain deferred until the transfer command. Real TLS tests cover protected round trips, SNI in insecure mode, custom and untrusted roots, hostname mismatch, required-login policy, invalid server credentials, missing upgrade support, and data-handshake recovery.

  • @webnet/ftp — explicit FTPS (AUTH TLS): Codex (GPT-5.6-Sol) added an "explicit" client security mode with verified control/data upgrades, configurable CA roots and an explicit insecure override that still sends the configured hostname for SNI. The server now handles AUTH TLS, advertises it through FEAT, supports optional or required TLS-before-login policy, and applies PROT P to passive data transports after the RFC-required protected-control and PBSZ 0 sequence. Passive listeners arm acceptance when PASV/EPSV starts so Node transports cannot lose early connections, while TLS handshakes remain deferred until the transfer command; abandoned early accepts are closed when a passive channel is replaced. Real TLS tests cover protected round trips, SNI in insecure mode, custom and untrusted roots, hostname mismatch, required-login policy, invalid server credentials, missing upgrade support, data-handshake recovery, and passive transport cleanup.

  • @webnet/smb2 — new SMB2/3 client package: Claude Code (Fable 5) designed and implemented a new @webnet/smb2 package: an SMB2/3 client that runs over the existing RawTransport/RawDialer abstraction (TCP/445 in Node, relayed through tsconnect's IPNDialer in the browser) and exposes a remote Windows/Samba share as an @webnet/vfs AsyncVFS (SMB2Client), mirroring how @webnet/drive exposes WebDAV. It targets default Windows 10/11 (including 24H2's mandatory SMB signing) and supported Samba 4.x, negotiating dialects {2.0.2, 2.1, 3.1.1}. Highlights:

    • Isomorphic crypto, no Node APIs, no new dependencies. The primitives Web Crypto lacks are hand-written in TypeScript and only run over small NTLM blobs: MD4, MD5, HMAC-MD5. SMB3 AES-CMAC signing is built on Web Crypto AES-CBC (RFC 4493 subkey construction); the SP800-108 key-derivation function on Web Crypto HMAC-SHA256; SHA-512 preauth-integrity and HMAC-SHA256 signing use Web Crypto natively. All primitives are covered by published test vectors (RFC 1320/1321/2202/4231/4493, NIST SP800-108, and the MS-NLMP §4.2.4 NTLMv2 sample).
    • Authentication: NTLMv2 inside NTLMSSP inside a minimal hand-rolled SPNEGO (DER) wrapper, with the message-integrity code (MIC) and channel-binding AV pair.
    • Signing (HMAC-SHA256 for 2.x, AES-CMAC for 3.x) and 3.1.1 SHA-512 preauth-integrity hashing are implemented; the client both signs its requests and verifies inbound response signatures (rejecting unsigned or invalid responses on a signed session, per [MS-SMB2] 3.2.5.1.3). SMB3 encryption is deferred (the transform layer is structured so it can be added without restructuring), and no cipher is advertised so an encryption-mandating share fails cleanly at TREE_CONNECT.
    • VFS surface: stat, readdir, readFile/readFileRange (streaming, credit-aware, chunked to the negotiated max read size), writeFile, mkdir, delete (recursive), move (rename), copy, and setProps (timestamps), plus connect/disconnect. NTSTATUS codes are translated to VFSError codes at the boundary.
    • Tests: unit tests with the crypto/auth vectors above, protocol codec round-trip tests, and an in-repo mock SMB2 server that performs a real SMB 3.1.1 signed round-trip end-to-end over the loopback transport (including tests that tampered and unsigned responses are rejected). An opt-in smb2.integration.test.ts (gated behind SMB2_TEST_* env vars) runs against a real Samba/Windows share.
    • Wired into @webnet/test-app (exposed as window.smb2) for a browser smoke check.
    • The protocol command codecs and the mock server were drafted by subordinate Sonnet agents from exact wire-layout specifications; the crypto, authentication, connection/session state machine, and VFS mapping were written and reviewed by Fable 5.
  • @webnet/sftp — new package (SFTP v3 client and server): Claude Code (Claude Fable 5, coordinating; Claude Opus 4.8 subagents implemented the SSH transport, auth/channels, and SFTP client/server layers, and a Claude Sonnet 5 subagent scaffolded the package and pure codecs) authored a new @webnet/sftp package: an SFTP v3 client (SFTPClient implements AsyncVFS, mirroring DAVClient/FTPClient) over a RawDialer, and an SFTPServer serving any AsyncVFS over a RawListener via an http-style listen(listener, opts) accept loop. Both run in the browser (via tsconnect's IPNDialer) and Node with no Node APIs in package source and no new dependencies. The package implements a full SSH-2 transport from scratch on Web Crypto (crypto.subtle/crypto.getRandomValues) with zero hand-written primitives: version-banner exchange, binary packet framing with per-direction sequence numbers, curve25519-sha256 key exchange with ssh-ed25519/rsa-sha2-256/rsa-sha2-512 host-key verification, RFC 4253 key derivation, aes128/256-gcm@openssh.com and aes128/256-ctr with hmac-sha2-256/hmac-sha2-256-etm@openssh.com ciphers (continuous CTR counter and GCM invocation nonce tracked across packets), and transparent peer- or self-initiated rekey. On top of that: ssh-userauth (client offers a direct signed Ed25519 publickey request then falls back to password; server drives none/publickey(PK_OK)/password through a pluggable authenticate callback returning a per-user AsyncVFS), a connection-protocol mux with a session channel and bidirectional window flow control giving end-to-end backpressure, and the SFTP v3 layer. The client pipelines requests over one channel (request-id dispatch map, serialized wire writes so a split WRITE stays contiguous), maps AsyncVFS verbs onto FXP operations with pull-driven read streams (≤8 outstanding 32 KiB READs) and bounded-inflight writes, client-side recursive delete, and posix-rename@openssh.com for overwriting move. The server maps FXP back onto the VFS with a handle table, 100-entry READDIR batches with unix ls -l longnames, sequential streaming reads/writes, SETSTAT/FSETSTAT no-ops (so OpenSSH put succeeds), REALPATH, and v3 rename semantics; responses are serialized per session and handler errors reply a status without tearing down the connection. Ed25519 auth keys are parsed from the unencrypted openssh-key-v1 format (encrypted keys are out of scope); host keys are optionally verified via a verifyHostKey({ type, key, fingerprint }) callback and can be generated with the exported generateHostKey(). 203 tests: crypto/kex/cipher/codec/key/path units, loopback transport suites (incl. a 1000-packet encrypted echo and mid-stream rekey), per-layer auth/channel/client/server suites, and a real-client-against-real-server end-to-end suite (4 MiB windowed transfer, ranged reads, pipelined concurrency, cancel-then-continue, password/publickey/per-user auth, host-key verification), plus an env-gated suite against a real OpenSSH sshd. Verified interoperable against the OpenSSH sftp CLI (which surfaced and fixed a pre-subsystem env channel-request handling gap). An autonomous code review (Claude Sonnet 5) followed by fixes (Claude Fable 5) hardened the server against a malformed post-handshake packet leaking the transport, a write-queue deadlock when a backing vfs.writeFile fails mid-stream, an unenforced channel receive window, and a zero-length SFTP packet. A second independent review (Claude Fable 5) found and fixed a client-side data-corruption bug: the pipelined reader trusted requested offsets, so a spec-legal short mid-file READ (pipes/special files, some non-OpenSSH servers) left an un-requested gap; the reader now reconciles against the bytes actually returned and re-issues from the true offset. It also now cancels the source stream on a writeFile error. Known intentional limitations (streaming model over AsyncVFS, which has no chmod/utimes/positioned-write primitives): SETSTAT/FSETSTAT are accepted as no-ops, and writes must be sequential from offset 0.

  • @webnet/sftp — new package (SFTP v3 client and server): Claude Code (Claude Fable 5, coordinating; Claude Opus 4.8 subagents implemented the SSH transport, auth/channels, and SFTP client/server layers, and a Claude Sonnet 5 subagent scaffolded the package and pure codecs) authored a new @webnet/sftp package: an SFTP v3 client (SFTPClient implements AsyncVFS, mirroring DAVClient/FTPClient) over a RawDialer, and an SFTPServer serving any AsyncVFS over a RawListener via an http-style listen(listener, opts) accept loop. Both run in the browser (via tsconnect's IPNDialer) and Node with no Node APIs in package source and no new dependencies. The package implements a full SSH-2 transport from scratch on Web Crypto (crypto.subtle/crypto.getRandomValues) with zero hand-written primitives: version-banner exchange, binary packet framing with per-direction sequence numbers, curve25519-sha256 key exchange with ssh-ed25519/rsa-sha2-256/rsa-sha2-512 host-key verification, RFC 4253 key derivation, aes128/256-gcm@openssh.com and aes128/256-ctr with hmac-sha2-256/hmac-sha2-256-etm@openssh.com ciphers (continuous CTR counter and GCM invocation nonce tracked across packets), and transparent peer- or self-initiated rekey. On top of that: ssh-userauth (client offers a direct signed Ed25519 publickey request then falls back to password; server drives none/publickey(PK_OK)/password through a pluggable authenticate callback returning a per-user AsyncVFS), a connection-protocol mux with a session channel and bidirectional window flow control giving end-to-end backpressure, and the SFTP v3 layer. The client pipelines requests over one channel (request-id dispatch map, serialized wire writes so a split WRITE stays contiguous), maps AsyncVFS verbs onto FXP operations with pull-driven read streams (≤8 outstanding 32 KiB READs) and bounded-inflight writes, client-side recursive delete, and posix-rename@openssh.com for overwriting move. The server maps FXP back onto the VFS with a handle table, 100-entry READDIR batches with unix ls -l longnames, sequential streaming reads/writes, SETSTAT/FSETSTAT no-ops (so OpenSSH put succeeds), REALPATH, and v3 rename semantics; responses are serialized per session and handler errors reply a status without tearing down the connection. Ed25519 auth keys are parsed from the unencrypted openssh-key-v1 format (encrypted keys are out of scope); host keys are optionally verified via a verifyHostKey({ type, key, fingerprint }) callback and can be generated with the exported generateHostKey(). 202 tests: crypto/kex/cipher/codec/key/path units, loopback transport suites (incl. a 1000-packet encrypted echo and mid-stream rekey), per-layer auth/channel/client/server suites, and a real-client-against-real-server end-to-end suite (4 MiB windowed transfer, ranged reads, pipelined concurrency, cancel-then-continue, password/publickey/per-user auth, host-key verification), plus an env-gated suite against a real OpenSSH sshd. Verified interoperable against the OpenSSH sftp CLI (which surfaced and fixed a pre-subsystem env channel-request handling gap). An autonomous code review (Claude Sonnet 5) followed by fixes (Claude Fable 5) hardened the server against a malformed post-handshake packet leaking the transport, a write-queue deadlock when a backing vfs.writeFile fails mid-stream, an unenforced channel receive window, and a zero-length SFTP packet. A second independent review (Claude Fable 5) found and fixed a client-side data-corruption bug: the pipelined reader trusted requested offsets, so a spec-legal short mid-file READ (pipes/special files, some non-OpenSSH servers) left an un-requested gap; the reader now reconciles against the bytes actually returned and re-issues from the true offset. It also now cancels the source stream on a writeFile error. Known intentional limitations (streaming model over AsyncVFS, which has no chmod/utimes/positioned-write primitives): SETSTAT/FSETSTAT are accepted as no-ops, and writes must be sequential from offset 0.

  • @webnet/ssh — SSH-2 package split out of @webnet/sftp, plus TCP port forwarding: Claude Code (Claude Fable 5) extracted the hand-written SSH-2 stack (transport, curve25519 kex, ciphers, host keys, userauth, connection-protocol channels) out of @webnet/sftp into a new standalone @webnet/ssh package via pure git mvs, so it can be reused independently; @webnet/sftp now depends on @webnet/ssh and consumes its low-level pieces through @webnet/ssh/_internals. The server auth path was decoupled from @webnet/vfs: authenticateServer<T> is now generic over an authentication context and rejects with a new SSHAuthError rather than VFSError, which @webnet/sftp maps back to VFSError("forbidden") at its own boundary to preserve behaviour. On top of the split, TCP forwarding primitives were added. The channel mux (ConnectionMux) gained configurable accepted channel types and a global-request handler, generic CHANNEL_OPEN handling that surfaces incoming opens (session / direct-tcpip / forwarded-tcpip with parsed endpoints) as an IncomingOpen the consumer can accept()/reject(), outbound openDirectTcpip/openForwardedTcpip, and an in-order globalRequest/onGlobalRequest path for tcpip-forward/cancel-tcpip-forward (RFC 4254 §7). A channelTransport() adapter wraps a Channel as a @webnet/transport RawTransport (EOF surfaced as a throw with readEnded, halfClose as CHANNEL_EOF, non-blocking close() so a forwarded stream can't deadlock on the peer close handshake), and an internal pipe() bridges two transports. The public API exposes SSHClientConnection (connect over a RawTransport, openSubsystem, openSession, openDirectTcpip, dialer() returning a RawDialer for local forwarding, and requestRemoteForward() returning a RemoteForward that implements RawListener for remote forwarding) and SSHServerConnection<T> (auth hook returning the context, acceptSession(), and optional directTcpip/tcpipForward hooks that plug arbitrary RawTransport/RawListener implementations into the forwarding paths). The design deliberately keeps openSession()/session-channel handling generic so later PRs can add exec/shell/pty request plumbing (for scp/rsync clients and a shell-delegating server) without reshaping the connection API. @webnet/sftp's client and server were refactored onto the new connection classes with no behaviour change (its full suite passes unmodified). New tests cover direct-tcpip open round-trips and accept/reject, global-request ordering/failure, the channel/RawTransport adapter (EOF, half-close, windowed backpressure), and loopback end-to-end forwarding in both directions; an env-gated (SSH_TEST_*) interop suite exercises ssh -L/ssh -R equivalents against a real OpenSSH sshd (direct-tcpip to the sshd banner, and a remote-forward loop dialed back through direct-tcpip). Two autonomous code-review rounds followed. A Sonnet review found port-only remote-forward keying (two forwards on the same port collided), unguarded CHANNEL_OPEN_CONFIRMATION sends in the accept loops, and unrejected pending RemoteForward.accept() waiters on close — all fixed. A second Fable review then found three more serious issues, all fixed by Claude Fable 5 with regression tests: (1) Channel.send() deadlocked forever if the peer closed the channel while the sender was blocked on window exhaustion (_deliverClose now wakes window waiters and send() aborts on a remotely-closed channel); (2) pipe()'s copy loop only guarded reads, so a write failure on a forwarded socket became an unhandledRejection that crashes the Node process by default — a remotely-triggerable DoS — now the write is guarded and both void pipe() call sites swallow; (3) connect-phase failures leaked the underlying socket (SFTPClient.#doConnect and SSHClientConnection.connect now close on openSubsystem/auth failure; verified against an sshd with no sftp subsystem, which previously hung the process to the runner timeout). Four smaller fixes: authenticateServer accepts a falsy auth context (uid 0 etc.) instead of rejecting it, #matchForward's port fallback fails closed on an ambiguous port rather than misrouting, acceptSession() rejects instead of hanging once the accept loop has died, and a duplicate tcpip-forward closes the superseded listener (and RemoteForward teardown closes queued-but-unaccepted transports).

  • @webnet/ftp — FTP client state transfer: GPT-5.6 Terra implemented transferable FTP control connections: FTPClient.transferState() exports worker-backed connections plus buffered reply bytes and negotiated session state; FTPClient.adopt() claims them or reconnects and authenticates on expiry. Active data transfers are refused, and focused tests cover claim, fallback, and the safety guard.

  • @webnet/ftp — state-transfer ownership fix: GPT-5.6 Terra addressed a GPT-5.6 Sol review finding by making detachment atomic under the control lock; queued commands now reject after handoff and a regression test covers that race.

  • @webnet/smb2 — cross-tab state transfer (StateTransferable): Claude Code (Fable 5) implemented worker-side ownership transfer for SMB2Client (issue #99), consistent with DAVClient. SMB2Client.transferState() detaches an idle client and exports a structured-cloneable SMB2TransferState: the parked-transport token plus everything needed to keep using the authenticated session on the adopting side — server/share/port config, negotiated dialect and max transact/read/write sizes, message-id and credit counters, session and tree ids, the 3.1.1 preauth hash, and the derived signing key with the signing-required flag. Credentials are never serialized; SMB2Client.adopt(state, { claim, dialer, username, password, reconnect? }) takes them from the adopter's configuration, validates state version/shape before claiming, resumes message-id and signing state exactly once on the claimed transport, closes the claimed transport if resumption fails, and (only with reconnect: true) falls back to a fresh dial-on-demand session when the claim is expired/duplicate — transferred signing/session state is never reused on a new transport. Transfer is rejected unless the client is quiescent (no request in flight, no connect pending, no read/write stream still owning an SMB file handle, no buffered incoming bytes), and the source client becomes permanently detached after a successful transfer instead of silently reconnecting. Tests extend the mock server with per-connection signing state, an SMB 2.1 mode, request-signature verification, and message-id recording, covering: signed 2.1/3.1.1 sessions continuing with the next message id after a structuredCloned transfer (with response-signature verification still enforced), busy/streaming/connect-pending rejection, config-only transfer for never-connected clients or non-transferable transports, malformed/version-mismatched/duplicate/expired state handling, claimed-resource cleanup on failed adoption, and reconnect fallback; the opt-in Samba integration suite gained a live transfer round-trip. A browser two-tab transfer test is deferred: it needs a reachable SMB server inside the worker's tailnet, which the browser CI environment does not provide (the same limitation applies to the existing DAV/FTP transfer work).

  • Agent label documentation: gpt-5.6-sol updated AGENTS.md to require exact Agent/<model-slug> labels and coordinating-model attribution for orchestrated work.

  • @webnet/utils — shared binary reader/writer (issue #79): GPT-5.6 Luna implemented a dependency-free, endian-aware binary codec primitive with bounds-checked reads, growable writes, offset-correct DataView handling, alignment, skipping, random access, patching, and explicit finish-copy semantics. SSH and SMB2 now delegate their cursor mechanics to it while retaining protocol-specific encodings, with coverage for both endiannesses, growth, subarray offsets, overruns, alignment, patching, and zero-length operations.

  • @webnet/state-transfer — generic state-transfer contract extraction: Codex (GPT-5.6 Luna) extracted StateTransferable and isStateTransferable from @webnet/transport into a dependency-free workspace package, migrated current DAV/FTP/SMB2/worker consumers, removed the unused transport re-exports, and added guard tests and package metadata.

  • @webnet/state-transfer — review follow-up: Codex (GPT-5.6 Luna) restored the original object-only type-guard semantics, excluded typetests from the production build, added package coverage metadata, and validated the initialized worker and full workspace with the root build, tests, typechecks, and typetests.

  • Root Node/npm engine declaration: gpt-5.6-sol declared Node 24 and npm 11 as the supported root workspace toolchain and synchronized the lockfile metadata.

  • @webnet/tailshare - original implementation review: Initial implementation (PR #28) hand-written, but reviewed by multiple agents: Claude Sonnet 4.6 during writing and Claude Opus 5 and GPT 5.6 Sol before merge.

  • @webnet/tailshare — SharedWorker support, type fixes, config consolidation, and fileOps UI: Claude Code (Claude Sonnet 4.6) rewired the IPN connection in tailshare to use @webnet/tsconnect-worker via useBuildIpnWorker, enabling multiple browser tabs to share a single IPN instance through a SharedWorker (with transparent fallback to main-thread mode when SharedWorker is unavailable). The IPN concrete class was replaced with the IpnClient interface throughout, and IpnClientHandle is used as the return type from the worker hook. The ipnPrepare Redux slice was removed entirely and replaced with an IpnPrepareContext (React context) that reads/writes a single tailshare:config localStorage key as a JSON object — consolidating ipn#hostname, ipn#controlURL, ipn#authKey, ipn#exitNode, ipn#auto and the new useWorker and fileOps settings into one key, with explicit defaults (useWorker: true, fileOps: "memory") set in parseConfig so call sites need no inline fallbacks. The config modal (TailscaleConfig) gained a file-storage select (memory / opfs, with OPFS disabled when SharedWorker is unavailable) and a SharedWorker toggle; the mode indicator shows whether the active connection is worker or main-thread. In worker mode, selecting OPFS enables fileOps: true in the WorkerConfig so received files survive page reloads; in main-thread fallback the storage is always in-memory and a warning is shown if OPFS is selected without a worker. autostart remains a tri-state: undefined latches to true on the first Running event (so subsequent page loads auto-connect), false permanently disables autostart, and true triggers it immediately on load. TailscaleRoute shows a spinner rather than a disabled "Enable Tailscale" button during the worker connection window. @webnet/react gained a useSharedWorkerAvailable() hook (SSR-safe via useSyncExternalStore) replacing the inline pattern in IpnContext.tsx. useBuildIpnWorker in @webnet/tsconnect-react was extended to accept (() => SharedWorker) | null in its first overload (implementation already handled it). @webnet/react/tsconfig.json and @webnet/tsconnect-react/tsconfig.json both received skipLibCheck: true to resolve a type conflict between @types/eslint-scope and eslint's built-in types.

  • @webnet/tsconnect-react — initialization error callbacks: gpt-5.6-sol added optional error callbacks to useBuildIpn and useBuildIpnWorker, covering synchronous builder/run failures and asynchronous worker/fallback connection failures while suppressing callbacks after effect cleanup.

  • @webnet/vfs — reusable AsyncVFS conformance suite (issue #80): Claude Code (Claude Opus 5, coordinating; Claude Sonnet 5 subagents adopted the suite in the FTP, SFTP and SMB2 packages) added a reusable conformance harness owned by @webnet/vfs and exposed from a test-only @webnet/vfs/conformance entry point, so production bundles do not acquire test code. testAsyncVFSConformance({ name, create, capabilities, errorCodes }) takes a factory producing a fresh filesystem per test, a capability descriptor for optional operations (optional methods absent from the instance are skipped rather than failed), and an error-code alias map for protocols that cannot distinguish two VFSError codes; the entry point also exports the stream helpers each package had been redefining. The baseline suite covers write/read round trips and multi-chunk streaming, stat and directory listing, mkdir/delete/recursive delete/move/copy/setProps, readFileRange inclusive-end semantics, empty files, unicode names, nested paths, the full VFSError contract, stream cancellation and write-source error propagation, and sequential plus concurrent operations. MemoryVFS and NodeVFS were migrated as the reference implementations and Drive, FTP, SFTP and SMB2 adopted it, each keeping its protocol-specific tests. The suite surfaced pre-existing bugs in every package it touched, all fixed here: MemoryVFS.readFile handed out the entry's own array, which a byte-stream consumer transfers and detaches, so reading a file over WebDAV and cancelling the stream truncated the stored file to zero bytes; NodeVFS.readFile on a directory deferred is-a-directory to the first stream read and delete("/", true) removed the entire served root; the DAV client defaulted copy/move to overwriting, only honoured a non-recursive delete when recursive was explicitly false, returned an empty listing for readdir of a file, and merged rather than replaced in setProps, while the DAV server reported is-a-directory as 409 instead of RFC 4918's 405; the FTP client silently clobbered on a default move and discarded the server's explicit "not a directory"/"is a directory" replies; the SFTP client guessed error codes from the request verb instead of reading the code its own server embeds, and opening a directory for reading succeeded; the SMB2 client accepted deleting the share root and ignored opts.overwrite in copy (which also could not copy directories), and its mock server ignored CreateOptions directory flags and ReplaceIfExists. Three of the six packages defaulted a destructive operation to overwriting, which is the contract drift the issue describes. The harness compares large payloads byte-wise rather than with assert.deepEqual, because building a diff between a 128 KiB array and an empty one exhausts memory and kills the process before the failure can be reported. The SFTP client was additionally verified against a real OpenSSH sshd to confirm the status-parsing change still falls back correctly for third-party servers. Two independent autonomous reviews followed — Claude Sonnet 5 and Codex (GPT-5.6) — and both were addressed by Claude Opus 5: the suite had lost readFileRange's error contract when the per-package tests were removed and never covered copy replacing an existing directory (which NodeVFS merged into instead); SMB2 copy could recurse without bound when the destination lay inside the source and could not overwrite a destination of the other type; the DAV client read every 405 as is-a-directory although RFC 4918 only gives it that meaning for GET and PUT; the FTP server relayed the backing filesystem's prose as 550 text and now sends conventional wording per code, which removed the need for the forbidden/not-found alias the reviewers correctly identified as covering a fixable bug rather than a protocol limit; and SFTP now marks the VFS code it embeds in SSH_FX_FAILURE messages so a third-party server's prose cannot be decoded as one of ours.

  • CI — bounded Node heap (issue #136): Claude Code (Claude Opus 5) added a workflow-level NODE_OPTIONS: --max-old-space-size=4096 to .gitea/workflows/ci.yml, so every CI job that runs Node (including the per-file workers node --test spawns and Turbo-invoked package scripts, which inherit the variable) fails with a legible V8 heap-limit error instead of growing until the runner's own memory limit kills the task and any jobs sharing the machine.

  • @webnet/tsconnect — minimal build output (issue #154): gpt-5.6-sol changed the Tailscale package build to use a temporary staging directory and copy only main.wasm, build-info.json, wasm_exec.js, and cacert.pem into dist/, excluding unused upstream demo bundles, source maps, styles, and package metadata from published packages and Turbo caches.

  • turbo setup - cache issue: written collaboratively between human codinget and agent claude-opus-5 who double checked and debugged my work.

  • @webnet/taildrive — share-bearing peer discovery: gpt-5.6-sol added a Webnet-only listDrivePeersWithShares(ipn) helper that probes the existing candidates through the shared WebDAV client, positively retains peers exporting at least one share, bounds concurrent and malformed responses, and exposes a reusable per-peer DAVClient factory for a later whole-tailnet AsyncVFS hierarchy. Focused tests cover populated, empty, invalid, unreachable, and ordered peer results.

  • @webnet/taildrive — peer-probe review hardening: gpt-5.6-sol addressed autonomous review findings by covering the IPN dial phase with the probe deadline, closing a connection that arrives after timeout, and requiring a valid root-directory response before accepting a nonempty WebDAV listing. Regression coverage includes a permanently stalled dial and a malformed 207 response that omits the requested root.

  • @webnet/taildrive — discovery API review follow-up: gpt-5.6-sol renamed the narrower discovery helper to listDrivePeersWithShares to distinguish it from IpnClient.listDrivePeers, and added the conventional package-root export alongside the existing client and server subpaths.

  • @webnet/vfs — combined stat and directory listing: gpt-5.6-sol added the optional AsyncVFS.statAndReaddir() operation, with optimized Memory, File System Access, WebDAV, and SMB2 implementations. Drive PROPFIND/COPY/MOVE and delete handling, FTP server listings, SMB2 recursive operations, and Taildrive peer discovery reuse combined metadata where available while retaining inline fallbacks for other VFS implementations. Conformance, browser, protocol, malformed-response, request-count, and integration coverage preserve existing readdir() semantics and verify files return their own stat with no entries. A Claude Opus 5 review found that SMB recursive deletion could accidentally request read-data access on files and identified implicit listing/native-copy contracts; gpt-5.6-sol fixed the access regression, aligned DAV self-resource validation, documented the contracts, and added request-level and conformance coverage. Fresh gpt-5.6-terra reviews then caught missing-source COPY/MOVE deleting an overwrite destination in the Drive fallback and native Node, Memory, and SFTP paths; gpt-5.6-sol reordered fallback handling, made each native operation source-safe, and added data-preservation regressions. A final Claude Opus 5 pass confirmed the fixes and prompted documentation that successful overwrites replace rather than merge directory destinations until #137 introduces explicit policies.

  • @webnet/vfs — unsupported error code and optional-operation fallbacks (issue #82): Claude Code (Claude Opus 5) added the unsupported VFSErrorCode and a @webnet/vfs/fallback entry point. The code is documented as a capability failure, distinct from forbidden (permitted but denied), precondition-failed (state prevents it) and a backend or transport failure, and carries the requirement that rejecting with it leaves the filesystem unchanged — which is what makes answering it by doing the work another way safe. Optional AsyncVFS members are correspondingly documented as absent when an implementation can never perform the operation and present-but-rejecting when it can for some paths and not others, so a backend spanning several shares, servers or negotiated dialects can say so per call rather than per instance. Each protocol adapter maps the code both ways: SFTP SSH_FX_OP_UNSUPPORTED, FTP 502 and WebDAV 501 outbound, and the same statuses plus SMB2 STATUS_NOT_SUPPORTED inbound — the SMB2 status previously reached callers as forbidden, claiming a permission problem the server never reported, and the WebDAV and FTP clients discarded their own servers' 501/502 entirely. withFallbacks(vfs, options) returns a Required<AsyncVFS> so a consumer can call any optional operation without branching on method presence; statAndReaddirFallback, readFileRangeFallback, copyFallback, moveFallback and setPropsFallback are also exported individually for call sites that need one operation without wrapping the filesystem they were handed. A per-operation policy chooses when a fallback may stand in (native-only, on-missing, on-unsupported), and only an unsupported rejection of the returned promise ever triggers one: any other code, a non-VFSError, or a stream that errors after bytes have been delivered propagates untouched, so a permission, state or transport failure is never retried into a partial success. setProps has no fallback, because arbitrary property storage cannot be built from the required operations, and says unsupported rather than appearing absent. The copy fallback rejects overlapping source and destination paths in both directions — a destination inside the source never terminates, and a source inside the destination would be deleted by the overwrite step before being read — reuses the complete metadata readdir guarantees rather than restatting each child, recurses sequentially so a wide tree does not open one transfer per entry, removes a partial destination on failure, and is documented as neither transactional nor atomic. The range fallback windows readFile lazily, holds at most one chunk, and cancels the source as soon as the window is satisfied, so serving a short range of a large file does not read the whole thing. The three byte-identical copies of the statAndReaddir fallback that arrived with #172 were replaced by the shared one. The conformance suite's optional-method capabilities became tri-state so a runtime unsupported rejection is itself testable, including that nothing changed, and the fallbacks are exercised by running the whole suite against a MemoryVFS with its optional operations first removed and then rejecting. Pinning readFileRange's boundary behaviour — a start at or past the end of a file, and an end below the start, yield an empty stream rather than an error — surfaced three genuine bugs, all fixed rather than excused by a capability opt-out: NodeVFS passed an inverted range straight to createReadStream, which throws a raw ERR_OUT_OF_RANGE; the WebDAV client reported the 416 that RFC 9110 requires for a range at or past the end of a representation as an unexpected status; and the FTP client, whose REST carries no end bound, transferred from the start to the end of the file instead of nothing. Review by the author then established that the migrated statAndReaddir call sites want the on-unsupported policy rather than the default: each needs the listing to answer the request it is serving, and the stat plus readdir it falls back to are required operations, so declining to fall back only loses. Issues #176 (a heap-exhausting unbounded recursion in MemoryVFS.copy when the destination lies inside the source), #177 (the WebDAV server answering a Range request with the whole file from offset zero while advertising the requested window), #178 and #179 (audits of the remaining inlined fallbacks and of what each caller should do with an unsupported rejection) were filed from what this work surfaced.

  • @webnet/vfs — bounded overlapping-copy rejection (issue #176): gpt-5.6-sol added a normalized, segment-aware pathsOverlap helper and made MemoryVFS, NodeVFS, SMB2, and WebDAV reject self-copies and ancestor/descendant copies with precondition-failed before deleting or creating anything. Shared conformance coverage verifies both overlap directions, self-copy, source preservation, and legal sibling prefixes. The 1.2 GB core dump created by the original bounded reproduction was removed from the repository.