Transferred conns keep their TLS state: claim replies and conn
ResourceMeta carry isTls, upgradeTls is serialized through the resource
chan lock (never overlapping an in-flight read, reply dropped on detach
mid-upgrade), and transferState refuses while an upgrade is in progress.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
transfer() only recognized a bare TransferToken, so composite states like
DavTransferState were sent with an empty tokens list, skipping worker-side
validation and queue-expiry cleanup. Handshake-buffered envelopes are now
replayed before awaiting the client lock so channel ordering is preserved,
and the main-thread promise-returning stubs reject instead of throwing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Results of reads/accepts that were in flight when a resource was detached
were delivered to the old owner's dead port, and accepted conns were
registered on (and leaked with) the old client entry. Each resource now
carries a generation-tagged channel; stale completions are deposited into
a backlog the next owner's bridge drains first, serialized so stream order
is preserved. TTL expiry and cancel close backlogged conns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Envelopes flushed by the worker when a client key registers arrive before
ready and were dropped by the temporary handshake handler.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses a blocking external-review finding: pre-handshake TLS
configuration failures could leave the connection state inconsistent
or leak the underlying socket.
- The Go upgradeTLS now closes the conn on configuration errors too
(submodule bump), so Conn.upgradeTls marking itself closed on any
raw rejection no longer strands a live, unclosable Go conn.
- NodeTransport.upgradeTls constructs the TLS socket inside the
try/catch: createSecureContext/connectTls throw synchronously on
invalid PEM, which previously escaped after listeners were detached
and #upgrading was set, leaving the transport permanently stuck.
- Contract documented on RawTransport: synchronous option-validation
rejections leave the transport usable; any failure after the upgrade
starts closes it.
Regression tests: malformed server certPem/keyPem (sync throw path)
and malformed client caCerts (verification failure path) on node, both
asserting the closed-and-not-stuck end state; fatal-rejection contract
on the tsconnect Conn.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfRgMke8p7QDxD5QM5thxH
- NodeTransport server-side upgrade no longer rejects on buffered data:
a pipelining peer's ClientHello can land in the internal buffer in the
gap between writing the go-ahead and calling upgradeTls. Buffered
chunks are unshifted back into the stream for the TLS wrap to consume.
Client-side, buffered data still rejects (there it is a protocol bug).
- The worker upgradeError reply now carries a closed flag so WorkerConn
only tears down the connection when the handshake actually failed;
rejected upgrades (bad options, stale wasm) leave it usable, matching
the direct Conn semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfRgMke8p7QDxD5QM5thxH
Review finding: the quiescence check ran only before starting the
upgrade; reads or writes issued while the handshake was in flight
would race it on the underlying socket (node) or hit the stale
pre-upgrade Go handle (tsconnect). All three implementations now
track an upgrading flag that read(), write() and a second
upgradeTls() reject on. Also front-load the serverName-required
check in Conn.upgradeTls to match NodeTransport.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfRgMke8p7QDxD5QM5thxH
Adds an optional in-place upgradeTls(options) method and an isTls
getter to RawTransport, implemented on the node and tsconnect
transports. Client mode takes serverName/insecureSkipVerify/caCerts
(serverName required unless skipping verification); server mode takes
isServer with a PEM cert/key pair. Upgrading requires a quiescent
transport and closes the connection on handshake failure.
NodeTransport wraps its existing socket with node:tls; tsconnect's
Conn swaps its raw handle via the new upgradeTLS wasm bridge method
(tailscale submodule bump); tsconnect-worker forwards the upgrade over
new per-conn protocol messages and carries isTls on conn/accepted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfRgMke8p7QDxD5QM5thxH
- server: reply 451 (and notify onError) instead of dropping the control
connection when a VFS backend throws a non-VFSError
- server: drop dead TYPE/PROT session state (accept-and-ignore is
deliberate; binary transfers are correct for the compat targets)
- client: guard close() against a failed connect; reconnect transparently
when the cached control connection has dropped (FTP idle timeouts)
- client: send REST immediately before RETR, after PASV/EPSV (RFC 3659 5.3)
- replies: map 550 'permission denied' text to forbidden, not not-found
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMqjFbejK5h7MJamdfh3VY
Prevents command injection when an argument (e.g. a VFS path) contains
CRLF, which would otherwise be written verbatim onto the control channel
and let a caller smuggle additional FTP commands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMqjFbejK5h7MJamdfh3VY
Several worker.test.ts tests (WorkerSSHSession.resize()/close() flagged
as flaky in CI, plus other close()-message, callback-firing, and
pumpStreamToPort tests) waited a flat setTimeout(r, 10) after
postMessage before asserting the message arrived — the same race
already fixed for closed-state checks in b917cd8. Under CI load the
10ms sleep can elapse before the message is delivered, causing
sporadic AssertionErrors.
Added a generic waitFor(predicate, timeoutMs?) helper that polls via
setImmediate, replaced every fixed sleep with a poll on the actual
condition, and consolidated the four pre-existing
`while (!x.closed) await setImmediate()` spin-loops from b917cd8 onto
the same helper for consistency.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every job in the old checks.yml/test-node.yml/test-browser.yml
independently repeated checkout + tailscale submodule clone + npm ci,
and 5 of 7 also independently rebuilt the whole workspace via Turbo.
There was no caching anywhere.
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 - a server-side issue, not fixable from
workflow YAML - and actions/upload-artifact/download-artifact v4
refuse to run at all (GHES detection). v3 of the artifact actions
work correctly, so they're used as the handoff mechanism instead.
.gitea/workflows/checks.yml, test-node.yml, and test-browser.yml are
merged into one ci.yml. A new `install` job does the real work once
(submodule clone, npm ci, build) and hands node_modules plus Turbo's
local cache off via v3 artifacts to typecheck/typetest/node-tests/
browser-tests (all `needs: install`), instead of every job reinstalling
and rebuilding from scratch. lint/format never touched the submodule
or build output, so they drop that step and stay independent for fast
feedback. The shared checkout+submodule+setup-node preamble is now 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). A concurrency group cancels
superseded runs on the same ref.
The node_modules archive step must include per-package node_modules
overrides, not just the root: 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. Missing those in
the archive caused those two packages to silently typecheck against
the wrong TypeScript version.
Cross-run caching (reusing a previous run's install/build) isn't
available until the Gitea instance's cache backend is fixed
server-side - that's a separate, out-of-scope follow-up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace racy `setTimeout(r, 10)` waits with a `setImmediate` poll on
the `.closed` property. The 10 ms sleep was insufficient under load,
causing `readFrom()`/`read()`/`write()`/`accept()` to run before the
"closed" message was delivered, resulting in "packet conn closed"
instead of the expected "already closed" error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix `suggegestedName` typo in showSaveFilePicker call (extra "ge");
the browser silently ignored the unknown key so the Save dialog opened
with a blank filename on Chrome/Edge
- Defer URL.revokeObjectURL to setTimeout() so Safari's download
manager has a task boundary to open the object URL before revocation
- Add AI_CHANGES.md entry for the @webnet/react/@webnet/utils integration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- example-app: replace local useClient duplicate with @webnet/react, add
useLocalStorage to persist SharedWorker preference across page reloads
- example-app: replace local fmtSize duplicate with @webnet/utils/fmtSize,
use download() util in WaitingFileDebug instead of inline logic
- test-app: add @webnet/utils dependency and expose it on window alongside
other globals for console testing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts the useClient and useLocalStorage hooks into @webnet/react,
and the download, upload, readBlob, and fmtSize utilities into @webnet/utils,
so they can be shared by tailshare and other apps without living inside
a single app package.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add instructions for agents to apply `Agentic` + `Agent/<model-line>`
labels to every PR they open (creating the labels via `tea labels create`
if absent), and to auto-finalise PRs without user prompting when the
work is trivial or was fully specified upfront.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- IpnClientHandle.disconnect() JSDoc clarifies that the main-thread path
terminates IPN while the worker path is a soft detach
- connectWithFallback logs a console.warn with the caught error when falling
back from the SharedWorker path, so unexpected fallbacks are diagnosable
- IpnMainThreadHandle constructor comment explains that pre-run() callbacks
are intentionally dropped (run() snapshots current state on subscribe)
- lock?.release ?? null -> lock ? () => lock.release() : null to avoid
passing an unbound method reference
- Browser test: first test adds stateStorage: "memory" to avoid leaving IDB
state between runs
- Browser test: new test covers the IDB lock-contention path — acquires the
lock manually then confirms connectMainThread rejects immediately
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>