New @webnet/sftp package skeleton: big-endian SSH cursor (string/nameList/
mpint), SSH message constants, openssh-key-v1 Ed25519 key parse/serialize
with PKCS#8 seed wrapping and generateHostKey(), host-key/signature blob
codecs with Ed25519/RSA verification and SHA256 fingerprints, SFTP v3
packet/ATTRS codecs with Stat mapping and packet reassembly, SSH_FX status
to VFSError mapping, unix ls longname formatter, and lexical path helpers.
95 unit tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3bV2ANs8WMHxefD5pF9LN
- 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>
- IpnClientHandle interface: shared contract for both SharedWorker client and
main-thread handle (connectionMode, store, state, running, fileOps, run,
disconnect)
- IpnMainThreadHandle: wraps IPN + Redux store; all IpnClient methods proxy
directly; run() fires user callbacks with current state and delegates future
state changes via runWithStore; disconnect() releases the lock and shuts down
- connectMainThread: IndexedDB-backed state uses a web lock keyed to the DB
name ("tsconnect-idb:<name>"), rejecting immediately if held; in-memory
state skips the lock entirely
- connectWithFallback: tries SharedWorker first, falls back to main thread if
unavailable, disabled (disableSharedWorker option), or on worker init failure
- useBuildIpnWorker updated to use connectWithFallback, returns IpnClientHandle
- Tests: 6 Node tests verifying main-thread init and fallback behaviour; 4
browser tests (Chromium + Firefox) verifying worker path and explicit
disable fallback via esbuild-bundled inline build
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
@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>
Adds @webnet/transport as a dependency. Conn now implements RawTransport
and TCPListener implements RawListener. close() on both returns void
instead of boolean (the raw Go return had no meaning to callers).
Removes the ./redux and ./react sub-exports (split to separate packages).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves upgradeWebSocket, connectWebSocket, and WebSocketConnection out of
@webnet/http into a new @webnet/websocket package. The new package depends
on @webnet/http (for ServerRequest/Response and client connection types)
and @webnet/transport (for RawTransport/RawDialer); the reverse dependency
no longer exists so @webnet/http consumers who don't need WebSocket pay
nothing for it.
Sub-exports ./server and ./client mirror the previous @webnet/http/websocket
structure.
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>
Cover previously missing branches:
- common/pair.ts: PairSync.close() idempotence and getA()-after-close
- common/reader.ts: _triggerFinishOk/Ko catch blocks (handler throws),
BasicBodyReader #chunks pre-buffer path (slice returns multiple arrays),
ChunkedBodyReader already-closed guard and truncated-chunk sanity check
- common/buffer.ts: WriteBuffer.flush() partial write of a second buffer
- client/objects.ts: method defaults to POST when body is truthy
- client/fetch.ts: rejectConnection called when conn.request() throws
- node/transport.ts: fast-fail read() path with null #readError (socket
destroyed without error event)
- server/connection.ts: console.warn for missing requireHeaders entry,
non-TimeoutError parse error propagates out of handle()
Result: 100% functions, 97.81% branches — remaining gaps are dead code
(defensive throws/catches that cannot fire under normal API usage).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces `npm run build --workspaces --if-present` with `turbo run build`,
which resolves build order from each package's package.json dependencies
and parallelises independent packages automatically.
- turbo.json: declares build task with `^build` topological dependency
- package.json: switches build script to turbo, adds packageManager field
(required by turbo v2), adds turbo to devDependencies
- .gitignore: add .turbo/ cache directory
- tailscale: bump submodule for tsconfig types-pinning fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vanilla webpack 5 app with:
- babel-loader + @babel/preset-typescript for TS/TSX compilation
- @pmmmwh/react-refresh-webpack-plugin for React Fast Refresh in dev
- sass-loader + css-loader + style-loader for SCSS support
- HtmlWebpackPlugin templating
- Dev server on port 3000 (npm run dev), production build (npm run build)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch to module/moduleResolution: NodeNext and verbatimModuleSyntax
- Drop DOM lib, add @types/node, bump target to ES2022
- Add test script (node --experimental-strip-types --test) and typecheck script
- Add mockTransport helper and smoke tests in connection.test.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add packages/http with package.json, tsconfig.json, and empty index.ts
- Add @webnet/http as a dependency of test-app
- Expose http namespace on window for console testing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a minimal @webnet/test-app package that boots a Vite dev server,
imports initIPN from @webnet/tsconnect and the bundled main.wasm as a
URL asset, and hangs both on window so you can drive the full IPN
lifecycle from devtools:
const newIPN = await initIPN(wasmURL)
const ipn = newIPN({ authKey, hostname })
ipn.run({ ... })
ipn.login()
const conn = await ipn.dial("tcp", "peer:22")
const pc = await ipn.listenICMP("icmp4")
The index.html doubles as usage documentation for the wrapper API.
Set up npm workspaces monorepo with the tailscale/tailscale repo as a
git submodule. Adds packages/tsconnect which builds the tsconnect wasm
via tailscale's Go toolchain and compiles a TypeScript wrapper that
initializes the wasm, captures newIPN from the global scope, removes it
to avoid pollution, and re-exports it with proper module-scoped types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>