Give every workspace a README describing what it does, its entry points,
and a short usage example, and link them from the root package table.
Frame Tailshare in the root README as the application the libraries exist
to be composed into, with the other apps as development and testing
surfaces. Its own README records what works today and what the open
issues intend for it.
Co-Authored-By: Claude Opus 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
- serveDirectory: replace regex path traversal guard with resolve+relative
check, which is simpler to reason about and handles encoded edge cases
- drop test:browser:coverage scripts; c8 only covers Node orchestration
code, not browser-executed code inside page.evaluate() — the resulting
lcov is nearly empty and would confuse CI coverage dashboards
- guard server?.close() in after() hooks so teardown doesn't throw a
TypeError if the before() hook failed to start the server
- extract setupLoopback() helper inside each page.evaluate() in
transport.browser.ts to avoid verbatim repetition between tests
- add comment in helpers.browser.ts explaining the IDB readonly-transaction
trick that ensures setState()'s async write has settled before opening
a second IndexedDBState instance
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses Playwright as a library within node:test (not @playwright/test) to
keep the same runner and script conventions. Browser test files use the
*.browser.ts extension so the existing src/**/*.test.ts glob picks up zero
browser tests, leaving the regular test suite unaffected.
Key pieces:
- .npmrc: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 prevents binary downloads on
npm ci; browsers are installed explicitly in CI via playwright install
- @webnet/browser-test-utils: new private package exporting forBrowsers()
(iterates chromium + firefox, handles browser lifecycle within node:test
suite/before/after) and a serveDirectory() helper (minimal http.createServer
that serves a built dist/ or out/ directory so the browser can fetch ES
modules via dynamic import())
- test:browser / test:browser:coverage scripts added to transport, vfs,
tsconnect following the same c8 + lcov pattern as test:coverage
- turbo.json: test:browser and test:browser:coverage tasks depend on build +
^build (dist/ must exist before the browser can import from it)
- .gitea/workflows/test-browser.yml: CI pipeline that installs browsers with
--with-deps then runs npm run test:browser
Integration tests:
- DataChannelTransport: real RTCPeerConnection loopback (both peers in one
page context), exercises send/receive and close propagation
- FsaVFS: OPFS round-trip (writeFile/readFile), stat, readdir, delete
- IndexedDBState: multi-instance persistence (write via instance 1, open
fresh instance 2 and verify IDB round-trip), empty-DB initialisation
- FsaFileOps: write/read/stat/remove cycle and rename/listFiles over OPFS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add five tests covering the previously-missed branches:
- read() after clean channel close (no stored error)
- read() after error event re-throws the stored error
- error event with null error field falls back to generic message
- close event while write drain is pending rejects the write
- open-phase error with null error field uses generic message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements RawTransport over an RTCDataChannel with write-side and
receive-side backpressure. Write-side: send() is always synchronous, but
write() defers its promise until bufferedamountlow fires when
bufferedAmount exceeds sendHighWatermark, using
bufferedAmountLowThreshold for the resume threshold. Receive-side:
removes the message event listener when buffered bytes exceed
receiveHighWatermark, letting SCTP's internal buffer fill and signal
flow control back to the sender; re-adds the listener once read() drains
below receiveLowWatermark.
Factory functions openDataChannel (creates + waits for open) and
acceptDataChannel (wraps an incoming channel) return a transport only
once the channel is fully open. Both accept separate send/receive
watermark options. All WebRTC types are defined as structural interfaces
so the package requires no DOM lib and tests use an in-process mock.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When close() was called, it fired #errcallback synchronously and also
left the server's "close" event free to fire it a second time.
Null out #errcallback before invoking it so both event-driven paths
are guarded and only one caller receives the rejection.
In accept(), the once("connection") listener was never removed when the
promise was rejected (e.g. via close()), so any late-arriving connection
would create a NodeTransport that was immediately dropped.
Replace the bare once() with a named handler that the error callback
removes before rejecting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- turbo.json: add test and test:coverage pipeline tasks (dependsOn: ^build)
- root package.json: add test and test:coverage scripts via turbo
- Remove test:unit, test:coverage:unit, test:watch:unit from all packages
- Remove test-helpers/flags.ts from transport, http, websocket (and dirs)
- Remove fetch.node.test.ts (external test hitting google.com; always skipped)
- Drop { skip: skipIfNotIntegration } from all suites — tests run unconditionally
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes @webnet/websocket's _internals dependency on @webnet/http.
http/common/buffer.ts becomes a thin re-export; all http-internal imports are unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pulls RawTransport, RawDialer, RawListener, Reader, Writer, and PairSync
out of @webnet/http into a new protocol-agnostic @webnet/transport package.
Node and loopback transport implementations move there as sub-exports
(./node, ./loopback); @webnet/http re-exports the types to keep existing
internal imports resolving.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>