Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
159c673
[remote-bindings] Add empty package
penalosa Jul 14, 2026
0c64cf4
[remote-bindings] Extract remote binding implementation
penalosa Jul 14, 2026
ee6e6c3
snapshot copy of DevEnv
penalosa Jul 15, 2026
7f297eb
remove obsolete inspector proxy
penalosa Jul 15, 2026
4ca824d
config pass through
penalosa Jul 15, 2026
1a24473
bundler pass through
penalosa Jul 15, 2026
b27a054
logger context
penalosa Jul 15, 2026
b16a696
remove no-op
penalosa Jul 15, 2026
9ea9466
fully specified remote bindings input config
penalosa Jul 15, 2026
9b75588
remove dead code
penalosa Jul 15, 2026
11a062a
packages
penalosa Jul 15, 2026
3f78aca
packages
penalosa Jul 15, 2026
ebdb131
packages
penalosa Jul 15, 2026
c9f84a2
base move
penalosa Jul 15, 2026
bcc6143
move more helpers
penalosa Jul 15, 2026
39cbe42
move retryOnApiFailure
penalosa Jul 15, 2026
fb41427
move access
penalosa Jul 15, 2026
a3700e2
copy ProxyWorekr
penalosa Jul 15, 2026
01b325e
make proxycontroller green with minimal helpers
penalosa Jul 15, 2026
1e0c276
reduction in more things we don't cara bout
penalosa Jul 15, 2026
d2dcd18
make remote bindings package functional
penalosa Jul 16, 2026
3806b52
[remote-bindings] Remove dead dev runtime code
penalosa Jul 16, 2026
920555b
[remote-bindings] Narrow remote runtime internals
penalosa Jul 16, 2026
b845d3a
[remote-bindings] Restore default account selection
penalosa Jul 16, 2026
464b4b6
chore: fix remote bindings lockfile
penalosa Jul 16, 2026
448c24a
[remote-bindings] Collapse remote runtime controllers
penalosa Jul 16, 2026
5bdef90
[remote-bindings] Embed workers for direct consumers
penalosa Jul 16, 2026
4376590
[remote-bindings] Fix direct consumer integration
penalosa Jul 16, 2026
710e34f
[remote-bindings] Fix Windows worker embedding build
penalosa Jul 16, 2026
fb1afc2
[wrangler] Update invalid account error snapshot
penalosa Jul 16, 2026
3ae2834
[remote-bindings] Prevent stale preview bindings
penalosa Jul 17, 2026
fe26a97
Update packages/remote-bindings/src/startDevWorker/RemoteRuntimeContr…
penalosa Jul 20, 2026
3f012b6
[remote-bindings] Address review feedback
penalosa Jul 21, 2026
2b3ea8b
[remote-bindings] Preserve UserError type from async proxy session st…
penalosa Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-remote-bindings-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": patch
---

Fix remote binding previews for accounts without a workers.dev subdomain

Wrangler now automatically registers a workers.dev subdomain when one is required to start a remote binding preview.
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ if (auth) {
).toMatchInlineSnapshot(`
"X [ERROR] A request to the Cloudflare API (/accounts/NOT a valid account id/workers/subdomain/edge-preview) failed.
Could not route to /client/v4/accounts/NOT%20a%20valid%20account%20id/workers/subdomain/edge-preview, perhaps your object identifier is invalid? [code: 7003]
"
`);
});
Expand Down
4 changes: 4 additions & 0 deletions packages/deploy-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"./context": {
"import": "./dist/context.mjs",
"types": "./dist/context.d.mts"
},
"./create-worker-upload-form": {
"import": "./dist/create-worker-upload-form.mjs",
"types": "./dist/create-worker-upload-form.d.mts"
}
},
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions packages/deploy-helpers/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export default defineConfig(() => [
entry: {
index: "src/index.ts",
context: "src/shared/context.ts",
"create-worker-upload-form":
"src/deploy/helpers/create-worker-upload-form.ts",
},
platform: "node",
format: "esm",
Expand Down
53 changes: 53 additions & 0 deletions packages/remote-bindings/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@cloudflare/remote-bindings",
"version": "0.0.0",
"private": true,
"homepage": "https://github.com/cloudflare/workers-sdk/tree/main/packages/remote-bindings#readme",
"bugs": {
"url": "https://github.com/cloudflare/workers-sdk/issues"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/cloudflare/workers-sdk.git",
"directory": "packages/remote-bindings"
},
"files": [
"dist"
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
}
},
"scripts": {
"build": "tsdown",
"check:type": "tsc",
"dev": "tsdown --watch",
"test:ci": "vitest run --passWithNoTests",
"test:watch": "vitest"
},
"dependencies": {
"@cloudflare/cli-shared-helpers": "workspace:*",
"@cloudflare/deploy-helpers": "workspace:*",
"@cloudflare/workers-auth": "workspace:*",
"@cloudflare/workers-utils": "workspace:*",
"chalk": "catalog:default",
"miniflare": "workspace:*",
"undici": "catalog:default",
"ws": "catalog:default"
},
"devDependencies": {
"@cloudflare/workers-tsconfig": "workspace:*",
"@cloudflare/workers-types": "catalog:default",
"@types/ws": "^8.5.13",
"capnweb": "catalog:default",
"esbuild": "catalog:default",
"tsdown": "0.16.3",
"typescript": "catalog:default",
"vitest": "catalog:default"
}
}
39 changes: 39 additions & 0 deletions packages/remote-bindings/scripts/embed-workers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import path from "node:path";
import { build } from "esbuild";

const WORKER_PREFIX = "\0worker:";
const templatesDir = path.resolve(import.meta.dirname, "../templates");

export function embedWorkersPlugin() {
return {
name: "embed-workers",
resolveId(id: string) {
if (!id.startsWith("worker:")) {
return;
}
return `${WORKER_PREFIX}${id.slice("worker:".length)}`;
},
async load(id: string) {
if (!id.startsWith(WORKER_PREFIX)) {
return;
}
const result = await build({
Comment thread
penalosa marked this conversation as resolved.
entryPoints: [
path.resolve(templatesDir, `${id.slice(WORKER_PREFIX.length)}.ts`),
],
platform: "node",
conditions: ["workerd", "worker", "browser"],
format: "esm",
target: "esnext",
bundle: true,
write: false,
external: ["cloudflare:email", "cloudflare:workers"],
});
const source = result.outputFiles[0]?.text;
if (source === undefined) {
throw new Error(`Failed to bundle Worker ${id}`);
}
return `export default ${JSON.stringify(source)};`;
},
};
}
143 changes: 143 additions & 0 deletions packages/remote-bindings/src/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import assert from "node:assert";
import { afterEach, beforeEach, describe, it, vi } from "vitest";
import { createRemoteBindingsAuth, getRemoteBindingsAuthHook } from "./auth";
import type { RemoteBindingsLogger } from "./logger";
import type { CfAccount } from "@cloudflare/workers-utils";

const mocks = vi.hoisted(() => ({
cfAuth: {
source: "cf",
setProfile: vi.fn(),
requireAuth: vi.fn().mockResolvedValue("selected-account-id"),
requireApiToken: vi.fn().mockReturnValue({ apiToken: "test-token" }),
},
wranglerAuth: {
source: "wrangler",
setProfile: vi.fn(),
requireAuth: vi.fn().mockResolvedValue("selected-account-id"),
requireApiToken: vi.fn().mockReturnValue({ apiToken: "test-token" }),
},
createCfAuth: vi.fn(),
createWranglerAuth: vi.fn(),
cfProfileStore: { resolve: vi.fn().mockReturnValue({ name: "cf-profile" }) },
wranglerProfileStore: {
resolve: vi.fn().mockReturnValue({ name: "wrangler-profile" }),
},
createCfProfileStore: vi.fn(),
createWranglerProfileStore: vi.fn(),
}));

vi.mock("@cloudflare/workers-auth/cf", () => ({
createCfAuth: mocks.createCfAuth.mockReturnValue(mocks.cfAuth),
createCfProfileStore: mocks.createCfProfileStore.mockReturnValue(
mocks.cfProfileStore
),
}));

vi.mock("@cloudflare/workers-auth/wrangler", () => ({
createWranglerAuth: mocks.createWranglerAuth.mockReturnValue(
mocks.wranglerAuth
),
createWranglerProfileStore: mocks.createWranglerProfileStore.mockReturnValue(
mocks.wranglerProfileStore
),
}));

const originalCfAuth = process.env.CLOUDFLARE_CF_AUTH;

function createTestLogger(): RemoteBindingsLogger {
return {
loggerLevel: "log",
debug: vi.fn(),
log: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
console: vi.fn(),
};
}

beforeEach(() => {
delete process.env.CLOUDFLARE_CF_AUTH;
});

afterEach(() => {
vi.clearAllMocks();
if (originalCfAuth === undefined) {
delete process.env.CLOUDFLARE_CF_AUTH;
} else {
process.env.CLOUDFLARE_CF_AUTH = originalCfAuth;
}
});

describe("createRemoteBindingsAuth", () => {
it("uses Wrangler auth by default", ({ expect }) => {
const result = createRemoteBindingsAuth(createTestLogger());

expect(result).toEqual({ auth: mocks.wranglerAuth, useCfAuth: false });
expect(mocks.createWranglerAuth).toHaveBeenCalledOnce();
expect(mocks.createCfAuth).not.toHaveBeenCalled();
});

it("uses CF auth when CLOUDFLARE_CF_AUTH is present", ({ expect }) => {
process.env.CLOUDFLARE_CF_AUTH = "";

const result = createRemoteBindingsAuth(createTestLogger());

expect(result).toEqual({ auth: mocks.cfAuth, useCfAuth: true });
expect(mocks.createCfAuth).toHaveBeenCalledOnce();
expect(mocks.createWranglerAuth).not.toHaveBeenCalled();
});
});

describe("getRemoteBindingsAuthHook", () => {
it("uses provided auth without resolving a profile", ({ expect }) => {
const auth: CfAccount = {
accountId: "provided-account-id",
apiToken: { apiToken: "provided-token" },
};

const result = getRemoteBindingsAuthHook(
auth,
undefined,
undefined,
createTestLogger()
);

expect(result).toBe(auth);
expect(mocks.createWranglerProfileStore).not.toHaveBeenCalled();
});

it("allows auth to select an account when none is configured", async ({
expect,
}) => {
const hook = getRemoteBindingsAuthHook(
undefined,
undefined,
undefined,
createTestLogger()
);
assert(typeof hook === "function");

await expect(hook()).resolves.toEqual({
accountId: "selected-account-id",
apiToken: { apiToken: "test-token" },
});
expect(mocks.wranglerAuth.requireAuth).toHaveBeenCalledWith({});
});

it("uses the configured account when provided", async ({ expect }) => {
const hook = getRemoteBindingsAuthHook(
undefined,
"configured-account-id",
undefined,
createTestLogger()
);
assert(typeof hook === "function");

await hook();
expect(mocks.wranglerAuth.requireAuth).toHaveBeenCalledWith({
account_id: "configured-account-id",
});
});
});
92 changes: 92 additions & 0 deletions packages/remote-bindings/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { inputPrompt } from "@cloudflare/cli-shared-helpers/interactive";
import {
createCfAuth,
createCfProfileStore,
} from "@cloudflare/workers-auth/cf";
import {
createWranglerAuth,
createWranglerProfileStore,
} from "@cloudflare/workers-auth/wrangler";
import { isNonInteractiveOrCI, UserError } from "@cloudflare/workers-utils";
import { version as packageVersion } from "../package.json";
import type { RemoteBindingsLogger } from "./logger";
import type { AsyncHook, CfAccount, Config } from "@cloudflare/workers-utils";

class NoDefaultValueProvided extends UserError {
constructor() {
super("This command cannot be run in a non-interactive context", {
telemetryMessage: "remote bindings prompt default missing",
});
}
}

export function createRemoteBindingsAuth(logger: RemoteBindingsLogger) {
const context = {
logger,
userAgent: `remote-bindings/${packageVersion}`,
async prompt(question: string) {
if (isNonInteractiveOrCI()) {
throw new NoDefaultValueProvided();
}
return inputPrompt<string>({
type: "text",
question,
label: "Answer",
throwOnError: true,
});
},
async select(
question: string,
options: { choices: { title: string; value: string }[] }
) {
if (isNonInteractiveOrCI()) {
throw new NoDefaultValueProvided();
}
return inputPrompt<string>({
type: "select",
question,
label: "Account",
options: options.choices.map((choice) => ({
label: choice.title,
value: choice.value,
})),
throwOnError: true,
});
},
isNoDefaultValueProvidedError: (error: unknown) =>
error instanceof NoDefaultValueProvided,
};
const useCfAuth = "CLOUDFLARE_CF_AUTH" in process.env;
return {
auth: useCfAuth ? createCfAuth(context) : createWranglerAuth(context),
useCfAuth,
};
}

export function getRemoteBindingsAuthHook(
auth: AsyncHook<CfAccount> | undefined,
accountId: Config["account_id"] | undefined,
profileDir: string | undefined,
logger: RemoteBindingsLogger
): AsyncHook<CfAccount> {
if (auth) {
return auth;
}

const { auth: remoteBindingsAuth, useCfAuth } =
createRemoteBindingsAuth(logger);
const profileStore = useCfAuth
? createCfProfileStore({ logger })
: createWranglerProfileStore({ logger });
const profile = profileStore.resolve({
cwd: profileDir ?? process.cwd(),
});
remoteBindingsAuth.setProfile(profile);

return async () => ({
accountId: await remoteBindingsAuth.requireAuth(
accountId ? { account_id: accountId } : {}
),
apiToken: remoteBindingsAuth.requireApiToken(),
});
}
15 changes: 15 additions & 0 deletions packages/remote-bindings/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export {
maybeStartOrUpdateRemoteProxySession,
pickRemoteBindings,
} from "./maybe-start-or-update-session";
export type {
RemoteBindingsContext,
RemoteProxySessionData,
WorkerConfigObject,
} from "./maybe-start-or-update-session";
export { startRemoteProxySession } from "./start-remote-proxy-session";
export type { RemoteBindingsLogger } from "./logger";
export type {
RemoteProxySession,
StartRemoteProxySessionOptions,
} from "./start-remote-proxy-session";
12 changes: 12 additions & 0 deletions packages/remote-bindings/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Logger, LoggerLevel } from "@cloudflare/workers-utils";

export type RemoteBindingsLogger = Logger & {
loggerLevel: LoggerLevel;
console: NonNullable<Logger["console"]>;
};

export let logger: RemoteBindingsLogger;

export function initLogger(value: RemoteBindingsLogger): void {
logger = value;
}
Loading
Loading