Files
webnet/AI_CHANGES.md
T
codinget 84437ed033
CI / format (pull_request) Successful in 1m37s
CI / lint (pull_request) Successful in 1m40s
CI / install (pull_request) Successful in 4m57s
CI / typetest (pull_request) Successful in 1m42s
CI / typecheck (pull_request) Successful in 1m57s
CI / node-tests (pull_request) Successful in 1m58s
CI / browser-tests (pull_request) Successful in 3m19s
chore(docs): add notice of AI review of the tailshare implementation
2026-07-25 20:58:34 +00:00

73 KiB
Raw Blame History

AI disclosure

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

  • @webnet/sftp — stable keyless server identity: GPT-5.6 Luna fixed SFTPServer to lazily generate and cache one ephemeral host key per server instance, and added a regression test covering sequential connections and host-key fingerprints.
  • @webnet/vfs — canonical path helpers: Codex (GPT-5.6 Luna) added public POSIX VFS path utilities for normalization, resolution, parents, basenames, and joining. The helpers clamp traversal at the VFS root, preserve backslashes and Unicode names, and are covered by focused edge-case tests. MemoryVFS, FsaVFS, FTP, and SFTP now reuse the shared implementation; NodeVFS retains its separate traversal-rejection guard.

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

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

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

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

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

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

  • The packages/tsconnect TypeScript SDK

  • The packages/test-app Vite test application

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

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

The following were hand-written:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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