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>
97 KiB
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/tsconnect—listDrivePeershonours online and peerAPI reachability (issue #143): Claude Code (Claude Opus 5) fixedlistDrivePeersin the tailscale fork (cmd/tsconnect/wasm/drive.go), which returned every peer carryingPeerCapabilityTaildriveSharer— 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 asLocalBackend.driveRemotesFromPeers: online, non-empty peerAPI base, and the sharer cap. The peerAPI URL was already computed and discarded. The TypeScript doc comments onIPN.listDrivePeersandIPNDrivePeerwere 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" aftershutdown()(issue #147): Claude Code (Claude Opus 5) fixed the wasm runtime teardown at its source. Go'swasm_exec.jscarries the runtime's pending work across program exit: the scheduler'ssetTimeoutstays armed, and every callback Go handed to JS (DERP WebSocket event listeners,fetchcontinuations) 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 cleanshutdown()— aTimeout._onTimeoutuncaught error, twoWebSocketclose-listener uncaught errors, and one unhandled rejection.initIPNnow wraps the Go instance it creates:runtime.wasmExitclears the pending scheduler timeouts, and_resumereturns quietly once the runtime has exited (nothing can be resumed after the instance is dropped). Because a quiet_resumealso means a call into a dead Go handle resolves withundefinedinstead of throwing — which would spin aread()loop rather than break it —Conn,PacketConnandTCPListenernow report themselvesclosedonce their runtime has exited, so such calls raiseClosedErrorandclose()becomes a no-op. An intermediate attempt to hand back a rejected promise instead was rejected after testing:node:testattributes 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-_resumebug forced, now that it is fixed: both*.shutdown.probe.tschild processes are gone (the shutdown tests run in-process again, each on its owninitIPNruntime), the shared@webnet/tsconnectintegration suite shuts its runtime down inafter()again, and the*.probe.tstsconfig excludes both packages carried are removed. -
CI — scheduled tsconnect CA bundle refresh (issue #142):
gpt-5.6-soladded 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-solfixed 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/dialTLSagainst a run-time-generated self-signed CA plus the wrong-CA rejection case,upgradeTlson a live connection, Taildrop end-to-end (listFileTargets,sendFile,waitingFiles,openWaitingFile,deleteWaitingFile),serveDrive/listDrivePeersover a real netmap,suggestExitNode, andsetExitNode.serveDrive,listDrivePeersandupgradeTlspreviously had tests only against mock raw objects that never loaded the wasm. Taildrop registers its own node pair from a new optionalTSCONNECT_TEST_USER_AUTH_KEY, becausecanPutFilerequires an untagged peer and the shared key registerstag:test;setExitNodeis gated onTSCONNECT_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.tstsconfig exclude) and the shared suite no longer tears the runtime down, because an exiting Go runtime leaves scheduler timeouts that throw afterwards andnode:testclaims the exception first (issue #147). Writing the tests surfaced two pre-existing gaps, worked around rather than fixed:":port"is normalised only for TCPlisten, solisten("udp4", ":0")andlistenTLS(":0", …)fail withParseAddrPort(":0"): no IP; and a UDP socket bound to0.0.0.0cannot 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-terraadded 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-terrapreserved the example HTTP demo's originalGET /behavior while migrating it to the current server API, following an independent Claude review. -
Repository package-boundary enforcement (issue #107):
gpt-5.6-solremoved all cross-package_internalsimports 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-solfixed a flaky Go/WASM test teardown by awaiting the runtime shutdown started bydisconnect(), 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 fixedSFTPServerto 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) addedwithoutOptionalandunsupportedOptionalto@webnet/vfs/conformance, replacing the copies in the fallback tests and the WebDAV tests, and gave each protocol package a secondtestAsyncVFSConformancerun 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 fullMemoryVFS. That run reproduces #177 before it is fixed: the WebDAV server's Range fallback discardedrange.startand 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;readFileRangeFallbacknow 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 andSSH_FX_OP_UNSUPPORTED, which reach the client asunsupported. 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 aliasesforbiddentounsupportedfor that, since 403 is the answer the protocol gives here. A Claude Sonnet 5 review caught that this run originally opted out ofsetPropsaltogether, 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/smb2has no server over anAsyncVFS— 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 exposingcreateStaticHandler, 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 honoringIf-None-Match: *. -
@webnet/http-static— precedence test vectors: GPT-5.6 Luna added a seededMemoryVFSmatrix 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
tailscalesubmodule fork and its Go-sidetsconnectpatches (thewebnetbranch) -
The
packages/tsconnectTypeScript SDK -
The
packages/test-appVite 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/httpHTTP/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,ServerConnectiondraining the body after closing the connection,PooledDialerthrowing instead of queuing waiters,shouldClose()doing a case-sensitive header comparison) and adding transport primitives (halfClose,readEnded,whenClosed,remoteAddr,localAddr)packages/http— timeout support: theheadersTimeout,keepAliveTimeout, andbodyTimeoutoptions across both client and server were implemented by Claude Codepackages/http— parse error messages: improved by Claude Code to include the offending valuespackages/http— package and build scaffolding: initialpackage.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/http—hijack()on server and client responses: thehijack()method onServerResponseandClientResponse, theReadBuffer.drain()helper, and theprependTransport()utility were implemented by Claude Code -
packages/http— 1xx informational response support: implemented by Claude Code. Server side: automatic100 Continue(sent lazily when the handler reads the body) andres.sendInformational()for 103 Early Hints etc. Client side: default skip mode,interim: "collect"to capture 1xx intores.informational[],conn.requestStream()async generator that yields each interim response and the final one as they arrive, andfetchStream()/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, orconnectWebSocket(res, key)to promote an existingfetch()/fetchStream()101 response — both return aWebSocketConnectionasync 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.digestfor SHA-1,crypto.getRandomValuesfor mask keys) with no external dependencies. Two fixed bugs infetch.tswere required for pool safety: a case-insensitiveConnection: upgradecheck 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"), andredirect.collectto gather followed 3xx intores.redirects[]. In streaming mode all redirects and interims are yielded as they arrive; adrain()method was added toClientConnectionto support clean connection hand-off between redirect hops. -
packages/http— lint fix and WebSocket test coverage: Claude Code fixed aprefer-constlint error inServerConnectionby refactoringHijackFnto acceptresas a parameter (eliminating a forward-referencelet 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-chunkReadBufferslicing, empty close frames, unsolicited PONG frames, invalid port URLs, and thefetchStream()101 early-break path. -
packages/http— typed context extension vianext(extra): theRouter<T>generic, theNext<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'sServerclass was refactored to take aHandlerargument instead of extendingRouter;Routerwas moved to a./routersub-export.@webnet/tsconnect'sConnandTCPListenerwere updated to formally implementRawTransportandRawListener;IPNDialerimplementingRawDialerwas added. -
@webnet/tsconnect— drop pre-compression of assets: Claude Code removed the gzip/brotli pre-compression ofmain.wasm(from thetsconnectfork's build-pkg) andcacert.pem(frompackages/tsconnect/build.sh), dropping the corresponding.br/.gzpackage exports and addingscripts/asset-sizes.sh(exposed asnpm 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 nativeDOMParser, Node.js builds use@xmldom/xmldom. Authored by Claude Code. -
packages/drive: WebDAV client and server with an async VFS abstraction. IncludesMemoryVFS,NodeVFS(node:fs),FsaVFS(browser File System Access API, with an OPFS factory method),createDAVHandler()(server-side HTTP handler compatible with@webnet/http), andDAVClient(which implementsAsyncVFSso it can be used as a backing store for another server instance). Full WebDAV Level 2 locking support (LockStoreinterface,InMemoryLockStore, LOCK/UNLOCK methods,If:header enforcement,lockdiscovery/supportedlockproperties, and client-sidelock()/unlock()/refreshLock()with optionallockTokenon 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.sendFilenow accepts aReadableStream<Uint8Array>+declaredSize;IPN.openWaitingFilereturnsPromise<ReadableStream<Uint8Array>>. On the Go/WASM side, a newjsStreamReader(io.ReadCloser) pulls chunks from a JSReadableStreamDefaultReadervia awaited.read()Promises (channel+js.FuncOfpattern), andjsReadableStreamwraps a Goio.ReadCloserin a pull-based JSReadableStream.UserIPNFileOps.openReadernow returns aReadableStreaminstead of aUint8Array. A newFsaFileOpsclass (withFsaFileOps.createFromOpfs()) provides an OPFS-backedUserIPNFileOpswhere received chunks land directly on disk and downloads stream back through Go without buffering.InMemoryFileOps.openReaderwas updated to emit stored chunks one-by-one via aReadableStream. -
@webnet/tsconnect—IPN.shutdown(): Claude Code added ashutdown()method toIPN(TypeScript) andjsIPN(Go/WASM). Calling it stops theLocalBackend, closes the safesocket listener to unblocksrv.Run, and signalsmain()to return so the Go runtime exits. The TypeScript side awaits thego.run()Promise (captured ininitIPNand threaded into eachIPN) as the authoritative "Go runtime has exited" signal, avoiding a race where Go deletes_instbefore a callback-based resolve could fire. Go-side nil guards were added forlbandlnsoshutdown()is safe to call even whenrun()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 andwasm_exec.jsare already Worker-compatible (js.Global()maps to the Worker'sglobalThis;wasm_exec.jsstubsfs/process/pathwhen absent). Three issues were found and fixed for Node.js:initIPNaccepted only a URL string and calledfetch(), which does not supportfile://URLs in Node.js. Claude Code added aWasmSourceunion type (string | URL | ArrayBuffer | ArrayBufferView | Response | ReadableStream<Uint8Array>) dispatching toWebAssembly.instantiatefor binary sources andWebAssembly.instantiateStreamingfor URL/Response/ReadableStream inputs.wasm_exec.jsinstalls ENOSYS stubs forglobalThis.fswhen it is falsy. In Node.jsglobalThis.fsis undefined, so the stubs are installed — and this is the correct behaviour: tsconnect's WASM routes all network calls through JavaScript'sfetch()and WebSocket APIs (which use Node.js's own DNS resolver), so the Go net package's/etc/resolv.confread should remain a no-op. An earlier iteration setglobalThis.fsto Node.js's realfs, which caused Go to read the host's/etc/resolv.confdirectly and attempt to use those nameservers, breaking in environments where they are unreachable (e.g. Tailscale-managed entries without Tailscale running).wasm_exec.jsis now imported directly inindex.tsandGois extracted fromglobalThisinline — a separateenv-node.ts/env-web.tssplit was explored but both files were identical, so the indirection was removed.- In the
tailscalesubmodule, two Go-side bugs were fixed:safesocket_js.goused a hardcoded memconn address"Tailscale-IPN", so a secondnewIPN()call in the same WASM process wouldlog.Fatal; an atomic counter now gives each instance a unique address. Andwasm_js.go'slisten()rejected the standard":0"(any-interface) address form that netstack does not accept; it now normalises:portto0.0.0.0:port. Claude Code also wrote the full automated test suite for@webnet/tsconnect: unit tests forInMemoryFileOpsandInMemoryState(no WASM required), and integration tests that spin up real Tailscale nodes against a headscale control server, verifyinginitIPNWASM loading,/localapi/v0/status, and two-node TCP dial/listen.
-
@webnet/tsconnect—getIceServers(): Claude Code implementedgetIceServers(ipn), which fetches the tailnet's DERPMap via LocalAPI and converts it to anRTCIceServer[]list for use withnew 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 addedDERPMap,DERPRegion, andDERPNodetypes. Unit and integration tests included. -
@webnet/tsconnect— service advertisement: Claude Code addedSetExplicitServicestoLocalBackendin the tailscale fork (bypassing the OS portlist gate), wired asetServices(services)WASM binding, and added aservicesfield to the netmap JSON for both self and peers (stripping internal peerapi entries). On the TypeScript side:IPNServicetype,services: IPNService[]onIPNNetMapNode, andIPN.setServices(). In@webnet/tsconnect-redux:getPeersByService,getPeersByServiceDescription(both memoized withcreateSelector), andgetSelfServices(referentially stable empty-array fallback). Claude Code also wrote integration tests forsetServices()and fixed two bugs found while making them reliable:SetExplicitServicespreviously only sent a "lite" map update that discarded the control server's response, sonotifyNetMapnever fired with the new services — fixed by addingAuto.RestartMap()to force a fresh streaming netmap; anduserServicesFromViewreturned a nil slice when a node advertised no services, which serialized asnullinstead of[]in the netmap JSON. -
@webnet/tsconnect— taildrive WebDAV bridge andlistDrivePeers(): Claude Code implementedjsFileSystemForRemotein the tailscale fork (cmd/tsconnect/wasm/drive.go), adrive.FileSystemForRemotebacked by a JS callback: request bodies stream to JS chunk-by-chunk viareadBodyChunk(), response bodies stream back viawrite()/end()withhttp.Flusher.Flush()after each chunk, so large transfers never buffer in memory.sys.DriveForRemoteis set atnewIPNtime;setDriveHandler(wired toIPN.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 addedlistDrivePeers(wired toIPN.listDrivePeers()), mirroring nativedriveRemotesFromPeers: returns peers carryingPeerCapabilityTaildriveSharer, gated onDriveAccessEnabled(). On the TypeScript side:DriveSharePermission,DrivePermissions,JsDriveRequest,JsDriveResponse,RawDriveHandler,IPNDrivePeertypes. Claude Code also created the new@webnet/taildrivepackage (./serversub-export) withbridgeDriveHandler(handler), which adapts an@webnet/httpHandler(e.g. from@webnet/drive'screateDAVHandler) to the Go bridge's raw request/response callback shape — kept in a separate package from@webnet/tsconnectand@webnet/driveto avoid a circular dependency between the two. Unit tests coverbridgeDriveHandler's request/response translation (headers, allBodyunion variants,hasBodydetection) andIPN.serveDrive/listDrivePeersargument validation and JSON handling against a fakeRawIPN, so they run without a WASM build. A true end-to-end test against a live control plane is blocked on Headscale ACL support fornodeAttrs/grants/Taildrive, which only lands in Headscale v0.29.0 (not yet released stably at the time of writing). -
packages/http—ReadableStreambody support: implemented by Claude Code. Renamed theAsyncIterable-returning method toiter()and added a newstream()returning aReadableStream<Uint8Array>with BYOB (byte stream) support where available.WritableHttp.bodynow acceptsReadableStream<Uint8Array>, written as chunked transfer encoding with BYOB reads and periodic flushes every 2^16 bytes. TheiterableToStream()/streamToIterable()helpers inpackages/drivewere removed as they are no longer needed.@webnet/taildrive'sbridgeDriveHandler(predating this rename) was updated to match:ctx.req.stream()now returns a real BYOB-capableReadableStream<Uint8Array>instead of anAsyncIterable, actx.req.iter()method was added for the old semantics, and response bodies that are aReadableStreamare now forwarded correctly instead of falling through to JSON serialization. -
@webnet/transport—NodeListenerbug fixes: two bugs fixed by Claude Code (Claude Sonnet 4.6): double invocation of the error callback (fired both directly fromclose()and from the server's"close"event), and a connection leak whenaccept()was rejected while a late-arriving connection triggered the still-registeredonce("connection")handler. -
@webnet/transport— WebRTC DataChannel transport: Claude Code (Claude Sonnet 4.6) implemented@webnet/transport/webrtc— aRawTransportover anRTCDataChannel. Designed to coexist on a sharedRTCPeerConnectionalongside other channels (multiplexed file transfers, audio/video). Write-side backpressure deferswrite()promises viabufferedamountlowwhenbufferedAmountexceeds the high-watermark. Receive-side backpressure for slow local readers (e.g. OPFS writes) removes themessagelistener 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 theIPNandtsconnect-reduxRedux store in aSharedWorker, exposing every IPN method to clients via message-port RPC with per-objectMessageChannels forConn,TCPListener,PacketConn, drive handler, and SSH sessions. Key features:IndexedDBStatefor sync IPN state storage (unavailablelocalStorageworkaround — loads entire store into aMapat 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 forIpnWorkerClientand proxy classes (WorkerConn,WorkerTCPListener,WorkerPacketConn,WorkerSSHSession); WebWorker lib for the worker entry point. Round-2 improvements (Claude Sonnet 4.6):IndexedDBStatemoved to@webnet/tsconnect/helpersand exported from the package;buildIpnStorenow accepts an optionalpreloadedState, and the worker sends the full Redux state snapshot (preloadStatemessage) instead of synthetic actions on client connect;WorkerConnformally implementsRawTransport,WorkerTCPListenerimplementsRawListener,WorkerPacketConnimplementsIpnPacketConn; a newIpnClientinterface added to@webnet/tsconnect(withIpnSSHTermConfigandIpnPacketConn) implemented by bothIPNandIpnWorkerClientto prevent method drift;IPN.ssh()made async;WorkerSSHSession.resize()/close()made fire-and-forget (sync) to implementIPNSSHSession;ReadableStreamtransfer fallback viaMessageChannelsub-protocol for Safari compatibility on bothsendFileandopenWaitingFile;stateStorage: "memory"option added toWorkerConfigfor ephemeral (non-persisted) IPN state.@webnet/tsconnect-reactupdated by Claude Code (Claude Sonnet 4.6):IpnContextwidened fromIPNtoIpnClient;useBuildIpnWorker(worker, config, runParams?, workerOptions?)hook added to connect to a SharedWorker and return anIpnWorkerClient, with StrictMode-safe cleanup viadisconnect();disconnect()method added toIpnWorkerClientto release the Web Lock without stopping the worker;workerOptionsparameter added to support both classic (webpack-bundled) and module workers.example-appupdated by Claude Code (Claude Sonnet 4.6): SharedWorker demo added —src/worker.tsis the webpack worker entry,IpnProviderrunsuseBuildIpnWorkerwhen SharedWorker is available (falling back to main-threaduseBuildIpn), switches the ReduxProviderto 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 (unifiedonHellohandler inonconnect;registerClientalways receiveslockNameand acquires the lock immediately); deadbodyCallbacksfield removed from drive pending map; drive cleanup only installs the no-op handler when no other client still has drive registered;wrapDispatchas anyreplaced with targeted two-step cast;IndexedDBState.setStatelogs write failures viatx.onerror;pumpStreamToPortfire-and-forget made explicit with.catch(() => {}). -
AGENTS.md/CLAUDE.mdandAI_CHANGES.md: PR review instructions (usetea, interactive vs autonomous modes) and AI disclosure consolidation intoAI_CHANGES.mdauthored 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/driveinto a new standalone@webnet/vfspackage, following the same pattern as the earlier@webnet/transportsplit from@webnet/http.@webnet/driveand@webnet/test-appnow import directly from@webnet/vfs. The drive package's./vfs/*sub-exports were removed. TheNodeVFStest suite moved with the implementation. -
@webnet/tsconnect—FsaFileOpslimits and change notification: Claude Code (Claude Sonnet 4.6) addedmaxFiles/maxTotalSize/maxFileSizelimit getters/setters,fileCount/totalSize/openFilesgetters, andonChange/offChangechange-notification toFsaFileOps, mirroring the existingInMemoryFileOpsAPI. File sizes are tracked in memory via a#fileSizesmap that is updated on everyopenWriter/write/remove/rename;createFromOpfsnow 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.setinopenWriterto after all async FSA ops so a failure doesn't leave a phantom entry; fixedrenameto stat and track previously-untracked files under their new name rather than letting them become invisible; documented thestat/totalSizelag during active writes. -
@webnet/tsconnect-InMemoryFileOpslimits 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=1in.npmrcprevents binary downloads onnpm ci(explicitnpx playwright installin CI only); browser test files use the*.browser.tsextension to avoid matching the existingsrc/**/*.test.tsglob so the regular test suite is unaffected;@webnet/browser-test-utilsis a new private package that exportsforBrowsers()(registers test suites for Chromium and Firefox, handles browser lifecycle) andserveDirectory()(minimal HTTP server usingpath.resolve/path.relativeto guard against path traversal).test:browserper-package scripts added;test:browser:coverageintentionally omitted since c8 only sees Node orchestration code, not browser-side code insidepage.evaluate(). Browser tests usepage.evaluate()with dynamicimport()to load built package modules into real browser contexts. Integration tests written for:DataChannelTransport(realRTCPeerConnectionloopback, 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—__namepolyfill: Claude Code (Claude Sonnet 4.6) fixed aReferenceError: __name is not definedcrash in all*.browser.tstests. tsx hardcodeskeepNames: truein its internal esbuild options, which injects a__namehelper at module scope and wraps named function/const assignments with__name(fn, "name"). Playwright'spage.evaluate()serializes callbacks via.toString(), capturing only the function body — so the module-level helper is absent in the browser context. Fixed by callingpage.addInitScript()inforBrowsers'snewPageto inject a matching polyfill (Object.defineProperty(target, "name", ...)) into every page before anyevaluate()runs. -
ControlledFileOps— observable file-ops binding: Claude Code (Claude Sonnet 4.6) added aControlledFileOpsinterface to@webnet/tsconnectthat extendsUserIPNFileOpswith readable metrics (fileCount,openFiles,totalSize), configurable limits (maxFiles,maxTotalSize,maxFileSize), and anonChange(handler) => unsubscribesubscription — already satisfied structurally byInMemoryFileOpsandFsaFileOps. Added afileOpsRedux slice to@webnet/tsconnect-redux(storesFileOpsState | null;nullwhen file ops are not configured) withsetFileOpsStateaction,selectFileOpsselector, and agetFileOpsStateconvenience selector. AddedbindFileOpsToStore(fileOps, store)to@webnet/tsconnect-redux's binding module: takes an initial snapshot and re-dispatches on everyonChangecall; the returned unsubscribe is stored by the worker for lifecycle safety. In@webnet/tsconnect-worker: theFsaFileOpsinstance was hoisted frominit()to module scope;bindFileOpsToStoreis called after the store is created; a newsetFileOpsConfig({ maxFiles?, maxTotalSize?, maxFileSize? })method onIpnWorkerClientproxies to a new worker-sidehandleCallcase that mutates the live instance and throws back any limit-violation errors. All state updates propagate via the existingwrapDispatchbroadcast, so every connected client's store reflects the current file-ops state and limits in real time. Unit tests forbindFileOpsToStoreadded to@webnet/tsconnect-redux. -
ControlledFileOps— observable file-ops binding: Claude Code (Claude Sonnet 4.6) added aControlledFileOpsinterface to@webnet/tsconnectthat extendsUserIPNFileOpswith readable metrics (fileCount,openFiles,totalSize), configurable limits (maxFiles,maxTotalSize,maxFileSize), and anonChange(handler) => unsubscribesubscription — already satisfied structurally byInMemoryFileOpsandFsaFileOps. Added afileOpsRedux slice to@webnet/tsconnect-redux(storesFileOpsState | null;nullwhen file ops are not configured) withsetFileOpsStateaction,selectFileOpsselector, and agetFileOpsStateconvenience selector. AddedbindFileOpsToStore(fileOps, store)to@webnet/tsconnect-redux's binding module: takes an initial snapshot and re-dispatches on everyonChangecall. In@webnet/tsconnect-worker: theFsaFileOpsinstance was hoisted frominit()to module scope;bindFileOpsToStoreis called after the store is created; a newsetFileOpsConfig({ maxFiles?, maxTotalSize?, maxFileSize? })method onIpnWorkerClientproxies to a new worker-sidehandleCallcase that mutates the live instance and throws back any limit-violation errors. All state updates propagate via the existingwrapDispatchbroadcast, 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/nodetodevDependenciesand"types": ["node"]totsconfig.jsonso that thenode:test/node:assert/strictimports inbinding.test.tsresolve duringtsc --noEmit. -
@webnet/tsconnect-worker— main-thread fallback andconnectWithFallback: Claude Code (Claude Sonnet 4.6) added a full main-thread fallback path for environments that do not supportSharedWorker(e.g. Chrome on Android before 2025). Key additions:IpnClientHandleinterface: extendsIpnClient, addingconnectionMode,store,state,running,fileOps,run(),disconnect(). BothIpnWorkerClientand the newIpnMainThreadHandleimplement it.IpnMainThreadHandle: wraps anIPNinstance with a Redux store (wired viarunWithStoreat construction so future state changes fire user callbacks set later inrun()). Proxies allIpnClientmethods 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 toconnectMainThreadifSharedWorkeris undefined,opts.disableSharedWorkeris true, or the worker path throws.useBuildIpnWorkerupdated in@webnet/tsconnect-react: now usesconnectWithFallbackand returnsIpnClientHandle | nullinstead ofIpnWorkerClient | null.- Tests: 6 Node tests (all pass — WASM loads,
IpnMainThreadHandleis returned,run()fires callbacks,disconnect()is idempotent,connectWithFallbackfalls back in Node); 6 browser tests in Chromium and Firefox (SharedWorker path →connectionMode=worker;disableSharedWorker: true→connectionMode=main-thread; IDB lock contention →connectMainThreadrejects immediately). Browser tests use esbuild (hoisted transitive dep) to bundle the package inline in the test'sbefore()hook.WorkerConfig.wasmUrlwidened tostring | ArrayBuffer | ArrayBufferViewso tests can pass raw WASM bytes (Node.jsfetch()does not supportfile://URLs). Node tests restructured to share a single WASM+IPN instance per suite viabefore()/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;connectWithFallbacklogs aconsole.warnfor 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.ymlwith five jobs (lint,format,typecheck,typetest,build) following the same structure as the existingtest-node.ymlandtest-browser.ymlworkflows. Also added a@webnet/tsconnect#typecheckpackage-level override inturbo.jsonso that package's typecheck task depends on its own build (required becausesrc/index.tsimports../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) addedfileOpsMaxFiles,fileOpsMaxTotalSize, andfileOpsMaxFileSizefields toWorkerConfiginprotocol.ts, and wired them through toFsaFileOps.createFromOpfs()inworker.ts. Limits can also be changed after startup via the existingsetFileOpsConfig()method onIpnWorkerClient. -
@webnet/tsconnect-worker— FileOps limits at init: Claude Code (Claude Sonnet 4.6) addedfileOpsMaxFiles,fileOpsMaxTotalSize, andfileOpsMaxFileSizefields toWorkerConfiginprotocol.ts, and wired them through toFsaFileOps.createFromOpfs()inworker.ts. Limits can also be changed after startup via the existingsetFileOpsConfig()method onIpnWorkerClient. AFileOpsLimitsnamed type was extracted toprotocol.tsand shared between the worker-side handler and the client-sidesetFileOpsConfigsignature to prevent future drift. -
AGENTS.md/CLAUDE.md— PR workflow improvements: Claude Code (Claude Sonnet 4.6) added guidance for always basing PRs onorigin/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 forel(),text(), andstringify()(namespace prefixing, escaping, self-closing vs. open/close tags, all namespaces hoisted to root), plustest/test:coveragescripts.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 directMemoryVFSunit tests (stat, readdir, writeFile, readFileRange, mkdir, delete recursive, copy, move, setProps; all VFSError codes).packages/tsconnect-worker: 38 tests forWorkerConn,WorkerTCPListener,WorkerPacketConn,WorkerSSHSession,pumpStreamToPort, andportToReadableStreamusingMessageChannelpairs (no browser APIs required); added@types/nodedevDep and"types":["node"]to tsconfig. HTTP redirect tests were found to already exist inpackages/http/src/client/fetch.test.ts(lines 585–1203). -
AGENTS.md/CLAUDE.md— PR labeling and auto-finalise: Claude Code (Claude Sonnet 4.6) added guidance to apply the org-levelAgenticandAgent/<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/reactand@webnet/utils— new packages: Claude Code (Claude Sonnet 4.6) extracted theuseClientanduseLocalStoragehooks frompackages/tailshareinto a new standalone@webnet/reactpackage, and extracted thedownload,upload,readBlob, andfmtSizeutilities into a new@webnet/utilspackage. Both packages are plain TypeScript with no dependencies beyond React (peer dep for@webnet/react) and are built withtsc. -
@webnet/reactand@webnet/utils— integration intoexample-appandtest-app: Claude Code (Claude Sonnet 4.6) wired@webnet/reactand@webnet/utilsinto the consumer apps. Inexample-app: replaced the localuseClientduplicate with the package version; addeduseLocalStorageto persist the SharedWorker toggle (ipn:useWorker) across page reloads; replaced the localfmtSizewith@webnet/utils; simplified theWaitingFileDebugdownload handler to usedownload()from@webnet/utils. Two bugs were fixed in@webnet/utils/downloadduring integration: a typo (suggegestedName→suggestedName) in theshowSaveFilePickercall, and synchronousURL.revokeObjectURLreverted to a deferredsetTimeoutfor Safari compatibility. Intest-app: added@webnet/utilsas a dependency and exposed it onwindow.utilsfor console testing. -
@webnet/tsconnect-worker— flaky closed-state test fix: Claude Code (Claude Sonnet 4.6) replaced racysetTimeout(r, 10)waits in four "rejects immediately when closed" tests (read,write,accept,readFrom) with a deterministicwhile (!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, andtest-browser.ymlinto a single.gitea/workflows/ci.yml. Previously all 7 jobs independently repeated checkout +tailscalesubmodule clone +npm ci, and 5 of them independently rebuilt the whole workspace via Turbo. Live smoke tests against this Gitea instance foundactions/cache(andsetup-node's built-incache: npm, same API) times out against the built-in cache proxy, andactions/upload-artifact/download-artifactv4 refuse to run at all (GHES detection), but v3 of the artifact actions work correctly. Given that, a newinstalljob now does the real work once (submodule clone,npm ci, build) and handsnode_modulesplus Turbo's local cache off totypecheck/typetest/node-tests/browser-tests(allneeds: install) viaactions/upload-artifact@v3/download-artifact@v3, instead of each job reinstalling and rebuilding from scratch.lint/formatnever 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, soactions/checkoutstays a separate first step in every job rather than being absorbed into the composite action). Aconcurrencygroup cancels superseded runs on the same ref. One real bug was found and fixed during this work:packages/xmlandpackages/vfspin a newer local TypeScript than the workspace root, installed by npm as nestedpackages/{xml,vfs}/node_modules/typescript; the first version of theinstalljob'snode_modulesarchive only captured the rootnode_modules, so downstream jobs silently typechecked those two packages against the wrong TypeScript version and produced spurious errors — fixed by archivingpackages/*/node_modulesalongside 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 ofworker.test.ts(theWorkerSSHSession.resize()/close()tests called out as flaky in CI, plus the otherclose()-message, callback-firing, andpumpStreamToPorttests), where a flatsetTimeout(r, 10)was used to wait for apostMessageto be delivered before asserting on it. Added a genericwaitFor(predicate, timeoutMs?)helper that polls viasetImmediateand 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 hocwhile (!x.closed) await setImmediate()spin-loops from the earlierb917cd8fix were also consolidated onto the samewaitForhelper for consistency. -
AGENTS.md/CLAUDE.md—teamergeability and symlink clarifications: Claude Code (Claude Fable 5) documented thatCLAUDE.mdis a symlink toAGENTS.md, thatteareportsmergeable: falsewhile theWIP:title prefix is present (so mergeability is only meaningful at finalisation), and that the conflicting-files section header inteaoutput 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, andAgent/*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 andpackage-lock.json, with Turbo as the task runner andtailscale/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
testandtest:coveragedepend 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/ftppackage: an FTP + implicit-FTPS server exposing anyAsyncVFSover theRawListenertransport interface, and anFTPClient implements AsyncVFS(mirroringDAVClient) over aRawDialer. Server command set targets FileZilla/KDE kio/classicftpcompatibility: USER/PASS (pluggableauthenticatecallback 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-injecteddataListenfactory (TLS listeners = FTPS data channels; PORT/EPRT and AUTH TLS are 502 stubs pending active-mode and transport TLS-upgrade support), LIST/NLST (unixlsformat), 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-lsLIST parsing (for vsftpd, which lacks MLSD); operations are serialized on a single control connection with a mutex held across each verb+data-stream sequence;readFileRangemaps to REST plus client-side truncation;copyis omitted (no server-side copy in FTP). Error translation both ways viavfsErrorToReply/replyToVFSErrortables. 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
devmode and an explicitdev:hostmode to the Vite test app and webpack example app. The hosted mode binds to all interfaces and permits forwarded hosts only whenWEBNET_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 unlessFTP_TEST_HOST,FTP_TEST_USER, andFTP_TEST_PASSare configured, with optionalFTP_TEST_PORTandFTP_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 requiredAgenticand model-specificAgent/*labels from pull requests to issues, and documented the org-levelHumanlabel for human-opened work. -
Explicit TLS upgrade (STARTTLS / AUTH TLS) on
RawTransport: Claude Code (Claude Fable 5) added an optional in-placeupgradeTls(options?: TlsUpgradeOptions)method and an optionalisTlsgetter toRawTransportin@webnet/transport, for protocols that negotiate in plaintext and then upgrade the existing connection (SMTP STARTTLS, FTPS AUTH TLS).TlsUpgradeOptionsis a union of client mode (serverName/insecureSkipVerify/caCerts;serverNamerequired unless skipping verification, since an upgraded connection only knows its peer IP) and server mode (isServer: truewithcertPem/keyPem). All implementations require a quiescent transport (no pending read, no buffered data) and close the connection on handshake failure.NodeTransportswaps its socket for anode:tlsTLSSocketwrapping 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'sConnswaps its raw Go handle via a newupgradeTLSwasm bridge method (added towrapConnin thetailscalesubmodule, with the TLS client-config construction factored out ofdialTLSintotlsClientConfigFromJS; server mode usestls.X509KeyPair+tls.ServerlikelistenTLS) and guards against in-flight reads/writes with an operation counter;dialTLS-created conns now reportisTls: true.@webnet/tsconnect-workergainedupgradeTls/upgraded/upgradeErrormessages in the per-conn protocol, anisTlsfield on theconn/acceptedmessages, and matchingWorkerConnsupport. 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 anupgradingguard 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 beforeupgradeTlsis called are now unshifted back into the stream) and the workerupgradeErrorconflating validation failures with handshake failures (the reply now carries aclosedflag); 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 onRawTransport: 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/transportgains theStateTransferableinterface (transferState()detaches and returns a structured-cloneable state) andisStateTransferable.@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-suppliedclientKeyinhello, andIpnWorkerClientgains a broker API (sendTransfer/onTransfer, conveniencetransfer/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 beforereadywere dropped).@webnet/http:ClientConnection.canExport()/exportTransport()andConnectionPool.exportIdle()/seed()move idle keep-alive connections (with any buffered prefix bytes) between pools.@webnet/drive:DAVClientimplementsStateTransferable<DavTransferState>andDAVClient.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/transportgains theStateTransferableinterface (transferState()detaches and returns a structured-cloneable state) andisStateTransferable.@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-suppliedclientKeyinhello, andIpnWorkerClientgains a broker API (sendTransfer/onTransfer, conveniencetransfer/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 beforereadywere dropped).@webnet/http:ClientConnection.canExport()/exportTransport()andConnectionPool.exportIdle()/seed()move idle keep-alive connections (with any buffered prefix bytes) between pools.@webnet/drive:DAVClientimplementsStateTransferable<DavTransferState>andDAVClient.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 intransfer(), 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 handlesAUTH TLS, advertises it throughFEAT, supports optional or required TLS-before-login policy, and appliesPROT Pto 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 handlesAUTH TLS, advertises it throughFEAT, supports optional or required TLS-before-login policy, and appliesPROT Pto passive data transports after the RFC-required protected-control andPBSZ 0sequence. 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/smb2package: an SMB2/3 client that runs over the existingRawTransport/RawDialerabstraction (TCP/445 in Node, relayed through tsconnect'sIPNDialerin the browser) and exposes a remote Windows/Samba share as an@webnet/vfsAsyncVFS(SMB2Client), mirroring how@webnet/driveexposes 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 CryptoHMAC-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, andsetProps(timestamps), plusconnect/disconnect. NTSTATUS codes are translated toVFSErrorcodes 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 behindSMB2_TEST_*env vars) runs against a real Samba/Windows share. - Wired into
@webnet/test-app(exposed aswindow.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.
- 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
-
@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/sftppackage: an SFTP v3 client (SFTPClient implements AsyncVFS, mirroringDAVClient/FTPClient) over aRawDialer, and anSFTPServerserving anyAsyncVFSover aRawListenervia an http-stylelisten(listener, opts)accept loop. Both run in the browser (via tsconnect'sIPNDialer) 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-sha256key exchange withssh-ed25519/rsa-sha2-256/rsa-sha2-512host-key verification, RFC 4253 key derivation,aes128/256-gcm@openssh.comandaes128/256-ctrwithhmac-sha2-256/hmac-sha2-256-etm@openssh.comciphers (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 pluggableauthenticatecallback returning a per-userAsyncVFS), 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, andposix-rename@openssh.comfor overwritingmove. The server maps FXP back onto the VFS with a handle table, 100-entry READDIR batches with unixls -llongnames, sequential streaming reads/writes,SETSTAT/FSETSTATno-ops (so OpenSSHputsucceeds), 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 unencryptedopenssh-key-v1format (encrypted keys are out of scope); host keys are optionally verified via averifyHostKey({ type, key, fingerprint })callback and can be generated with the exportedgenerateHostKey(). 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 OpenSSHsftpCLI (which surfaced and fixed a pre-subsystemenvchannel-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 backingvfs.writeFilefails 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 awriteFileerror. Known intentional limitations (streaming model overAsyncVFS, 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/sftppackage: an SFTP v3 client (SFTPClient implements AsyncVFS, mirroringDAVClient/FTPClient) over aRawDialer, and anSFTPServerserving anyAsyncVFSover aRawListenervia an http-stylelisten(listener, opts)accept loop. Both run in the browser (via tsconnect'sIPNDialer) 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-sha256key exchange withssh-ed25519/rsa-sha2-256/rsa-sha2-512host-key verification, RFC 4253 key derivation,aes128/256-gcm@openssh.comandaes128/256-ctrwithhmac-sha2-256/hmac-sha2-256-etm@openssh.comciphers (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 pluggableauthenticatecallback returning a per-userAsyncVFS), 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, andposix-rename@openssh.comfor overwritingmove. The server maps FXP back onto the VFS with a handle table, 100-entry READDIR batches with unixls -llongnames, sequential streaming reads/writes,SETSTAT/FSETSTATno-ops (so OpenSSHputsucceeds), 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 unencryptedopenssh-key-v1format (encrypted keys are out of scope); host keys are optionally verified via averifyHostKey({ type, key, fingerprint })callback and can be generated with the exportedgenerateHostKey(). 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 OpenSSHsftpCLI (which surfaced and fixed a pre-subsystemenvchannel-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 backingvfs.writeFilefails 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 awriteFileerror. Known intentional limitations (streaming model overAsyncVFS, 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/sftpinto a new standalone@webnet/sshpackage via puregit mvs, so it can be reused independently;@webnet/sftpnow depends on@webnet/sshand 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 newSSHAuthErrorrather thanVFSError, which@webnet/sftpmaps back toVFSError("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, genericCHANNEL_OPENhandling that surfaces incoming opens (session / direct-tcpip / forwarded-tcpip with parsed endpoints) as anIncomingOpenthe consumer canaccept()/reject(), outboundopenDirectTcpip/openForwardedTcpip, and an in-orderglobalRequest/onGlobalRequestpath fortcpip-forward/cancel-tcpip-forward(RFC 4254 §7). AchannelTransport()adapter wraps aChannelas a@webnet/transportRawTransport(EOF surfaced as a throw withreadEnded,halfCloseasCHANNEL_EOF, non-blockingclose()so a forwarded stream can't deadlock on the peer close handshake), and an internalpipe()bridges two transports. The public API exposesSSHClientConnection(connect over aRawTransport,openSubsystem,openSession,openDirectTcpip,dialer()returning aRawDialerfor local forwarding, andrequestRemoteForward()returning aRemoteForwardthat implementsRawListenerfor remote forwarding) andSSHServerConnection<T>(auth hook returning the context,acceptSession(), and optionaldirectTcpip/tcpipForwardhooks that plug arbitraryRawTransport/RawListenerimplementations into the forwarding paths). The design deliberately keepsopenSession()/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 exercisesssh -L/ssh -Requivalents 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), unguardedCHANNEL_OPEN_CONFIRMATIONsends in the accept loops, and unrejected pendingRemoteForward.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 (_deliverClosenow wakes window waiters andsend()aborts on a remotely-closed channel); (2)pipe()'s copy loop only guarded reads, so a write failure on a forwarded socket became anunhandledRejectionthat crashes the Node process by default — a remotely-triggerable DoS — now the write is guarded and bothvoid pipe()call sites swallow; (3) connect-phase failures leaked the underlying socket (SFTPClient.#doConnectandSSHClientConnection.connectnow close onopenSubsystem/auth failure; verified against an sshd with no sftp subsystem, which previously hung the process to the runner timeout). Four smaller fixes:authenticateServeraccepts a falsy auth context (uid0etc.) 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 duplicatetcpip-forwardcloses the superseded listener (andRemoteForwardteardown 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 forSMB2Client(issue #99), consistent withDAVClient.SMB2Client.transferState()detaches an idle client and exports a structured-cloneableSMB2TransferState: 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 withreconnect: 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 astructuredCloned 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-solupdatedAGENTS.mdto require exactAgent/<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-correctDataViewhandling, 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) extractedStateTransferableandisStateTransferablefrom@webnet/transportinto 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-soldeclared 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 intailshareto use@webnet/tsconnect-workerviauseBuildIpnWorker, enabling multiple browser tabs to share a single IPN instance through a SharedWorker (with transparent fallback to main-thread mode when SharedWorker is unavailable). TheIPNconcrete class was replaced with theIpnClientinterface throughout, andIpnClientHandleis used as the return type from the worker hook. TheipnPrepareRedux slice was removed entirely and replaced with anIpnPrepareContext(React context) that reads/writes a singletailshare:configlocalStorage key as a JSON object — consolidatingipn#hostname,ipn#controlURL,ipn#authKey,ipn#exitNode,ipn#autoand the newuseWorkerandfileOpssettings into one key, with explicit defaults (useWorker: true,fileOps: "memory") set inparseConfigso 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 isworkerormain-thread. In worker mode, selecting OPFS enablesfileOps: truein 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.autostartremains a tri-state:undefinedlatches totrueon the firstRunningevent (so subsequent page loads auto-connect),falsepermanently disables autostart, andtruetriggers it immediately on load.TailscaleRouteshows a spinner rather than a disabled "Enable Tailscale" button during the worker connection window.@webnet/reactgained auseSharedWorkerAvailable()hook (SSR-safe viauseSyncExternalStore) replacing the inline pattern inIpnContext.tsx.useBuildIpnWorkerin@webnet/tsconnect-reactwas extended to accept(() => SharedWorker) | nullin its first overload (implementation already handled it).@webnet/react/tsconfig.jsonand@webnet/tsconnect-react/tsconfig.jsonboth receivedskipLibCheck: trueto resolve a type conflict between@types/eslint-scopeandeslint's built-in types. -
@webnet/tsconnect-react— initialization error callbacks:gpt-5.6-soladded optional error callbacks touseBuildIpnanduseBuildIpnWorker, 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/vfsand exposed from a test-only@webnet/vfs/conformanceentry 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 twoVFSErrorcodes; 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,readFileRangeinclusive-end semantics, empty files, unicode names, nested paths, the fullVFSErrorcontract, 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.readFilehanded 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.readFileon a directory deferredis-a-directoryto the first stream read anddelete("/", true)removed the entire served root; the DAV client defaultedcopy/moveto overwriting, only honoured a non-recursivedeletewhenrecursivewas explicitlyfalse, returned an empty listing forreaddirof a file, and merged rather than replaced insetProps, while the DAV server reportedis-a-directoryas 409 instead of RFC 4918's 405; the FTP client silently clobbered on a defaultmoveand 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 ignoredopts.overwriteincopy(which also could not copy directories), and its mock server ignoredCreateOptionsdirectory flags andReplaceIfExists. 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 withassert.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 OpenSSHsshdto 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 lostreadFileRange's error contract when the per-package tests were removed and never coveredcopyreplacing an existing directory (whichNodeVFSmerged into instead); SMB2copycould 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 asis-a-directoryalthough 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 theforbidden/not-foundalias the reviewers correctly identified as covering a fixable bug rather than a protocol limit; and SFTP now marks the VFS code it embeds inSSH_FX_FAILUREmessages 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=4096to.gitea/workflows/ci.yml, so every CI job that runs Node (including the per-file workersnode --testspawns 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-solchanged the Tailscale package build to use a temporary staging directory and copy onlymain.wasm,build-info.json,wasm_exec.js, andcacert.pemintodist/, 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-soladded a Webnet-onlylistDrivePeersWithShares(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-peerDAVClientfactory for a later whole-tailnetAsyncVFShierarchy. Focused tests cover populated, empty, invalid, unreachable, and ordered peer results. -
@webnet/taildrive— peer-probe review hardening:gpt-5.6-soladdressed 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-solrenamed the narrower discovery helper tolistDrivePeersWithSharesto distinguish it fromIpnClient.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-soladded the optionalAsyncVFS.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 existingreaddir()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-solfixed the access regression, aligned DAV self-resource validation, documented the contracts, and added request-level and conformance coverage. Freshgpt-5.6-terrareviews then caught missing-source COPY/MOVE deleting an overwrite destination in the Drive fallback and native Node, Memory, and SFTP paths;gpt-5.6-solreordered 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 theunsupportedVFSErrorCodeand a@webnet/vfs/fallbackentry point. The code is documented as a capability failure, distinct fromforbidden(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. OptionalAsyncVFSmembers 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: SFTPSSH_FX_OP_UNSUPPORTED, FTP 502 and WebDAV 501 outbound, and the same statuses plus SMB2STATUS_NOT_SUPPORTEDinbound — the SMB2 status previously reached callers asforbidden, 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 aRequired<AsyncVFS>so a consumer can call any optional operation without branching on method presence;statAndReaddirFallback,readFileRangeFallback,copyFallback,moveFallbackandsetPropsFallbackare 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 anunsupportedrejection 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.setPropshas no fallback, because arbitrary property storage cannot be built from the required operations, and saysunsupportedrather 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 metadatareaddirguarantees 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 windowsreadFilelazily, 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 thestatAndReaddirfallback that arrived with #172 were replaced by the shared one. The conformance suite's optional-method capabilities became tri-state so a runtimeunsupportedrejection is itself testable, including that nothing changed, and the fallbacks are exercised by running the whole suite against aMemoryVFSwith its optional operations first removed and then rejecting. PinningreadFileRange'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:NodeVFSpassed an inverted range straight tocreateReadStream, which throws a rawERR_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, whoseRESTcarries no end bound, transferred from the start to the end of the file instead of nothing. Review by the author then established that the migratedstatAndReaddircall sites want theon-unsupportedpolicy rather than the default: each needs the listing to answer the request it is serving, and thestatplusreaddirit falls back to are required operations, so declining to fall back only loses. Issues #176 (a heap-exhausting unbounded recursion inMemoryVFS.copywhen 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 anunsupportedrejection) were filed from what this work surfaced. -
@webnet/vfs— bounded overlapping-copy rejection (issue #176):gpt-5.6-soladded a normalized, segment-awarepathsOverlaphelper and made MemoryVFS, NodeVFS, SMB2, and WebDAV reject self-copies and ancestor/descendant copies withprecondition-failedbefore 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.