openclaw-cloudflare 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,28 +1,37 @@
1
1
  # openclaw-cloudflare
2
2
 
3
- Cloudflare integration plugin for [OpenClaw](https://github.com/openclaw/openclaw). Provides Cloudflare Tunnel and Access support, with room for future Cloudflare features (Workers, R2, KV, etc.).
3
+ Cloudflare Access JWT verification plugin for [OpenClaw](https://github.com/openclaw/openclaw). Verifies `Cf-Access-Jwt-Assertion` headers and sets identity headers for authenticated requests.
4
4
 
5
- ## Installation
5
+ Assumes `cloudflared` is already running externally (Docker sidecar, systemd, Cloudflare's own connector, etc.).
6
+
7
+ ## Setup Guide
8
+
9
+ ### Step 1 — Install the plugin
6
10
 
7
11
  ```bash
8
12
  openclaw plugins install openclaw-cloudflare
9
13
  ```
10
14
 
11
- ## Configuration
15
+ ### Step 2 — Set up Cloudflare Access
16
+
17
+ 1. In the [Cloudflare Zero Trust dashboard](https://one.dash.cloudflare.com/) → **Access > Applications** → **Add an application**
18
+ 2. Choose **Self-hosted**
19
+ 3. Set the **Application domain** to the hostname pointing at your OpenClaw gateway (e.g. `openclaw.example.com`)
20
+ 4. Configure the identity providers and policies (who is allowed to access)
21
+ 5. Note your **Team domain** — visible at **Settings > Custom Pages** or in the URL: `https://<team>.cloudflareaccess.com`
12
22
 
13
- Add to your `openclaw.json`:
23
+ ### Step 3 — Configure the plugin
24
+
25
+ Add to your `~/.openclaw/openclaw.json`:
14
26
 
15
27
  ```json
16
28
  {
17
29
  "plugins": {
18
30
  "entries": {
19
- "cloudflare": {
31
+ "openclaw-cloudflare": {
20
32
  "config": {
21
- "tunnel": {
22
- "mode": "managed",
23
- "tunnelToken": "your-tunnel-token",
24
- "teamDomain": "myteam",
25
- "audience": "optional-aud-tag"
33
+ "access": {
34
+ "teamDomain": "myteam"
26
35
  }
27
36
  }
28
37
  }
@@ -31,113 +40,126 @@ Add to your `openclaw.json`:
31
40
  }
32
41
  ```
33
42
 
34
- ## Modes
35
43
 
36
- ### `off` (default)
44
+ ### Step 4 — Start OpenClaw
37
45
 
38
- Cloudflare integration is disabled.
46
+ ```bash
47
+ openclaw gateway --force
48
+ ```
39
49
 
40
- ### `managed`
50
+ The plugin will verify Cloudflare Access JWTs on every incoming request and set `x-openclaw-user-email` for authenticated users.
41
51
 
42
- OpenClaw spawns and manages a `cloudflared` tunnel process automatically.
52
+ ---
43
53
 
44
- **Requirements:**
45
- - A pre-configured tunnel token from the Cloudflare Zero Trust dashboard
54
+ ## Running cloudflared on your VM
46
55
 
47
- > **Auto-install:** If `cloudflared` is not found in PATH or known locations, the plugin automatically downloads the latest release from GitHub to `~/.openclaw/bin/cloudflared`. No manual installation required.
56
+ This plugin only handles JWT verification — you need `cloudflared` running on the VM to route traffic through Cloudflare. Here's how to set it up as a persistent system service so it starts automatically.
48
57
 
49
- **Setup:**
58
+ ### 1 — Create a tunnel in the Cloudflare dashboard
50
59
 
51
- 1. In the [Cloudflare Zero Trust dashboard](https://one.dash.cloudflare.com/), create a tunnel under **Networks > Tunnels**
52
- 2. Add a public hostname pointing to your OpenClaw gateway (e.g., `openclaw.example.com` `http://localhost:3000`)
53
- 3. Create an Access Application under **Access > Applications** for the hostname
54
- 4. Copy the tunnel token and configure it:
60
+ 1. Go to [Cloudflare Zero Trust](https://one.dash.cloudflare.com/) **Networks > Tunnels** → **Create a tunnel**
61
+ 2. Choose **Cloudflared**, name it (e.g. `my-openclaw`), click **Save tunnel**
62
+ 3. Under **Public Hostnames**, add a hostname pointing to your OpenClaw gateway:
63
+ - Subdomain + domain: e.g. `openclaw.example.com`
64
+ - Service: `HTTP` → `localhost:18789` (OpenClaw's default port)
65
+ 4. Copy the **tunnel token** shown on the connector install page
55
66
 
56
- ```json
57
- {
58
- "plugins": {
59
- "entries": {
60
- "cloudflare": {
61
- "config": {
62
- "tunnel": {
63
- "mode": "managed",
64
- "tunnelToken": "eyJhIjoiYWNj...",
65
- "teamDomain": "myteam"
66
- }
67
- }
68
- }
69
- }
70
- }
71
- }
67
+ ### 2 — Install cloudflared
68
+
69
+ **Debian/Ubuntu:**
70
+ ```bash
71
+ curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
72
+ sudo dpkg -i cloudflared.deb
73
+ ```
74
+
75
+ **RHEL/Fedora:**
76
+ ```bash
77
+ curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-x86_64.rpm -o cloudflared.rpm
78
+ sudo rpm -i cloudflared.rpm
72
79
  ```
73
80
 
74
- Or via environment variable:
81
+ **ARM64:**
82
+ ```bash
83
+ curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64 -o cloudflared
84
+ sudo install -m 755 cloudflared /usr/local/bin/cloudflared
85
+ ```
86
+
87
+ **macOS:**
88
+ ```bash
89
+ brew install cloudflare/cloudflare/cloudflared
90
+ ```
91
+
92
+ ### 3 — Install as a system service
93
+
94
+ **Linux:**
95
+ ```bash
96
+ sudo cloudflared service install <your-tunnel-token>
97
+ sudo systemctl enable cloudflared
98
+ sudo systemctl start cloudflared
99
+ ```
100
+
101
+ Verify it's running:
102
+ ```bash
103
+ sudo systemctl status cloudflared
104
+ ```
75
105
 
106
+ **macOS:**
76
107
  ```bash
77
- export OPENCLAW_CLOUDFLARE_TUNNEL_TOKEN="eyJhIjoiYWNj..."
108
+ sudo cloudflared service install <your-tunnel-token>
78
109
  ```
79
110
 
80
- ### `access-only`
111
+ This registers a launchd plist that starts cloudflared automatically on boot.
81
112
 
82
- Use when `cloudflared` is managed externally (e.g., Docker sidecar, systemd service). The plugin only handles Cloudflare Access JWT verification.
113
+ Once the tunnel is active, all traffic arriving at your public hostname passes through Cloudflare Access and this plugin verifies the resulting JWTs on each request.
114
+
115
+ ---
116
+
117
+ ## OpenClaw gateway configuration
118
+
119
+ With `cloudflared` running on the same machine, the gateway only needs to be reachable on loopback. Configure `~/.openclaw/openclaw.json` to bind locally, trust the local proxy, and delegate authentication to the identity headers this plugin sets:
83
120
 
84
121
  ```json
85
122
  {
86
- "plugins": {
87
- "entries": {
88
- "cloudflare": {
89
- "config": {
90
- "tunnel": {
91
- "mode": "access-only",
92
- "teamDomain": "myteam",
93
- "audience": "aud-tag-from-access-app"
94
- }
95
- }
123
+ "gateway": {
124
+ "bind": "loopback",
125
+ "trustedProxies": ["127.0.0.1"],
126
+ "auth": {
127
+ "mode": "trusted-proxy",
128
+ "trustedProxy": {
129
+ "userHeader": "x-openclaw-user-email"
96
130
  }
97
131
  }
98
132
  }
99
133
  }
100
134
  ```
101
135
 
102
- **Docker Compose example** (external cloudflared):
103
-
104
- ```yaml
105
- services:
106
- openclaw:
107
- image: openclaw:latest
108
- # ...
136
+ | Field | Value | Why |
137
+ |-------|-------|-----|
138
+ | `gateway.bind` | `"loopback"` | cloudflared connects to OpenClaw locally — no need to expose to LAN |
139
+ | `gateway.trustedProxies` | `["127.0.0.1"]` | Only trust identity headers from cloudflared running on the same host |
140
+ | `gateway.auth.mode` | `"trusted-proxy"` | Delegate authentication to this plugin instead of using a password/token |
141
+ | `gateway.auth.trustedProxy.userHeader` | `"x-openclaw-user-email"` | This plugin sets this header after verifying the Cloudflare Access JWT |
109
142
 
110
- cloudflared:
111
- image: cloudflare/cloudflared:latest
112
- command: tunnel run
113
- environment:
114
- TUNNEL_TOKEN: "eyJhIjoiYWNj..."
115
- ```
143
+ ---
116
144
 
117
- ## Authentication
145
+ ## How it works
118
146
 
119
147
  When a request arrives with a `Cf-Access-Jwt-Assertion` header, the plugin:
120
148
 
121
149
  1. Verifies the JWT signature against Cloudflare's JWKS endpoint (`https://<teamDomain>.cloudflareaccess.com/cdn-cgi/access/certs`)
122
- 2. Validates issuer, expiry, and audience (if configured)
123
- 3. Sets `x-openclaw-user-email` and `x-openclaw-auth-source` headers for downstream auth
150
+ 2. Validates issuer, expiry, and audience (if `audience` is configured)
151
+ 3. Sets `x-openclaw-user-email` and `x-openclaw-auth-source: cloudflare-access` headers for downstream use
124
152
 
125
- Supported algorithms: RS256, ES256 (via Node.js WebCrypto).
153
+ Identity headers are always stripped from incoming requests before verification to prevent spoofing.
126
154
 
127
- JWKS keys are cached for 10 minutes with automatic refresh on key rotation.
155
+ Supported algorithms: RS256, ES256 (via Node.js WebCrypto, no external deps). JWKS keys are cached for 10 minutes with automatic refresh on key rotation.
128
156
 
129
- ## Configuration Reference
157
+ ---
130
158
 
131
- | Key | Type | Default | Description |
132
- |-----|------|---------|-------------|
133
- | `tunnel.mode` | `"off" \| "managed" \| "access-only"` | `"off"` | Operation mode |
134
- | `tunnel.tunnelToken` | `string` | — | Tunnel token (managed mode) |
135
- | `tunnel.teamDomain` | `string` | — | Team domain for `<team>.cloudflareaccess.com` |
136
- | `tunnel.audience` | `string` | — | Optional AUD tag for stricter JWT validation |
159
+ ## Configuration Reference
137
160
 
138
- ## Environment Variables
161
+ | Key | Type | Description |
162
+ |-----|------|-------------|
163
+ | `access.teamDomain` | `string` | Team domain for `<team>.cloudflareaccess.com` (required to enable) |
164
+ | `access.audience` | `string` | Optional AUD tag for stricter JWT validation |
139
165
 
140
- | Variable | Description |
141
- |----------|-------------|
142
- | `OPENCLAW_CLOUDFLARE_TUNNEL_TOKEN` | Tunnel token (alternative to config) |
143
- | `OPENCLAW_TEST_CLOUDFLARED_BINARY` | Override cloudflared binary path (testing) |
@@ -1,19 +1,13 @@
1
1
  {
2
- "id": "cloudflare",
2
+ "id": "openclaw-cloudflare",
3
3
  "configSchema": {
4
4
  "type": "object",
5
5
  "additionalProperties": false,
6
6
  "properties": {
7
- "tunnel": {
7
+ "access": {
8
8
  "type": "object",
9
9
  "additionalProperties": false,
10
10
  "properties": {
11
- "mode": {
12
- "type": "string",
13
- "enum": ["off", "managed", "access-only"],
14
- "default": "off"
15
- },
16
- "tunnelToken": { "type": "string" },
17
11
  "teamDomain": { "type": "string" },
18
12
  "audience": { "type": "string" }
19
13
  }
@@ -21,17 +15,12 @@
21
15
  }
22
16
  },
23
17
  "uiHints": {
24
- "tunnel.tunnelToken": {
25
- "label": "Tunnel Token",
26
- "sensitive": true,
27
- "help": "Token from Cloudflare Zero Trust dashboard (managed mode)"
28
- },
29
- "tunnel.teamDomain": {
18
+ "access.teamDomain": {
30
19
  "label": "Team Domain",
31
20
  "placeholder": "myteam",
32
21
  "help": "Team domain for myteam.cloudflareaccess.com"
33
22
  },
34
- "tunnel.audience": {
23
+ "access.audience": {
35
24
  "label": "Application Audience (AUD)",
36
25
  "help": "Optional AUD tag for stricter JWT validation",
37
26
  "advanced": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-cloudflare",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Cloudflare integration plugin for OpenClaw (Tunnel, Access, and more)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,6 +44,11 @@
44
44
  "type": "git",
45
45
  "url": "git+https://github.com/G4brym/openclaw-cloudflare.git"
46
46
  },
47
+ "files": [
48
+ "src",
49
+ "!src/**/*.test.ts",
50
+ "openclaw.plugin.json"
51
+ ],
47
52
  "publishConfig": {
48
53
  "provenance": true,
49
54
  "access": "public"
package/src/index.ts CHANGED
@@ -1,107 +1,53 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import { createCloudflareAccessVerifier, type CloudflareAccessVerifier } from "./tunnel/access.js";
3
- import { startGatewayCloudflareExposure } from "./tunnel/exposure.js";
4
-
5
- type TunnelConfig = {
6
- mode?: "off" | "managed" | "access-only";
7
- tunnelToken?: string;
8
- teamDomain?: string;
9
- audience?: string;
10
- };
2
+ import { createCloudflareAccessVerifier } from "./tunnel/access.js";
11
3
 
12
4
  type PluginConfig = {
13
- tunnel?: TunnelConfig;
5
+ access?: {
6
+ teamDomain?: string;
7
+ audience?: string;
8
+ };
14
9
  };
15
10
 
16
11
  export default {
17
- id: "cloudflare",
12
+ id: "openclaw-cloudflare",
18
13
  name: "Cloudflare",
19
14
 
20
15
  register(api: {
21
16
  pluginConfig?: PluginConfig;
22
17
  logger: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
23
- registerService(service: { id: string; start: () => Promise<void>; stop: () => Promise<void> }): void;
24
18
  registerHttpHandler(handler: (req: IncomingMessage, res: ServerResponse) => Promise<boolean> | boolean): void;
25
19
  }) {
26
- const config = api.pluginConfig?.tunnel;
27
- const mode = config?.mode ?? "off";
28
- if (mode === "off") return;
29
-
30
- const tunnelToken =
31
- config?.tunnelToken ?? process.env.OPENCLAW_CLOUDFLARE_TUNNEL_TOKEN;
20
+ const config = api.pluginConfig?.access;
32
21
  const teamDomain = config?.teamDomain;
33
22
 
34
- // Validate config
35
- if (mode === "managed" && !tunnelToken) {
36
- api.logger.error(
37
- "[cloudflare] managed mode requires tunnelToken config or OPENCLAW_CLOUDFLARE_TUNNEL_TOKEN env var",
38
- );
23
+ if (!teamDomain) {
24
+ api.logger.warn("[cloudflare] no teamDomain configured — plugin disabled");
39
25
  return;
40
26
  }
41
- if (teamDomain === undefined) {
42
- api.logger.warn(
43
- "[cloudflare] no teamDomain configured — JWT verification will be skipped",
44
- );
45
- }
46
27
 
47
- let verifier: CloudflareAccessVerifier | null = null;
48
- let stopTunnel: (() => Promise<void>) | null = null;
49
-
50
- // Register background service for tunnel lifecycle
51
- api.registerService({
52
- id: "cloudflare-tunnel",
53
- async start() {
54
- // Create JWT verifier if teamDomain is set
55
- if (teamDomain) {
56
- verifier = createCloudflareAccessVerifier({
57
- teamDomain,
58
- audience: config?.audience,
59
- });
60
- api.logger.info(
61
- `[cloudflare] Access JWT verifier active for ${teamDomain}.cloudflareaccess.com`,
62
- );
63
- }
64
-
65
- // Start tunnel exposure (managed mode)
66
- stopTunnel = await startGatewayCloudflareExposure({
67
- cloudflareMode: mode,
68
- tunnelToken,
69
- logCloudflare: {
70
- info: (msg) => api.logger.info(`[cloudflare] ${msg}`),
71
- warn: (msg) => api.logger.warn(`[cloudflare] ${msg}`),
72
- error: (msg) => api.logger.error(`[cloudflare] ${msg}`),
73
- },
74
- });
75
- },
76
- async stop() {
77
- if (stopTunnel) {
78
- await stopTunnel();
79
- stopTunnel = null;
80
- }
81
- verifier = null;
82
- },
28
+ const verifier = createCloudflareAccessVerifier({
29
+ teamDomain,
30
+ audience: config.audience,
83
31
  });
84
32
 
85
- // Register HTTP handler for JWT auth
33
+ api.logger.info(`[cloudflare] Access JWT verifier active for ${teamDomain}.cloudflareaccess.com`);
34
+
86
35
  api.registerHttpHandler(async (req: IncomingMessage, _res: ServerResponse) => {
87
36
  // Always strip identity headers to prevent spoofing from untrusted clients
88
37
  delete req.headers["x-openclaw-user-email"];
89
38
  delete req.headers["x-openclaw-auth-source"];
90
39
 
91
- if (!verifier) return false;
92
-
93
40
  const jwtHeader = req.headers["cf-access-jwt-assertion"];
94
41
  const token = Array.isArray(jwtHeader) ? jwtHeader[0] : jwtHeader;
95
42
  if (!token) return false;
96
43
 
97
44
  const user = await verifier.verify(token);
98
45
  if (user) {
99
- // Set identity headers for gateway auth flow
100
46
  req.headers["x-openclaw-user-email"] = user.email;
101
47
  req.headers["x-openclaw-auth-source"] = "cloudflare-access";
102
48
  }
103
49
 
104
- return false; // don't consume the request
50
+ return false;
105
51
  });
106
52
  },
107
53
  };
@@ -1,8 +0,0 @@
1
- # Changesets
2
-
3
- Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4
- with multi-package repos, or single-package repos to help you version and publish your code. You can
5
- find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6
-
7
- We have a quick list of common questions to get you started engaging with this project in
8
- [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
@@ -1,11 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
3
- "changelog": ["@changesets/changelog-github", { "repo": "G4brym/openclaw-cloudflare" }],
4
- "commit": false,
5
- "fixed": [],
6
- "linked": [],
7
- "access": "public",
8
- "baseBranch": "main",
9
- "updateInternalDependencies": "patch",
10
- "ignore": []
11
- }
@@ -1,25 +0,0 @@
1
- name: Changeset Check
2
-
3
- on:
4
- pull_request:
5
- branches: [main]
6
-
7
- jobs:
8
- check:
9
- name: Check for changeset
10
- runs-on: ubuntu-latest
11
- # Skip on the "Version Packages" PR itself — it has no changeset by design
12
- if: github.head_ref != 'changeset-release/main'
13
- steps:
14
- - uses: actions/checkout@v4
15
- with:
16
- fetch-depth: 0
17
-
18
- - uses: actions/setup-node@v4
19
- with:
20
- node-version: 22
21
- cache: npm
22
-
23
- - run: npm ci
24
-
25
- - run: npx changeset status --since=origin/main
@@ -1,25 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- test:
11
- name: Test & Typecheck
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v4
15
-
16
- - uses: actions/setup-node@v4
17
- with:
18
- node-version: 22
19
- cache: npm
20
-
21
- - run: npm ci
22
-
23
- - run: npm run typecheck
24
-
25
- - run: npm test
@@ -1,39 +0,0 @@
1
- name: Release
2
-
3
- on:
4
- push:
5
- branches: [main]
6
-
7
- concurrency: ${{ github.workflow }}-${{ github.ref }}
8
-
9
- jobs:
10
- release:
11
- name: Release
12
- runs-on: ubuntu-latest
13
- permissions:
14
- contents: write
15
- pull-requests: write
16
- id-token: write
17
- steps:
18
- - uses: actions/checkout@v4
19
- with:
20
- fetch-depth: 0
21
-
22
- - uses: actions/setup-node@v4
23
- with:
24
- node-version: 24
25
- cache: npm
26
- registry-url: https://registry.npmjs.org
27
-
28
- - run: npm ci
29
-
30
- - name: Create Release PR or Publish to npm
31
- uses: changesets/action@v1
32
- with:
33
- publish: npm run release
34
- version: npm run version
35
- commit: "chore: version packages"
36
- title: "chore: version packages"
37
- env:
38
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39
- NPM_CONFIG_PROVENANCE: true
package/CHANGELOG.md DELETED
@@ -1,17 +0,0 @@
1
- # openclaw-cloudflare
2
-
3
- ## 0.2.1
4
-
5
- ### Patch Changes
6
-
7
- - [#4](https://github.com/G4brym/openclaw-cloudflare/pull/4) [`a363c11`](https://github.com/G4brym/openclaw-cloudflare/commit/a363c119aa808284762306a529e0572496735684) Thanks [@G4brym](https://github.com/G4brym)! - Add MIT LICENSE file
8
-
9
- ## 0.2.0
10
-
11
- ### Minor Changes
12
-
13
- - [#1](https://github.com/G4brym/openclaw-cloudflare/pull/1) [`cea0045`](https://github.com/G4brym/openclaw-cloudflare/commit/cea00459c619b0f75b51bc21d31f4f428c970cd3) Thanks [@G4brym](https://github.com/G4brym)! - Auto-install cloudflared binary when not found in managed mode
14
-
15
- ### Patch Changes
16
-
17
- - [#2](https://github.com/G4brym/openclaw-cloudflare/pull/2) [`f395045`](https://github.com/G4brym/openclaw-cloudflare/commit/f395045742bb31c55c0d4d953f8c79f5fb7ac478) Thanks [@G4brym](https://github.com/G4brym)! - Update GitHub repository references to match renamed repo
package/CLAUDE.md DELETED
@@ -1,81 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- OpenClaw plugin that integrates Cloudflare Tunnel and Cloudflare Access. It spawns/manages a `cloudflared` process for tunnel mode and verifies Cloudflare Access JWTs to identify users. Published to npm as `openclaw-cloudflare`.
8
-
9
- ## Commands
10
-
11
- - **Run all tests:** `npm test` (runs `vitest run`)
12
- - **Run a single test file:** `npx vitest run src/tunnel/access.test.ts`
13
- - **Run tests matching a name:** `npx vitest run -t "pattern"`
14
- - **Typecheck:** `npm run typecheck` (runs `tsc --noEmit`)
15
-
16
- ## Architecture
17
-
18
- ES module TypeScript project (`"type": "module"`) with no build/bundle step for development — the entry point is `./src/index.ts` directly.
19
-
20
- ### Module Layers
21
-
22
- ```
23
- src/index.ts Plugin interface — exports {id, name, register()}
24
- register() receives OpenClaw API (logger, registerService, registerHttpHandler)
25
- Validates config, wires service + HTTP handler
26
-
27
- src/tunnel/exposure.ts Orchestration — routes to correct mode (off / managed / access-only)
28
- Returns a stop function or null
29
-
30
- src/tunnel/cloudflared.ts Process management — finds/installs binary, spawns `cloudflared tunnel run`
31
- Binary auto-install downloads from GitHub to ~/.openclaw/bin/
32
- Passes token via TUNNEL_TOKEN env var (not CLI args)
33
- Waits for connector registration on stderr, with timeout
34
-
35
- src/tunnel/access.ts JWT verification — JWKS fetching with 10min cache, RS256/ES256
36
- Uses Node.js WebCrypto (no external crypto deps)
37
- Returns {email} or null on failure (never throws)
38
- ```
39
-
40
- ### Plugin Registration Flow
41
-
42
- 1. `register(api)` validates config and exits early if mode is `"off"`
43
- 2. Registers a **service** (`cloudflare-tunnel`) that on `start()`:
44
- - Creates a JWT verifier if `teamDomain` is configured
45
- - Calls `startGatewayCloudflareExposure()` which spawns cloudflared in managed mode
46
- 3. Registers an **HTTP handler** that on every request:
47
- - Strips `x-openclaw-user-email` and `x-openclaw-auth-source` headers (anti-spoofing)
48
- - If verifier exists, reads `Cf-Access-Jwt-Assertion` header, verifies JWT, sets identity headers
49
-
50
- ### Key Design Patterns
51
-
52
- - **Dependency injection everywhere** — logger, exec functions, fetch are all injected, making every module fully testable with mocks
53
- - **Graceful degradation** — functions return null instead of throwing; errors are logged and the plugin continues
54
- - **Anti-spoofing** — identity headers are always stripped before verification, then re-set only after successful JWT validation
55
-
56
- ## Configuration
57
-
58
- Three modes configured via `tunnel.mode`:
59
- - `"off"` (default) — plugin does nothing
60
- - `"managed"` — spawns cloudflared, requires `tunnelToken` (config or `OPENCLAW_CLOUDFLARE_TUNNEL_TOKEN` env var)
61
- - `"access-only"` — JWT verification only, expects external cloudflared
62
-
63
- Config schema is defined in `openclaw.plugin.json`.
64
-
65
- ## Workflow
66
-
67
- The `main` branch is protected. All code changes must go through a pull request — never commit directly to main.
68
-
69
- ## Versioning and Releases
70
-
71
- Uses [changesets](https://github.com/changesets/changesets). PRs to main require a changeset file (enforced by CI). Merging a changeset to main triggers automated npm publish via GitHub Actions with OIDC provenance.
72
-
73
- **Every PR to main must include a changeset.** Add one with `npx changeset` or manually create a file in `.changeset/` with this format:
74
-
75
- ```md
76
- ---
77
- "openclaw-cloudflare": patch # or minor/major
78
- ---
79
-
80
- Description of the change
81
- ```