Commit Graph
100 Commits
Author SHA1 Message Date
codingetandClaude 13c25e6f4d ci: add lint, format, typecheck, typetest, and build workflows
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:51:51 +00:00
codinget 57702a3943 chore: run prettier
Node Tests / node-tests (pull_request) Successful in 4m44s
Browser Tests / browser-tests (push) Failing after 7m18s
Browser Tests / browser-tests (pull_request) Successful in 10m9s
Node Tests / node-tests (push) Failing after 11m24s
2026-06-23 23:44:49 +00:00
codingetandClaude a0cc6fc4da docs(ai): log ControlledFileOps binding
Node Tests / node-tests (pull_request) Successful in 6m28s
Browser Tests / browser-tests (pull_request) Successful in 8m21s
Node Tests / node-tests (push) Successful in 5m29s
Browser Tests / browser-tests (push) Successful in 7m40s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:24:50 +00:00
codingetandClaude 630f21c898 feat(fileops): bind ControlledFileOps state to ipn store
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>
2026-06-23 23:24:50 +00:00
codingetandClaude 22232c921b fix(browser-test-utils): polyfill __name for tsx-compiled page.evaluate callbacks
Node Tests / node-tests (pull_request) Successful in 4m45s
Browser Tests / browser-tests (pull_request) Successful in 4m22s
Node Tests / node-tests (push) Successful in 3m52s
Browser Tests / browser-tests (push) Successful in 5m39s
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>
2026-06-23 22:13:38 +00:00
codinget a1081a8194 test: setup node tests in CI 2026-06-23 22:13:38 +00:00
codingetandClaude b7df7c35e3 chore(test): update AI_CHANGES.md to reflect coverage script removal
Browser Tests / browser (push) Failing after 1m5s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 12:49:27 +00:00
codingetandClaude 673fa3aba3 fix(test): address code review feedback on browser testing PR
- 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>
2026-06-21 12:47:31 +00:00
codingetandClaude c89c478286 feat(test): add browser integration testing with Playwright
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>
2026-06-21 12:47:30 +00:00
codinget bba2c5c6f5 feat(tsconnect): add limits and change handlers to InMemoryFileOps (#43)
Reviewed-on: #43
Co-authored-by: Codinget <codinget@codi.moe>
Co-committed-by: Codinget <codinget@codi.moe>
2026-06-21 13:16:59 +02:00
codingetandClaude 39b6cdb8ec chore(tsconnect): update AI_CHANGES for FsaFileOps review fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 23:15:10 +00:00
codingetandClaude a15be0f378 fix(tsconnect): address FsaFileOps review feedback
- 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>
2026-06-20 23:04:04 +00:00
codingetandClaude 1a3bd53348 feat(tsconnect): add limits and change handlers to FsaFileOps
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>
2026-06-20 22:41:42 +00:00
codinget be031f0655 fix(tsconnect-redux): export all selectors 2026-06-20 17:11:29 +00:00
codinget 050de5c2f3 fix(tsconnect-react): add overloads to useBuildIpnWorker to make it clear options are incompatible with factory 2026-06-20 16:23:56 +00:00
codinget 52069887f3 chore(tsconnect-react): remove console.log 2026-06-20 16:21:16 +00:00
codinget 44d6a073bf fix(tsconnect-react): add webpack support for worker via factory 2026-06-20 16:08:37 +00:00
codingetandClaude 5ba7de1998 docs(ai-changes): log @webnet/vfs extraction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 16:21:00 +00:00
codingetandClaude 68313fdcda feat(vfs): extract @webnet/vfs package from @webnet/drive
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>
2026-06-19 16:20:27 +00:00
codingetandClaude 1e683507e7 docs(agents): PR review instructions, AI_CHANGES.md consolidation
- 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>
2026-06-19 12:06:41 +00:00
codingetandClaude f11eaaa670 style: fix prettier formatting in hooks and Debug component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:38:44 +00:00
codingetandClaude b12c2fe0b1 fix(tsconnect-worker): address round-3 review feedback
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>
2026-06-19 11:35:52 +00:00
codingetandClaude 720a0c9d59 feat(example-app): SharedWorker demo with main-thread fallback
- 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>
2026-06-19 11:35:07 +00:00
codingetandClaude c65dd00f71 fix(tsconnect-react): narrow useBuildIpnWorker worker arg to string | URL
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>
2026-06-19 11:35:07 +00:00
codingetandClaude 6a6fbc3e5b feat(tsconnect-react): add useBuildIpnWorker hook for SharedWorker clients
- 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>
2026-06-19 11:34:14 +00:00
codingetandClaude 1a94620141 fix(tsconnect): move IndexedDBState and use IpnClient in helpers
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>
2026-06-19 11:34:14 +00:00
codingetandClaude b4273702db feat(tsconnect-worker): round-2 improvements
- 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>
2026-06-19 11:34:14 +00:00
codingetandClaude 75a841a15d docs(readme): update AI disclosure for tsconnect-worker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:31:20 +00:00
codingetandClaude 5ee74ffc9d feat(tsconnect-worker): add SharedWorker binding for tsconnect
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>
2026-06-19 11:30:15 +00:00
codingetandClaude 6b769462a4 docs(readme): update AI disclosure for WebRTC DataChannel transport
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 19:48:57 +00:00
codingetandClaude f0be079706 test(transport/webrtc): achieve 100% branch coverage
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>
2026-06-18 19:45:20 +00:00
codingetandClaude eab3d69723 feat(transport): add WebRTC DataChannel transport
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>
2026-06-18 19:21:05 +00:00
codinget 922e14b480 fix(tsconnect-react): clean up useBuildIpn hook 2026-06-17 22:16:42 +00:00
codinget 567ee517a6 fix(tsconnect-redux): ordering protection in exit node and file target updates 2026-06-17 22:14:01 +00:00
codinget c049145481 feat(tsconnect-redux): add exit node suggestion and file targets to the ipn store 2026-06-17 21:52:40 +00:00
codinget f0aebd08cf fix(tsconnect-react): build ipn in effect to avoid react issues 2026-06-17 21:46:00 +00:00
codingetandClaude 332a766814 docs(readme): update AI disclosure for NodeListener bug fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:37:25 +00:00
codingetandClaude 9bfd119b21 fix(transport): fix NodeListener double-callback and connection leak
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>
2026-06-16 20:36:41 +00:00
codingetandClaude 2bf551b616 chore: fix lint errors and apply prettier formatting
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>
2026-06-16 20:22:57 +00:00
codingetandClaude 8b735cc674 docs(readme): update AI disclosure for taildrive bridge iter()/stream() fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:10:13 +00:00
codingetandClaude 5d7ba382d2 fix(taildrive): update WebDAV bridge to iter()/stream() contract
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>
2026-06-16 20:09:51 +00:00
codingetandClaude 6163500b61 test(http): add flush-threshold tests for writeChunkedBodyFromStream
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>
2026-06-16 19:51:40 +00:00
codingetandClaude 6fb28013b1 feat(http, drive): rename stream() to iter(), add ReadableStream body support
- 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>
2026-06-16 19:51:40 +00:00
codingetandClaude 90b8c80843 chore: rebase tailscale submodule onto current origin/webnet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:40:16 +00:00
codingetandClaude 42f8176a41 docs: update AI disclosure for taildrive WebDAV bridge work
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:24:34 +00:00
codingetandClaude 859f048e30 test(tsconnect,taildrive): add unit tests for drive bridge and listDrivePeers
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>
2026-06-16 19:24:15 +00:00
codingetandClaude 01a49a0784 feat(tsconnect,taildrive): add taildrive WebDAV server bridge and listDrivePeers
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>
2026-06-16 10:30:48 +00:00
codingetandClaude 000e118a35 docs: update AI disclosure for service advertisement fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:24:01 +00:00
codingetandClaude 563f786969 test(tsconnect): poll for self.services instead of checking the first netmap
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>
2026-06-16 08:21:53 +00:00
codingetandClaude 772db900c4 chore: bump tailscale submodule — avoid nil services slice in netmap JSON
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:09:37 +00:00
codingetandClaude ac468bff98 chore: bump tailscale submodule — restart map on setServices
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>
2026-06-16 01:10:34 +00:00
codingetandClaude afe5109343 test(tsconnect): add integration tests for setServices
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>
2026-06-15 22:36:06 +00:00
codingetandClaude c44845eafd chore: update AI disclosure for service advertisement work
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:29:12 +00:00
codingetandClaude dbe1a3bcb2 fix(tsconnect-redux): stabilise service selector references
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>
2026-06-15 22:25:18 +00:00
codingetandClaude cd18cd0000 chore: bump tailscale submodule to webnet/tailscale#9
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>
2026-06-15 22:25:18 +00:00
codingetandClaude 8ebc16fff1 feat(tsconnect): expose service advertisement to JS
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>
2026-06-15 22:24:00 +00:00
codingetandClaude 9820b6645b feat(tsconnect): add getIceServers() to use DERP nodes as WebRTC STUN
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>
2026-06-15 22:08:36 +00:00
codingetandClaude a502735fd3 docs(readme): update AI disclosure for WasmSource, Node.js compat, shutdown signal, test suite
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:44:16 +00:00
codingetandClaude 73a74dcb15 test(tsconnect): add unit and integration test suite
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>
2026-06-15 20:44:04 +00:00
codingetandClaude 9dbce83f9f fix(tsconnect): resolve shutdown() race using go.run() as exit signal
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>
2026-06-15 20:43:46 +00:00
codingetandClaude 29d82feaca fix(tsconnect): node.js compat — safesocket unique addr, listen addr normalisation
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>
2026-06-15 20:43:33 +00:00
codingetandClaude 7be31df1f0 feat(tsconnect): accept WasmSource — multi-environment WASM loading
initIPN now accepts a WasmSource union instead of a plain URL string:

  - string | URL          → fetch() + instantiateStreaming (browser / CDN)
  - ArrayBuffer | View    → instantiate (Node.js fs.readFile, Bun)
  - Response              → instantiateStreaming (Cloudflare Worker bindings)
  - ReadableStream        → Response-wrapped instantiateStreaming

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:43:17 +00:00
codingetandClaude 805df7c6ef docs(readme): update AI disclosure for IPN.shutdown()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 00:23:28 +00:00
codingetandClaude 250b033878 feat(tsconnect): add IPN.shutdown() method
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>
2026-06-13 00:23:03 +00:00
codingetandClaude 7c054523cf chore: apply Prettier formatting; update AI disclosure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 22:23:56 +00:00
codinget b07a236355 fix(example-app): use tsconnect-react and tsconnect-redux instead of the old exports 2026-06-11 22:21:19 +00:00
codingetandClaude a3ef8431f9 chore(submodule): advance tailscale to streaming-taildrop branch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 21:13:23 +00:00
codingetandClaude adb140ddf0 feat(taildrop): stream files via ReadableStream, add FsaFileOps
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>
2026-06-11 21:12:09 +00:00
codingetandClaude 959b0fa558 docs(readme): fix OpfsVFS reference — folded into FsaVFS as a factory method
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 02:15:03 +02:00
codingetandClaude 1e582b06ec docs(readme): update package list and overview for monorepo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 02:15:03 +02:00
codingetandClaude c506bd836a docs(readme): update AI disclosure for AGENTS.md changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:44:15 +00:00
codingetandClaude 585ccd52c0 docs(agents): add PR finalisation checklist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:41:48 +00:00
codingetandClaude 2c45a3cf10 docs(agents): use actual model name in Co-Authored-By trailer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:37:01 +00:00
codingetandClaude a759756dae docs(agents): add worktree and commit-trailer guidelines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:34:35 +00:00
codinget 5a6c6e97bc feat(test-app): expose http/router, websocket and drive 2026-06-08 19:18:39 +00:00
codinget 8210516ff7 chore(drive): format drive tests 2026-06-08 18:28:16 +00:00
codingetandClaude 6ed5416fc9 test(drive): improve coverage to 100% functions, 99.6% statements
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>
2026-06-08 16:21:42 +00:00
codingetandClaude 70a32d16f9 test(drive): fix connection pool keepalive keeping event loop alive
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>
2026-06-08 00:55:20 +00:00
codingetandClaude 7c299e793c test(drive): use mock timers for lock expiry test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 00:16:40 +00:00
codingetandClaude 26c311fed7 docs: update AI disclosure for drive level 2 locking
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 21:15:56 +00:00
codingetandClaude 288abcb02d feat(drive): webdav level 2 locking (LOCK/UNLOCK, If: header, lockdiscovery)
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>
2026-06-07 21:14:48 +00:00
codingetandClaude e0a0360ed4 test(drive): add NodeVFS tests including traversal protection, fix semantic bugs
- 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>
2026-06-07 19:15:17 +00:00
codingetandClaude 3921195100 test(drive): comprehensive branch coverage for VFS, server HTTP, and client
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 14:28:29 +00:00
codingetandClaude 3610a1ec99 fix(drive): apply onCollectionGet to HEAD as well as GET
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 03:06:37 +00:00
codingetandClaude c0c2499dec feat(drive): add onCollectionGet hook for custom handling of GET on directories
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 03:03:48 +00:00
codingetandClaude a0d4f6481a refactor(drive): deduplicate stream adapters and fold OpfsVFS into FsaVFS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 02:51:20 +00:00
codingetandClaude 979f3c3ee8 fix(drive): filter readdir by path and respect recursive=false in delete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 02:35:46 +00:00
codingetandClaude 74f3b4c9a4 feat(drive): add @webnet/xml and @webnet/drive packages
@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>
2026-06-07 01:15:07 +00:00
codinget fbd6417f6a chore(tsconnect): add more digits to asset-sizes script because the wasm was too big 2026-06-06 23:21:29 +00:00
codingetandClaude 545afeecfd chore(tsconnect): move asset compression to consumers
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>
2026-06-06 23:02:39 +00:00
codinget 862a52ee0a chore: run scripts on everything we can 2026-06-06 22:35:51 +00:00
codinget 6a9918d722 chore(tsconnect): add npm script for update-ca-bundle 2026-06-06 22:26:35 +00:00
codingetandClaude ff48c40aae chore(turbo): add typecheck and typetest pipeline tasks
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>
2026-06-06 22:19:44 +00:00
codingetandClaude f5569263f4 chore(turbo): filter test pipeline to packages with tests
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>
2026-06-06 22:03:30 +00:00
codingetandClaude c05f20c0bf chore: add test/test:coverage to turbo; remove unit/external test split
- 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>
2026-06-06 21:52:42 +00:00
codinget d9fe6841c2 chore: run prettier on everything else 2026-06-06 21:43:41 +00:00
codingetandClaude 762d19b402 docs(readme): update AI disclosure for monorepo restructuring
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 21:35:37 +00:00
codingetandClaude 817726f666 feat(tsconnect): add IPNDialer implementing RawDialer
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>
2026-06-06 21:00:00 +00:00
codingetandClaude 936adfd233 feat(tsconnect-react): split React hooks and context into @webnet/tsconnect-react
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>
2026-06-06 20:33:13 +00:00
codingetandClaude c07f917217 feat(tsconnect-redux): split Redux state bindings into @webnet/tsconnect-redux
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>
2026-06-06 20:33:04 +00:00