docs(sftp): note the code-review hardening in the AI disclosure
CI / lint (pull_request) Successful in 1m31s
CI / format (pull_request) Successful in 1m33s
CI / install (pull_request) Successful in 4m59s
CI / typetest (pull_request) Successful in 1m18s
CI / typecheck (pull_request) Successful in 1m30s
CI / node-tests (pull_request) Successful in 1m30s
CI / browser-tests (pull_request) Successful in 2m52s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3bV2ANs8WMHxefD5pF9LN
This commit is contained in:
2026-07-07 06:55:33 +00:00
co-authored by Claude
parent c03d0f4c0d
commit 251eb6862d
+1 -1
View File
@@ -75,4 +75,4 @@ The test suite for `packages/http` was mostly generated by Claude Code, which al
- **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.md``tea` 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.
- **`@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()`. 196 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).
- **`@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.