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
12 changes: 12 additions & 0 deletions .changeset/dependabot-update-12935.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"miniflare": patch
"wrangler": patch
---

Update dependencies of "miniflare", "wrangler"

The following dependency versions have been updated:

| Dependency | From | To |
| ---------- | ------------ | ------------ |
| workerd | 1.20260316.1 | 1.20260317.1 |
11 changes: 11 additions & 0 deletions .changeset/migrate-devtools-to-workers-assets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@cloudflare/chrome-devtools-patches": patch
"wrangler": patch
"miniflare": patch
---

Migrate chrome-devtools-patches deployment from Cloudflare Pages to Workers + Assets

The DevTools frontend is now deployed as a Cloudflare Workers + Assets project instead of a Cloudflare Pages project. This uses `wrangler deploy` for production deployments and `wrangler versions upload` for PR preview deployments.

The inspector proxy origin allowlists in both wrangler and miniflare have been updated to accept connections from the new `workers.dev` domain patterns, while retaining the legacy `pages.dev` patterns for backward compatibility.
18 changes: 18 additions & 0 deletions .changeset/tunnel-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"wrangler": minor
---

feat: add `wrangler tunnel` commands for managing Cloudflare Tunnels

Adds a new set of commands for managing remotely-managed Cloudflare Tunnels directly from Wrangler:

- `wrangler tunnel create <name>` - Create a new Cloudflare Tunnel
- `wrangler tunnel list` - List all tunnels in your account
- `wrangler tunnel info <tunnel>` - Display details about a specific tunnel
- `wrangler tunnel delete <tunnel>` - Delete a tunnel (with confirmation)
- `wrangler tunnel run <tunnel>` - Run a tunnel using cloudflared
- `wrangler tunnel quick-start <url>` - Start a temporary tunnel (Try Cloudflare)

The `run` and `quick-start` commands automatically download and manage the cloudflared binary, caching it in `~/.wrangler/cloudflared/`. Users are prompted before downloading and warned if their PATH-installed cloudflared is outdated. You can override the binary location with the `CLOUDFLARED_PATH` environment variable.

All commands are marked as experimental.
7 changes: 4 additions & 3 deletions .github/workflows/deploy-previews.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ jobs:
- name: Deploy Wrangler DevTools preview
if: contains(github.event.*.labels.*.name, 'preview:chrome-devtools-patches')
run: |
output=$(pnpm --filter @cloudflare/chrome-devtools-patches run deploy)
output=$(pnpm --filter @cloudflare/chrome-devtools-patches run deploy:preview)
echo "Command output: $output"
echo "Extracting deployed URL from command output"
url=$(echo "$output" | sed -nE "s/.*Take a peek over at (\S+).*/\1/p")
url=$(echo "$output" | sed -nE "s/.*Version Preview URL: ([^[:space:]]+).*/\1/p")
echo "Extracted URL: $url"
echo "VITE_DEVTOOLS_PREVIEW_URL=$url" >> $GITHUB_ENV
env:
Expand Down Expand Up @@ -84,5 +85,5 @@ jobs:

```
- https://devtools.devprod.cloudflare.dev/js_app?theme=systemPreferred&ws=127.0.0.1%3A9229%2Fws&domain=tester&debugger=true
+ https://8afc7d3d.cloudflare-devtools.pages.dev/js_app?theme=systemPreferred&ws=127.0.0.1%3A9229%2Fws&domain=tester&debugger=true
+ ${{ env.VITE_DEVTOOLS_PREVIEW_URL }}/js_app?theme=systemPreferred&ws=127.0.0.1%3A9229%2Fws&domain=tester&debugger=true
```
5 changes: 4 additions & 1 deletion packages/chrome-devtools-patches/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ devtools-frontend/out/Default/gen/front_end: devtools-frontend
cd devtools-frontend && PATH="$(PATH_WITH_DEPOT)" $(ROOT)/depot/autoninja -C out/Default

publish: cleanup devtools-frontend/out/Default/gen/front_end
npx wrangler pages deploy --project-name cloudflare-devtools devtools-frontend/out/Default/gen/front_end
npx wrangler deploy

publish-preview: cleanup devtools-frontend/out/Default/gen/front_end
npx wrangler versions upload

cleanup:
rm -rf devtools-frontend .gclient* .cipd node_modules depot
Expand Down
12 changes: 8 additions & 4 deletions packages/chrome-devtools-patches/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Workers Devtools Pages Project
# Workers Devtools

This package contains a Workers specific version of Chrome Devtools that is used by the Wrangler dev command and other applications. It is a customized fork of Chrome DevTools specifically tailored for debugging Cloudflare Workers. This package provides Worker-specific functionality through carefully maintained patches on top of Chrome DevTools.

Expand Down Expand Up @@ -76,11 +76,15 @@ When making changes:

## Deployment

This package is deployed as a Cloudflare Workers + Assets project. The static DevTools frontend is served directly from Workers Assets, configured via `wrangler.jsonc`.

Deployments are managed by GitHub Actions:

- deploy-pages-previews.yml:
- deploy-previews.yml:
- Runs on any PR that has the `preview:chrome-devtools-patches` label.
- Deploys a preview, which can then be accessed via [https://<SHA>.cloudflare-devtools.pages.dev/].
- Uploads a preview version (without activating it in production) via `wrangler versions upload`.
- The preview URL is posted as a comment on the PR.
- changesets.yml:
- Runs when a "Version Packages" PR, containing a changeset that touches this package, is merged to `main`.
- Deploys this package to production, which can then be accessed via [https://cloudflare-devtools.pages.dev/].
- Deploys this package to production via `wrangler deploy`.
- Production is accessible via the custom domain [https://devtools.devprod.cloudflare.dev/].
1 change: 1 addition & 0 deletions packages/chrome-devtools-patches/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"author": "workers-devprod@cloudflare.com",
"scripts": {
"deploy": "CLOUDFLARE_ACCOUNT_ID=e35fd947284363a46fd7061634477114 make publish",
"deploy:preview": "CLOUDFLARE_ACCOUNT_ID=e35fd947284363a46fd7061634477114 make publish-preview",
"testenv": "make testenv"
},
"devDependencies": {
Expand Down
10 changes: 10 additions & 0 deletions packages/chrome-devtools-patches/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "cloudflare-devtools",
"compatibility_date": "2025-07-01",
"assets": {
"directory": "devtools-frontend/out/Default/gen/front_end",
},
"preview_urls": true,
"workers_dev": true,
}
2 changes: 1 addition & 1 deletion packages/miniflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@cspotcode/source-map-support": "0.8.1",
"sharp": "^0.34.5",
"undici": "catalog:default",
"workerd": "1.20260316.1",
"workerd": "1.20260317.1",
"ws": "catalog:default",
"youch": "4.1.0-beta.10"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,10 @@ function getWebsocketURL(host: string, port: number): URL {
const ALLOWED_HOST_HOSTNAMES = ["127.0.0.1", "[::1]", "localhost"];
const ALLOWED_ORIGIN_HOSTNAMES = [
"devtools.devprod.cloudflare.dev",
// Workers + Assets (current deployment)
"cloudflare-devtools.devprod.workers.dev",
/^[a-z0-9]+-cloudflare-devtools\.devprod\.workers\.dev$/,
// Cloudflare Pages (legacy deployment)
"cloudflare-devtools.pages.dev",
/^[a-z0-9]+\.cloudflare-devtools\.pages\.dev$/,
"127.0.0.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/workers-utils/src/environment-variables/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ type VariableNames =

/** Custom directory for Wrangler's cache files (overrides `node_modules/.cache/wrangler`). */
| "WRANGLER_CACHE_DIR"
/** Custom path to cloudflared binary (overrides automatic binary management). */
| "CLOUDFLARED_PATH"

// ## Advanced Configuration

Expand Down
10 changes: 10 additions & 0 deletions packages/workers-utils/src/environment-variables/misc-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,3 +392,13 @@ export const getCfFetchPathFromEnv = getEnvironmentVariableFactory({
export const getWranglerCacheDirFromEnv = getEnvironmentVariableFactory({
variableName: "WRANGLER_CACHE_DIR",
});

/**
* `CLOUDFLARED_PATH` specifies a custom path to a cloudflared binary.
*
* If set, Wrangler will use this cloudflared binary instead of downloading one.
* The path must point to an existing executable file.
*/
export const getCloudflaredPathFromEnv = getEnvironmentVariableFactory({
variableName: "CLOUDFLARED_PATH",
});
2 changes: 1 addition & 1 deletion packages/wrangler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"miniflare": "workspace:*",
"path-to-regexp": "6.3.0",
"unenv": "2.0.0-rc.24",
"workerd": "1.20260316.1"
"workerd": "1.20260317.1"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.721.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe("wrangler", () => {
NETWORKING & SECURITY
wrangler cert 🪪 Manage client mTLS certificates and CA certificate chains used for secured connections [open beta]
wrangler mtls-certificate 🪪 Manage certificates used for mTLS connections
wrangler tunnel 🚇 Manage Cloudflare Tunnels [experimental]

GLOBAL FLAGS
-c, --config Path to Wrangler configuration file [string]
Expand Down Expand Up @@ -149,6 +150,7 @@ describe("wrangler", () => {
NETWORKING & SECURITY
wrangler cert 🪪 Manage client mTLS certificates and CA certificate chains used for secured connections [open beta]
wrangler mtls-certificate 🪪 Manage certificates used for mTLS connections
wrangler tunnel 🚇 Manage Cloudflare Tunnels [experimental]

GLOBAL FLAGS
-c, --config Path to Wrangler configuration file [string]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it } from "vitest";
import { redactCloudflaredArgsForLogging } from "../../tunnel/cloudflared";

describe("cloudflared arg redaction", () => {
it("redacts --token and other sensitive values", ({ expect }) => {
const args = ["tunnel", "run", "--token", "SECRET_TOKEN"];

expect(redactCloudflaredArgsForLogging(args)).toEqual([
"tunnel",
"run",
"--token",
"[REDACTED]",
]);
});

it("redacts --token=... style", ({ expect }) => {
expect(redactCloudflaredArgsForLogging(["--token=SECRET"])).toEqual([
"--token=[REDACTED]",
]);
});
});
123 changes: 123 additions & 0 deletions packages/wrangler/src/__tests__/tunnel/tunnel-cloudflared.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import fs from "node:fs";
import path from "node:path";
import { getGlobalWranglerConfigPath } from "@cloudflare/workers-utils";
import { describe, it, vi } from "vitest";
import {
getAssetFilename,
getCloudflaredBinPath,
isVersionOutdated,
} from "../../tunnel/cloudflared";
import { runInTempDir } from "../helpers/run-in-tmp";

describe("cloudflared binary management", () => {
runInTempDir();

describe("getCloudflaredBinPath", () => {
it("should return path in wrangler config directory cache including version", ({
expect,
}) => {
const version = "2026.1.0";
const binPath = getCloudflaredBinPath(version);
const expectedDir = path.join(
getGlobalWranglerConfigPath(),
"cloudflared",
version
);

expect(binPath).toContain(expectedDir);

if (process.platform === "win32") {
expect(binPath.endsWith("cloudflared.exe")).toBe(true);
} else {
expect(binPath.endsWith("cloudflared")).toBe(true);
}
});
});

describe("getAssetFilename", () => {
it("returns .tgz for darwin", ({ expect }) => {
expect(getAssetFilename("darwin", "amd64")).toBe(
"cloudflared-darwin-amd64.tgz"
);
expect(getAssetFilename("darwin", "arm64")).toBe(
"cloudflared-darwin-arm64.tgz"
);
});

it("returns .exe for windows", ({ expect }) => {
expect(getAssetFilename("windows", "amd64")).toBe(
"cloudflared-windows-amd64.exe"
);
});

it("returns bare binary name for linux", ({ expect }) => {
expect(getAssetFilename("linux", "amd64")).toBe(
"cloudflared-linux-amd64"
);
expect(getAssetFilename("linux", "arm64")).toBe(
"cloudflared-linux-arm64"
);
expect(getAssetFilename("linux", "arm")).toBe("cloudflared-linux-arm");
});
});

describe("isVersionOutdated", () => {
it("returns true when installed is older by year", ({ expect }) => {
expect(isVersionOutdated("2024.1.0", "2025.1.0")).toBe(true);
});

it("returns true when installed is older by month", ({ expect }) => {
expect(isVersionOutdated("2025.1.0", "2025.7.0")).toBe(true);
});

it("returns true when installed is older by patch", ({ expect }) => {
expect(isVersionOutdated("2025.7.0", "2025.7.1")).toBe(true);
});

it("returns false when versions are equal", ({ expect }) => {
expect(isVersionOutdated("2025.7.0", "2025.7.0")).toBe(false);
});

it("returns false when installed is newer", ({ expect }) => {
expect(isVersionOutdated("2026.1.0", "2025.12.0")).toBe(false);
});

it("handles double-digit months correctly", ({ expect }) => {
expect(isVersionOutdated("2025.9.0", "2025.12.0")).toBe(true);
expect(isVersionOutdated("2025.12.0", "2025.9.0")).toBe(false);
});
});
});

describe("environment variable override", () => {
runInTempDir();

it("should respect CLOUDFLARED_PATH when set to existing file", async ({
expect,
}) => {
// Create a temporary file to use as the cloudflared path
const tempBin = path.join(process.cwd(), "cloudflared");
fs.writeFileSync(tempBin, "#!/bin/sh\necho test");
fs.chmodSync(tempBin, 0o755);

vi.stubEnv("CLOUDFLARED_PATH", tempBin);

// Import fresh to pick up env change
const { getCloudflaredPath } = await import("../../tunnel/cloudflared");

// This should return the env var path without downloading
const binPath = await getCloudflaredPath();
expect(binPath).toBe(tempBin);
});

it("should throw error when CLOUDFLARED_PATH points to non-existent file", async ({
expect,
}) => {
vi.stubEnv("CLOUDFLARED_PATH", "/nonexistent/path/to/cloudflared");

// Import fresh to pick up env change
const { getCloudflaredPath } = await import("../../tunnel/cloudflared");

await expect(getCloudflaredPath()).rejects.toThrow("CLOUDFLARED_PATH");
});
});
57 changes: 57 additions & 0 deletions packages/wrangler/src/__tests__/tunnel/tunnel-resolve.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, it } from "vitest";
import { resolveTunnelId } from "../../tunnel/client";
import type Cloudflare from "cloudflare";

function asyncIterableFromArray<T>(items: T[]): AsyncIterable<T> {
return {
async *[Symbol.asyncIterator]() {
for (const item of items) {
yield item;
}
},
};
}

describe("resolveTunnelId", () => {
it("returns UUID input without calling API", async ({ expect }) => {
const sdk = {
zeroTrust: {
tunnels: {
cloudflared: {
list() {
throw new Error("should not be called");
},
},
},
},
} as unknown as Cloudflare;

await expect(
resolveTunnelId(sdk, "account", "f70ff985-a4ef-4643-bbbc-4a0ed4fc8415")
).resolves.toBe("f70ff985-a4ef-4643-bbbc-4a0ed4fc8415");
});

it("resolves a unique tunnel name via SDK list", async ({ expect }) => {
const sdk = {
zeroTrust: {
tunnels: {
cloudflared: {
list({ name }: { name?: string }) {
expect(name).toBe("my-tunnel");
return asyncIterableFromArray([
{
id: "11111111-1111-4111-8111-111111111111",
name: "my-tunnel",
},
]);
},
},
},
},
} as unknown as Cloudflare;

await expect(resolveTunnelId(sdk, "account", "my-tunnel")).resolves.toBe(
"11111111-1111-4111-8111-111111111111"
);
});
});
Loading
Loading