Add a ControlledFileOps interface (UserIPNFileOps + observable limits +
onChange) satisfied by InMemoryFileOps and FsaFileOps. Bind its state
into a new fileOps Redux slice so file storage metrics and limits are
observable via store.getState().fileOps and broadcast to all clients.
In tsconnect-worker: hoist the FsaFileOps instance to module scope,
wire bindFileOpsToStore after init, and handle a new setFileOpsConfig
call that lets clients reconfigure maxFiles/maxTotalSize/maxFileSize
at runtime. Limit changes propagate back via the existing action
broadcast path so all connected clients see updated state immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tsx hardcodes keepNames:true in its esbuild transform, injecting a __name
helper at module scope. Playwright's page.evaluate() serializes callbacks
via .toString(), so the helper is absent in the browser context. Inject a
matching polyfill via addInitScript on every new page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Move #fileSizes.set in openWriter to after all async FSA ops, so a
failed getFileHandle/createWritable/seek doesn't leave a phantom entry
inflating fileCount and totalSize
- Fix rename to stat the file when it wasn't previously tracked, so
untracked files don't become permanently invisible after a rename
- Document stat/totalSize inconsistency during active writes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds maxFiles/maxTotalSize/maxFileSize limit getters/setters,
fileCount/totalSize/openFiles getters, and onChange/offChange
notification to FsaFileOps, mirroring InMemoryFileOps. File sizes
are tracked in memory via a #fileSizes map; createFromOpfs now scans
the directory at startup so pre-existing files count toward limits.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Splits the VFS abstraction (AsyncVFS, Stat, VFSError, VFSErrorCode) and
its implementations (MemoryVFS, NodeVFS, FsaVFS) into a new standalone
@webnet/vfs package, following the same pattern as the @webnet/transport
split from @webnet/http.
packages/drive now depends on @webnet/vfs and imports directly from it.
packages/test-app likewise imports MemoryVFS and FsaVFS from @webnet/vfs.
The drive package.json export map drops the ./vfs/* sub-exports.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add "Reviewing pull requests" section to AGENTS.md: use `tea` (not
`gh`), validate assumptions with the user in interactive mode, and
post feedback directly on the PR in autonomous mode.
- Consolidate the AI disclosure out of README.md into a standalone
AI_CHANGES.md; README.md now links to it.
- Add .gitattributes with merge=union on AI_CHANGES.md so parallel
branches each appending entries never produce conflicts.
- Update the "AI disclosure" section in AGENTS.md to point agents at
AI_CHANGES.md instead of README.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bugs:
- Liveness detection now fires for all clients, including those that connect
while init is pending. Previously, the "hello" message was consumed by the
onFirst handler before registerClient set up its port.onmessage listener,
so navigator.locks.request was never called for the first tab and any tab
that connected during init. Fix: unified onHello handler in onconnect
extracts lockName from every "hello"; registerClient always takes lockName
and acquires the lock immediately.
- Remove dead bodyCallbacks field from bridgeDriveHandler pending map. The
map was allocated on every request but never read or populated; body chunk
reads go directly through entry.req.readBodyChunk().
- Drive cleanup in cleanupClient now only installs the no-op handler when no
other client still has driveRegistered = true, preventing the surviving
client's handler from being silently replaced.
Code quality:
- wrapDispatch: replace (s as any).dispatch with a two-step cast through
unknown — avoids the any escape hatch while satisfying RTK's overloaded
ThunkDispatch type.
- IndexedDBState.setState: add tx.onerror handler so failed IDB writes are
logged rather than silently dropped.
- pumpStreamToPort fire-and-forget in openWaitingFile fallback: add
.catch(() => {}) to make the intentional discard explicit.
- tsconfig.json: add skipLibCheck: true to match tsconfig.worker.json (both
transitively import @webnet/tsconnect which has an unresolved wasm_exec.js
declaration issue).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add src/worker.ts as the webpack worker entry (imports @webnet/tsconnect-worker/worker)
- IpnProvider: add useWorker/workerAvailable state; when useWorker is on,
useBuildIpnWorker() creates the client and its embedded store is passed to
IpnStoreProvider; when off, existing useBuildIpn() path runs unchanged
- Debug: add "use SharedWorker" checkbox (checked by default when available,
disabled when not); state dropdown shows indexeddb/memory in worker mode
and localStorage/sessionStorage/disable in main-thread mode; taildrop
option label reflects opfs vs memory depending on mode
- useBuildIpnWorker: add optional workerOptions param instead of hardcoding
{ type: "module" } — webpack bundles the worker as a classic script, so
the caller passes no options; module-mode callers opt in explicitly
- Add @webnet/tsconnect-react, @webnet/tsconnect-redux, @webnet/tsconnect-worker
to example-app package.json dependencies
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Accepting a SharedWorker instance was unsafe: in StrictMode, React runs
effects twice in quick succession, and since the connect() promise is
async, clientRef.current is still null when the second effect fires —
so both calls would share the same sw.port, causing two IpnWorkerClient
instances to collide on one MessagePort.
With string | URL, each call to new SharedWorker() fires a fresh connect
event in the worker and returns an independent MessagePort, so the
StrictMode double-invoke is safe: the first connection gets disconnect()ed
when its promise resolves (cleanedUp is true), and the second becomes
the active client.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add disconnect() to IpnWorkerClient (releases Web Lock without shutdown)
- Widen IpnContext from IPN to IpnClient
- Add useBuildIpnWorker(worker, config, runParams) hook with StrictMode-safe
cleanup: disconnect() fires if the cleanup runs before connect() resolves,
or immediately after if it resolves first
- Export useBuildIpnWorker from package index
- Add @webnet/tsconnect-worker to package dependencies
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Place IndexedDBState in the state-storage section alongside
InMemoryState and WebStorageState rather than between the
getIceServers doc comment and its function body.
Change getIceServers to accept IpnClient instead of the raw IPN
interface so callers can pass either IPN or IpnWorkerClient.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move IndexedDBState to @webnet/tsconnect/helpers and export from
package index; remove it from tsconnect-worker/storage.ts
- Add IpnClient interface, IpnSSHTermConfig, and IpnPacketConn to
@webnet/tsconnect; IPN class and IpnWorkerClient both implement it
- Make IPN.ssh() async to unify the sync/async split
- Add preloadedState parameter to buildIpnStore; worker now sends a
preloadState message (full Redux state) on connect instead of
replaying synthetic actions
- WorkerConn implements RawTransport, WorkerTCPListener implements
RawListener, WorkerPacketConn implements IpnPacketConn
- WorkerSSHSession.resize/close fire-and-forget (sync) to implement
IPNSSHSession; remove resizeResult/closeResult round-trips
- ReadableStream transfer fallback via MessageChannel sub-protocol for
Safari compatibility on sendFile and openWaitingFile
- Add stateStorage: "indexeddb" | "memory" option to WorkerConfig
- Export TLSCertKeyPair, WhoIsResponse, PingType, PingResult,
DNSQueryResult from @webnet/tsconnect package index
- Add @webnet/transport to tsconnect-worker dependencies
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps the IPN and tsconnect-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 design points:
- IndexedDBState: sync-read/write Map cache with async IDB flush, replacing
the unavailable localStorage in SharedWorkerGlobalScope
- Redux store on the worker broadcasts every dispatched action to all clients;
new clients receive a full state snapshot as synthetic actions on connect
- Web Locks API tracks client liveness: when the client's tab closes its lock
is released and the worker closes all resources that client opened
(Conn, TCPListener, PacketConn, drive handler)
- Graceful IPN shutdown when the last client leaves
- FsaFileOps (OPFS) available for Taildrop when fileOps: true in config
- Two tsconfigs: DOM lib for client code, WebWorker lib for worker.ts
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>
no-empty, no-useless-assignment, and prefer-const violations introduced
by this branch's earlier test additions, plus stray formatting drift.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bridgeDriveHandler still exposed ctx.req.stream() as an AsyncIterable
and had no iter() method, and didn't support ReadableStream response
bodies — both stale from before packages/http's stream()/iter() split.
Add a BYOB-aware byte stream for the request body and a ReadableStream
branch when writing the response body.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers default (non-BYOB) and BYOB byte-stream paths at chunk sizes just
below, at, and just above FLUSH_THRESHOLD (65536). Verifies both the
correctness of the chunked encoding output and that intermediate flushes
to the underlying transport occur at the right boundary.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ReadableHttp.stream() renamed to iter() (returns AsyncIterable)
- ReadableHttp.stream() now returns ReadableStream<Uint8Array> with BYOB
byte-stream support where the environment supports it
- WritableHttp.body now accepts ReadableStream<Uint8Array>; sent as
chunked transfer encoding with BYOB reads and periodic 64 KiB flushes
- Removed iterableToStream() and streamToIterable() helpers from
packages/drive (no longer needed)
- Updated all consumers in packages/drive to use the new APIs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers bridgeDriveHandler request/response translation (headers, body
streaming, status, all Body union variants) and IPN.serveDrive /
listDrivePeers argument validation and JSON handling, using fake
RawIPN/JsDriveRequest/JsDriveResponse objects so no WASM build is needed.
Bumps the tailscale submodule pointer to the rebased feat/drive-wasm-bridge
tip.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add drive types to @webnet/tsconnect:
- DriveSharePermission, DrivePermissions, JsDriveRequest, JsDriveResponse,
RawDriveHandler, IPNDrivePeer types in types.ts
- setDriveHandler / listDrivePeers on the raw IPN interface
Add IPN class methods in ipn.ts:
- serveDrive(fn): registers a WebDAV handler for /v0/drive peerapi endpoint
- listDrivePeers(): lists peers with PeerCapabilityTaildriveSharer in their ACL caps
Add new package @webnet/taildrive with ./server sub-export:
- bridgeDriveHandler(handler: Handler): RawDriveHandler adapts an
@webnet/http Handler to the Go bridge's raw callback interface, streaming
the request body chunk-by-chunk and piping the response body to write()/end()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The first notifyNetMap after setServices() can be an unrelated update
(other nodes joining) that fires before the service advertisement
propagates, so checking only the first one was flaky. Poll like the
peer.services test already does.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Points to 3a9f6f4 which adds RestartMap() to controlclient.Auto and
calls it from SetExplicitServices, so notifyNetMap fires immediately
after setServices instead of waiting for the next periodic control
plane push.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers three scenarios:
- setServices() resolves without error
- self.services is reflected in the next notifyNetMap after advertising
- A second IPN (watcher) sees the peer's services in its netmap update
Also extends connectIPN() to accept additional IPNRunOptions so callers
can wire notifyNetMap (and other callbacks) at run() time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getSelfServices returned a new [] literal on every call when self was
null/undefined, causing re-render loops. Replace with a shared constant.
getPeersByService and the new getPeersByServiceDescription are wrapped
with createSelector so the result array is only replaced when the peers
slice or the selector arguments actually change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Points the submodule at feat/tsconnect-service-advertisement
(dd9c9f68), which adds SetExplicitServices and the setServices
WASM binding.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add IPNService type and a services field on every netmap node (self and
peers). Browser WASM nodes can now call ipn.setServices([{proto, port,
description?}]) to advertise TCP/UDP services on the tailnet; the
declaration is distributed to all peers via the control server.
Two convenience selectors added to tsconnect-redux:
getPeersByService(proto, port) — peers advertising a given service
getSelfServices() — services declared by this node
Bumps the tailscale submodule to webnet/tailscale#9.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DERP servers run an integrated RFC 5389 STUN server (UDP 3478 by
default). Since tsconnect in the browser relays all traffic through
DERP over WebSocket and has no direct UDP path, WebRTC is the only
way to establish a direct peer-to-peer connection. getIceServers()
fetches the tailnet's DERPMap via LocalAPI and converts it to an
RTCIceServer[] list suitable for RTCPeerConnection({ iceServers }).
Unit tests cover parsing, port defaulting, and all filter conditions
(STUNPort < 0, empty HostName, non-200 status) — no WASM required.
Integration test verifies a live IPN returns at least one stun: URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unit tests cover InMemoryFileOps (read/write/stat/list/rename/remove, seek,
misuse guards, constructor seed) and InMemoryState. No WASM needed.
Integration tests spin up three real Tailscale nodes against a headscale
control server, verifying initIPN WASM loading, /localapi/v0/status, and
two-node TCP dial/listen. Credentials loaded from .env.local (gitignored).
All IPN instances share one Go WASM runtime (factory compiled once); shutdown()
on any one exits the runtime, so a single before()/after() pair wraps the suite.
test:coverage script added for consistency with other packages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
go.run() returns a Promise that resolves exactly when the Go runtime exits.
Storing it in initIPN and awaiting it in shutdown() eliminates a race where
Go deletes _inst before a callback-based resolve fires.
Go side: add nil guards on lb and ln so shutdown() is safe even when run()
was never called (e.g. testing IPN construction without connecting).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two Go-side bugs fixed in the tailscale submodule:
- safesocket_js.go: hardcoded "Tailscale-IPN" memconn addr caused log.Fatal
on a second newIPN() call; atomic counter now gives each instance a unique name.
- wasm_js.go: listen() rejected ":port" (any-interface form); netstack requires
an explicit bind addr; now normalises :port → 0.0.0.0:port.
wasm_exec.js ENOSYS stubs (installed when globalThis.fs is falsy) are the correct
behaviour: all network calls go through JS fetch()/WebSocket. Setting globalThis.fs
to Node.js's real fs caused Go to read /etc/resolv.conf and use host nameservers.
tsconfig.json: add test-file exclude; add DOM.AsyncIterable to lib.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a shutdown() method to the IPN class (TypeScript) and RawIPN
interface (types.ts), backed by the new Go shutdown() on jsIPN in
the tailscale wasm submodule.
Calling shutdown() stops the Tailscale backend, closes all held
resources, and lets the Go runtime exit cleanly — so Node.js processes
can exit without hanging and browser service workers can be paused or
killed for inactivity.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace whole-file buffering on both send and receive paths with Web
Streams API so arbitrarily large files can be transferred without being
bounded by available RAM.
Send: IPN.sendFile now accepts a ReadableStream<Uint8Array> + declaredSize
instead of a Uint8Array. The WASM layer pulls chunks from the stream via
ReadableStreamDefaultReader.read() Promises, feeding them directly to the
HTTP PUT body — no js.CopyBytesToGo of the full file.
Receive: IPN.openWaitingFile now returns Promise<ReadableStream<Uint8Array>>.
The WASM layer wraps the Go io.ReadCloser in a pull-based ReadableStream,
enqueuing 64 KiB chunks on demand — no io.ReadAll.
FileOps: UserIPNFileOps.openReader now returns ReadableStream<Uint8Array>
instead of Uint8Array. The JS→Go bridge wraps the stream in a new
jsStreamReader (io.ReadCloser) that blocks on a channel per chunk, matching
the existing channel+FuncOf pattern used throughout taildrop.go.
Add FsaFileOps in packages/tsconnect/src/helpers.ts: a UserIPNFileOps
backed by the File System Access API (any FileSystemDirectoryHandle), with
FsaFileOps.createFromOpfs(subdir?) as the OPFS factory. Received chunks
land directly in OPFS via FileSystemWritableFileStream with no intermediate
buffer. openReader returns file.stream(), so downloads are also zero-copy
through Go.
InMemoryFileOps.openReader is updated to return a ReadableStream that emits
stored chunks one by one (no concatenation).
The example app is updated to use file.stream()/file.size for send and
showSaveFilePicker (with Response.blob() fallback) for receive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix buildLockRequest to produce correct <D:owner><D:href>value</D:href></D:owner>
structure so parseLockinfo can read the owner from child element text. Add tests
targeting: parseLockinfo empty-owner branch, DAV-namespace dead props via PROPFIND
named, buildPropResponse default hrefForLock, and lock expiry edge cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All test helpers now pass keepAlive:false to makeFetch and share the
resulting pool with DAVClient, so connections close immediately after
each request instead of holding the event loop open for 5 seconds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
adds RFC 4918 DAV compliance level 2 to both server and client:
server: LockStore interface + InMemoryLockStore, handleLock/handleUnlock
methods, If: header validation before all mutating methods, supportedlock
and lockdiscovery in PROPFIND responses, DAV: 1, 2 in OPTIONS.
client: lock(), unlock(), refreshLock() methods; optional lockToken
parameter on writeFile, delete, mkdir, copy, move, setProps; 423 mapped
to VFSError("locked").
LockStore is a separate optional interface passed to createDAVHandler;
AsyncVFS is unchanged and all existing VFS impls are unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix NodeVFS.toFsPath: block path traversal via .. segments (throws forbidden)
- Fix NodeVFS.delete: use unlink+rmdir for non-recursive to correctly return
not-empty (ENOTEMPTY) instead of is-a-directory (EISDIR from fsp.rm)
- Fix NodeVFS.copy: explicitly check dest existence when overwrite:false to
return precondition-failed consistently with MemoryVFS
- Add node.test.ts: 43 tests covering all CRUD operations, contentType from
extension, and 10 directory traversal attack scenarios
- Add targeted coverage tests in drive.test.ts: VFSError code mapping in handler,
GET/HEAD with contentType, propname with dead props, named prop with custom
namespace, PROPPATCH with props:undefined VFS, COPY fallback with setProps,
COPY dir to missing parent, move overwrite:true, buildErrorXml, propsToStat
with invalid date, iterableToStream cancel callback
- Export buildErrorXml from server/_internals
- Add tmp/ to .gitignore for NodeVFS temp dir tests
Statement coverage: 99.49%, branch coverage: 89.7%, all 163 tests pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@webnet/xml: minimal XML parse/serialize with conditional exports —
browser builds use the native DOMParser, Node.js builds use
@xmldom/xmldom. Custom recursive serializer keeps output predictable
and avoids XMLSerializer differences across environments.
@webnet/drive: WebDAV client + server built on @webnet/http and
@webnet/transport, running in both browser and Node.js via the same
transport abstraction used by the rest of the monorepo.
- AsyncVFS interface with optional readFileRange (Range header support),
copy/move, and setProps (dead property storage for PROPPATCH)
- MemoryVFS: in-memory implementation for testing
- NodeVFS: wraps node:fs/promises
- FsaVFS / OpfsVFS: browser File System Access API and OPFS
- createDAVHandler(vfs, opts): Handler compatible with @webnet/http
Server; supports GET/HEAD/PUT/DELETE/MKCOL/COPY/MOVE/PROPFIND/
PROPPATCH with correct 207 Multi-Status XML responses; LOCK/UNLOCK
stub 501; readOnly option; prefix stripping
- DAVClient implements AsyncVFS so it can be used as the backing store
for another DAV server instance; accepts RawDialer or ConnectionPool
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove gzip/brotli pre-compression of main.wasm (tsconnect fork) and
cacert.pem (packages/tsconnect/build.sh). Drop the .br/.gz exports from
package.json. Add scripts/asset-sizes.sh (npm run asset-sizes) to measure
raw, gzip, and brotli sizes of the built assets.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same filter approach as test: scoped to transport/http/websocket to avoid
pulling in tsconnect via example-app's ^build chain. typetest is http-only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
turbo run test was building the full workspace graph (including tsconnect,
which requires the tailscale submodule) because example-app and test-app
transitively pull it in via ^build. Scope test and test:coverage to the
three packages that actually have test scripts.
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>
IPNDialer wraps an IPN instance and adapts dial/dialTLS to the
RawDialer interface. Obtain via ipn.dialer(options?) or construct
directly. dialTls() uses the host argument as SNI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves react/ out of @webnet/tsconnect. Imports updated to reference
@webnet/tsconnect and @webnet/tsconnect-redux. Peer deps: react, react-redux.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves redux/ out of @webnet/tsconnect. Imports updated to reference
@webnet/tsconnect for IPN types. Peer dep: @reduxjs/toolkit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>