npm package: @kyndlo/cftunnel
Description: CLI + library to manage Cloudflare Tunnels, ingress routes, and DNS records for AI agents
Latest version: 1.0.0

# cftunnel

A CLI + library to manage Cloudflare Tunnel traffic routing, built for AI agents to programmatically expose local services to the internet.

Built with the official [Cloudflare Node.js SDK](https://github.com/cloudflare/cloudflare-node) and [Commander.js](https://github.com/tj/commander.js).

## Overview

`cftunnel` lets you create Cloudflare Tunnels, configure ingress routes (hostname-to-service mappings), manage DNS records, and run `cloudflared` -- all from a single CLI that outputs JSON for easy parsing by AI agents and scripts. It also exports every operation as a library function for direct use in Node.js/TypeScript code.

### What it does

```
Internet → Cloudflare Edge → Cloudflare Tunnel → cloudflared → localhost:PORT
              (DNS CNAME)      (ingress route)     (connector)   (your app)
```

1. **Tunnel** - A secure outbound-only connection from your machine to Cloudflare's edge
2. **Ingress Route** - Maps a public hostname to a local service (e.g. `app.example.com` → `http://localhost:3000`)
3. **DNS CNAME** - Points your domain at the tunnel (`app.example.com` → `<tunnel-id>.cfargotunnel.com`)
4. **cloudflared** - The connector daemon that maintains the tunnel connection

## Installation

### npx (zero-install)

```bash
npx cftunnel tunnel list
```

### Global install

```bash
npm i -g cftunnel
```

### As a library

```bash
npm i cftunnel
```

## Prerequisites

- Node.js >= 18
- A Cloudflare account with a registered domain
- A Cloudflare **API Key** + account email, or an **API Token** with these permissions:
  - Account > Cloudflare Tunnel > Edit
  - Zone > DNS > Edit
- `cloudflared` installed (the `run` command will auto-detect from PATH or use the `cloudflared` npm package)

### Getting your credentials

| Credential | Where to find it |
|---|---|
| **API Key** | Cloudflare Dashboard → My Profile → API Tokens → Global API Key |
| **API Token** | Cloudflare Dashboard → My Profile → API Tokens → Create Token |
| **Account ID** | Cloudflare Dashboard → any domain → Overview → right sidebar |
| **Zone ID** | Cloudflare Dashboard → your domain → Overview → right sidebar |
| **Email** | The email address on your Cloudflare account |

## Authentication

`cftunnel` supports two auth methods:

### API Token (Bearer auth)

```bash
export CLOUDFLARE_API_TOKEN=your-api-token
# or
npx cftunnel tunnel list --api-token your-api-token
```

### API Key + Email (Global key auth)

```bash
export CLOUDFLARE_API_KEY=your-api-key
export CLOUDFLARE_EMAIL=you@example.com
# or
npx cftunnel tunnel list --api-key your-api-key --api-email you@example.com
```

### Account ID

Required for all tunnel operations:

```bash
export CLOUDFLARE_ACCOUNT_ID=your-account-id
# or
npx cftunnel tunnel list --account-id your-account-id
```

## CLI Commands

### Quickstart (all-in-one)

Create a tunnel, configure routing, set up DNS, and get the run token in a single command:

```bash
npx cftunnel quickstart \
  --name my-app \
  --hostname app.example.com \
  --service http://localhost:8080 \
  --zone-id your-zone-id
```

Output:
```json
{
  "tunnel_id": "uuid",
  "tunnel_name": "my-app",
  "hostname": "app.example.com",
  "service": "http://localhost:8080",
  "dns_record": "record-id",
  "token": "eyJ...",
  "run_cmd": "cloudflared tunnel run --token eyJ...",
  "install_cmd": "sudo cloudflared service install eyJ..."
}
```

### Tunnel Management

```bash
# Create a new tunnel
npx cftunnel tunnel create my-tunnel
npx cftunnel tunnel create my-tunnel --config-src local

# List all tunnels
npx cftunnel tunnel list
npx cftunnel tunnel list --name my-tunnel

# Get tunnel details
npx cftunnel tunnel get <tunnel-id>

# Delete a tunnel
npx cftunnel tunnel delete <tunnel-id>

# Get the cloudflared run token
npx cftunnel tunnel token <tunnel-id>
```

### Ingress Route Management

Ingress routes control how traffic arriving at the tunnel is forwarded to local services.

```bash
# List current routes
npx cftunnel route list <tunnel-id>

# Add a route (appends before the catch-all 404)
npx cftunnel route add <tunnel-id> --hostname app.example.com --service http://localhost:8080
npx cftunnel route add <tunnel-id> --hostname api.example.com --service http://localhost:3000 --path "/v1/*"

# Remove a route by hostname
npx cftunnel route remove <tunnel-id> --hostname app.example.com

# Replace all routes at once
npx cftunnel route set <tunnel-id> \
  --route app.example.com=http://localhost:8080 \
  --route api.example.com=http://localhost:3000
```

Supported service protocols: `http://`, `https://`, `unix://`, `tcp://`, `ssh://`, `rdp://`, `unix+tls://`, `smb://`, `http_status:<code>`

### DNS Management

```bash
# Create a CNAME record pointing to a tunnel
npx cftunnel dns create --zone-id <zone-id> --hostname app.example.com --tunnel-id <tunnel-id>

# List DNS records for a zone
npx cftunnel dns list --zone-id <zone-id>

# Delete a DNS record
npx cftunnel dns delete <record-id> --zone-id <zone-id>
```

### Run cloudflared

```bash
# Run cloudflared in the foreground
npx cftunnel run <tunnel-id>

# Install cloudflared as a system service (persists across reboots)
npx cftunnel run <tunnel-id> --install-service
```

## Library API

Every CLI operation is also available as a library function:

```typescript
import {
  createClient,
  createTunnel,
  listTunnels,
  getTunnel,
  deleteTunnel,
  getTunnelToken,
  listRoutes,
  addRoute,
  removeRoute,
  setRoutes,
  createDNS,
  listDNS,
  deleteDNS,
  quickstart,
} from 'cftunnel';

// Create a client (reads from env vars if no opts provided)
const client = createClient({ apiKey: '...', apiEmail: '...' });

// Create a tunnel
const tunnel = await createTunnel(client, {
  accountId: '...',
  name: 'my-app',
});

// Add a route
await addRoute(client, {
  accountId: '...',
  tunnelId: tunnel.id,
  hostname: 'app.example.com',
  service: 'http://localhost:3000',
});

// Create DNS
await createDNS(client, {
  zoneId: '...',
  hostname: 'app.example.com',
  tunnelId: tunnel.id,
});

// Or do it all at once
const result = await quickstart(client, {
  accountId: '...',
  name: 'my-app',
  hostname: 'app.example.com',
  service: 'http://localhost:3000',
  zoneId: '...',
  onProgress: (msg) => console.error(msg),
});
console.log(result.run_cmd);
```

## End-to-End Examples

### Expose a local web app

```bash
# 1. Start your app
python3 -m http.server 3000 &

# 2. Set credentials
export CLOUDFLARE_API_KEY=your-key
export CLOUDFLARE_EMAIL=you@example.com
export CLOUDFLARE_ACCOUNT_ID=your-account-id

# 3. One-shot setup
npx cftunnel quickstart \
  --name web-app \
  --hostname app.example.com \
  --service http://localhost:3000 \
  --zone-id your-zone-id

# 4. Run the tunnel connector
npx cftunnel run <tunnel-id-from-output>

# Site is now live at https://app.example.com
```

### Add a second service to an existing tunnel

```bash
# Add API backend on a subdomain
npx cftunnel route add <tunnel-id> \
  --hostname api.example.com \
  --service http://localhost:4000

# Create DNS for it
npx cftunnel dns create \
  --zone-id <zone-id> \
  --hostname api.example.com \
  --tunnel-id <tunnel-id>
```

### Multiple services on one tunnel

```bash
npx cftunnel route set <tunnel-id> \
  --route app.example.com=http://localhost:3000 \
  --route api.example.com=http://localhost:4000 \
  --route admin.example.com=http://localhost:5000
```

## Output Format

All commands output JSON to stdout. Status/progress messages go to stderr. This makes it easy to parse output programmatically:

```bash
# Get tunnel ID from create output
TUNNEL_ID=$(npx cftunnel tunnel create my-app | jq -r '.id')

# Get the run token
TOKEN=$(npx cftunnel tunnel token $TUNNEL_ID | jq -r '.token')

# Run it
cloudflared tunnel run --token $TOKEN
```

## Global Flags

| Flag | Env Var | Description |
|---|---|---|
| `--api-token` | `CLOUDFLARE_API_TOKEN` | Cloudflare API token (bearer auth) |
| `--api-key` | `CLOUDFLARE_API_KEY` | Cloudflare API key (requires --api-email) |
| `--api-email` | `CLOUDFLARE_EMAIL` | Cloudflare account email |
| `--account-id` | `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID |

## Architecture

```
cftunnel/
├── src/
│   ├── index.ts              # Library exports (public API)
│   ├── cli.ts                # CLI entry point (commander setup)
│   ├── client.ts             # Cloudflare client factory + auth
│   ├── utils.ts              # printJSON, exitError, parseRoute
│   ├── commands/
│   │   ├── tunnel.ts         # CLI: tunnel create/list/get/delete/token
│   │   ├── route.ts          # CLI: route list/add/remove/set
│   │   ├── dns.ts            # CLI: dns create/list/delete
│   │   ├── run.ts            # CLI: run cloudflared
│   │   └── quickstart.ts     # CLI: all-in-one
│   └── lib/
│       ├── types.ts          # All interfaces
│       ├── tunnel.ts         # Core: tunnel operations
│       ├── route.ts          # Core: ingress route operations + merge helpers
│       ├── dns.ts            # Core: DNS operations
│       └── quickstart.ts     # Core: quickstart orchestration
├── package.json
├── tsconfig.json
└── tsup.config.ts
```

Built on:
- [cloudflare](https://github.com/cloudflare/cloudflare-node) - Official Cloudflare Node.js SDK
- [commander](https://github.com/tj/commander.js) - CLI framework
- [cloudflared](https://www.npmjs.com/package/cloudflared) - Cross-platform cloudflared binary management
- [tsup](https://github.com/egoist/tsup) - TypeScript bundler (dual ESM/CJS)

## License

MIT