diff --git a/AI_CHANGES.md b/AI_CHANGES.md index 166d76c..bc1514d 100644 --- a/AI_CHANGES.md +++ b/AI_CHANGES.md @@ -99,3 +99,5 @@ The test suite for `packages/http` was mostly generated by Claude Code, which al - 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 mv`s, 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` 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` (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).