Skip to content
Merged
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
31 changes: 31 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,37 @@
"valid-typeof": "error"
},
"overrides": [
{
"files": ["src/**"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"regex": ".*",
"message": "Root library source may only import capnweb."
}
]
}
]
}
},
{
"files": [
"examples/**",
"oxlint-plugin-captun.js",
"scripts/**",
"src/cli/**",
"src/hosted/**",
"src/server/**",
"src/worker/**",
"test/**"
],
"rules": {
"no-restricted-imports": "off"
}
},
{
"files": ["**/test/**", "**/*.{test,spec}.{js,jsx,ts,tsx,mjs,cjs,mts,cts}"],
"rules": {
Expand Down
4 changes: 2 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ _Avoid_: Agent layer, agent product
- `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.
- `captun` and `gateway` should be **Reserved Tunnel Names** for custom-domain **Self-Hosted Deployments** created by the wizard, because those labels can be the **Gateway** hostname inside the wildcard route.
- The **Hosted Service** owns a broader **Reserved Tunnel Name** list, including likely future **Control Plane** names such as `billing`, `dashboard`, and `tunnel`.
- 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.
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ That's all you need! No local ports, just a fetch function.

## Advanced usage

The captun [worker.ts](./src/worker.ts) implementation has useful opinions about "named tunnels", but you can also take full control of the server implementation (which is what we do in [iterate/iterate](https://github.com/iterate/iterate)). For example, here's a weather application which allows mocking its egress to the weather API:
The captun [worker.ts](./src/server/worker.ts) implementation has useful opinions about "named tunnels", but you can also take full control of the server implementation (which is what we do in [iterate/iterate](https://github.com/iterate/iterate)). For example, here's a weather application which allows mocking its egress to the weather API:

```ts
import { DurableObject } from "cloudflare:workers";
Expand Down Expand Up @@ -124,10 +124,10 @@ export default {
} satisfies ExportedHandler<WeatherReporterEnv>;
```

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 self-hosted Cloudflare Tunnel Gateway, copy or adapt [src/worker.ts](./src/worker.ts) and the Durable Object binding in [wrangler.jsonc](./wrangler.jsonc). The Iterate-operated hosted service is separate: its product surface lives under [src/hosted](./src/hosted), with [wrangler.hosted.jsonc](./wrangler.hosted.jsonc) as its deployment config.
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 self-hosted Cloudflare Tunnel Gateway, copy or adapt [src/server/worker.ts](./src/server/worker.ts) and the Durable Object binding in [wrangler.jsonc](./wrangler.jsonc). The Iterate-operated hosted service is separate: its product surface lives under [src/hosted](./src/hosted), with [wrangler.hosted.jsonc](./wrangler.hosted.jsonc) as its deployment config.

Runtime adapters for accepting Fetcher Capabilities outside Cloudflare Workers live under
`captun/node`, `captun/bun`, and `captun/deno`. See [examples/node](./examples/node),
Runtime Adapters for accepting Fetcher Capabilities outside Cloudflare Workers are implemented under
`src/server` and exported as `captun/node`, `captun/bun`, and `captun/deno`. See [examples/node](./examples/node),
[examples/bun](./examples/bun), and [examples/deno](./examples/deno) for the same small
weather egress test running in each runtime.

Expand All @@ -151,7 +151,7 @@ By default the worker routes `/my-tunnel/foo/bar` to the capnweb session for "my

Running `npx captun deploy` interactively walks you through where the tunnel URLs should live. There are four options, and which one is best for you depends on the kind of apps you want to tunnel to and whether you already have a domain on Cloudflare.

Routing is controlled by a single Worker env var, `CUSTOM_HOSTNAME`. When unset (workers.dev deploys), tunnels use folder routing: the first path segment is the tunnel name. When set (custom-domain deploys), tunnels use subdomain routing — the _last_ DNS label before `CUSTOM_HOSTNAME` is the tunnel name, and anything to the left of it is ignored. The deploy wizard sets `CUSTOM_HOSTNAME` for you; the parsing logic lives in `getTunnelNameFromUrl` in [src/routing.ts](./src/routing.ts).
Routing is controlled by a single Worker env var, `CUSTOM_HOSTNAME`. When unset (workers.dev deploys), tunnels use folder routing: the first path segment is the tunnel name. When set (custom-domain deploys), tunnels use subdomain routing — the _last_ DNS label before `CUSTOM_HOSTNAME` is the tunnel name, and anything to the left of it is ignored. The deploy wizard sets `CUSTOM_HOSTNAME` for you; the parsing logic lives in `getTunnelNameFromUrl` in [src/server/tunnel-addressing.ts](./src/server/tunnel-addressing.ts).

#### 1. `<tunnel>.<account>.workers.dev/<tunnel-name>` (default)

Expand Down
2 changes: 1 addition & 1 deletion examples/deno/deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"imports": {
"captun/deno": "../../src/deno.ts",
"captun/deno": "../../src/server/deno.ts",
"capnweb": "npm:capnweb@0.8.0"
}
}
32 changes: 16 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@
"import": "./src/index.ts"
},
"./bun": {
"types": "./src/bun.ts",
"import": "./src/bun.ts"
"types": "./src/server/bun.ts",
"import": "./src/server/bun.ts"
},
"./deno": {
"types": "./src/deno.ts",
"import": "./src/deno.ts"
"types": "./src/server/deno.ts",
"import": "./src/server/deno.ts"
},
"./node": {
"types": "./src/node.ts",
"import": "./src/node.ts"
"types": "./src/server/node.ts",
"import": "./src/server/node.ts"
},
"./worker": {
"types": "./src/worker.ts",
"import": "./src/worker.ts"
"types": "./src/server/worker.ts",
"import": "./src/server/worker.ts"
}
},
"publishConfig": {
Expand All @@ -49,20 +49,20 @@
"import": "./dist/index.js"
},
"./bun": {
"types": "./dist/bun.d.ts",
"import": "./dist/bun.js"
"types": "./dist/server/bun.d.ts",
"import": "./dist/server/bun.js"
},
"./deno": {
"types": "./dist/deno.d.ts",
"import": "./dist/deno.js"
"types": "./dist/server/deno.d.ts",
"import": "./dist/server/deno.js"
},
"./node": {
"types": "./dist/node.d.ts",
"import": "./dist/node.js"
"types": "./dist/server/node.d.ts",
"import": "./dist/server/node.js"
},
"./worker": {
"types": "./dist/worker.d.ts",
"import": "./dist/worker.js"
"types": "./dist/server/worker.d.ts",
"import": "./dist/server/worker.js"
}
}
},
Expand Down
16 changes: 7 additions & 9 deletions src/cli/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import { createCli, yamlTableConsoleLogger } from "trpc-cli";
import { z } from "zod/v4";
import { color } from "./ansi.js";
import { CliFriendlyError } from "./cli-error.js";
import { CaptunTunnelConnectError, createCaptunTunnel } from "../index.js";
import {
CaptunTunnelConnectError,
createCaptunTunnel,
HOSTED_CAPTUN_GATEWAY,
randomConnectToken,
} from "../index.js";
import { assertLocalTargetAcceptingConnections } from "./local-target.js";
import { withSpinner } from "./spinner.js";
import { HOSTED_CAPTUN_GATEWAY, HOSTED_CAPTUN_HOSTNAME } from "../routing.js";
import { randomConnectToken } from "../token.js";
import {
captunHealthResponse,
confirmTunnelHealth,
Expand Down Expand Up @@ -447,7 +450,7 @@ function resolveTunnel(input: TunnelCliInput, config?: Config): ResolvedTunnel {

const name = input.name || randomName();
const target = normalizeTarget(input.target);
const token = input.token || config?.token || hostedGatewayToken(gateway);
const token = input.token || config?.token || randomConnectToken();

return {
name,
Expand All @@ -458,11 +461,6 @@ function resolveTunnel(input: TunnelCliInput, config?: Config): ResolvedTunnel {
};
}

function hostedGatewayToken(gateway: string) {
if (new URL(gateway).hostname !== HOSTED_CAPTUN_HOSTNAME) return undefined;
return randomConnectToken();
}

function normalizeTarget(target: string) {
const value = target.trim();
if (/^\d+$/.test(value)) return `http://127.0.0.1:${value}`;
Expand Down
2 changes: 1 addition & 1 deletion src/cli/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ export async function deployWorker(
string,
unknown
>;
const worker = resolve(packageRoot, "dist/worker.js");
const worker = resolve(packageRoot, "dist/server/worker.js");
baseConfig.main = worker;
if (input.name) baseConfig.name = input.name;
if (input.accountId) baseConfig.account_id = input.accountId;
Expand Down
Loading
Loading