73 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/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.
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.