Migrate captun to published 0.0.3#1439
Conversation
Replaces the pkg.pr.new/captun@14 pin with the published package and its renamed API (acceptCaptunTunnel -> acceptFetcherCapability etc.). (Changes made by Misha in the working tree; committed as found so the branch can merge latest main.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Unless I am mistaken, this means that the magic string that allows anyone to do ANYTHING in iterate gets logged as a query parameter in cloudflare logs? FWIW in our own capnweb client for itx and streams there are runtime-specific connect functions so that node and workers can send a header, whereas browser relies on If you want a single consistent API with "captun key" concept but don't want secrets to be leaked in logs, you can also do what orpc's websocket transport (and many other such systems) do and shoehorn the secret into the |
Yes. This is another case of my "create as a draft" guidance for clankers being too weak. I just asked to get captun up to date and for whatever reason it decided the change is ready for review. Anyway, yeah the sec-websocket-protocol header sounds like a good idea to me |
|
FWIW i don't think we'll need the captun server library in apps/os at all any more v shortly. This whole egress fetch interception malarky will be covered entirely by the itx system vitest e2e test runner can just do something like this using itx = withItx({projectId})
itx.caps.provide({
name: "egressFetch",
cap: async (req) => {
// intercept egress fetch here
}
})(or something like that) |
Adopts iterate/captun#26 (pinned via pkg.pr.new/captun@26): the admin token now rides in the Sec-WebSocket-Protocol header instead of a ?captun-token= query param, keeping it out of Cloudflare request logs. The project DO echoes the negotiated subprotocol on the 101 response and reads the token via captun's connectTokenFromRequest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 You were right — that was the iterate admin secret going into Cloudflare request logs as a query param. Fixed with your
One thing I deliberately left for a follow-up: we're still using |
|
🤖 Verified against the deployed preview ( |
captun#26 now has no back-compat: gateways ignore ?captun-token= entirely. Pinned by commit (pkg.pr.new/captun@f1778c8) so the lockfile integrity stays stable while the captun PR evolves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…26) `createCaptunTunnel` currently puts the Connect Token in a `?captun-token=` query param, which means it lands in Cloudflare request logs, logpush, and anything else that prints `request.url`. Fine for hosted captun.sh's random per-tunnel ownership tokens; bad for self-hosted deployments whose Gateway Secret may be a high-value credential — flagged by @jonastemplestein on iterate/iterate#1439, where the token was an iterate admin secret. WebSocket clients can't set arbitrary headers (browsers especially), but they can offer subprotocols, so this moves the token into `Sec-WebSocket-Protocol` — the same pattern used by the Kubernetes exec API, graphql-ws, and orpc's WS transport: ```ts // the client now connects with new WebSocket(connectUrl, ["captun", `captun-token.${base64url(token)}`]); // and the gateway echoes the marker subprotocol on the 101 response // (mandatory — browsers/undici abort the handshake if no offered subprotocol is selected) new Response(null, { status: 101, webSocket, headers: { "sec-websocket-protocol": "captun" } }); ``` - Token resolution on the server is shared: subprotocol → `x-captun-connect-token` header. The URL is never a token transport — `?captun-token=` is ignored entirely, and even a correct token sent that way is rejected. - The connect-rejection diagnostic probe sends the token via the header, so wrong-token failures still diagnose as 401 without putting the token back in a URL. - Embedders calling `acceptFetcherCapability` directly pass the request so the 101 echoes correctly, and a new `connectTokenFromRequest(request)` export reads the token wherever it was sent. - base64url encoding because RFC 6455 limits subprotocol values to HTTP token characters — this also frees tokens from the previous URL-safe charset restriction. Compatibility: none, deliberately. New client ↔ old gateway breaks, and old client ↔ new gateway breaks too (`CONNECT_TOKEN_QUERY_PARAM` is deleted; query-param tokens are ignored, so token-protected gateways 401 and hosted anonymous connects 400). Acceptable at 0.0.x — hosted deploys from this repo in lockstep, embedded deployments pin client+worker from one package version. First commit is the task file with the full design; implementation follows on this branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how gateway secrets are transmitted and validated at tunnel admission; new clients require a matching gateway deploy, though legacy query-param clients remain supported on upgraded gateways. > > **Overview** > **Connect Tokens** no longer ride on the connect URL. `createCaptunTunnel` opens the WebSocket with `captun` and `captun-token.<base64url(token)>` subprotocols; rejection diagnostics send the token via `x-captun-connect-token` instead of the query string. > > Gateways and embedders share **`connectTokenFromRequest`** (subprotocol → header → legacy `captun-token` query). **`acceptFetcherCapability({ request })`** echoes the `captun` subprotocol on 101 when offered; **`connectProtocolFromRequest`** supports runtimes like Deno. Self-hosted shards treat non-upgrade connect requests as diagnostic probes so wrong-token failures surface as 401. Hosted ownership tokens accept broader printable-ASCII charsets. Examples and generated browser bundle follow the new handshake; six tests cover subprotocol auth, URL hygiene, back-compat, and parsing. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f396481. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
|
🤖 Closing: the itx grand cleanup (#1485, #1490) deleted the project egress-intercept tunnel this PR spent most of its diff migrating, so it's now conflicting and half-moot. The token-transport discussion above was resolved upstream in iterate/captun#26 (merged — Connect Tokens ride Sec-WebSocket-Protocol, query params rejected outright). What remains for the iterate side is a much smaller change off current main: bump |
apps/os was pinned to
captun@https://pkg.pr.new/captun@14— a frozen pre-merge snapshot of iterate/captun#14 from May 22. That PR merged long ago andcaptun@0.0.3is now on npm, so this migrates to the published API. Future captun features (reconnect-on-drop, WebSocket passthrough for the dev-tunnel plan intasks/switch-dev-tunnels-to-captun.md) can now be diffed against a real release instead of a stale PR build.API migration
The client lost its
headersoption because browser WebSockets can't set headers — auth moved into thetokenoption:In captun 0.0.3 that token travelled as a
?captun-token=query param — which would have put the iterate admin secret into Cloudflare request logs (caught by @jonastemplestein below). iterate/captun#26 fixes the transport upstream: the token now rides in theSec-WebSocket-Protocolheader ascaptun-token.<base64url>, and the gateway echoes thecaptunmarker subprotocol on the 101 response (strict WebSocket clients abort the handshake otherwise). This PR pinspkg.pr.new/captun@26until that merges and 0.0.4 ships.Server side:
acceptCaptunTunnel()→acceptFetcherCapability(), returning{response, fetcher}instead of{response, tunnel}. The project DO passes the upgraderequestin so the 101 echoes the negotiated subprotocol.CAPTUN_SECRETenv key →CAPTUN_TOKEN(captun throws a descriptive error if you pass the old key).connectTokenFromRequest(subprotocol → probe header → legacy query param), falling back to theAuthorizationheader for non-captun callers.ready({url})RPC after accepting a tunnel — captun clients block on it with a 5s timeout. The captun gateway worker does this itself, but our custom DO accept path didn't, so without it every e2e egress-intercept connect would hang and fail.Verification
pnpm test:project-ingress(6/6, includes the 401 unauthenticated-connect case) all pass.Follow-up (own task)
We still use
adminApiSecretas the tunnel-admission token. With the transport fixed, the remaining improvement is scoping: mint a dedicated tunnel secret so a leak grants "can connect tunnels", not "can administer iterate".History note
This change was originally committed onto
stream-tui-iterate-cli(1a0401f) and has been reverted there (22d53e2); this PR is the same work cherry-picked onto main so it can be reviewed and merged independently.🤖 Generated with Claude Code
Note
Medium Risk
Changes admin-gated WebSocket tunnel admission and connect handshake for project egress intercept; regressions could break e2e egress capture or leak/block tunnel connects, though scope is limited to captun integration paths.
Overview
Moves apps/os off the stale
pkg.pr.new/captun@14pin to a newer captun build (f1778c8, toward 0.0.3/0.0.4) and updates every integration to the renamed client/server APIs.Tunnel clients (e2e helpers and the egress-intercept benchmark) now call
createCaptunTunnelwithgateway/name/tokeninstead ofurlplusAuthorizationheaders. OS worker passesCAPTUN_TOKENinto the captun gateway worker (replacingCAPTUN_SECRET).Project egress intercept on the project DO switches from
acceptCaptunTunneltoacceptFetcherCapabilitywith the upgraderequestso the 101 echoes the negotiated WebSocket subprotocol. Auth accepts captun’s connect token viaconnectTokenFromRequest(e.g.Sec-WebSocket-Protocol) as well asAuthorization. After accept, the DO callstunnel.ready({ url })so clients no longer hang on connect.Also adds an oxlint
no-single-use-helpersrule, reorders imports in severalscripts/preview/*tests, and records the migration in a completed task note.Reviewed by Cursor Bugbot for commit 6b377dd. Bugbot is set up for automated code reviews on this repo. Configure here.
Environment Config Lease
Lease:
preview-2Doppler config:
preview_2Type:
environment-config-leaseLeased until: 2026-06-10T17:54:46.236Z
OS
Status: deployed
Commit:
7790b42Preview: https://os.iterate-preview-2.com
Workflow run
Updated: 2026-06-10T16:56:56.660Z