Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pkg-pr-new.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ jobs:
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- run: pnpm dlx pkg-pr-new publish --pnpm
- run: pnpm dlx pkg-pr-new publish --pnpm --bin
161 changes: 161 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Captun

Captun exposes HTTP request handlers through public tunnel URLs. Its domain separates the low-level Cap'n Web fetcher capability from the gateway, deployment, hosted product, and future control-plane concerns built on top of it.

## Language

**Fetcher**:
A `fetch(request)` request handler that can produce a `Response`.
_Avoid_: Server, app

**Fetcher Capability**:
A **Fetcher** exposed over Cap'n Web so another runtime can call it.
_Avoid_: Fetch Tunnel, transport, client/server connection

**Fetcher Stub**:
The local Cap'n Web proxy for a remote **Fetcher Capability**.
_Avoid_: Tunnel, client, fetcher

**Tunnel**:
An active **Tunnel Gateway** registration with a public URL, lifecycle, and optional reusable **Connect Token**.
_Avoid_: Fetcher Capability, WebSocket session

**Tunnel Client**:
The process, browser tab, test, or agent environment that exposes a **Fetcher Capability** to a **Tunnel Gateway**.
_Avoid_: Local server, node process

**Tunnel Gateway**:
A public ingress that resolves incoming HTTP requests to active **Tunnels**.
_Avoid_: Server, Worker, hosted server

**Cloudflare Tunnel Gateway**:
A **Tunnel Gateway** implemented with a Cloudflare Worker and Durable Object.
_Avoid_: Server, Worker

**Tunnel Name**:
The public routing key a **Tunnel Gateway** uses to select an active **Tunnel**.
_Avoid_: Subdomain, path segment, slug

**Reserved Tunnel Name**:
A **Tunnel Name** held back for gateway, product, documentation, or future control-plane use.
_Avoid_: Blocked subdomain, reserved subdomain

**Tunnel Addressing**:
The **Tunnel Gateway**'s own scheme for turning a **Tunnel Name** into a public tunnel URL.
_Avoid_: Client routing mode, URL pattern

**Gateway Connect Request**:
The internal WebSocket request a **Tunnel Client** opens to a **Gateway** with Captun query parameters that register a **Tunnel**.
_Avoid_: Magic connect path, server URL

**Gateway Policy**:
Rules a **Tunnel Gateway** applies to **Gateway Connect Requests**, active **Tunnels**, and forwarded public requests.
_Avoid_: Hosted safety, middleware

**Trusted Gateway Policy**:
**Gateway Policy** for cooperative **Self-Hosted Deployments**, usually based on a **Gateway Secret**.
_Avoid_: Self-hosted policy, private policy

**Public Gateway Policy**:
**Gateway Policy** for untrusted public tunnel creation, including reserved names, anonymous ownership, and rate limits.
_Avoid_: Hosted policy, safety policy

**Tunnel Admission**:
The **Tunnel Gateway** policy decision that accepts, rejects, or diagnoses a **Gateway Connect Request** before it becomes an active **Tunnel**.
_Avoid_: Hosted admission, auth check

**Connect Token**:
A credential carried on a **Gateway Connect Request** and interpreted by **Tunnel Admission**.
_Avoid_: Secret, owner token

**Gateway Secret**:
A **Connect Token** that authorizes use of a whole **Self-Hosted Deployment**.
_Avoid_: Captun secret, auth secret

**Ownership Token**:
A **Connect Token** that preserves claim over one active anonymous **Tunnel Name**.
_Avoid_: User token, auth token

**Gateway**:
The public API option naming a **Tunnel Gateway** by URL.
_Avoid_: serverUrl, gatewayUrl

**Runtime Adapter**:
An integration that accepts **Fetcher Capabilities** in a specific WebSocket runtime without necessarily providing a full **Tunnel Gateway**.
_Avoid_: Gateway, server

**Self-Hosted Deployment**:
A user-controlled **Tunnel Gateway** deployment.
_Avoid_: Private hosted service, user server

**Hosted Service**:
The Iterate-operated `captun.sh` **Tunnel Gateway** for public, untrusted tunnel creation.
_Avoid_: Default server, public Worker

**Hosted Site**:
The `www.captun.sh` documentation and browser-demo surface for the **Hosted Service**.
_Avoid_: Landing page, marketing site

**Control Plane**:
The future account, authentication, billing, reservation, and policy system for the **Hosted Service**.
_Avoid_: Dashboard, app, auth layer

**Agent Preview Use Case**:
A use case where an agent creates a public URL through the **Hosted Service** so a human can inspect work quickly.
_Avoid_: Agent layer, agent product

## Relationships

- A **Tunnel Client** exposes a **Fetcher Capability**.
- A **Tunnel Gateway** receives a **Fetcher Stub** for the **Tunnel Client**'s **Fetcher Capability**.
- A **Tunnel** is backed by one active **Fetcher Stub**.
- A **Tunnel Gateway** stores active **Tunnels**; the **Fetcher Stub** is the backing capability, not the gateway's domain object.
- Low-level accepting APIs should be named `acceptFetcherCapability` and `acceptFetcherCapabilityFromSocket`.
- `createCaptunTunnel` is the high-level public API for creating a **Tunnel**.
- `connectFetcherCapability` is the preferred internal name for the client-side primitive that opens a **Gateway Connect Request** and exposes the **Fetcher Capability**.
- Do not export `connectFetcherCapability` until there is a concrete non-gateway use case.
- A **Tunnel Gateway** maps each active **Tunnel Name** to at most one **Tunnel**.
- The current concrete **Tunnel Gateway** implementation is the **Cloudflare Tunnel Gateway**.
- A **Tunnel Gateway** owns **Tunnel Addressing**; **Tunnel Clients** should not need to know whether public tunnel URLs use paths or subdomains.
- A **Tunnel Client** builds a **Gateway Connect Request** from the user-supplied **Gateway** URL, the optional **Tunnel Name**, and Captun-owned query parameters.
- **Tunnel Admission** is **Gateway Policy** for **Gateway Connect Requests**.
- **Self-Hosted Deployments** created by the wizard use **Trusted Gateway Policy** by default.
- The **Hosted Service** uses **Public Gateway Policy**.
- **Gateway Policy** must be configured explicitly and not inferred from **Tunnel Addressing** or hostnames such as `captun.sh`.
- Renaming the current `CUSTOM_HOSTNAME` addressing env var is deferred. The priority is separating **Gateway Policy** from **Tunnel Addressing** first.
- Public Captun APIs should call the user-supplied **Tunnel Gateway** URL `gateway`.
- Public Captun APIs should call the user-supplied **Connect Token** `token`. **Tunnel Admission** decides whether that token is a **Gateway Secret**, **Ownership Token**, or future **Control Plane** credential.
- `createCaptunTunnel` should return a reusable `token` when the **Tunnel Gateway** provides or accepts one.
- New code should not support `/__captun-connect`; connect intent belongs in Captun query parameters on the **Gateway Connect Request**.
- Default custom-domain **Self-Hosted Deployments** should choose a **Gateway** hostname inside the wildcard tunnel route and make that hostname a **Reserved Tunnel Name**.
- `captun`, `gateway`, and `tunnel` should be **Reserved Tunnel Names** by default, along with a small set of likely future **Control Plane** names.
- **Reserved Tunnel Names** apply to the **Hosted Service** and to wizard-generated **Self-Hosted Deployments**. Manual/custom deployments may change the list.
- A **Self-Hosted Deployment** runs a **Tunnel Gateway** in a user's own infrastructure.
- The current deploy wizard creates a **Cloudflare Tunnel Gateway**, but future runtime gateways could also be **Self-Hosted Deployments**.
- The **Hosted Service** is a public **Tunnel Gateway** operated for untrusted users.
- The **Hosted Service** is currently the **Cloudflare Tunnel Gateway** running with public hosted policy and the `captun.sh` product surface.
- The **Hosted Site** is part of the **Hosted Service** product surface, but it is not tunnel routing or **Tunnel Admission**.
- **Hosted Site** code should not live in **Cloudflare Tunnel Gateway** core. A real browser package can wait until the demo surface needs it.
- The **Control Plane** governs future **Hosted Service** accounts, reservations, billing, and policy.
- The **Agent Preview Use Case** uses the **Hosted Service** and may later use the **Control Plane**.
- The **Agent Preview Use Case** should not shape the current gateway/core split until **Control Plane** support exists.
- A **Runtime Adapter** can be used to build a **Tunnel Gateway**, but accepting a WebSocket and producing a **Fetcher Stub** is not the same as managing named **Tunnels**.

## Example Dialogue

> **Dev:** "Does the Node adapter make Node a Tunnel Gateway?"
> **Domain expert:** "No. The adapter accepts a Fetcher Capability in Node and produces a Fetcher Stub, but a Tunnel Gateway also needs named public routing and active tunnel management."

> **Dev:** "When `npx captun 3000` has no local config, is that still self-hosted?"
> **Domain expert:** "No. That uses the Hosted Service. Self-Hosted Deployment starts after `npx captun deploy` writes a user-controlled gateway URL and token."

## Flagged Ambiguities

- "Server" has meant the **Tunnel Gateway**, the **Fetcher**, and the **Hosted Service**. Use the precise term.
- "Hosted" has meant both **Self-Hosted Deployment** and the public **Hosted Service**. Use **Self-Hosted Deployment** for user-owned infrastructure and **Hosted Service** for `captun.sh`.
- Node, Bun, and Deno support should be described as **Runtime Adapters** unless they also provide named public routing and active tunnel management.
- "Fetch Tunnel" sounded natural but conflicts with Cap'n Web vocabulary. Use **Fetcher Capability** and **Fetcher Stub** for the low-level Cap'n Web layer, and **Tunnel** for the gateway/product registration.
- "serverUrl" currently means a client-side tunnel URL construction pattern. The preferred model is a **Gateway** URL plus gateway-owned **Tunnel Addressing**.
- "secret" and "ownerToken" are implementation-specific kinds of **Connect Token**. Public APIs should prefer `token`.
- `/__captun-connect` encoded connect intent as a path. This is rejected for the pre-user API because it makes the public gateway URL model harder to understand.
- Apex gateway URLs such as `https://example.com` are not the default deploy-wizard path. Users who want them can compose the exported pieces and Cloudflare routes themselves.
65 changes: 33 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
# Captun (cap[tainweb] tun[nel])
# Captun (cap[nweb] tun[nel])

Captun is a tiny reference implementation of a self-hosted ngrok or Cloudflare Tunnel alternative. It runs the public side on Cloudflare Workers and sends matching HTTP requests back to a Node process over [Cap'n Web](https://github.com/cloudflare/capnweb).

## Quick start

First deploy a captun worker to your cloudflare account. You can think of this like your own personal ngrok server, but [faster](#performance):

`deploy` expects Cloudflare auth to already be available. Run `npx wrangler login` once, or set `CLOUDFLARE_API_TOKEN` for CI and other non-interactive shells.
Expose a local HTTP server with the hosted `captun.sh` tunnel service:

```bash
npx captun deploy
npx captun 3000
```

Then tunnel to it:
That prints a public URL like `https://abc123.captun.sh` and forwards requests to `localhost:3000`.

If you want your own tunnel server, deploy a captun Worker to your Cloudflare account. You can think of this like your own personal ngrok server, but [faster](#performance):

`deploy` expects Cloudflare auth to already be available. Run `npx wrangler login` once, or set `CLOUDFLARE_API_TOKEN` for CI and other non-interactive shells.

```bash
npx captun 3000
npx captun deploy
```

<!-- # https://captun.my-account.workers.dev/funny-banana-wall
Expand All @@ -26,7 +28,7 @@ npx captun 3000
# mydomain.com and www.mydomain.com and something.mydomain.com
# then you _could_ say *.mydomain.com goes to tunnels - and that could include <tunnel-name>__tunnels.mydomain.com -->

The deploy command will use `wrangler` under the hood to deploy an opinionated captun-tunneler-worker to your cloudflare account, and will store the server url in an XDG config file, and uses it when you tunnel to it.
The deploy command uses `wrangler` under the hood to deploy an opinionated captun Tunnel Gateway to your Cloudflare account, then stores its gateway URL and token in an XDG config file for later tunnel commands.

<!-- - With captun you can make your own faster ngrok for free in 10 seconds
- This is how you use it on the cli
Expand All @@ -39,15 +41,12 @@ The deploy command will use `wrangler` under the hood to deploy an opinionated c

### Programmatic usage

You can use the worker you just deployed to create a tunnel from code for receiving HTTP requests. First `npm install captun` to add it as a dependency. Then create it:
You can use the hosted service from code for receiving HTTP requests. First `npm install captun` to add it as a dependency. Then create it:

```ts
import { createCaptunTunnel } from "captun";

const url = "https://captun.account.workers.dev/my-cool-tunnel";

const tunnel = await createCaptunTunnel({
url: `${url}/__captun-connect`, // creates a tunnel named "my-tunnel". choose any slug-safe string here
fetch: async (request) => {
const url = new URL(request.url);
if (url.pathname.endsWith("/webhook")) {
Expand All @@ -59,7 +58,7 @@ const tunnel = await createCaptunTunnel({
},
});

console.log(`Listening to webhooks on ${url}/webhook`);
console.log(`Listening to webhooks on ${tunnel.url}/webhook`);

await new Promise(() => {}); // stay alive until killed
```
Expand All @@ -72,14 +71,14 @@ The captun [worker.ts](./src/worker.ts) implementation has useful opinions about

```ts
import { DurableObject } from "cloudflare:workers";
import { acceptCaptunTunnel } from "captun";
import { acceptFetcherCapability, type FetcherStub } from "captun";

type WeatherReporterEnv = Env & {
WEATHER_REPORTER_EGRESS: DurableObjectNamespace<WeatherReporterEgressTunnel>;
};

export class WeatherReporterEgressTunnel extends DurableObject<WeatherReporterEnv> {
private egressTunnel: ReturnType<typeof acceptCaptunTunnel>["tunnel"] | undefined;
private egressFetcher: FetcherStub | undefined;

async fetch(request: Request) {
const url = new URL(request.url);
Expand All @@ -96,22 +95,23 @@ export class WeatherReporterEgressTunnel extends DurableObject<WeatherReporterEn

if (url.pathname === "/__intercept-egress-traffic") {
// Here we set up our worker to allow clients/tests to intercept egress traffic
this.egressTunnel?.[Symbol.dispose]();
const { response, tunnel } = acceptCaptunTunnel({
this.egressFetcher?.[Symbol.dispose]();
const { response, fetcher } = acceptFetcherCapability({
onDisconnect: () => {
if (this.egressTunnel === tunnel) this.egressTunnel = undefined;
if (this.egressFetcher === fetcher) this.egressFetcher = undefined;
},
});
this.egressTunnel = tunnel;
this.egressFetcher = fetcher;
queueMicrotask(() => void fetcher.ready({ url: new URL(request.url).origin }));
return response;
}

return new Response("Not found\n", { status: 404 });
}

get egressFetch(): typeof fetch {
if (this.egressTunnel) {
return async (input, init) => this.egressTunnel!.fetch(new Request(input, init));
if (this.egressFetcher) {
return async (input, init) => this.egressFetcher!.fetch(new Request(input, init));
}
return fetch;
}
Expand All @@ -124,14 +124,14 @@ export default {
} satisfies ExportedHandler<WeatherReporterEnv>;
```

The core client/server pieces (`createCaptunTunnel`, `acceptCaptunTunnel`, and the `Fetcher` type) live in [src/index.ts](./src/index.ts) — small TypeScript wrappers around [Cap'n Web](https://github.com/cloudflare/capnweb). For a deployable Cloudflare Worker, also copy or adapt [src/worker.ts](./src/worker.ts) and the Durable Object binding in [wrangler.jsonc](./wrangler.jsonc).
The core client/server pieces (`createCaptunTunnel`, `acceptFetcherCapability`, `acceptFetcherCapabilityFromSocket`, `Fetcher`, and `FetcherStub`) live in [src/index.ts](./src/index.ts) — small TypeScript wrappers around [Cap'n Web](https://github.com/cloudflare/capnweb). For a deployable Cloudflare Tunnel Gateway, also copy or adapt [src/worker.ts](./src/worker.ts) and the Durable Object binding in [wrangler.jsonc](./wrangler.jsonc).

## Advanced CLI Usage

The CLI is mostly focused on ngrok-style use-cases with our opinionated worker deployment. Once you have run `npx captun deploy`, further commands will pick up the server URL and connection secret from your machine's captun config. You can also pass them explicitly (for example, to create a tunnel using a deployment created from someone else's machine):
The CLI is mostly focused on ngrok-style use-cases. Without local config it uses the hosted `captun.sh` service. Once you have run `npx captun deploy`, further commands will pick up your self-hosted gateway URL and token from your machine's captun config. You can also pass them explicitly (for example, to create a tunnel using a deployment created from someone else's machine):

```bash
npx captun 3000 --server-url 'https://abc123.captun.youraccount.workers.dev' --secret abc123
npx captun 3000 --gateway 'https://captun.youraccount.workers.dev' --token abc123
```

By default, the `npx captun 3000` command will generate a name for the tunnel it creates. You can customise this with `--name`:
Expand Down Expand Up @@ -198,7 +198,7 @@ By default, all tunnel names live in one warm `CaptunServerShard` Durable Object
npx captun deploy --shards 256
```

All of captun's public API (both the client `createCaptunTunnel` and the server-side `acceptCaptunTunnel`) is exported from the single `captun` entry point. `acceptCaptunTunnelFromSocket(socket)` is also exported for Workers that have already performed the WebSocket upgrade themselves.
All of captun's public API (both the client `createCaptunTunnel` and the server-side `acceptFetcherCapability`) is exported from the single `captun` entry point. `acceptFetcherCapabilityFromSocket(socket)` is also exported for Workers that have already performed the WebSocket upgrade themselves.

## Performance

Expand Down Expand Up @@ -235,14 +235,15 @@ All you need is `fetch()`. Requests, responses, headers, bodies, streams, SSE, a
```mermaid
sequenceDiagram
participant HTTP as HTTP client
participant Server as Cloudflare Worker / CaptunServerShard
participant Gateway as Tunnel Gateway / CaptunServerShard
participant Client as Node client

Client->>Server: WebSocket RPC connect to /demo/__captun-connect with fetcher as main capability
HTTP->>Server: GET /demo/report
Server->>Client: fetch(request)
Client-->>Server: Response
Server-->>HTTP: Response
Client->>Gateway: WebSocket RPC connect to ?captun-connect=1&captun-name=demo with fetcher as main capability
Gateway-->>Client: ready({ url })
HTTP->>Gateway: GET /demo/report
Gateway->>Client: fetch(request)
Client-->>Gateway: Response
Gateway-->>HTTP: Response
```

See [examples/weather-reporter](./examples/weather-reporter) for a small workspace package that imports `captun` and has its own e2e tests.
Expand All @@ -257,7 +258,7 @@ pnpm run build
pnpm run dev
```

Run tests with `pnpm test`. The root e2e suite uses Miniflare by default; set `CAPTUN_SERVER_URL`, with optional `CAPTUN_SECRET`, to run the same cases against a deployed Worker.
Run tests with `pnpm test`. The root e2e suite uses Miniflare by default; set `CAPTUN_GATEWAY`, with optional `CAPTUN_TOKEN`, to run the same cases against a deployed Worker.

End-to-end smoke tests for build, dry-run deploy, local `wrangler dev`, tunnel, and `curl` live in [`scripts/smoke/`](./scripts/smoke) with documentation in [docs/smoke-test.md](./docs/smoke-test.md):

Expand Down
Loading
Loading