diff --git a/.changeset/dependabot-update-12935.md b/.changeset/dependabot-update-12935.md new file mode 100644 index 000000000000..201562443b9f --- /dev/null +++ b/.changeset/dependabot-update-12935.md @@ -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 | diff --git a/.changeset/migrate-devtools-to-workers-assets.md b/.changeset/migrate-devtools-to-workers-assets.md new file mode 100644 index 000000000000..c91d4c68d74c --- /dev/null +++ b/.changeset/migrate-devtools-to-workers-assets.md @@ -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. diff --git a/.changeset/tunnel-commands.md b/.changeset/tunnel-commands.md new file mode 100644 index 000000000000..f4e7f9baf210 --- /dev/null +++ b/.changeset/tunnel-commands.md @@ -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 ` - Create a new Cloudflare Tunnel +- `wrangler tunnel list` - List all tunnels in your account +- `wrangler tunnel info ` - Display details about a specific tunnel +- `wrangler tunnel delete ` - Delete a tunnel (with confirmation) +- `wrangler tunnel run ` - Run a tunnel using cloudflared +- `wrangler tunnel quick-start ` - 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. diff --git a/.github/workflows/deploy-previews.yml b/.github/workflows/deploy-previews.yml index 27812207a20f..0100d5f8abd9 100644 --- a/.github/workflows/deploy-previews.yml +++ b/.github/workflows/deploy-previews.yml @@ -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: @@ -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 ``` diff --git a/packages/chrome-devtools-patches/Makefile b/packages/chrome-devtools-patches/Makefile index d54fbc2a3b09..95d3741940c2 100644 --- a/packages/chrome-devtools-patches/Makefile +++ b/packages/chrome-devtools-patches/Makefile @@ -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 diff --git a/packages/chrome-devtools-patches/README.md b/packages/chrome-devtools-patches/README.md index e20e4e8ed264..6f12095d9850 100644 --- a/packages/chrome-devtools-patches/README.md +++ b/packages/chrome-devtools-patches/README.md @@ -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. @@ -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://.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/]. diff --git a/packages/chrome-devtools-patches/package.json b/packages/chrome-devtools-patches/package.json index 098e8432c895..192bf18cd891 100644 --- a/packages/chrome-devtools-patches/package.json +++ b/packages/chrome-devtools-patches/package.json @@ -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": { diff --git a/packages/chrome-devtools-patches/wrangler.jsonc b/packages/chrome-devtools-patches/wrangler.jsonc new file mode 100644 index 000000000000..68f024d3dced --- /dev/null +++ b/packages/chrome-devtools-patches/wrangler.jsonc @@ -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, +} diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index 184ed1c27b62..7a90ecfa740c 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -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" }, diff --git a/packages/miniflare/src/plugins/core/inspector-proxy/inspector-proxy-controller.ts b/packages/miniflare/src/plugins/core/inspector-proxy/inspector-proxy-controller.ts index 0483ed7481cd..04b8605f1aed 100644 --- a/packages/miniflare/src/plugins/core/inspector-proxy/inspector-proxy-controller.ts +++ b/packages/miniflare/src/plugins/core/inspector-proxy/inspector-proxy-controller.ts @@ -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", diff --git a/packages/workers-utils/src/environment-variables/factory.ts b/packages/workers-utils/src/environment-variables/factory.ts index eaec3de0a083..782e0f163904 100644 --- a/packages/workers-utils/src/environment-variables/factory.ts +++ b/packages/workers-utils/src/environment-variables/factory.ts @@ -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 diff --git a/packages/workers-utils/src/environment-variables/misc-variables.ts b/packages/workers-utils/src/environment-variables/misc-variables.ts index e584d04243cb..2f1840de9aa4 100644 --- a/packages/workers-utils/src/environment-variables/misc-variables.ts +++ b/packages/workers-utils/src/environment-variables/misc-variables.ts @@ -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", +}); diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 5d4fc2da9a03..adf627865754 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -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", diff --git a/packages/wrangler/src/__tests__/index.test.ts b/packages/wrangler/src/__tests__/index.test.ts index 7d5fd7c23c6c..e936e2ff3768 100644 --- a/packages/wrangler/src/__tests__/index.test.ts +++ b/packages/wrangler/src/__tests__/index.test.ts @@ -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] @@ -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] diff --git a/packages/wrangler/src/__tests__/tunnel/tunnel-cloudflared-redaction.test.ts b/packages/wrangler/src/__tests__/tunnel/tunnel-cloudflared-redaction.test.ts new file mode 100644 index 000000000000..8f805e4b1305 --- /dev/null +++ b/packages/wrangler/src/__tests__/tunnel/tunnel-cloudflared-redaction.test.ts @@ -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]", + ]); + }); +}); diff --git a/packages/wrangler/src/__tests__/tunnel/tunnel-cloudflared.test.ts b/packages/wrangler/src/__tests__/tunnel/tunnel-cloudflared.test.ts new file mode 100644 index 000000000000..10f1ba9ea7e9 --- /dev/null +++ b/packages/wrangler/src/__tests__/tunnel/tunnel-cloudflared.test.ts @@ -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"); + }); +}); diff --git a/packages/wrangler/src/__tests__/tunnel/tunnel-resolve.test.ts b/packages/wrangler/src/__tests__/tunnel/tunnel-resolve.test.ts new file mode 100644 index 000000000000..b13fbd1ca43c --- /dev/null +++ b/packages/wrangler/src/__tests__/tunnel/tunnel-resolve.test.ts @@ -0,0 +1,57 @@ +import { describe, it } from "vitest"; +import { resolveTunnelId } from "../../tunnel/client"; +import type Cloudflare from "cloudflare"; + +function asyncIterableFromArray(items: T[]): AsyncIterable { + 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" + ); + }); +}); diff --git a/packages/wrangler/src/__tests__/tunnel/tunnel.test.ts b/packages/wrangler/src/__tests__/tunnel/tunnel.test.ts new file mode 100644 index 000000000000..9ab68b939866 --- /dev/null +++ b/packages/wrangler/src/__tests__/tunnel/tunnel.test.ts @@ -0,0 +1,439 @@ +import { EventEmitter } from "node:events"; +import { UserError } from "@cloudflare/workers-utils"; +import { http, HttpResponse } from "msw"; +import { afterEach, beforeEach, describe, it, vi } from "vitest"; +import { endEventLoop } from "../helpers/end-event-loop"; +import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; +import { mockConsoleMethods } from "../helpers/mock-console"; +import { clearDialogs, mockConfirm } from "../helpers/mock-dialogs"; +import { useMockIsTTY } from "../helpers/mock-istty"; +import { createFetchResult, msw } from "../helpers/msw"; +import { runInTempDir } from "../helpers/run-in-tmp"; +import { runWrangler } from "../helpers/run-wrangler"; +import type { CloudflareTunnel } from "../../tunnel/client"; + +// Mock spawnCloudflared so `tunnel run` tests don't need a real binary. +// The mock emits "exit" on next tick so the handler's Promise resolves. +vi.mock("../../tunnel/cloudflared", async () => { + const actual = await vi.importActual("../../tunnel/cloudflared"); + return { + ...actual, + spawnCloudflared: vi.fn(async () => { + const cp = new EventEmitter() as EventEmitter & { + stderr: null; + killed: boolean; + kill: () => boolean; + }; + cp.stderr = null; + cp.killed = false; + cp.kill = () => { + cp.killed = true; + return true; + }; + process.nextTick(() => cp.emit("exit", 0, null)); + return cp; + }), + }; +}); + +// Default tunnel for mocking responses +const defaultTunnel: CloudflareTunnel = { + id: "f70ff985-a4ef-4643-bbbc-4a0ed4fc8415", + name: "my-tunnel", + status: "healthy", + created_at: "2024-01-15T10:30:00Z", + tun_type: "cfd_tunnel", + account_tag: "some-account-id", +}; + +const secondTunnel: CloudflareTunnel = { + id: "550e8400-e29b-41d4-a716-446655440000", + name: "api-tunnel", + status: "inactive", + created_at: "2024-01-10T15:45:00Z", + tun_type: "cfd_tunnel", + account_tag: "some-account-id", +}; + +describe("tunnel help", () => { + const std = mockConsoleMethods(); + runInTempDir(); + + it("should show help text when no arguments are passed", async ({ + expect, + }) => { + await runWrangler("tunnel"); + await endEventLoop(); + + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.out).toMatchInlineSnapshot(` + "wrangler tunnel + + 🚇 Manage Cloudflare Tunnels [experimental] + + COMMANDS + wrangler tunnel create Create a new Cloudflare Tunnel [experimental] + wrangler tunnel delete Delete a Cloudflare Tunnel [experimental] + wrangler tunnel info Display details about a Cloudflare Tunnel [experimental] + wrangler tunnel list List all Cloudflare Tunnels in your account [experimental] + wrangler tunnel run [tunnel] Run a Cloudflare Tunnel using cloudflared [experimental] + wrangler tunnel quick-start Start a free, temporary tunnel without an account (https://try.cloudflare.com) [experimental] + + GLOBAL FLAGS + -c, --config Path to Wrangler configuration file [string] + --cwd Run as if Wrangler was started in the specified directory instead of the current working directory [string] + -e, --env Environment to use for operations, and for selecting .env and .dev.vars files [string] + --env-file Path to an .env file to load - can be specified multiple times - values from earlier files are overridden by values in later files [array] + -h, --help Show help [boolean] + -v, --version Show version number [boolean]" + `); + }); + + it("should show help when an invalid argument is passed", async ({ + expect, + }) => { + await expect(() => runWrangler("tunnel invalid")).rejects.toThrow( + "Unknown argument: invalid" + ); + + expect(std.err).toMatchInlineSnapshot(` + "X [ERROR] Unknown argument: invalid + + " + `); + }); +}); + +describe("tunnel commands", () => { + mockAccountId(); + mockApiToken(); + runInTempDir(); + const { setIsTTY } = useMockIsTTY(); + const std = mockConsoleMethods(); + + beforeEach(() => { + // @ts-expect-error we're using a very simple setTimeout mock here + vi.spyOn(global, "setTimeout").mockImplementation((fn, _period) => { + setImmediate(fn); + }); + setIsTTY(true); + }); + + afterEach(() => { + clearDialogs(); + }); + + describe("tunnel create", () => { + it("should create a tunnel", async ({ expect }) => { + const reqPromise = mockTunnelCreate(); + + await runWrangler("tunnel create my-new-tunnel"); + + const req = await reqPromise; + expect(req.name).toBe("my-new-tunnel"); + expect(req.config_src).toBe("cloudflare"); + + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Creating tunnel "my-new-tunnel" + Created tunnel. + ID: f70ff985-a4ef-4643-bbbc-4a0ed4fc8415 + Name: my-new-tunnel + + To run this tunnel, configure its ingress rules in the Cloudflare dashboard, then run: + wrangler tunnel run f70ff985-a4ef-4643-bbbc-4a0ed4fc8415" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + }); + + it("should require a tunnel name", async ({ expect }) => { + await expect(() => runWrangler("tunnel create")).rejects.toThrow( + "Not enough non-option arguments" + ); + }); + }); + + describe("tunnel list", () => { + it("should list all tunnels", async ({ expect }) => { + mockTunnelList([defaultTunnel, secondTunnel]); + + await runWrangler("tunnel list"); + + expect(std.out).toContain("Listing Cloudflare Tunnels"); + expect(std.out).toContain("f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"); + expect(std.out).toContain("my-tunnel"); + expect(std.out).toContain("550e8400-e29b-41d4-a716-446655440000"); + expect(std.out).toContain("api-tunnel"); + expect(std.err).toMatchInlineSnapshot(`""`); + }); + + it("should show message when no tunnels exist", async ({ expect }) => { + mockTunnelList([]); + + await runWrangler("tunnel list"); + + expect(std.out).toContain("No tunnels found."); + expect(std.err).toMatchInlineSnapshot(`""`); + }); + }); + + describe("tunnel info", () => { + it("should get tunnel details", async ({ expect }) => { + mockTunnelGet(defaultTunnel); + + await runWrangler("tunnel info f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"); + + expect(std.out).toContain("Getting tunnel details"); + expect(std.out).toContain("ID: f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"); + expect(std.out).toContain("Name: my-tunnel"); + expect(std.out).toContain("Status: healthy"); + expect(std.err).toMatchInlineSnapshot(`""`); + }); + + it("should require a tunnel ID", async ({ expect }) => { + await expect(() => runWrangler("tunnel info")).rejects.toThrow( + "Not enough non-option arguments" + ); + }); + + it("should handle non-existent tunnel", async ({ expect }) => { + mockTunnelGetNotFound("f70ff985-a4ef-4643-bbbc-4a0ed4fc0000"); + + await expect( + runWrangler("tunnel info f70ff985-a4ef-4643-bbbc-4a0ed4fc0000") + ).rejects.toThrowError(UserError); + + expect(std.err).toContain("ERROR"); + }); + }); + + describe("tunnel delete", () => { + it("should delete tunnel with confirmation", async ({ expect }) => { + mockConfirm({ + text: 'Are you sure you want to delete tunnel "f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"? This action cannot be undone.', + result: true, + }); + mockTunnelDelete("f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"); + + await runWrangler("tunnel delete f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"); + + expect(std.out).toContain("Deleting tunnel"); + expect(std.out).toContain("Tunnel deleted."); + expect(std.err).toMatchInlineSnapshot(`""`); + }); + + it("should cancel deletion when not confirmed", async ({ expect }) => { + mockConfirm({ + text: 'Are you sure you want to delete tunnel "f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"? This action cannot be undone.', + result: false, + }); + + await runWrangler("tunnel delete f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"); + + expect(std.out).toContain("Deletion cancelled."); + expect(std.err).toMatchInlineSnapshot(`""`); + }); + + it("should skip confirmation with --force", async ({ expect }) => { + mockTunnelDelete("f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"); + + await runWrangler( + "tunnel delete f70ff985-a4ef-4643-bbbc-4a0ed4fc8415 --force" + ); + + expect(std.out).toContain("Deleting tunnel"); + expect(std.out).toContain("Tunnel deleted."); + expect(std.err).toMatchInlineSnapshot(`""`); + }); + + it("should require a tunnel ID", async ({ expect }) => { + await expect(() => runWrangler("tunnel delete")).rejects.toThrow( + "Not enough non-option arguments" + ); + }); + }); + + describe("tunnel quick-start", () => { + it("should spawn cloudflared with correct args for quick tunnel", async ({ + expect, + }) => { + const { spawnCloudflared } = await import("../../tunnel/cloudflared"); + + await runWrangler("tunnel quick-start http://localhost:3000"); + + expect(spawnCloudflared).toHaveBeenCalledTimes(1); + const [calledArgs] = vi.mocked(spawnCloudflared).mock.calls[0] as [ + string[], + ]; + + // Verify quick tunnel args: no auth, uses --url + expect(calledArgs).toContain("tunnel"); + expect(calledArgs).toContain("--url"); + expect(calledArgs).toContain("http://localhost:3000"); + expect(calledArgs).toContain("--no-autoupdate"); + }); + + it("should require a URL argument", async ({ expect }) => { + await expect(() => runWrangler("tunnel quick-start")).rejects.toThrow( + "Not enough non-option arguments" + ); + }); + }); + + describe("tunnel run", () => { + it("should pass token via TUNNEL_TOKEN env var, not CLI args", async ({ + expect, + }) => { + const { spawnCloudflared } = await import("../../tunnel/cloudflared"); + + await runWrangler("tunnel run --token TEST_TOKEN"); + + expect(spawnCloudflared).toHaveBeenCalledTimes(1); + const [calledArgs, calledOpts] = vi.mocked(spawnCloudflared).mock + .calls[0] as [string[], { env?: Record }]; + + // Token must NOT appear in CLI args (would leak via `ps`) + expect(calledArgs).not.toContain("--token"); + expect(calledArgs).not.toContain("TEST_TOKEN"); + + // Token must be passed via env var + expect(calledOpts?.env?.TUNNEL_TOKEN).toBe("TEST_TOKEN"); + }); + + it("should require tunnel or token", async ({ expect }) => { + await expect(runWrangler("tunnel run")).rejects.toThrowError(UserError); + }); + }); +}); + +// Mock helper functions + +function mockTunnelCreate(): Promise<{ + name: string; + config_src: string; +}> { + return new Promise((resolve) => { + msw.use( + http.post( + "*/accounts/:accountId/cfd_tunnel", + async ({ request }) => { + const body = (await request.json()) as { + name: string; + config_src: string; + }; + resolve(body); + + return HttpResponse.json( + createFetchResult({ + ...defaultTunnel, + name: body.name, + }) + ); + }, + { once: true } + ) + ); + }); +} + +function mockTunnelList(tunnels: CloudflareTunnel[]) { + msw.use( + http.get("*/accounts/:accountId/cfd_tunnel", ({ request }) => { + const url = new URL(request.url); + const page = Number(url.searchParams.get("page") || 1); + const perPage = Number(url.searchParams.get("per_page") || 20); + + // Return tunnels on first page, empty on subsequent pages + const result = page === 1 ? tunnels : []; + + return HttpResponse.json( + createFetchResult(result, true, [], [], { + page, + per_page: perPage, + count: result.length, + total_count: tunnels.length, + }) + ); + }) + ); +} + +function mockTunnelGet(tunnel: CloudflareTunnel) { + msw.use( + http.get( + "*/accounts/:accountId/cfd_tunnel/:tunnelId", + () => { + return HttpResponse.json(createFetchResult(tunnel)); + }, + { once: true } + ) + ); +} + +function mockTunnelGetNotFound(tunnelId: string) { + msw.use( + http.get( + `*/accounts/:accountId/cfd_tunnel/${tunnelId}`, + () => { + return HttpResponse.json( + createFetchResult(null, false, [ + { code: 10000, message: "Tunnel not found" }, + ]), + { status: 404 } + ); + }, + { once: true } + ) + ); +} + +function mockTunnelDelete(tunnelId: string) { + msw.use( + http.delete( + `*/accounts/:accountId/cfd_tunnel/${tunnelId}`, + () => { + return HttpResponse.json(createFetchResult(null)); + }, + { once: true } + ) + ); +} + +function mockTunnelPermissionError() { + msw.use( + http.get( + "*/accounts/:accountId/cfd_tunnel", + () => { + return HttpResponse.json( + createFetchResult(null, false, [ + { code: 10000, message: "Authentication error" }, + ]), + { status: 403 } + ); + }, + { once: true } + ) + ); +} + +describe("tunnel permission errors", () => { + mockAccountId(); + mockApiToken(); + runInTempDir(); + const std = mockConsoleMethods(); + + it("should show helpful error message when permission is denied", async ({ + expect, + }) => { + mockTunnelPermissionError(); + + await expect(runWrangler("tunnel list")).rejects.toThrow( + "Cloudflare Tunnel commands require API token authentication with tunnel permissions." + ); + + expect(std.err).toContain("API token authentication"); + expect(std.err).toContain("CLOUDFLARE_API_TOKEN"); + }); +}); diff --git a/packages/wrangler/src/core/teams.d.ts b/packages/wrangler/src/core/teams.d.ts index 11121d2e3027..fe14280a5225 100644 --- a/packages/wrangler/src/core/teams.d.ts +++ b/packages/wrangler/src/core/teams.d.ts @@ -20,4 +20,5 @@ export type Teams = | "Product: Workflows" | "Product: Cloudchamber" | "Product: SSL" - | "Product: WVPC"; + | "Product: WVPC" + | "Product: Tunnels"; diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index 819a02d0f18f..06aec79ee639 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -331,6 +331,13 @@ import { closeSentry, setupSentry } from "./sentry"; import { setupCommand } from "./setup"; import { tailCommand } from "./tail"; import { triggersDeployCommand, triggersNamespace } from "./triggers"; +import { tunnelCreateCommand } from "./tunnel/create"; +import { tunnelDeleteCommand } from "./tunnel/delete"; +import { tunnelNamespace } from "./tunnel/index"; +import { tunnelInfoCommand } from "./tunnel/info"; +import { tunnelListCommand } from "./tunnel/list"; +import { tunnelQuickStartCommand } from "./tunnel/quick-start"; +import { tunnelRunCommand } from "./tunnel/run"; import { typesCommand } from "./type-generation"; import { authNamespace, @@ -1300,6 +1307,30 @@ export function createCLIParser(argv: string[]) { ]); registry.registerNamespace("hyperdrive"); + // tunnel + registry.define([ + { command: "wrangler tunnel", definition: tunnelNamespace }, + { + command: "wrangler tunnel create", + definition: tunnelCreateCommand, + }, + { + command: "wrangler tunnel delete", + definition: tunnelDeleteCommand, + }, + { command: "wrangler tunnel info", definition: tunnelInfoCommand }, + { command: "wrangler tunnel list", definition: tunnelListCommand }, + { + command: "wrangler tunnel run", + definition: tunnelRunCommand, + }, + { + command: "wrangler tunnel quick-start", + definition: tunnelQuickStartCommand, + }, + ]); + registry.registerNamespace("tunnel"); + // cert - includes mtls-certificates and CA cert management registry.define([ { command: "wrangler cert", definition: certNamespace }, diff --git a/packages/wrangler/src/tunnel/client.ts b/packages/wrangler/src/tunnel/client.ts new file mode 100644 index 000000000000..4bb68164ae4d --- /dev/null +++ b/packages/wrangler/src/tunnel/client.ts @@ -0,0 +1,234 @@ +import { UserError } from "@cloudflare/workers-utils"; +import { Cloudflare as CloudflareSDK } from "cloudflare"; +import type Cloudflare from "cloudflare"; +import type { CloudflareTunnel } from "cloudflare/resources/shared"; +import type { CloudflaredCreateResponse } from "cloudflare/resources/zero-trust/tunnels/cloudflared"; + +/** + * Error message for tunnel permission issues when using OAuth login. + * Tunnel commands require specific API token permissions that are not yet + * available via OAuth scopes. + */ +const TUNNEL_PERMISSION_ERROR_MESSAGE = ` +Cloudflare Tunnel commands require API token authentication with tunnel permissions. + +OAuth login (via 'wrangler login') does not currently include tunnel permissions. +To use tunnel commands, please authenticate using an API token: + +1. Create an API token at https://dash.cloudflare.com/profile/api-tokens +2. Create a custom token with: + - Account > Cloudflare Tunnel > Edit +3. Set the token and account ID as environment variables: + export CLOUDFLARE_API_TOKEN= + export CLOUDFLARE_ACCOUNT_ID= + +Then run your tunnel command again. +`.trim(); + +/** + * Check if an error is a tunnel permission/authentication error + */ +function isTunnelPermissionError(error: unknown): boolean { + if (error instanceof CloudflareSDK.APIError) { + // 403 Forbidden or 401 Unauthorized typically indicate permission issues + if (error.status === 403 || error.status === 401) { + return true; + } + } + return false; +} + +/** + * Wrap a tunnel API call with error handling. + * - Permission errors get a helpful message about API token setup. + * - Other Cloudflare API errors are wrapped as UserError to prevent Sentry spam, + * since these are typically user-facing issues (bad input, missing resources, etc.). + */ +async function withTunnelErrorHandling( + operation: () => Promise +): Promise { + try { + return await operation(); + } catch (error) { + if (isTunnelPermissionError(error)) { + throw new UserError(TUNNEL_PERMISSION_ERROR_MESSAGE); + } + if (error instanceof CloudflareSDK.APIError) { + throw new UserError(error.message, { cause: error }); + } + throw error; + } +} + +/** + * Re-export the SDK tunnel type for use by commands. + */ +export type { CloudflareTunnel }; + +/** + * Create a new tunnel + */ +export async function createTunnel( + sdk: Cloudflare, + accountId: string, + name: string +): Promise { + return withTunnelErrorHandling(async () => { + const response = (await sdk.zeroTrust.tunnels.cloudflared.create({ + account_id: accountId, + name, + config_src: "cloudflare", + })) as CloudflaredCreateResponse; + + // Handle both standard tunnel and WARP connector responses + return normalizeTunnelResponse(response); + }); +} + +/** + * Get a specific tunnel + */ +export async function getTunnel( + sdk: Cloudflare, + accountId: string, + tunnelId: string +): Promise { + return withTunnelErrorHandling(async () => { + const response = await sdk.zeroTrust.tunnels.cloudflared.get(tunnelId, { + account_id: accountId, + }); + + return normalizeTunnelResponse(response); + }); +} + +/** + * List all tunnels + */ +export async function listTunnels( + sdk: Cloudflare, + accountId: string +): Promise { + return withTunnelErrorHandling(async () => { + const tunnels: CloudflareTunnel[] = []; + for await (const tunnel of sdk.zeroTrust.tunnels.cloudflared.list({ + account_id: accountId, + })) { + tunnels.push(normalizeTunnelResponse(tunnel)); + } + + return tunnels; + }); +} + +/** + * Delete a tunnel + */ +export async function deleteTunnel( + sdk: Cloudflare, + accountId: string, + tunnelId: string +): Promise { + return withTunnelErrorHandling(async () => { + await sdk.zeroTrust.tunnels.cloudflared.delete(tunnelId, { + account_id: accountId, + }); + }); +} + +/** + * Get tunnel token for running cloudflared + */ +export async function getTunnelToken( + sdk: Cloudflare, + accountId: string, + tunnelId: string +): Promise { + return withTunnelErrorHandling(async () => { + const response = await sdk.zeroTrust.tunnels.cloudflared.token.get( + tunnelId, + { + account_id: accountId, + } + ); + + // The SDK types declare this as string + return String(response); + }); +} + +/** + * Normalize tunnel response from SDK to consistent format. + * The SDK returns a union type (CloudflareTunnel | TunnelWARPConnectorTunnel) + * but both share the same shape — cast to CloudflareTunnel. + */ +function normalizeTunnelResponse(response: unknown): CloudflareTunnel { + return response as CloudflareTunnel; +} + +/** + * Tunnel ID regex pattern. + * Accepts any UUID format (not restricted to v4) since tunnel IDs + * are not guaranteed to be strictly UUID v4. + */ +const TUNNEL_ID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Check if a string looks like a tunnel ID (UUID format). + */ +function isTunnelId(input: string): boolean { + return TUNNEL_ID_REGEX.test(input); +} + +/** + * Resolve a tunnel name or ID to a tunnel ID. + * + * If the input is already a UUID, return it directly. + * Otherwise, look up the tunnel by name via the API. + * + * This matches cloudflared's behavior in findID(). + */ +export async function resolveTunnelId( + sdk: Cloudflare, + accountId: string, + input: string +): Promise { + // If it's already a UUID, return it directly + if (isTunnelId(input)) { + return input; + } + + // Look up tunnel by name using the SDK list filter. + const tunnels = await withTunnelErrorHandling(async () => { + const results: CloudflareTunnel[] = []; + for await (const tunnel of sdk.zeroTrust.tunnels.cloudflared.list({ + account_id: accountId, + name: input, + is_deleted: false, + })) { + results.push(normalizeTunnelResponse(tunnel)); + } + return results; + }); + + if (tunnels.length === 0) { + throw new UserError( + `"${input}" is neither the ID nor the name of any of your tunnels` + ); + } + + if (tunnels.length > 1) { + throw new UserError( + `Found multiple tunnels named "${input}". Please use the tunnel ID instead.` + ); + } + + const tunnelId = tunnels[0].id; + if (!tunnelId) { + throw new UserError( + `Tunnel "${input}" was found but has no ID. This is unexpected — please try again or use the tunnel ID directly.` + ); + } + return tunnelId; +} diff --git a/packages/wrangler/src/tunnel/cloudflared.ts b/packages/wrangler/src/tunnel/cloudflared.ts new file mode 100644 index 000000000000..20171379bc25 --- /dev/null +++ b/packages/wrangler/src/tunnel/cloudflared.ts @@ -0,0 +1,661 @@ +/** + * cloudflared binary management for Wrangler tunnel commands. + * + * This module handles downloading, caching, and running the cloudflared binary. + * It uses the Cloudflare update worker (update.argotunnel.com) to resolve + * the latest version and download URL, matching cloudflared's own update mechanism. + */ + +import { execFileSync, spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + accessSync, + chmodSync, + constants, + existsSync, + mkdirSync, + renameSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import { arch } from "node:os"; +import { dirname, join } from "node:path"; +import { + getCloudflaredPathFromEnv, + getGlobalWranglerConfigPath, + removeDirSync, + UserError, +} from "@cloudflare/workers-utils"; +import { sync as commandExistsSync } from "command-exists"; +import { fetch } from "undici"; +import { confirm } from "../dialogs"; +import { logger } from "../logger"; +import type { ChildProcess } from "node:child_process"; + +/** + * Cloudflare update worker URL. + * This is the same endpoint cloudflared itself uses for self-update. + * It takes os, arch, and version query parameters and returns JSON with + * the download URL, version, checksum, and whether compression is used. + * The worker uses KV for caching. + */ +const UPDATE_SERVICE_URL = "https://update.argotunnel.com"; + +/** + * Response shape from the Cloudflare update worker. + */ +interface VersionResponse { + url: string; + version: string; + checksum: string; + compressed: boolean; + shouldUpdate: boolean; + userMessage: string; + error: string; +} + +function sha256Hex(buffer: Buffer): string { + return createHash("sha256").update(buffer).digest("hex"); +} + +/** + * Map Node.js arch() values to Go runtime.GOARCH values + * used by the update worker. + */ +function getGoArch(): string { + const nodeArch = arch(); + switch (nodeArch) { + case "x64": + return "amd64"; + case "arm64": + return "arm64"; + case "arm": + return "arm"; + default: + throw new UserError( + `Unsupported architecture for cloudflared: ${nodeArch}\n\n` + + `cloudflared supports: x64 (amd64), arm64, arm\n\n` + + `You can manually install cloudflared and set the CLOUDFLARED_PATH environment variable.\n` + + `Download instructions: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/` + ); + } +} + +/** + * Map Node.js process.platform to Go runtime.GOOS values + * used by the update worker. + */ +function getGoOS(): string { + switch (process.platform) { + case "darwin": + return "darwin"; + case "linux": + return "linux"; + case "win32": + return "windows"; + default: + throw new UserError( + `Unsupported platform for cloudflared: ${process.platform}\n\n` + + `cloudflared supports: darwin (macOS), linux, win32 (Windows)\n\n` + + `You can manually install cloudflared and set the CLOUDFLARED_PATH environment variable.\n` + + `Download instructions: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/` + ); + } +} + +/** GitHub release URL pattern for cloudflared binaries. */ +const GITHUB_RELEASE_BASE = + "https://github.com/cloudflare/cloudflared/releases/download"; + +/** + * Build the expected GitHub release asset filename for this platform. + */ +export function getAssetFilename(goOS: string, goArch: string): string { + if (goOS === "windows") { + return `cloudflared-${goOS}-${goArch}.exe`; + } + if (goOS === "darwin") { + return `cloudflared-${goOS}-${goArch}.tgz`; + } + return `cloudflared-${goOS}-${goArch}`; +} + +/** + * Query the update worker for a specific os/arch combination. + * Returns the parsed response, or null if the request failed or the + * worker returned an error (e.g. "no release found"). + */ +async function queryUpdateService( + goOS: string, + goArch: string +): Promise { + const url = new URL(UPDATE_SERVICE_URL); + url.searchParams.set("os", goOS); + url.searchParams.set("arch", goArch); + + logger.debug(`Checking for latest cloudflared: ${url.toString()}`); + + let response: Response; + try { + response = await fetch(url.toString(), { + headers: { "User-Agent": "wrangler" }, + }); + } catch (e) { + logger.debug( + `Failed to reach update service: ${e instanceof Error ? e.message : String(e)}` + ); + return null; + } + + if (!response.ok) { + logger.debug( + `Update service returned ${response.status} for ${goOS}/${goArch}` + ); + return null; + } + + let data: VersionResponse; + try { + data = (await response.json()) as VersionResponse; + } catch (e) { + logger.debug( + `Update service returned non-JSON response: ${e instanceof Error ? e.message : String(e)}` + ); + return null; + } + + // The update worker may return a response with only a version but no download URL + // (e.g. when querying a platform it doesn't have a binary for). In that case we + // still return the data so callers can use the version to construct a fallback URL. + if (data.error || !data.url || !data.version) { + return data.version ? data : null; + } + + return data; +} + +/** + * Query the Cloudflare update worker to get the latest cloudflared version info. + * + * The update worker doesn't have entries for every os/arch combination + * (e.g. darwin/arm64 is missing even though the GitHub release exists). + * When the primary query fails, we fall back to querying a known-working + * combination (linux/amd64) to discover the latest version, then construct + * the GitHub release URL directly. + */ +async function getLatestVersionInfo(): Promise { + const goOS = getGoOS(); + const goArch = getGoArch(); + + // Try the update worker for our exact platform first + const primary = await queryUpdateService(goOS, goArch); + if (primary && primary.url && primary.version) { + return primary; + } + + // Fallback: query a known-working combination to get the latest version, + // then construct the GitHub download URL for our actual platform. + logger.debug( + `Update worker had no result for ${goOS}/${goArch}, falling back to GitHub release URL` + ); + + const fallback = await queryUpdateService("linux", "amd64"); + if (!fallback?.version) { + throw new UserError( + `[cloudflared] Failed to determine the latest cloudflared version.\n\n` + + `The update service did not return results for ${goOS}/${goArch},\n` + + `and the fallback query also failed.\n\n` + + `You can manually install cloudflared from:\n` + + `https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/` + ); + } + + const version = fallback.version; + const filename = getAssetFilename(goOS, goArch); + const url = `${GITHUB_RELEASE_BASE}/${version}/${filename}`; + const compressed = filename.endsWith(".tgz"); + + return { + url, + version, + checksum: "", // no checksum available for fallback URLs + compressed, + shouldUpdate: true, + userMessage: "", + error: "", + }; +} + +/** + * Get the directory where cloudflared binary should be cached. + * Uses the resolved version so the cache is per-version. + */ +function getCacheDir(version: string): string { + return join(getGlobalWranglerConfigPath(), "cloudflared", version); +} + +/** + * Get the expected path for the cloudflared binary within a version cache dir. + */ +export function getCloudflaredBinPath(version: string): string { + const binName = + process.platform === "win32" ? "cloudflared.exe" : "cloudflared"; + return join(getCacheDir(version), binName); +} + +/** + * Check if cloudflared binary exists and is executable at a given path. + */ +function isBinaryExecutable(binPath: string): boolean { + try { + accessSync(binPath, constants.X_OK); + return true; + } catch { + return false; + } +} + +/** + * Validate that a binary works correctly by running --version. + */ +function validateBinary(binPath: string): void { + try { + const output = execFileSync(binPath, ["--version"], { + stdio: ["pipe", "pipe", "pipe"], + timeout: 10000, + encoding: "utf8", + }).trim(); + logger.debug(`cloudflared version: ${output}`); + } catch { + let errorMessage = `[cloudflared] Failed to validate cloudflared binary at ${binPath}\n\n`; + errorMessage += `This usually means:\n`; + errorMessage += ` - The binary is corrupted or incomplete\n`; + errorMessage += ` - You're missing required system libraries\n`; + + if (process.platform === "linux") { + errorMessage += `\nOn Linux, make sure you have the required dependencies:\n`; + errorMessage += ` - glibc (GNU C Library)\n`; + errorMessage += ` - For Debian/Ubuntu: sudo apt-get install libc6\n`; + } + + const cacheDir = join(getGlobalWranglerConfigPath(), "cloudflared"); + errorMessage += `\nYou can try:\n`; + errorMessage += ` 1. Deleting the cache directory: rm -rf ${cacheDir}\n`; + errorMessage += ` 2. Running the command again to re-download\n`; + errorMessage += ` 3. Manually installing cloudflared: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/\n`; + errorMessage += ` 4. Setting CLOUDFLARED_PATH to point to your cloudflared binary`; + + throw new UserError(errorMessage); + } +} + +export function redactCloudflaredArgsForLogging(args: string[]): string[] { + const redacted = [...args]; + for (let i = 0; i < redacted.length; i++) { + const arg = redacted[i]; + if (arg === "--token" && i + 1 < redacted.length) { + redacted[i + 1] = "[REDACTED]"; + } + if (arg.startsWith("--token=")) { + redacted[i] = "--token=[REDACTED]"; + } + } + return redacted; +} + +function tryGetCloudflaredFromPath(): string | null { + if (!commandExistsSync("cloudflared")) { + return null; + } + try { + validateBinary("cloudflared"); + return "cloudflared"; + } catch (e) { + logger.debug("cloudflared found in PATH but failed validation", e); + return null; + } +} + +/** + * Extract the version string from `cloudflared --version` output. + * Output format: "cloudflared version 2025.7.0 (built 2025-07-03-1703 UTC)" + * Returns null if the version cannot be parsed. + */ +function getInstalledVersion(binPath: string): string | null { + try { + const output = execFileSync(binPath, ["--version"], { + stdio: ["pipe", "pipe", "pipe"], + timeout: 10000, + encoding: "utf8", + }); + const match = output.match(/(\d+\.\d+\.\d+)/); + return match ? match[1] : null; + } catch { + return null; + } +} + +/** + * Compare two cloudflared version strings (e.g. "2025.7.0" vs "2026.2.0"). + * Returns true if `installed` is older than `latest`. + */ +export function isVersionOutdated(installed: string, latest: string): boolean { + const parse = (v: string) => v.split(".").map(Number); + const [iYear, iMonth, iPatch] = parse(installed); + const [lYear, lMonth, lPatch] = parse(latest); + + if (iYear !== lYear) { + return iYear < lYear; + } + if (iMonth !== lMonth) { + return iMonth < lMonth; + } + return iPatch < lPatch; +} + +/** + * Check if a PATH-installed cloudflared is outdated compared to the latest + * version from the update worker. Logs a warning if outdated. + * This runs asynchronously and never throws — it's purely advisory. + */ +async function warnIfOutdated(binPath: string): Promise { + try { + const installed = getInstalledVersion(binPath); + if (!installed) { + return; + } + + // Try our platform first, fall back to linux/amd64 (always available) + const latest = + (await queryUpdateService(getGoOS(), getGoArch())) ?? + (await queryUpdateService("linux", "amd64")); + const latestVersion = latest?.version; + if (!latestVersion) { + return; + } + + if (isVersionOutdated(installed, latestVersion)) { + logger.warn( + `Your cloudflared (${installed}) is outdated. Latest version is ${latestVersion}.\n` + + `Update: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/` + ); + } + } catch (e) { + logger.debug("Failed to check cloudflared version", e); + } +} + +function writeFileAtomic(filePath: string, contents: Buffer): void { + const dir = dirname(filePath); + const tmpPath = join( + dir, + `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}` + ); + try { + writeFileSync(tmpPath, contents); + renameSync(tmpPath, filePath); + } finally { + try { + if (existsSync(tmpPath)) { + unlinkSync(tmpPath); + } + } catch { + // Ignore cleanup errors + } + } +} + +/** + * Download cloudflared binary using the version info from the update worker. + */ +async function downloadCloudflared( + versionInfo: VersionResponse, + binPath: string +): Promise { + const { url, version, checksum, compressed } = versionInfo; + + logger.log(`Downloading cloudflared ${version}...`); + logger.debug(`Download URL: ${url}`); + + const cacheDir = dirname(binPath); + mkdirSync(cacheDir, { recursive: true }); + + let response: Response; + try { + response = await fetch(url, { + headers: { "User-Agent": "wrangler" }, + }); + } catch (e) { + throw new UserError( + `[cloudflared] Failed to download cloudflared from ${url}\n\n` + + `Network error: ${e instanceof Error ? e.message : String(e)}\n\n` + + `Please check your internet connection and try again.\n` + + `If you're behind a proxy, make sure it's configured correctly.` + ); + } + + if (!response.ok) { + throw new UserError( + `[cloudflared] Failed to download cloudflared from ${url}\n\n` + + `HTTP ${response.status}: ${response.statusText}\n\n` + + `You can manually download cloudflared from:\n` + + `https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/` + ); + } + + try { + if (compressed) { + await downloadAndExtractTarball(response, checksum, binPath, cacheDir); + } else { + await downloadBinary(response, checksum, binPath); + } + } catch (e) { + // Clean up partial downloads + try { + if (existsSync(binPath)) { + unlinkSync(binPath); + } + } catch { + // Ignore cleanup errors + } + + if (e instanceof UserError) { + throw e; + } + + throw new UserError( + `[cloudflared] Failed to save cloudflared binary\n\n` + + `Error: ${e instanceof Error ? e.message : String(e)}\n\n` + + `Please ensure you have write permissions to: ${cacheDir}` + ); + } + + // Make executable on Unix systems + if (process.platform !== "win32") { + chmodSync(binPath, 0o755); + } + + logger.log(`cloudflared ${version} installed`); + logger.debug(`Binary location: ${binPath}`); +} + +/** + * Download and extract a tarball (for macOS) + */ +async function downloadAndExtractTarball( + response: Response, + expectedChecksum: string, + binPath: string, + cacheDir: string +): Promise { + const tempTarPath = join(cacheDir, "cloudflared.tgz"); + + const buffer = Buffer.from(await response.arrayBuffer()); + if (expectedChecksum) { + const actualSha256 = sha256Hex(buffer); + if (actualSha256 !== expectedChecksum) { + throw new UserError( + `[cloudflared] SHA256 mismatch for downloaded cloudflared tarball.\n\n` + + `Expected: ${expectedChecksum}\n` + + `Actual: ${actualSha256}` + ); + } + } + writeFileSync(tempTarPath, buffer); + + try { + execFileSync("tar", ["-xzf", tempTarPath, "-C", cacheDir], { + stdio: "ignore", + }); + + const extractedPath = join(cacheDir, "cloudflared"); + if (extractedPath !== binPath && existsSync(extractedPath)) { + renameSync(extractedPath, binPath); + } + } finally { + try { + if (existsSync(tempTarPath)) { + unlinkSync(tempTarPath); + } + } catch { + // Ignore cleanup errors + } + } +} + +/** + * Download binary directly (for Linux/Windows) + */ +async function downloadBinary( + response: Response, + expectedChecksum: string, + binPath: string +): Promise { + const buffer = Buffer.from(await response.arrayBuffer()); + if (expectedChecksum) { + const actualSha256 = sha256Hex(buffer); + if (actualSha256 !== expectedChecksum) { + throw new UserError( + `[cloudflared] SHA256 mismatch for downloaded cloudflared binary.\n\n` + + `Expected: ${expectedChecksum}\n` + + `Actual: ${actualSha256}` + ); + } + } + writeFileAtomic(binPath, buffer); +} + +/** + * Get cloudflared binary path, installing if necessary. + * + * Resolution order: + * 1. CLOUDFLARED_PATH environment variable (user override) + * 2. cloudflared in system PATH + * 3. Cached binary in ~/.wrangler/cloudflared/{version}/ + * 4. Download latest from Cloudflare update worker + */ +export async function getCloudflaredPath(): Promise { + // Check for environment variable override first + const envPath = getCloudflaredPathFromEnv(); + if (envPath) { + if (!existsSync(envPath)) { + throw new UserError( + `CLOUDFLARED_PATH is set to "${envPath}" but the file does not exist.\n\n` + + `Please ensure the path points to a valid cloudflared binary.` + ); + } + logger.debug(`Using cloudflared from CLOUDFLARED_PATH: ${envPath}`); + // Skip validation — the user explicitly set the path, so trust it. + // This also avoids issues on platforms where the binary format + // differs (e.g. shell scripts won't pass --version on Windows). + return envPath; + } + + // Next, prefer a user-installed cloudflared in PATH. + const pathBin = tryGetCloudflaredFromPath(); + if (pathBin) { + logger.debug("Using cloudflared from PATH"); + await warnIfOutdated(pathBin); + return pathBin; + } + + // Query the update worker for the latest version + const versionInfo = await getLatestVersionInfo(); + const binPath = getCloudflaredBinPath(versionInfo.version); + + // Check if this version is already cached and valid + let needsDownload = !isBinaryExecutable(binPath); + if (!needsDownload) { + try { + validateBinary(binPath); + logger.debug( + `Using cached cloudflared ${versionInfo.version}: ${binPath}` + ); + return binPath; + } catch (e) { + logger.debug("Cached cloudflared failed validation; re-downloading", e); + needsDownload = true; + } + } + + // Prompt user before downloading + const shouldDownload = await confirm( + `cloudflared (${versionInfo.version}) is needed but not installed. Download to ${binPath}?`, + { defaultValue: true, fallbackValue: true } + ); + if (!shouldDownload) { + throw new UserError( + `cloudflared is required to run this command.\n\n` + + `You can install it manually from:\n` + + `https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/\n\n` + + `Then either add it to your PATH or set CLOUDFLARED_PATH.` + ); + } + + // Remove corrupted cache only after user has confirmed re-download + if (existsSync(binPath)) { + removeCloudflaredCache(versionInfo.version); + } + + // Download cloudflared + await downloadCloudflared(versionInfo, binPath); + + // Validate the downloaded binary + validateBinary(binPath); + + return binPath; +} + +/** + * Spawn cloudflared process with automatic binary management + */ +export async function spawnCloudflared( + args: string[], + options?: { stdio?: "inherit" | "pipe"; env?: Record } +): Promise { + const binPath = await getCloudflaredPath(); + + logger.debug( + `Spawning cloudflared: ${binPath} ${redactCloudflaredArgsForLogging(args).join(" ")}` + ); + + const cloudflared = spawn(binPath, args, { + stdio: options?.stdio ?? "inherit", + env: options?.env ? { ...process.env, ...options.env } : undefined, + }); + + return cloudflared; +} + +/** + * Remove cached cloudflared binary for a specific version, or all versions. + */ +export function removeCloudflaredCache(version?: string): void { + const cacheDir = version + ? getCacheDir(version) + : join(getGlobalWranglerConfigPath(), "cloudflared"); + if (existsSync(cacheDir)) { + removeDirSync(cacheDir); + logger.log(`Removed cloudflared cache: ${cacheDir}`); + } +} diff --git a/packages/wrangler/src/tunnel/create.ts b/packages/wrangler/src/tunnel/create.ts new file mode 100644 index 000000000000..e92d70031b31 --- /dev/null +++ b/packages/wrangler/src/tunnel/create.ts @@ -0,0 +1,34 @@ +import { createCommand } from "../core/create-command"; +import { requireAuth } from "../user"; +import { createTunnel } from "./client"; + +export const tunnelCreateCommand = createCommand({ + metadata: { + description: "Create a new Cloudflare Tunnel", + status: "experimental", + owner: "Product: Tunnels", + }, + args: { + name: { + type: "string", + demandOption: true, + description: "The name of the tunnel", + }, + }, + positionalArgs: ["name"], + async handler(args, { config, sdk, logger }) { + const accountId = await requireAuth(config); + + logger.log(`Creating tunnel "${args.name}"`); + + const tunnel = await createTunnel(sdk, accountId, args.name); + + logger.log(`Created tunnel.`); + logger.log(`ID: ${tunnel.id}`); + logger.log(`Name: ${tunnel.name}`); + logger.log( + `\nTo run this tunnel, configure its ingress rules in the Cloudflare dashboard, then run:` + ); + logger.log(` wrangler tunnel run ${tunnel.id}`); + }, +}); diff --git a/packages/wrangler/src/tunnel/delete.ts b/packages/wrangler/src/tunnel/delete.ts new file mode 100644 index 000000000000..02df11bf430f --- /dev/null +++ b/packages/wrangler/src/tunnel/delete.ts @@ -0,0 +1,44 @@ +import { createCommand } from "../core/create-command"; +import { confirm } from "../dialogs"; +import { requireAuth } from "../user"; +import { deleteTunnel, resolveTunnelId } from "./client"; + +export const tunnelDeleteCommand = createCommand({ + metadata: { + description: "Delete a Cloudflare Tunnel", + status: "experimental", + owner: "Product: Tunnels", + }, + args: { + tunnel: { + type: "string", + demandOption: true, + description: "The name or UUID of the tunnel", + }, + force: { + alias: "y", + type: "boolean", + description: "Skip confirmation", + }, + }, + positionalArgs: ["tunnel"], + async handler(args, { config, logger, sdk }) { + const accountId = await requireAuth(config); + + if (!args.force) { + const confirmed = await confirm( + `Are you sure you want to delete tunnel "${args.tunnel}"? This action cannot be undone.` + ); + if (!confirmed) { + logger.log("Deletion cancelled."); + return; + } + } + + logger.log(`Deleting tunnel "${args.tunnel}"`); + const tunnelId = await resolveTunnelId(sdk, accountId, args.tunnel); + await deleteTunnel(sdk, accountId, tunnelId); + + logger.log(`Tunnel deleted.`); + }, +}); diff --git a/packages/wrangler/src/tunnel/index.ts b/packages/wrangler/src/tunnel/index.ts new file mode 100644 index 000000000000..996e6d116c05 --- /dev/null +++ b/packages/wrangler/src/tunnel/index.ts @@ -0,0 +1,10 @@ +import { createNamespace } from "../core/create-command"; + +export const tunnelNamespace = createNamespace({ + metadata: { + description: "🚇 Manage Cloudflare Tunnels", + status: "experimental", + owner: "Product: Tunnels", + category: "Networking & security", + }, +}); diff --git a/packages/wrangler/src/tunnel/info.ts b/packages/wrangler/src/tunnel/info.ts new file mode 100644 index 000000000000..5fcc891c09bf --- /dev/null +++ b/packages/wrangler/src/tunnel/info.ts @@ -0,0 +1,41 @@ +import { createCommand } from "../core/create-command"; +import { requireAuth } from "../user"; +import { getTunnel, resolveTunnelId } from "./client"; + +export const tunnelInfoCommand = createCommand({ + metadata: { + description: "Display details about a Cloudflare Tunnel", + status: "experimental", + owner: "Product: Tunnels", + }, + args: { + tunnel: { + type: "string", + demandOption: true, + description: "The name or UUID of the tunnel", + }, + }, + positionalArgs: ["tunnel"], + async handler(args, { config, logger, sdk }) { + const accountId = await requireAuth(config); + const tunnelId = await resolveTunnelId(sdk, accountId, args.tunnel); + + logger.log(`Getting tunnel details for "${args.tunnel}"`); + + const tunnel = await getTunnel(sdk, accountId, tunnelId); + + logger.log(`\nTunnel Information:`); + logger.log(` ID: ${tunnel.id}`); + logger.log(` Name: ${tunnel.name}`); + logger.log(` Status: ${tunnel.status ?? "unknown"}`); + logger.log(` Type: ${tunnel.tun_type ?? "cfd_tunnel"}`); + if (tunnel.created_at) { + logger.log(` Created: ${new Date(tunnel.created_at).toLocaleString()}`); + } + if (tunnel.conns_active_at) { + logger.log( + ` Last Active: ${new Date(tunnel.conns_active_at).toLocaleString()}` + ); + } + }, +}); diff --git a/packages/wrangler/src/tunnel/list.ts b/packages/wrangler/src/tunnel/list.ts new file mode 100644 index 000000000000..4cb04dda5cfa --- /dev/null +++ b/packages/wrangler/src/tunnel/list.ts @@ -0,0 +1,37 @@ +import { createCommand } from "../core/create-command"; +import { requireAuth } from "../user"; +import { listTunnels } from "./client"; + +export const tunnelListCommand = createCommand({ + metadata: { + description: "List all Cloudflare Tunnels in your account", + status: "experimental", + owner: "Product: Tunnels", + }, + args: {}, + behaviour: { printBanner: false, printResourceLocation: false }, + async handler(args, { config, logger, sdk }) { + const accountId = await requireAuth(config); + + logger.log(`Listing Cloudflare Tunnels`); + + const tunnels = await listTunnels(sdk, accountId); + + if (tunnels.length === 0) { + logger.log("No tunnels found."); + return; + } + + logger.table( + tunnels.map((tunnel) => ({ + id: tunnel.id ?? "", + name: tunnel.name ?? "", + status: tunnel.status ?? "unknown", + created_at: tunnel.created_at + ? new Date(tunnel.created_at).toLocaleString() + : "", + tun_type: tunnel.tun_type ?? "cfd_tunnel", + })) + ); + }, +}); diff --git a/packages/wrangler/src/tunnel/quick-start.ts b/packages/wrangler/src/tunnel/quick-start.ts new file mode 100644 index 000000000000..b5d83d16ce9a --- /dev/null +++ b/packages/wrangler/src/tunnel/quick-start.ts @@ -0,0 +1,169 @@ +import { join } from "node:path"; +import { + getGlobalWranglerConfigPath, + UserError, +} from "@cloudflare/workers-utils"; +import { createCommand } from "../core/create-command"; +import { spawnCloudflared } from "./cloudflared"; + +/** + * Quick tunnel command - uses cloudflared to create a temporary tunnel + * without needing to create it via the API first. + * + * Uses the Try Cloudflare / Quick Tunnel feature: + * https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/do-more-with-tunnels/trycloudflare/ + * + * Quick tunnels: + * - Don't require a Cloudflare account or authentication + * - Are temporary and expire when the process stops + * - Get a random *.trycloudflare.com subdomain + * - Include automatic HTTPS and DDoS protection + * - Are great for quick demos, previews, and testing + * + * Learn more at https://try.cloudflare.com/ + */ +export const tunnelQuickStartCommand = createCommand({ + metadata: { + description: + "Start a free, temporary tunnel without an account (https://try.cloudflare.com)", + status: "experimental", + owner: "Product: Tunnels", + }, + args: { + url: { + type: "string", + demandOption: true, + description: "The local URL to expose (e.g., http://localhost:3000)", + }, + "log-level": { + type: "string", + default: "info", + choices: ["debug", "info", "warn", "error", "fatal"] as const, + description: + "Log level for cloudflared (does not affect Wrangler logs, which are controlled by WRANGLER_LOG)", + }, + }, + positionalArgs: ["url"], + async handler(args, { logger }) { + logger.log(`Starting Quick Tunnel (https://try.cloudflare.com)`); + logger.log(`Local URL: ${args.url}`); + + // Build cloudflared command for quick tunnel + // Using the --url flag without authentication creates a temporary tunnel + const cloudflaredArgs = [ + "tunnel", + "--no-autoupdate", + "--url", + args.url, + "--loglevel", + args.logLevel || "info", + ]; + + logger.log(`\nStarting cloudflared...`); + logger.log(` No account required - this is a free, temporary tunnel.`); + logger.log(` You'll get a random *.trycloudflare.com URL to share.`); + logger.log(` The tunnel will stop when you press Ctrl+C.\n`); + + // Spawn cloudflared process with automatic binary management + const cloudflared = await spawnCloudflared(cloudflaredArgs); + + // Track if we've already started shutting down + let isShuttingDown = false; + + // Handle SIGINT/SIGTERM to gracefully shut down + const shutdownHandler = () => { + if (isShuttingDown) { + return; + } + isShuttingDown = true; + + logger.log("\n\nShutting down tunnel..."); + + // Give cloudflared time to clean up + cloudflared.kill("SIGTERM"); + + // Force kill after timeout + const forceKillTimer = setTimeout(() => { + if (!cloudflared.killed) { + logger.debug("Force killing cloudflared..."); + cloudflared.kill("SIGKILL"); + } + }, 5000); + forceKillTimer.unref(); + }; + + process.on("SIGINT", shutdownHandler); + process.on("SIGTERM", shutdownHandler); + + const cleanup = () => { + process.removeListener("SIGINT", shutdownHandler); + process.removeListener("SIGTERM", shutdownHandler); + }; + + // Handle stderr for cloudflared output + if (cloudflared.stderr) { + cloudflared.stderr.on("data", (data: Buffer) => { + process.stderr.write(data.toString()); + }); + } + + // Return a promise that resolves/rejects based on the child process lifecycle. + // This avoids calling process.exit() and lets the command infrastructure handle exit. + return new Promise((resolve, reject) => { + cloudflared.on("error", (error) => { + cleanup(); + if (isShuttingDown) { + resolve(); + return; + } + + let message = `Failed to run cloudflared: ${error.message}`; + + if (error.message.includes("ENOENT")) { + message += + `\n\nThe cloudflared binary could not be executed.\n` + + `This might be a permissions issue or the binary is corrupted.\n\n` + + `Try removing the cache and running again:\n` + + ` rm -rf ${join(getGlobalWranglerConfigPath(), "cloudflared")}\n` + + ` wrangler tunnel quick-start ${args.url}`; + } + + reject(new UserError(message)); + }); + + cloudflared.on("exit", (code, signal) => { + cleanup(); + if (isShuttingDown) { + logger.log("Tunnel stopped."); + resolve(); + return; + } + + if (signal) { + logger.log(`\ncloudflared terminated by signal: ${signal}`); + resolve(); + return; + } + + if (code !== 0 && code !== null) { + let message = `cloudflared exited with code ${code}`; + + if (code === 1) { + message += + `\n\nThis might indicate:\n` + + ` - The local URL "${args.url}" is not reachable\n` + + ` - Network connectivity issues\n` + + ` - Port already in use\n\n` + + `Make sure your local service is running at ${args.url}\n` + + `Try running with --log-level debug for more information.`; + } + + reject(new UserError(message)); + return; + } + + resolve(); + }); + }); + }, +}); diff --git a/packages/wrangler/src/tunnel/run.ts b/packages/wrangler/src/tunnel/run.ts new file mode 100644 index 000000000000..9a09a80ed65a --- /dev/null +++ b/packages/wrangler/src/tunnel/run.ts @@ -0,0 +1,206 @@ +import { join } from "node:path"; +import { + getGlobalWranglerConfigPath, + UserError, +} from "@cloudflare/workers-utils"; +import { createCommand } from "../core/create-command"; +import { requireAuth } from "../user"; +import { getTunnelToken, resolveTunnelId } from "./client"; +import { spawnCloudflared } from "./cloudflared"; + +export const tunnelRunCommand = createCommand({ + metadata: { + description: "Run a Cloudflare Tunnel using cloudflared", + status: "experimental", + owner: "Product: Tunnels", + }, + args: { + tunnel: { + type: "string", + description: + "The name or UUID of the tunnel to run (required unless --token is provided)", + }, + token: { + type: "string", + description: + "The tunnel token to use (skips API auth, useful for running on remote machines)", + }, + "log-level": { + type: "string", + default: "info", + choices: ["debug", "info", "warn", "error", "fatal"] as const, + description: + "Log level for cloudflared (does not affect Wrangler logs, which are controlled by WRANGLER_LOG)", + }, + }, + positionalArgs: ["tunnel"], + async handler(args, { config, logger, sdk }) { + let tokenStr = args.token; + + let tunnelId: string | undefined; + if (!tokenStr) { + if (!args.tunnel) { + throw new UserError( + `Either a tunnel name/UUID or --token must be provided.\n\n` + + `Usage:\n` + + ` wrangler tunnel run # Fetch token via API\n` + + ` wrangler tunnel run --token # Use provided token` + ); + } + const accountId = await requireAuth(config); + tunnelId = await resolveTunnelId(sdk, accountId, args.tunnel); + + logger.log(`Running tunnel "${args.tunnel}"`); + logger.log(`Fetching tunnel credentials...`); + try { + tokenStr = await getTunnelToken(sdk, accountId, tunnelId); + } catch (e) { + // Re-throw UserErrors (e.g. permission errors) as-is so the + // detailed guidance from withTunnelErrorHandling isn't lost. + if (e instanceof UserError) { + throw e; + } + throw new UserError( + `Failed to get tunnel token for "${args.tunnel}".\n\n` + + `This could mean:\n` + + ` - The tunnel doesn't exist\n` + + ` - You don't have permission to access this tunnel\n` + + ` - The tunnel has been deleted\n\n` + + `Use "wrangler tunnel list" to see available tunnels.\n\n` + + `Original error: ${e instanceof Error ? e.message : String(e)}` + ); + } + if (!tokenStr) { + throw new UserError( + `Failed to get tunnel token for "${args.tunnel}".\n\n` + + `The API returned an empty token. Please ensure the tunnel exists and is properly configured.` + ); + } + } else { + logger.log(`Running tunnel with provided token`); + } + + // Build cloudflared command. + // The token is passed via TUNNEL_TOKEN env var rather than CLI args + // to avoid leaking it in process listings (ps) and to maintain + // compatibility with all cloudflared versions. + const cloudflaredArgs = [ + "tunnel", + "--no-autoupdate", + "--loglevel", + args.logLevel || "info", + "run", + ]; + + logger.log(`\nStarting cloudflared...`); + if (tunnelId) { + logger.log(` Tunnel ID: ${tunnelId}`); + } + logger.log(`\nPress Ctrl+C to stop the tunnel.\n`); + + // Spawn cloudflared process with automatic binary management. + // Token is passed via env var to avoid leaking in `ps` output. + const cloudflared = await spawnCloudflared(cloudflaredArgs, { + env: { TUNNEL_TOKEN: tokenStr }, + }); + + // Track if we've already started shutting down + let isShuttingDown = false; + + // Handle SIGINT/SIGTERM to gracefully shut down + const shutdownHandler = () => { + if (isShuttingDown) { + return; + } + isShuttingDown = true; + + logger.log("\n\nShutting down tunnel..."); + + // Give cloudflared time to clean up + cloudflared.kill("SIGTERM"); + + // Force kill after timeout + const forceKillTimer = setTimeout(() => { + if (!cloudflared.killed) { + logger.debug("Force killing cloudflared..."); + cloudflared.kill("SIGKILL"); + } + }, 5000); + forceKillTimer.unref(); + }; + + process.on("SIGINT", shutdownHandler); + process.on("SIGTERM", shutdownHandler); + + const cleanup = () => { + process.removeListener("SIGINT", shutdownHandler); + process.removeListener("SIGTERM", shutdownHandler); + }; + + // Handle stderr for cloudflared output + if (cloudflared.stderr) { + cloudflared.stderr.on("data", (data: Buffer) => { + // cloudflared outputs info to stderr + process.stderr.write(data.toString()); + }); + } + + // Return a promise that resolves/rejects based on the child process lifecycle. + // This avoids calling process.exit() and lets the command infrastructure handle exit. + return new Promise((resolve, reject) => { + cloudflared.on("error", (error) => { + cleanup(); + if (isShuttingDown) { + resolve(); + return; + } + + let message = `Failed to run cloudflared: ${error.message}`; + + if (error.message.includes("ENOENT")) { + message += + `\n\nThe cloudflared binary could not be executed.\n` + + `This might be a permissions issue or the binary is corrupted.\n\n` + + `Try removing the cache and running again:\n` + + ` rm -rf ${join(getGlobalWranglerConfigPath(), "cloudflared")}\n` + + ` wrangler tunnel run ${tunnelId || "--token "}`; + } + + reject(new UserError(message)); + }); + + cloudflared.on("exit", (code, signal) => { + cleanup(); + if (isShuttingDown) { + logger.log("Tunnel stopped."); + resolve(); + return; + } + + if (signal) { + logger.log(`\ncloudflared terminated by signal: ${signal}`); + resolve(); + return; + } + + if (code !== 0 && code !== null) { + let message = `cloudflared exited with code ${code}`; + + if (code === 1) { + message += + `\n\nThis might indicate:\n` + + ` - Invalid tunnel configuration\n` + + ` - Network connectivity issues\n` + + ` - Authentication problems\n\n` + + `Try running with --log-level debug for more information.`; + } + + reject(new UserError(message)); + return; + } + + resolve(); + }); + }); + }, +}); diff --git a/packages/wrangler/templates/startDevWorker/InspectorProxyWorker.ts b/packages/wrangler/templates/startDevWorker/InspectorProxyWorker.ts index 7f30de52910e..d3fbe26a5e96 100644 --- a/packages/wrangler/templates/startDevWorker/InspectorProxyWorker.ts +++ b/packages/wrangler/templates/startDevWorker/InspectorProxyWorker.ts @@ -24,6 +24,10 @@ import type { 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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49d33a142a28..25791c144f16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: default: '@cloudflare/workers-types': - specifier: ^4.20260316.1 - version: 4.20260316.1 + specifier: ^4.20260317.1 + version: 4.20260317.1 '@hey-api/openapi-ts': specifier: ^0.94.0 version: 0.94.0 @@ -195,7 +195,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@fixture/shared': specifier: workspace:* version: link:../shared @@ -246,7 +246,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 ts-dedent: specifier: ^2.2.0 version: 2.2.0 @@ -267,7 +267,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -291,7 +291,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -318,7 +318,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -360,7 +360,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 undici: specifier: catalog:default version: 7.24.4 @@ -375,7 +375,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/mimetext': specifier: ^2.0.4 version: 2.0.4 @@ -420,7 +420,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/jest-image-snapshot': specifier: ^6.4.0 version: 6.4.0 @@ -450,7 +450,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 miniflare: specifier: workspace:* version: link:../../packages/miniflare @@ -526,7 +526,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/node': specifier: ^20.19.9 version: 20.19.9 @@ -550,7 +550,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/is-even': specifier: ^1.0.2 version: 1.0.2 @@ -593,7 +593,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -624,7 +624,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/node': specifier: ^20.19.9 version: 20.19.9 @@ -657,7 +657,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 undici: specifier: catalog:default version: 7.24.4 @@ -678,7 +678,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/debug': specifier: 4.1.12 version: 4.1.12 @@ -714,7 +714,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -738,7 +738,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -766,7 +766,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@fixture/pages-plugin': specifier: workspace:* version: link:../pages-plugin-example @@ -793,7 +793,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -838,7 +838,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -862,7 +862,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -886,7 +886,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -904,7 +904,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 is-odd: specifier: ^3.0.1 version: 3.0.1 @@ -926,7 +926,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@fixture/pages-plugin': specifier: workspace:* version: link:../pages-plugin-example @@ -995,7 +995,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1019,7 +1019,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1223,19 +1223,19 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 fixtures/rules-app: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 fixtures/secrets-store: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 wrangler: specifier: workspace:* version: link:../../packages/wrangler @@ -1262,7 +1262,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/is-even': specifier: ^1.0.2 version: 1.0.2 @@ -1289,7 +1289,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 vitest: specifier: catalog:default version: 4.1.0(@types/node@20.19.9)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@20.19.9)(typescript@5.9.3))(vite@7.3.1(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1)) @@ -1304,7 +1304,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 esbuild: specifier: catalog:default version: 0.27.3 @@ -1322,7 +1322,7 @@ importers: devDependencies: '@better-auth/stripe': specifier: ^1.4.6 - version: 1.5.4(arsnuwqfehfdddggbbrlvaf63a) + version: 1.5.4(6nd2gmicymoh3wga7szbzermte) '@cloudflare/containers': specifier: ^0.0.25 version: 0.0.25 @@ -1334,7 +1334,7 @@ importers: version: link:../../packages/vitest-pool-workers '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@microlabs/otel-cf-workers': specifier: 1.0.0-rc.45 version: 1.0.0-rc.45(@opentelemetry/api@1.7.0) @@ -1349,7 +1349,7 @@ importers: version: 3.2.6 better-auth: specifier: ^1.4.6 - version: 1.5.4(@cloudflare/workers-types@4.20260316.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260316.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) + version: 1.5.4(@cloudflare/workers-types@4.20260317.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) discord-api-types: specifier: 0.37.98 version: 0.37.98 @@ -1420,7 +1420,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@fixture/shared': specifier: workspace:* version: link:../shared @@ -1481,7 +1481,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 wrangler: specifier: workspace:* version: link:../../packages/wrangler @@ -1496,7 +1496,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 miniflare: specifier: workspace:* version: link:../../packages/miniflare @@ -1550,7 +1550,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 run-script-os: specifier: ^1.1.6 version: 1.1.6 @@ -1577,7 +1577,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1601,7 +1601,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1625,7 +1625,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1649,7 +1649,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1673,7 +1673,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/jest-image-snapshot': specifier: ^6.4.0 version: 6.4.0 @@ -1709,7 +1709,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/node': specifier: ^20.19.9 version: 20.19.9 @@ -1739,7 +1739,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1760,7 +1760,7 @@ importers: version: link:../../packages/eslint-config-shared '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1860,7 +1860,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -1998,7 +1998,7 @@ importers: version: link:../eslint-config-shared '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@octokit/types': specifier: ^13.8.0 version: 13.8.0 @@ -2025,7 +2025,7 @@ importers: version: link:../vitest-pool-workers '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/cookie': specifier: ^0.6.0 version: 0.6.0 @@ -2094,7 +2094,7 @@ importers: version: link:../eslint-config-shared '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 eslint: specifier: catalog:default version: 9.39.1(jiti@2.6.1) @@ -2127,10 +2127,10 @@ importers: version: link:../eslint-config-shared '@cloudflare/vitest-pool-workers': specifier: catalog:vitest-3 - version: 0.10.15(@cloudflare/workers-types@4.20260316.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@3.2.4) + version: 0.10.15(@cloudflare/workers-types@4.20260317.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@3.2.4) '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/mime': specifier: ^3.0.4 version: 3.0.4 @@ -2274,8 +2274,8 @@ importers: specifier: catalog:default version: 7.24.4 workerd: - specifier: 1.20260316.1 - version: 1.20260316.1 + specifier: 1.20260317.1 + version: 1.20260317.1 ws: specifier: catalog:default version: 8.18.0 @@ -2303,7 +2303,7 @@ importers: version: link:../workers-shared '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2484,7 +2484,7 @@ importers: version: link:../eslint-config-shared '@cloudflare/vitest-pool-workers': specifier: catalog:vitest-3 - version: 0.10.15(@cloudflare/workers-types@4.20260316.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@3.2.4) + version: 0.10.15(@cloudflare/workers-types@4.20260317.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@3.2.4) '@cloudflare/workers-shared': specifier: workspace:* version: link:../workers-shared @@ -2493,7 +2493,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -2530,7 +2530,7 @@ importers: version: link:../eslint-config-shared '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/cookie': specifier: ^0.6.0 version: 0.6.0 @@ -2570,7 +2570,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/node': specifier: ^20.19.9 version: 20.19.9 @@ -2600,7 +2600,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 esbuild: specifier: catalog:default version: 0.27.3 @@ -2689,7 +2689,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2776,7 +2776,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2797,7 +2797,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2818,7 +2818,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2839,7 +2839,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2860,7 +2860,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2881,7 +2881,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2902,7 +2902,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2923,7 +2923,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2944,7 +2944,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2965,7 +2965,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2986,7 +2986,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3007,7 +3007,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3028,7 +3028,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3049,7 +3049,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3070,7 +3070,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3091,7 +3091,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3112,7 +3112,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/mimetext': specifier: ^2.0.4 version: 2.0.4 @@ -3145,7 +3145,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3166,7 +3166,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3187,7 +3187,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3208,7 +3208,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3229,7 +3229,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3250,7 +3250,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3271,7 +3271,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@playground/main-resolution-package': specifier: file:./package version: file:packages/vite-plugin-cloudflare/playground/main-resolution/package @@ -3295,7 +3295,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/express': specifier: ^5.0.1 version: 5.0.1 @@ -3322,7 +3322,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@playground/module-resolution-excludes': specifier: file:./packages/excludes version: file:packages/vite-plugin-cloudflare/playground/module-resolution/packages/excludes @@ -3334,7 +3334,7 @@ importers: version: file:packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires '@remix-run/cloudflare': specifier: 2.12.0 - version: 2.12.0(@cloudflare/workers-types@4.20260316.1)(typescript@5.8.3) + version: 2.12.0(@cloudflare/workers-types@4.20260317.1)(typescript@5.8.3) '@types/react': specifier: ^18.3.11 version: 18.3.18 @@ -3367,7 +3367,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3388,7 +3388,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@fixture/shared': specifier: workspace:* version: link:../../../../fixtures/shared @@ -3440,7 +3440,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3461,7 +3461,7 @@ importers: dependencies: partyserver: specifier: ^0.0.64 - version: 0.0.64(@cloudflare/workers-types@4.20260316.1) + version: 0.0.64(@cloudflare/workers-types@4.20260317.1) partysocket: specifier: ^1.0.3 version: 1.0.3 @@ -3480,7 +3480,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@tailwindcss/vite': specifier: ^4.0.15 version: 4.0.15(vite@7.1.12(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1)) @@ -3516,7 +3516,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3537,7 +3537,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../../../workers-utils @@ -3577,7 +3577,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3607,7 +3607,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3628,7 +3628,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3656,7 +3656,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3689,7 +3689,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3710,7 +3710,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3731,7 +3731,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@vitejs/plugin-basic-ssl': specifier: ^2.0.0 version: 2.0.0(vite@7.1.12(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1)) @@ -3755,7 +3755,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3776,7 +3776,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3797,7 +3797,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3818,7 +3818,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3858,7 +3858,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -4091,13 +4091,13 @@ importers: version: link:../eslint-config-shared '@cloudflare/vitest-pool-workers': specifier: catalog:vitest-3 - version: 0.10.15(@cloudflare/workers-types@4.20260316.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@3.2.4) + version: 0.10.15(@cloudflare/workers-types@4.20260317.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@3.2.4) '@cloudflare/workers-tsconfig': specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@sentry/cli': specifier: ^2.37.0 version: 2.41.1(encoding@0.1.13) @@ -4208,13 +4208,13 @@ importers: version: link:../eslint-config-shared '@cloudflare/vitest-pool-workers': specifier: catalog:vitest-3 - version: 0.10.15(@cloudflare/workers-types@4.20260316.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@3.2.4) + version: 0.10.15(@cloudflare/workers-types@4.20260317.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@3.2.4) '@cloudflare/workers-tsconfig': specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@types/mime': specifier: ^3.0.4 version: 3.0.4 @@ -4255,8 +4255,8 @@ importers: specifier: 2.0.0-rc.24 version: 2.0.0-rc.24 workerd: - specifier: 1.20260316.1 - version: 1.20260316.1 + specifier: 1.20260317.1 + version: 1.20260317.1 optionalDependencies: fsevents: specifier: ~2.3.2 @@ -4291,7 +4291,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260316.1 + version: 4.20260317.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -5049,6 +5049,10 @@ packages: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/standalone@7.26.4': resolution: {integrity: sha512-SF+g7S2mhTT1b7CHyfNjDkPU1corxg4LPYsyP0x5KuCl+EbtBQHRLqr9N3q7e7+x7NQ5LYxQf8mJ2PmzebLr0A==} engines: {node: '>=6.9.0'} @@ -5516,8 +5520,8 @@ packages: cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260316.1': - resolution: {integrity: sha512-Eg52Vq1jT0f7xjCYIrUwIPpjxeCMQbDuqz0OGV2rQLPzvGgdn01LM+aO5RyMIFHN14wNe6mS9wWuFxjfpMFS+g==} + '@cloudflare/workerd-darwin-64@1.20260317.1': + resolution: {integrity: sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==} engines: {node: '>=16'} cpu: [x64] os: [darwin] @@ -5534,8 +5538,8 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260316.1': - resolution: {integrity: sha512-NBsSJeVnl8B0taTiLCckrViCppScwMrVC2Imo9J5GULtiq6rTpy6EEYs/Bo6m1nRsQtTMuEtKA4nsygq+Y9gEg==} + '@cloudflare/workerd-darwin-arm64@1.20260317.1': + resolution: {integrity: sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] @@ -5552,8 +5556,8 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20260316.1': - resolution: {integrity: sha512-243aYSTqbCXQi4a9GziV/xuiD+8ldZicdMooyDLajGEifujuZg0CnvkMaFFRItPbQWp5rX8q0D7bflvOR3wuSg==} + '@cloudflare/workerd-linux-64@1.20260317.1': + resolution: {integrity: sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==} engines: {node: '>=16'} cpu: [x64] os: [linux] @@ -5570,8 +5574,8 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260316.1': - resolution: {integrity: sha512-CnqHDJzXgr+TfxduxdYs5OYwkXsQjry4qLq+nLlWeM8yCpQWt0yq2nmOXgWQ3ecEonho7a0cBfv2+dpGnss+6A==} + '@cloudflare/workerd-linux-arm64@1.20260317.1': + resolution: {integrity: sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==} engines: {node: '>=16'} cpu: [arm64] os: [linux] @@ -5588,8 +5592,8 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20260316.1': - resolution: {integrity: sha512-Qf9gGSytvWCaZdJ+R2xSVB3uFULUrKElsHssG4zB+hblBhc8QzTfqpGrJdFRC+R53wOzp2MYa6j41sXEwLquWg==} + '@cloudflare/workerd-windows-64@1.20260317.1': + resolution: {integrity: sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -5602,8 +5606,8 @@ packages: react: ^17.0.2 || ^18.2.21 react-dom: ^17.0.2 || ^18.2.21 - '@cloudflare/workers-types@4.20260316.1': - resolution: {integrity: sha512-HUZ+vQD8/1A4Fz/8WAlzYWcS5W5u3Nu7Dv9adkIkmLfeKqMIRn01vc4nSUBar60KkmohyQHkPi8jtWV/zazvAg==} + '@cloudflare/workers-types@4.20260317.1': + resolution: {integrity: sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==} '@codemirror/autocomplete@6.20.0': resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} @@ -15907,8 +15911,8 @@ packages: engines: {node: '>=16'} hasBin: true - workerd@1.20260316.1: - resolution: {integrity: sha512-zWWJxsPMDz04OfO8m8p3q8I6z/jaiw0fVA1BRxZ//WzK52c0yyoV1grUaTprNyFHrbCiapZdpj27ANxK9sxDaA==} + workerd@1.20260317.1: + resolution: {integrity: sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==} engines: {node: '>=16'} hasBin: true @@ -16938,6 +16942,8 @@ snapshots: '@babel/runtime@7.28.6': {} + '@babel/runtime@7.29.2': {} + '@babel/standalone@7.26.4': {} '@babel/template@7.28.6': @@ -17005,7 +17011,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.13 - '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)': + '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -17016,50 +17022,50 @@ snapshots: nanostores: 1.1.1 zod: 4.3.6 optionalDependencies: - '@cloudflare/workers-types': 4.20260316.1 + '@cloudflare/workers-types': 4.20260317.1 - '@better-auth/drizzle-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260316.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))': + '@better-auth/drizzle-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260316.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) - '@better-auth/kysely-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)': + '@better-auth/kysely-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 kysely: 0.28.11 - '@better-auth/memory-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': + '@better-auth/memory-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - '@better-auth/mongo-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': + '@better-auth/mongo-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 mongodb: 7.1.0 - '@better-auth/prisma-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))': + '@better-auth/prisma-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) prisma: 7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3) - '@better-auth/stripe@1.5.4(arsnuwqfehfdddggbbrlvaf63a)': + '@better-auth/stripe@1.5.4(6nd2gmicymoh3wga7szbzermte)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) - better-auth: 1.5.4(@cloudflare/workers-types@4.20260316.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260316.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + better-auth: 1.5.4(@cloudflare/workers-types@4.20260317.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) better-call: 1.3.2(zod@4.3.6) defu: 6.1.4 stripe: 20.4.1(@types/node@20.19.9) zod: 4.3.6 - '@better-auth/telemetry@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))': + '@better-auth/telemetry@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -17618,7 +17624,7 @@ snapshots: lodash.memoize: 4.1.2 marked: 0.3.19 - '@cloudflare/vitest-pool-workers@0.10.15(@cloudflare/workers-types@4.20260316.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@3.2.4)': + '@cloudflare/vitest-pool-workers@0.10.15(@cloudflare/workers-types@4.20260317.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@3.2.4)': dependencies: '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -17628,7 +17634,7 @@ snapshots: miniflare: 4.20251210.0 semver: 7.7.3 vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.4(@types/node@20.19.9)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.1) - wrangler: 4.54.0(@cloudflare/workers-types@4.20260316.1) + wrangler: 4.54.0(@cloudflare/workers-types@4.20260317.1) zod: 3.25.76 transitivePeerDependencies: - '@cloudflare/workers-types' @@ -17641,7 +17647,7 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20260301.1': optional: true - '@cloudflare/workerd-darwin-64@1.20260316.1': + '@cloudflare/workerd-darwin-64@1.20260317.1': optional: true '@cloudflare/workerd-darwin-arm64@1.20251210.0': @@ -17650,7 +17656,7 @@ snapshots: '@cloudflare/workerd-darwin-arm64@1.20260301.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260316.1': + '@cloudflare/workerd-darwin-arm64@1.20260317.1': optional: true '@cloudflare/workerd-linux-64@1.20251210.0': @@ -17659,7 +17665,7 @@ snapshots: '@cloudflare/workerd-linux-64@1.20260301.1': optional: true - '@cloudflare/workerd-linux-64@1.20260316.1': + '@cloudflare/workerd-linux-64@1.20260317.1': optional: true '@cloudflare/workerd-linux-arm64@1.20251210.0': @@ -17668,7 +17674,7 @@ snapshots: '@cloudflare/workerd-linux-arm64@1.20260301.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260316.1': + '@cloudflare/workerd-linux-arm64@1.20260317.1': optional: true '@cloudflare/workerd-windows-64@1.20251210.0': @@ -17677,7 +17683,7 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260301.1': optional: true - '@cloudflare/workerd-windows-64@1.20260316.1': + '@cloudflare/workerd-windows-64@1.20260317.1': optional: true '@cloudflare/workers-editor-shared@0.1.1(@cloudflare/style-const@6.1.3(react@19.2.4))(@cloudflare/style-container@7.12.2(@cloudflare/style-const@6.1.3(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -17688,7 +17694,7 @@ snapshots: react-dom: 19.2.4(react@19.2.4) react-split-pane: 0.1.92(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@cloudflare/workers-types@4.20260316.1': {} + '@cloudflare/workers-types@4.20260317.1': {} '@codemirror/autocomplete@6.20.0': dependencies: @@ -19175,7 +19181,7 @@ snapshots: '@prisma/adapter-d1@7.0.1': dependencies: - '@cloudflare/workers-types': 4.20260316.1 + '@cloudflare/workers-types': 4.20260317.1 '@prisma/driver-adapter-utils': 7.0.1 ky: 1.7.5 @@ -19402,10 +19408,10 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 - '@remix-run/cloudflare@2.12.0(@cloudflare/workers-types@4.20260316.1)(typescript@5.8.3)': + '@remix-run/cloudflare@2.12.0(@cloudflare/workers-types@4.20260317.1)(typescript@5.8.3)': dependencies: '@cloudflare/kv-asset-handler': 0.1.3 - '@cloudflare/workers-types': 4.20260316.1 + '@cloudflare/workers-types': 4.20260317.1 '@remix-run/server-runtime': 2.12.0(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 @@ -21837,15 +21843,15 @@ snapshots: before-after-hook@2.2.3: {} - better-auth@1.5.4(@cloudflare/workers-types@4.20260316.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260316.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0): + better-auth@1.5.4(@cloudflare/workers-types@4.20260317.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0): dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) - '@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260316.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))) - '@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) - '@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) - '@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) - '@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260316.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))) + '@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) + '@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) + '@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) + '@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + '@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260317.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 @@ -21858,7 +21864,7 @@ snapshots: zod: 4.3.6 optionalDependencies: '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) - drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260316.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) mongodb: 7.1.0 mysql2: 3.15.3 pg: 8.16.3 @@ -22707,9 +22713,9 @@ snapshots: dependencies: wordwrap: 1.0.0 - drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260316.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)): + drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.7.0)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)): optionalDependencies: - '@cloudflare/workers-types': 4.20260316.1 + '@cloudflare/workers-types': 4.20260317.1 '@electric-sql/pglite': 0.3.2 '@opentelemetry/api': 1.7.0 '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) @@ -25591,9 +25597,9 @@ snapshots: parseurl@1.3.3: {} - partyserver@0.0.64(@cloudflare/workers-types@4.20260316.1): + partyserver@0.0.64(@cloudflare/workers-types@4.20260317.1): dependencies: - '@cloudflare/workers-types': 4.20260316.1 + '@cloudflare/workers-types': 4.20260317.1 nanoid: 5.1.0 partysocket@1.0.3: @@ -25785,7 +25791,7 @@ snapshots: polished@4.3.1: dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 possible-typed-array-names@1.0.0: {} @@ -28566,15 +28572,15 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260301.1 '@cloudflare/workerd-windows-64': 1.20260301.1 - workerd@1.20260316.1: + workerd@1.20260317.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260316.1 - '@cloudflare/workerd-darwin-arm64': 1.20260316.1 - '@cloudflare/workerd-linux-64': 1.20260316.1 - '@cloudflare/workerd-linux-arm64': 1.20260316.1 - '@cloudflare/workerd-windows-64': 1.20260316.1 + '@cloudflare/workerd-darwin-64': 1.20260317.1 + '@cloudflare/workerd-darwin-arm64': 1.20260317.1 + '@cloudflare/workerd-linux-64': 1.20260317.1 + '@cloudflare/workerd-linux-arm64': 1.20260317.1 + '@cloudflare/workerd-windows-64': 1.20260317.1 - wrangler@4.54.0(@cloudflare/workers-types@4.20260316.1): + wrangler@4.54.0(@cloudflare/workers-types@4.20260317.1): dependencies: '@cloudflare/kv-asset-handler': 0.4.1 '@cloudflare/unenv-preset': 2.7.13(unenv@2.0.0-rc.24)(workerd@1.20251210.0) @@ -28585,7 +28591,7 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20251210.0 optionalDependencies: - '@cloudflare/workers-types': 4.20260316.1 + '@cloudflare/workers-types': 4.20260317.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b5336653382c..3279839cbfae 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -33,8 +33,8 @@ catalog: "ws": "8.18.0" esbuild: "0.27.3" playwright-chromium: "^1.56.1" - "@cloudflare/workers-types": "^4.20260316.1" - workerd: "1.20260316.1" + "@cloudflare/workers-types": "^4.20260317.1" + workerd: "1.20260317.1" eslint: "^9.39.1" jsonc-parser: "^3.2.0" smol-toml: "^1.5.2" diff --git a/turbo.json b/turbo.json index 70c14f0d94da..09100bcbc6c4 100644 --- a/turbo.json +++ b/turbo.json @@ -16,6 +16,7 @@ "VSCODE_INSPECTOR_OPTIONS", "WRANGLER_API_ENVIRONMENT", "WRANGLER_CACHE_DIR", + "CLOUDFLARED_PATH", "MINIFLARE_CONTAINER_EGRESS_IMAGE", "WRANGLER_DOCKER_HOST", "WRANGLER_LOG_PATH",