From df2dcd57086db878f37ce61b5f2dbe24f90b7d8f Mon Sep 17 00:00:00 2001 From: Ricardo Antunes Date: Fri, 20 Mar 2026 16:02:18 +0000 Subject: [PATCH 1/3] [workers-utils] Add vpc_networks binding type --- .changeset/vpc-networks-binding.md | 7 + .../workers-utils/src/config/environment.ts | 38 ++--- .../workers-utils/src/config/validation.ts | 69 +++++++--- .../src/map-worker-metadata-bindings.ts | 18 ++- packages/workers-utils/src/types.ts | 10 +- packages/workers-utils/src/worker.ts | 14 +- .../normalize-and-validate-config.test.ts | 130 +++++++++--------- 7 files changed, 170 insertions(+), 116 deletions(-) create mode 100644 .changeset/vpc-networks-binding.md diff --git a/.changeset/vpc-networks-binding.md b/.changeset/vpc-networks-binding.md new file mode 100644 index 000000000000..c4797aa60469 --- /dev/null +++ b/.changeset/vpc-networks-binding.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +"miniflare": minor +"@cloudflare/workers-utils": minor +--- + +Add `vpc_networks` binding support for routing Worker traffic through a Cloudflare Tunnel diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index 4836d7e9a60c..b6c3f9ac5e13 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -7,7 +7,8 @@ import type { Json } from "../types"; * This could be the top-level default environment, or a specific named environment. */ export interface Environment - extends EnvironmentInheritable, EnvironmentNonInheritable {} + extends EnvironmentInheritable, + EnvironmentNonInheritable {} type SimpleRoute = string; export type ZoneIdRoute = { @@ -1113,23 +1114,6 @@ export interface EnvironmentNonInheritable { } | undefined; - /** - * Binding to Cloudflare Stream - * - * NOTE: This field is not automatically inherited from the top level environment, - * and so must be specified in every named environment. - * - * @default {} - * @nonInheritable - */ - stream: - | { - binding: string; - /** Whether the Stream binding should be remote or not in local development */ - remote?: boolean; - } - | undefined; - /** * Binding to the Worker Version's metadata */ @@ -1352,6 +1336,24 @@ export interface EnvironmentNonInheritable { /** Whether the VPC service is remote or not */ remote?: boolean; }[]; + + /** + * Specifies VPC networks that are bound to this Worker environment. + * + * NOTE: This field is not automatically inherited from the top level environment, + * and so must be specified in every named environment. + * + * @default [] + * @nonInheritable + */ + vpc_networks: { + /** The binding name used to refer to the VPC network in the Worker. */ + binding: string; + /** The tunnel ID of the Cloudflare Tunnel to route traffic through. */ + tunnel_id: string; + /** Whether the VPC network is remote or not */ + remote?: boolean; + }[]; } /** diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index 19997250de01..8b30a45ed7fc 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -88,7 +88,6 @@ export type ConfigBindingFieldName = | "browser" | "ai" | "images" - | "stream" | "media" | "version_metadata" | "unsafe" @@ -103,7 +102,8 @@ export type ConfigBindingFieldName = | "assets" | "unsafe_hello_world" | "worker_loaders" - | "vpc_services"; + | "vpc_services" + | "vpc_networks"; /** * @deprecated new code should use getBindingTypeFriendlyName() instead @@ -125,7 +125,6 @@ export const friendlyBindingNames: Record = { browser: "Browser", ai: "AI", images: "Images", - stream: "Stream", media: "Media", version_metadata: "Worker Version Metadata", unsafe: "Unsafe Metadata", @@ -141,6 +140,7 @@ export const friendlyBindingNames: Record = { unsafe_hello_world: "Hello World", worker_loaders: "Worker Loader", vpc_services: "VPC Service", + vpc_networks: "VPC Network", } as const; /** @@ -159,7 +159,6 @@ const bindingTypeFriendlyNames: Record = { browser: "Browser", ai: "AI", images: "Images", - stream: "Stream", version_metadata: "Worker Version Metadata", data_blob: "Data Blob", durable_object_namespace: "Durable Object", @@ -181,6 +180,7 @@ const bindingTypeFriendlyNames: Record = { ratelimit: "Rate Limit", worker_loader: "Worker Loader", vpc_service: "VPC Service", + vpc_network: "VPC Network", media: "Media", assets: "Assets", inherit: "Inherited", @@ -702,7 +702,7 @@ function normalizeAndValidateDev( inspector_ip, local_protocol = localProtocolArg ?? "http", // In remote mode upstream_protocol must be https, otherwise it defaults to local_protocol. - upstream_protocol = (upstreamProtocolArg ?? remoteArg) + upstream_protocol = upstreamProtocolArg ?? remoteArg ? "https" : local_protocol, host, @@ -1805,16 +1805,6 @@ function normalizeAndValidateEnvironment( validateNamedSimpleBinding(envName), undefined ), - stream: notInheritable( - diagnostics, - topLevelEnv, - rawConfig, - rawEnv, - envName, - "stream", - validateNamedSimpleBinding(envName), - undefined - ), media: notInheritable( diagnostics, topLevelEnv, @@ -1885,6 +1875,16 @@ function normalizeAndValidateEnvironment( validateBindingArray(envName, validateVpcServiceBinding), [] ), + vpc_networks: notInheritable( + diagnostics, + topLevelEnv, + rawConfig, + rawEnv, + envName, + "vpc_networks", + validateBindingArray(envName, validateVpcNetworkBinding), + [] + ), version_metadata: notInheritable( diagnostics, topLevelEnv, @@ -2930,7 +2930,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => { "pipeline", "worker_loader", "vpc_service", - "stream", + "vpc_network", "media", ]; @@ -3994,6 +3994,43 @@ const validateVpcServiceBinding: ValidatorFn = (diagnostics, field, value) => { return isValid; }; +const validateVpcNetworkBinding: ValidatorFn = (diagnostics, field, value) => { + if (typeof value !== "object" || value === null) { + diagnostics.errors.push( + `"vpc_networks" bindings should be objects, but got ${JSON.stringify( + value + )}` + ); + return false; + } + let isValid = true; + // VPC network bindings must have a binding and a tunnel_id. + if (!isRequiredProperty(value, "binding", "string")) { + diagnostics.errors.push( + `"${field}" bindings should have a string "binding" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + if (!isRequiredProperty(value, "tunnel_id", "string")) { + diagnostics.errors.push( + `"${field}" bindings must have a string "tunnel_id" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + + validateAdditionalProperties(diagnostics, field, Object.keys(value), [ + "binding", + "tunnel_id", + "remote", + ]); + + return isValid; +}; + /** * Check that bindings whose names might conflict, don't. * diff --git a/packages/workers-utils/src/map-worker-metadata-bindings.ts b/packages/workers-utils/src/map-worker-metadata-bindings.ts index ce3682c728f2..3d026d98b040 100644 --- a/packages/workers-utils/src/map-worker-metadata-bindings.ts +++ b/packages/workers-utils/src/map-worker-metadata-bindings.ts @@ -94,13 +94,6 @@ export function mapWorkerMetadataBindings( }; } break; - case "stream": - { - configObj.stream = { - binding: binding.name, - }; - } - break; case "media": { configObj.media = { @@ -350,6 +343,17 @@ export function mapWorkerMetadataBindings( ]; } break; + case "vpc_network": + { + configObj.vpc_networks = [ + ...(configObj.vpc_networks ?? []), + { + binding: binding.name, + tunnel_id: binding.tunnel_id, + }, + ]; + } + break; default: { configObj.unsafe = { bindings: [...(configObj.unsafe?.bindings ?? []), binding], diff --git a/packages/workers-utils/src/types.ts b/packages/workers-utils/src/types.ts index ec96a43bad03..febc9dfa297a 100644 --- a/packages/workers-utils/src/types.ts +++ b/packages/workers-utils/src/types.ts @@ -29,11 +29,11 @@ import type { CfSecretsStoreSecrets, CfSendEmailBindings, CfService, - CfStreamBinding, CfTailConsumer, CfUnsafeBinding, CfUserLimits, CfVectorize, + CfVpcNetwork, CfVpcService, CfWorkerLoader, CfWorkflow, @@ -63,7 +63,6 @@ export type WorkerMetadataBinding = | { type: "browser"; name: string; raw?: boolean } | { type: "ai"; name: string; staging?: boolean; raw?: boolean } | { type: "images"; name: string; raw?: boolean } - | { type: "stream"; name: string } | { type: "version_metadata"; name: string } | { type: "data_blob"; name: string; part: string } | { type: "kv_namespace"; name: string; namespace_id: string; raw?: boolean } @@ -161,6 +160,11 @@ export type WorkerMetadataBinding = simple: { limit: number; period: 10 | 60 }; } | { type: "vpc_service"; name: string; service_id: string } + | { + type: "vpc_network"; + name: string; + tunnel_id: string; + } | { type: "worker_loader"; name: string; @@ -300,7 +304,6 @@ export type Binding = | ({ type: "browser" } & BindingOmit) | ({ type: "ai" } & BindingOmit) | ({ type: "images" } & BindingOmit) - | ({ type: "stream" } & BindingOmit) | { type: "version_metadata" } | { type: "data_blob"; source: BinaryFile } | ({ type: "durable_object_namespace" } & NameOmit) @@ -322,6 +325,7 @@ export type Binding = | ({ type: "ratelimit" } & NameOmit) | ({ type: "worker_loader" } & BindingOmit) | ({ type: "vpc_service" } & BindingOmit) + | ({ type: "vpc_network" } & BindingOmit) | ({ type: "media" } & BindingOmit) | ({ type: `unsafe_${string}` } & Omit) | { type: "assets" } diff --git a/packages/workers-utils/src/worker.ts b/packages/workers-utils/src/worker.ts index e0306908c9d2..b2d92b3b9f50 100644 --- a/packages/workers-utils/src/worker.ts +++ b/packages/workers-utils/src/worker.ts @@ -148,14 +148,6 @@ export interface CfMediaBinding { remote?: boolean; } -/** - * A binding to Cloudflare Stream - */ -export interface CfStreamBinding { - binding: string; - remote?: boolean; -} - /** * A binding to the Worker Version's metadata */ @@ -276,6 +268,12 @@ export interface CfVpcService { remote?: boolean; } +export interface CfVpcNetwork { + binding: string; + tunnel_id: string; + remote?: boolean; +} + export interface CfAnalyticsEngineDataset { binding: string; dataset?: string; diff --git a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts index d3d6375953ff..d33b74bf8769 100644 --- a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts +++ b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts @@ -75,6 +75,7 @@ describe("normalizeAndValidateConfig()", () => { unsafe_hello_world: [], ratelimits: [], vpc_services: [], + vpc_networks: [], services: [], analytics_engine_datasets: [], route: undefined, @@ -129,7 +130,6 @@ describe("normalizeAndValidateConfig()", () => { compliance_region: undefined, images: undefined, media: undefined, - stream: undefined, } satisfies Config); expect(diagnostics.hasErrors()).toBe(false); expect(diagnostics.hasWarnings()).toBe(false); @@ -2295,69 +2295,6 @@ describe("normalizeAndValidateConfig()", () => { }); }); - // Stream - describe("[stream]", () => { - it("should error if stream is an array", ({ expect }) => { - const { diagnostics } = normalizeAndValidateConfig( - { stream: [] } as unknown as RawConfig, - undefined, - undefined, - { env: undefined } - ); - - expect(diagnostics.hasWarnings()).toBe(false); - expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` - "Processing wrangler configuration: - - The field "stream" should be an object but got []." - `); - }); - - it("should error if stream is a string", ({ expect }) => { - const { diagnostics } = normalizeAndValidateConfig( - { stream: "BAD" } as unknown as RawConfig, - undefined, - undefined, - { env: undefined } - ); - - expect(diagnostics.hasWarnings()).toBe(false); - expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` - "Processing wrangler configuration: - - The field "stream" should be an object but got "BAD"." - `); - }); - - it("should error if stream is a number", ({ expect }) => { - const { diagnostics } = normalizeAndValidateConfig( - { stream: 999 } as unknown as RawConfig, - undefined, - undefined, - { env: undefined } - ); - - expect(diagnostics.hasWarnings()).toBe(false); - expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` - "Processing wrangler configuration: - - The field "stream" should be an object but got 999." - `); - }); - - it("should error if stream is null", ({ expect }) => { - const { diagnostics } = normalizeAndValidateConfig( - { stream: null } as unknown as RawConfig, - undefined, - undefined, - { env: undefined } - ); - - expect(diagnostics.hasWarnings()).toBe(false); - expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` - "Processing wrangler configuration: - - The field "stream" should be an object but got null." - `); - }); - }); - // Worker Version Metadata describe("[version_metadata]", () => { it("should error if version_metadata is an array", ({ expect }) => { @@ -4667,6 +4604,71 @@ describe("normalizeAndValidateConfig()", () => { }); }); + describe("[vpc_networks]", () => { + it("should accept valid bindings with tunnel_id", ({ expect }) => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + vpc_networks: [ + { + binding: "MY_NETWORK", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { + binding: "MY_OTHER_NETWORK", + tunnel_id: "0299295b-b3ac-7760-8246-bca40877b3e0", + }, + ], + } as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(config.vpc_networks).toEqual([ + { + binding: "MY_NETWORK", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { + binding: "MY_OTHER_NETWORK", + tunnel_id: "0299295b-b3ac-7760-8246-bca40877b3e0", + }, + ]); + expect(diagnostics.hasErrors()).toBe(false); + }); + + it("should error if vpc_networks bindings are not valid", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + vpc_networks: [ + {}, + { + binding: "VALID", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { binding: null, tunnel_id: 123, invalid: true }, + { binding: "MISSING_ID" }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "vpc_networks[0]" bindings should have a string "binding" field but got {}. + - "vpc_networks[0]" bindings must have a string "tunnel_id" field but got {}. + - "vpc_networks[2]" bindings should have a string "binding" field but got {"binding":null,"tunnel_id":123,"invalid":true}. + - "vpc_networks[2]" bindings must have a string "tunnel_id" field but got {"binding":null,"tunnel_id":123,"invalid":true}. + - "vpc_networks[3]" bindings must have a string "tunnel_id" field but got {"binding":"MISSING_ID"}." + `); + }); + }); + describe("[unsafe.bindings]", () => { it("should error if unsafe is an array", ({ expect }) => { const { diagnostics } = normalizeAndValidateConfig( From 22906bb8c8726b245997b91e6b03d806630297bf Mon Sep 17 00:00:00 2001 From: Ricardo Antunes Date: Fri, 20 Mar 2026 16:35:07 +0000 Subject: [PATCH 2/3] [miniflare] Add vpc-networks plugin --- packages/miniflare/src/plugins/index.ts | 4 + .../src/plugins/vpc-networks/index.ts | 75 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 packages/miniflare/src/plugins/vpc-networks/index.ts diff --git a/packages/miniflare/src/plugins/index.ts b/packages/miniflare/src/plugins/index.ts index 449f88d05108..006804aacc36 100644 --- a/packages/miniflare/src/plugins/index.ts +++ b/packages/miniflare/src/plugins/index.ts @@ -36,6 +36,7 @@ import { VERSION_METADATA_PLUGIN, VERSION_METADATA_PLUGIN_NAME, } from "./version-metadata"; +import { VPC_NETWORKS_PLUGIN, VPC_NETWORKS_PLUGIN_NAME } from "./vpc-networks"; import { VPC_SERVICES_PLUGIN, VPC_SERVICES_PLUGIN_NAME } from "./vpc-services"; import { WORKER_LOADER_PLUGIN, @@ -64,6 +65,7 @@ export const PLUGINS = { [DISPATCH_NAMESPACE_PLUGIN_NAME]: DISPATCH_NAMESPACE_PLUGIN, [IMAGES_PLUGIN_NAME]: IMAGES_PLUGIN, [VECTORIZE_PLUGIN_NAME]: VECTORIZE_PLUGIN, + [VPC_NETWORKS_PLUGIN_NAME]: VPC_NETWORKS_PLUGIN, [VPC_SERVICES_PLUGIN_NAME]: VPC_SERVICES_PLUGIN, [MTLS_PLUGIN_NAME]: MTLS_PLUGIN, [HELLO_WORLD_PLUGIN_NAME]: HELLO_WORLD_PLUGIN, @@ -128,6 +130,7 @@ export type WorkerOptions = z.input & z.input & z.input & z.input & + z.input & z.input & z.input & z.input & @@ -207,6 +210,7 @@ export * from "./browser-rendering"; export * from "./dispatch-namespace"; export * from "./images"; export * from "./vectorize"; +export * from "./vpc-networks"; export * from "./vpc-services"; export * from "./mtls"; export * from "./hello-world"; diff --git a/packages/miniflare/src/plugins/vpc-networks/index.ts b/packages/miniflare/src/plugins/vpc-networks/index.ts new file mode 100644 index 000000000000..70c46eb889b5 --- /dev/null +++ b/packages/miniflare/src/plugins/vpc-networks/index.ts @@ -0,0 +1,75 @@ +import { z } from "zod"; +import { + getUserBindingServiceName, + Plugin, + ProxyNodeBinding, + remoteProxyClientWorker, + RemoteProxyConnectionString, +} from "../shared"; + +const VpcNetworksSchema = z.object({ + tunnel_id: z.string(), + remoteProxyConnectionString: z + .custom() + .optional(), +}); + +export const VpcNetworksOptionsSchema = z.object({ + vpcNetworks: z.record(VpcNetworksSchema).optional(), +}); + +export const VPC_NETWORKS_PLUGIN_NAME = "vpc-networks"; + +export const VPC_NETWORKS_PLUGIN: Plugin = { + options: VpcNetworksOptionsSchema, + async getBindings(options) { + if (!options.vpcNetworks) { + return []; + } + + return Object.entries(options.vpcNetworks).map( + ([name, { tunnel_id, remoteProxyConnectionString }]) => { + return { + name, + + service: { + name: getUserBindingServiceName( + VPC_NETWORKS_PLUGIN_NAME, + tunnel_id, + remoteProxyConnectionString + ), + }, + }; + } + ); + }, + getNodeBindings(options: z.infer) { + if (!options.vpcNetworks) { + return {}; + } + return Object.fromEntries( + Object.keys(options.vpcNetworks).map((name) => [ + name, + new ProxyNodeBinding(), + ]) + ); + }, + async getServices({ options }) { + if (!options.vpcNetworks) { + return []; + } + + return Object.entries(options.vpcNetworks).map( + ([name, { tunnel_id, remoteProxyConnectionString }]) => { + return { + name: getUserBindingServiceName( + VPC_NETWORKS_PLUGIN_NAME, + tunnel_id, + remoteProxyConnectionString + ), + worker: remoteProxyClientWorker(remoteProxyConnectionString, name), + }; + } + ); + }, +}; From db9567c59420a6078bece5bcd0295e72d180c7a4 Mon Sep 17 00:00:00 2001 From: Ricardo Antunes Date: Fri, 20 Mar 2026 16:35:12 +0000 Subject: [PATCH 3/3] [wrangler] Add vpc_networks binding support --- .../api/startDevWorker/utils.test.ts | 10 +++ .../__tests__/deploy/durable-objects.test.ts | 89 +++++++++++++++++++ .../deploy/get-remote-config-diff.test.ts | 13 +++ .../src/__tests__/type-generation.test.ts | 51 +++++++++++ .../wrangler/src/api/remoteBindings/index.ts | 5 ++ .../wrangler/src/api/startDevWorker/utils.ts | 12 ++- .../deploy/check-remote-secrets-override.ts | 3 +- packages/wrangler/src/deploy/config-diffs.ts | 7 ++ .../create-worker-upload-form.ts | 13 ++- packages/wrangler/src/dev/miniflare/index.ts | 17 ++++ .../wrangler/src/type-generation/index.ts | 34 +++++++ packages/wrangler/src/utils/print-bindings.ts | 25 ++++-- 12 files changed, 268 insertions(+), 11 deletions(-) diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/utils.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/utils.test.ts index b9d72c49a3c3..56555eee7b95 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/utils.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/utils.test.ts @@ -85,6 +85,12 @@ describe("convertConfigBindingsToStartWorkerBindings", () => { service_id: "0199295b-b3ac-7760-8246-bca40877b3e9", }, ], + vpc_networks: [ + { + binding: "MY_VPC_NETWORK", + tunnel_id: "0399295b-b3ac-7760-8246-bca40877b3e1", + }, + ], }); expect(result).toEqual({ AI: { @@ -143,6 +149,10 @@ describe("convertConfigBindingsToStartWorkerBindings", () => { service_id: "0199295b-b3ac-7760-8246-bca40877b3e9", type: "vpc_service", }, + MY_VPC_NETWORK: { + tunnel_id: "0399295b-b3ac-7760-8246-bca40877b3e1", + type: "vpc_network", + }, }); }); diff --git a/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts b/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts index eb3301526667..b09b6cda06ac 100644 --- a/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts +++ b/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts @@ -1213,6 +1213,95 @@ describe("deploy", () => { `); }); }); + describe("vpc_networks", () => { + it("should upload VPC network bindings", async ({ expect }) => { + writeWranglerConfig({ + vpc_networks: [ + { + binding: "VPC_NETWORK", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + ], + }); + await fs.promises.writeFile("index.js", `export default {};`); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ + { + type: "vpc_network", + name: "VPC_NETWORK", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + ], + }); + + await runWrangler("deploy index.js"); + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Total Upload: xx KiB / gzip: xx KiB + Worker Startup Time: 100 ms + Your Worker has access to the following bindings: + Binding Resource + env.VPC_NETWORK (0199295b-b3ac-7760-8246-bca40877b3e9) VPC Network + + Uploaded test-name (TIMINGS) + Deployed test-name triggers (TIMINGS) + https://test-name.test-sub-domain.workers.dev + Current Version ID: Galaxy-Class" + `); + }); + + it("should upload multiple VPC network bindings", async ({ expect }) => { + writeWranglerConfig({ + vpc_networks: [ + { + binding: "VPC_NET_A", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { + binding: "VPC_NET_B", + tunnel_id: "0299295b-b3ac-7760-8246-bca40877b3e0", + }, + ], + }); + await fs.promises.writeFile("index.js", `export default {};`); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ + { + type: "vpc_network", + name: "VPC_NET_A", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { + type: "vpc_network", + name: "VPC_NET_B", + tunnel_id: "0299295b-b3ac-7760-8246-bca40877b3e0", + }, + ], + }); + + await runWrangler("deploy index.js"); + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Total Upload: xx KiB / gzip: xx KiB + Worker Startup Time: 100 ms + Your Worker has access to the following bindings: + Binding Resource + env.VPC_NET_A (0199295b-b3ac-7760-8246-bca40877b3e9) VPC Network + env.VPC_NET_B (0299295b-b3ac-7760-8246-bca40877b3e0) VPC Network + + Uploaded test-name (TIMINGS) + Deployed test-name triggers (TIMINGS) + https://test-name.test-sub-domain.workers.dev + Current Version ID: Galaxy-Class" + `); + }); + }); describe("mtls_certificates", () => { it("should upload mtls_certificate bindings", async ({ expect }) => { writeWranglerConfig({ diff --git a/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts b/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts index 22867bd4663f..ef9f69b75a13 100644 --- a/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts +++ b/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts @@ -387,6 +387,12 @@ describe("getRemoteConfigsDiff", () => { service_id: "my-vpc", }, ], + vpc_networks: [ + { + binding: "MY_NETWORK", + tunnel_id: "my-tunnel", + }, + ], }, { name: "my-worker-id", @@ -495,6 +501,13 @@ describe("getRemoteConfigsDiff", () => { remote: true, }, ], + vpc_networks: [ + { + binding: "MY_NETWORK", + tunnel_id: "my-tunnel", + remote: true, + }, + ], } as unknown as Config ); expect(diff).toBeNull(); diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index b81b149106cd..fa31a98c593f 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -3301,6 +3301,57 @@ describe("generate types", () => { " `); }); + + it("should generate types for VPC networks bindings", async ({ expect }) => { + fs.writeFileSync( + "./index.ts", + `export default { async fetch(request, env) { return await env.VPC_NET.fetch(request); } };` + ); + fs.writeFileSync( + "./wrangler.json", + JSON.stringify({ + compatibility_date: "2022-01-12", + name: "test-vpc-networks", + main: "./index.ts", + vpc_networks: [ + { + binding: "VPC_NET", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { + binding: "VPC_NET_B", + tunnel_id: "0299295b-b3ac-7760-8246-bca40877b3e0", + }, + ], + }), + "utf-8" + ); + + await runWrangler("types --include-runtime=false"); + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Generating project types... + + declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env { + VPC_NET: Fetcher; + VPC_NET_B: Fetcher; + } + } + interface Env extends Cloudflare.Env {} + + ──────────────────────────────────────────────────────────── + ✨ Types written to worker-configuration.d.ts + + 📣 Remember to rerun 'wrangler types' after you change your wrangler.json file. + " + `); + }); }); describe("pipeline schema type generation", () => { diff --git a/packages/wrangler/src/api/remoteBindings/index.ts b/packages/wrangler/src/api/remoteBindings/index.ts index a7df42d1a6a9..da0bee651ad8 100644 --- a/packages/wrangler/src/api/remoteBindings/index.ts +++ b/packages/wrangler/src/api/remoteBindings/index.ts @@ -30,6 +30,11 @@ export function pickRemoteBindings( return true; } + if (binding.type === "vpc_network") { + // VPC Network is always remote + return true; + } + return "remote" in binding && binding["remote"]; }) ); diff --git a/packages/wrangler/src/api/startDevWorker/utils.ts b/packages/wrangler/src/api/startDevWorker/utils.ts index 89b5281254cd..ff3f04c03da6 100644 --- a/packages/wrangler/src/api/startDevWorker/utils.ts +++ b/packages/wrangler/src/api/startDevWorker/utils.ts @@ -133,7 +133,7 @@ export function convertConfigToBindings( output[binding] = { type: "kv_namespace", ...x, - id: usePreviewIds ? (x.preview_id ?? x.id) : x.id, + id: usePreviewIds ? x.preview_id ?? x.id : x.id, }; } break; @@ -215,7 +215,7 @@ export function convertConfigToBindings( type: "r2_bucket", ...x, bucket_name: usePreviewIds - ? (x.preview_bucket_name ?? x.bucket_name) + ? x.preview_bucket_name ?? x.bucket_name : x.bucket_name, }; } @@ -227,7 +227,7 @@ export function convertConfigToBindings( type: "d1", ...x, database_id: usePreviewIds - ? (x.preview_database_id ?? x.database_id) + ? x.preview_database_id ?? x.database_id : x.database_id, }; } @@ -361,6 +361,12 @@ export function convertConfigToBindings( } break; } + case "vpc_networks": { + for (const { binding, ...x } of info) { + output[binding] = { type: "vpc_network", ...x }; + } + break; + } case "media": { const { binding, ...x } = info; output[binding] = { type: "media", ...x }; diff --git a/packages/wrangler/src/deploy/check-remote-secrets-override.ts b/packages/wrangler/src/deploy/check-remote-secrets-override.ts index 3ccf8804e3ce..258daf362748 100644 --- a/packages/wrangler/src/deploy/check-remote-secrets-override.ts +++ b/packages/wrangler/src/deploy/check-remote-secrets-override.ts @@ -115,7 +115,8 @@ function extractBindingNames(config: Config): string[] { case "services": case "mtls_certificates": case "dispatch_namespaces": - case "vpc_services": { + case "vpc_services": + case "vpc_networks": { const value: Config[typeof key] = untypedValue; return (value ?? []).map((workflowBinding) => workflowBinding.binding); } diff --git a/packages/wrangler/src/deploy/config-diffs.ts b/packages/wrangler/src/deploy/config-diffs.ts index c22b6f035227..09144d9b2920 100644 --- a/packages/wrangler/src/deploy/config-diffs.ts +++ b/packages/wrangler/src/deploy/config-diffs.ts @@ -33,6 +33,7 @@ const reorderableBindings = { unsafe_hello_world: true, worker_loaders: true, vpc_services: true, + vpc_networks: true, // Wrapper objects containing binding arrays durable_objects: true, @@ -170,6 +171,12 @@ function removeRemoteConfigFieldFromBindings(normalizedConfig: Config): void { ); } + if (normalizedConfig.vpc_networks?.length) { + normalizedConfig.vpc_networks = normalizedConfig.vpc_networks.map( + ({ remote: _, ...binding }) => binding + ); + } + if (normalizedConfig.workflows?.length) { normalizedConfig.workflows = normalizedConfig.workflows.map( ({ remote: _, ...binding }) => binding diff --git a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts index 3c085e50cb99..8e59b70479d9 100644 --- a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts +++ b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts @@ -137,6 +137,7 @@ export function createWorkerUploadForm( ); const ratelimits = extractBindingsOfType("ratelimit", bindings); const vpc_services = extractBindingsOfType("vpc_service", bindings); + const vpc_networks = extractBindingsOfType("vpc_network", bindings); const services = extractBindingsOfType("service", bindings); const analytics_engine_datasets = extractBindingsOfType( "analytics_engine", @@ -365,6 +366,14 @@ export function createWorkerUploadForm( }); }); + vpc_networks.forEach(({ binding, tunnel_id }) => { + metadataBindings.push({ + name: binding, + type: "vpc_network", + tunnel_id, + }); + }); + services.forEach( ({ binding, @@ -457,7 +466,7 @@ export function createWorkerUploadForm( ? source.contents : readFileSync(source.path as string), ], - "path" in source ? (source.path ?? name) : name, + "path" in source ? source.path ?? name : name, { type: "application/wasm" } ) ); @@ -557,7 +566,7 @@ export function createWorkerUploadForm( ? source.contents : readFileSync(source.path as string), ], - "path" in source ? (source.path ?? name) : name, + "path" in source ? source.path ?? name : name, { type: "application/octet-stream" } ) ); diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index 5710af81149e..ec29a8f7eb65 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -415,6 +415,7 @@ type WorkerOptionsBindings = Pick< | "browserRendering" | "vectorize" | "vpcServices" + | "vpcNetworks" | "dispatchNamespaces" | "mtlsCertificates" | "helloWorld" @@ -480,6 +481,7 @@ export function buildMiniflareBindingOptions( const mtlsCertificates = extractBindingsOfType("mtls_certificate", bindings); const vectorizeBindings = extractBindingsOfType("vectorize", bindings); const vpcServices = extractBindingsOfType("vpc_service", bindings); + const vpcNetworks = extractBindingsOfType("vpc_network", bindings); const secretsStoreSecrets = extractBindingsOfType( "secrets_store_secret", bindings @@ -815,6 +817,21 @@ export function buildMiniflareBindingOptions( ]; }) ), + vpcNetworks: Object.fromEntries( + vpcNetworks.map((vpc) => { + warnOrError("vpc_network", vpc.remote, "always-remote"); + return [ + vpc.binding, + { + tunnel_id: vpc.tunnel_id, + remoteProxyConnectionString: + vpc.remote && remoteProxyConnectionString + ? remoteProxyConnectionString + : undefined, + }, + ]; + }) + ), dispatchNamespaces: Object.fromEntries( dispatchNamespaces.map((dispatchNamespace) => { diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 77874f43fc3a..abca566b8919 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -1853,6 +1853,21 @@ function collectCoreBindings( addBinding(vpcService.binding, "Fetcher", "vpc_services", envName); } + for (const [index, vpcNetwork] of (env.vpc_networks ?? []).entries()) { + if (!vpcNetwork.binding) { + throwMissingBindingError({ + binding: vpcNetwork, + bindingType: "vpc_networks", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + addBinding(vpcNetwork.binding, "Fetcher", "vpc_networks", envName); + } + // Pipelines handled separately for async schema fetching if (env.logfwdr?.bindings?.length) { @@ -2771,6 +2786,25 @@ function collectCoreBindingsPerEnvironment( }); } + for (const [index, vpcNetwork] of (env.vpc_networks ?? []).entries()) { + if (!vpcNetwork.binding) { + throwMissingBindingError({ + binding: vpcNetwork, + bindingType: "vpc_networks", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + bindings.push({ + bindingCategory: "vpc_networks", + name: vpcNetwork.binding, + type: "Fetcher", + }); + } + // Pipelines handled separately for async schema fetching if (env.logfwdr?.bindings?.length) { diff --git a/packages/wrangler/src/utils/print-bindings.ts b/packages/wrangler/src/utils/print-bindings.ts index 5df301da18e3..a6b099d54b22 100644 --- a/packages/wrangler/src/utils/print-bindings.ts +++ b/packages/wrangler/src/utils/print-bindings.ts @@ -92,6 +92,7 @@ export function printBindings( ); const services = extractBindingsOfType("service", bindings); const vpc_services = extractBindingsOfType("vpc_service", bindings); + const vpc_networks = extractBindingsOfType("vpc_network", bindings); const analytics_engine_datasets = extractBindingsOfType( "analytics_engine", bindings @@ -293,7 +294,7 @@ export function printBindings( const value = typeof database_id == "symbol" ? database_id - : (preview_database_id ?? database_name ?? database_id); + : preview_database_id ?? database_name ?? database_id; return { name: binding, @@ -353,6 +354,22 @@ export function printBindings( ); } + if (vpc_networks.length > 0) { + output.push( + ...vpc_networks.map(({ binding, tunnel_id, remote }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("vpc_network"), + value: tunnel_id, + mode: getMode({ + isSimulatedLocally: + remote && !context.remoteBindingsDisabled ? false : undefined, + }), + }; + }) + ); + } + if (r2_buckets.length > 0) { output.push( ...r2_buckets.map(({ binding, bucket_name, jurisdiction, remote }) => { @@ -736,9 +753,7 @@ export function printBindings( const maxValueLength = Math.max( ...output.map((b) => - typeof b.value === "symbol" - ? "inherited".length - : (b.value?.length ?? 0) + typeof b.value === "symbol" ? "inherited".length : b.value?.length ?? 0 ) ); const maxNameLength = Math.max(...output.map((b) => b.name.length)); @@ -785,7 +800,7 @@ export function printBindings( const bindingValue = dim( typeof binding.value === "symbol" ? chalk.italic("inherited") - : (binding.value ?? "") + : binding.value ?? "" ); const bindingString = padEndAnsi( `${white(`env.${binding.name}`)}${binding.value && !shouldWrap ? ` (${bindingValue})` : ""}`,