From 1412c56437cf6f2253d54e6b81ba12a3a83ae082 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 31 Mar 2026 13:42:06 +0530 Subject: [PATCH 01/28] [workers-utils] feat: add Flagship feature flag binding types and config Add the Flagship binding to the shared config and type system: - Add flagship binding type to EnvironmentNonInheritable (environment.ts) - Add default value flagship: [] (config.ts) - Add friendly names, notInheritable() validation, and validateFlagshipBinding (validation.ts) - Add CfFlagship interface and CfWorkerInit.bindings.flagship field (worker.ts) - Add flagship to WorkerMetadataBinding and Binding union types (types.ts) - Add flagship case in mapWorkerMetadataBindings (map-worker-metadata-bindings.ts) --- packages/workers-utils/src/config/config.ts | 1 + .../workers-utils/src/config/environment.ts | 17 +++++++ .../workers-utils/src/config/validation.ts | 46 +++++++++++++++++++ .../src/map-worker-metadata-bindings.ts | 10 ++++ packages/workers-utils/src/types.ts | 7 +++ packages/workers-utils/src/worker.ts | 6 +++ packages/wrangler/src/deploy/config-diffs.ts | 1 + 7 files changed, 88 insertions(+) diff --git a/packages/workers-utils/src/config/config.ts b/packages/workers-utils/src/config/config.ts index a937cdf651..fc3e783715 100644 --- a/packages/workers-utils/src/config/config.ts +++ b/packages/workers-utils/src/config/config.ts @@ -347,6 +347,7 @@ export const defaultWranglerConfig: Config = { media: undefined, version_metadata: undefined, unsafe_hello_world: [], + flagship: [], ratelimits: [], worker_loaders: [], diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index 06ddde671e..99f7aa524f 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -1336,6 +1336,23 @@ export interface EnvironmentNonInheritable { enable_timer?: boolean; }[]; + /** + * Specifies Flagship feature flag bindings 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 + */ + flagship: { + /** The binding name used to refer to the bound Flagship service. */ + binding: string; + + /** The Flagship app ID to bind to. */ + app_id: string; + }[]; + /** * Specifies rate limit bindings that are bound to this Worker environment. * diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index fe739e7fab..70e78ceb33 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -104,6 +104,7 @@ export type ConfigBindingFieldName = | "ratelimits" | "assets" | "unsafe_hello_world" + | "flagship" | "worker_loaders" | "vpc_services"; @@ -143,6 +144,7 @@ export const friendlyBindingNames: Record = { ratelimits: "Rate Limit", assets: "Assets", unsafe_hello_world: "Hello World", + flagship: "Flagship", worker_loaders: "Worker Loader", vpc_services: "VPC Service", } as const; @@ -184,6 +186,7 @@ const bindingTypeFriendlyNames: Record = { secrets_store_secret: "Secrets Store Secret", logfwdr: "logfwdr", unsafe_hello_world: "Hello World", + flagship: "Flagship", ratelimit: "Rate Limit", worker_loader: "Worker Loader", vpc_service: "VPC Service", @@ -1881,6 +1884,16 @@ function normalizeAndValidateEnvironment( validateBindingArray(envName, validateHelloWorldBinding), [] ), + flagship: notInheritable( + diagnostics, + topLevelEnv, + rawConfig, + rawEnv, + envName, + "flagship", + validateBindingArray(envName, validateFlagshipBinding), + [] + ), worker_loaders: notInheritable( diagnostics, topLevelEnv, @@ -4696,6 +4709,39 @@ const validateHelloWorldBinding: ValidatorFn = (diagnostics, field, value) => { return isValid; }; +const validateFlagshipBinding: ValidatorFn = (diagnostics, field, value) => { + if (typeof value !== "object" || value === null) { + diagnostics.errors.push( + `"flagship" bindings should be objects, but got ${JSON.stringify(value)}` + ); + return false; + } + let isValid = true; + if (!isRequiredProperty(value, "binding", "string")) { + diagnostics.errors.push( + `"${field}" bindings must have a string "binding" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + if (!isRequiredProperty(value, "app_id", "string")) { + diagnostics.errors.push( + `"${field}" bindings must have a string "app_id" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + + validateAdditionalProperties(diagnostics, field, Object.keys(value), [ + "binding", + "app_id", + ]); + + return isValid; +}; + const validateWorkerLoaderBinding: ValidatorFn = ( diagnostics, field, diff --git a/packages/workers-utils/src/map-worker-metadata-bindings.ts b/packages/workers-utils/src/map-worker-metadata-bindings.ts index 2b256fcb9a..70fdbaea0f 100644 --- a/packages/workers-utils/src/map-worker-metadata-bindings.ts +++ b/packages/workers-utils/src/map-worker-metadata-bindings.ts @@ -142,6 +142,16 @@ export function mapWorkerMetadataBindings( ]; break; } + case "flagship": { + configObj.flagship = [ + ...(configObj.flagship ?? []), + { + binding: binding.name, + app_id: binding.app_id, + }, + ]; + break; + } case "service": { configObj.services = [ diff --git a/packages/workers-utils/src/types.ts b/packages/workers-utils/src/types.ts index 48b3eebc97..21f4c57a02 100644 --- a/packages/workers-utils/src/types.ts +++ b/packages/workers-utils/src/types.ts @@ -16,6 +16,7 @@ import type { CfDispatchNamespace, CfDurableObject, CfDurableObjectMigrations, + CfFlagship, CfHelloWorld, CfHyperdrive, CfImagesBinding, @@ -158,6 +159,11 @@ export type WorkerMetadataBinding = name: string; enable_timer?: boolean; } + | { + type: "flagship"; + name: string; + app_id: string; + } | { type: "ratelimit"; name: string; @@ -325,6 +331,7 @@ export type Binding = | ({ type: "secrets_store_secret" } & BindingOmit) | ({ type: "logfwdr" } & NameOmit) | ({ type: "unsafe_hello_world" } & BindingOmit) + | ({ type: "flagship" } & BindingOmit) | ({ type: "ratelimit" } & NameOmit) | ({ type: "worker_loader" } & BindingOmit) | ({ type: "vpc_service" } & BindingOmit) diff --git a/packages/workers-utils/src/worker.ts b/packages/workers-utils/src/worker.ts index 37fd9ddefc..157f78cb13 100644 --- a/packages/workers-utils/src/worker.ts +++ b/packages/workers-utils/src/worker.ts @@ -253,6 +253,11 @@ export interface CfHelloWorld { enable_timer?: boolean; } +export interface CfFlagship { + binding: string; + app_id: string; +} + export interface CfWorkerLoader { binding: string; } @@ -425,6 +430,7 @@ export interface CfWorkerInit { */ sourceMaps: CfWorkerSourceMap[] | undefined; + containers: { class_name: string }[] | undefined; migrations: CfDurableObjectMigrations | undefined; diff --git a/packages/wrangler/src/deploy/config-diffs.ts b/packages/wrangler/src/deploy/config-diffs.ts index b68af06ba4..ccc75b9f3b 100644 --- a/packages/wrangler/src/deploy/config-diffs.ts +++ b/packages/wrangler/src/deploy/config-diffs.ts @@ -33,6 +33,7 @@ const reorderableBindings = { ratelimits: true, analytics_engine_datasets: true, unsafe_hello_world: true, + flagship: true, worker_loaders: true, vpc_services: true, From 1c08e92011ba536a04cd7c19a99acfbb9a026370 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 31 Mar 2026 13:42:25 +0530 Subject: [PATCH 02/28] [miniflare] feat: add Flagship remote-only plugin Create a new miniflare plugin for Flagship feature flag bindings: - New plugins/flagship/index.ts with zod schema, getBindings, getNodeBindings, and getServices (remote proxy only, no local simulation) - Register FLAGSHIP_PLUGIN in the PLUGINS object and WorkerOptions type --- .../miniflare/src/plugins/flagship/index.ts | 85 +++++++++++++++++++ packages/miniflare/src/plugins/index.ts | 4 + 2 files changed, 89 insertions(+) create mode 100644 packages/miniflare/src/plugins/flagship/index.ts diff --git a/packages/miniflare/src/plugins/flagship/index.ts b/packages/miniflare/src/plugins/flagship/index.ts new file mode 100644 index 0000000000..bebb4db217 --- /dev/null +++ b/packages/miniflare/src/plugins/flagship/index.ts @@ -0,0 +1,85 @@ +import { z } from "zod"; +import { Worker_Binding } from "../../runtime"; +import { + getUserBindingServiceName, + Plugin, + ProxyNodeBinding, + remoteProxyClientWorker, + RemoteProxyConnectionString, +} from "../shared"; + +const FlagshipSchema = z.object({ + app_id: z.string(), + remoteProxyConnectionString: z + .custom() + .optional(), +}); + +export const FlagshipOptionsSchema = z.object({ + flagship: z.record(FlagshipSchema).optional(), +}); + +export const FLAGSHIP_PLUGIN_NAME = "flagship"; + +export const FLAGSHIP_PLUGIN: Plugin = { + options: FlagshipOptionsSchema, + async getBindings(options) { + if (!options.flagship) { + return []; + } + + return Object.entries(options.flagship).map( + ([name, config]) => ({ + name, + service: { + name: getUserBindingServiceName( + FLAGSHIP_PLUGIN_NAME, + name, + config.remoteProxyConnectionString + ), + }, + }) + ); + }, + getNodeBindings(options: z.infer) { + if (!options.flagship) { + return {}; + } + return Object.fromEntries( + Object.keys(options.flagship).map((name) => [ + name, + new ProxyNodeBinding(), + ]) + ); + }, + async getServices({ options }) { + if (!options.flagship) { + return []; + } + + // Flagship is a remote-only binding — flag evaluation always happens + // against the deployed Flagship service. When remoteProxyConnectionString + // is provided (i.e. `wrangler dev --remote`), the proxy client worker + // forwards requests to the real service. Without it, there is no local + // simulation available. + return Object.entries(options.flagship).flatMap(([name, config]) => { + if (!config.remoteProxyConnectionString) { + return []; + } + + return [ + { + name: getUserBindingServiceName( + FLAGSHIP_PLUGIN_NAME, + name, + config.remoteProxyConnectionString + ), + worker: remoteProxyClientWorker( + config.remoteProxyConnectionString, + name + ), + }, + ]; + }); + }, +}; diff --git a/packages/miniflare/src/plugins/index.ts b/packages/miniflare/src/plugins/index.ts index 903e7cf17d..6a8d007969 100644 --- a/packages/miniflare/src/plugins/index.ts +++ b/packages/miniflare/src/plugins/index.ts @@ -21,6 +21,7 @@ import { } from "./dispatch-namespace"; import { DURABLE_OBJECTS_PLUGIN, DURABLE_OBJECTS_PLUGIN_NAME } from "./do"; import { EMAIL_PLUGIN, EMAIL_PLUGIN_NAME } from "./email"; +import { FLAGSHIP_PLUGIN, FLAGSHIP_PLUGIN_NAME } from "./flagship"; import { HELLO_WORLD_PLUGIN, HELLO_WORLD_PLUGIN_NAME } from "./hello-world"; import { HYPERDRIVE_PLUGIN, HYPERDRIVE_PLUGIN_NAME } from "./hyperdrive"; import { IMAGES_PLUGIN, IMAGES_PLUGIN_NAME } from "./images"; @@ -71,6 +72,7 @@ export const PLUGINS = { [VPC_SERVICES_PLUGIN_NAME]: VPC_SERVICES_PLUGIN, [MTLS_PLUGIN_NAME]: MTLS_PLUGIN, [HELLO_WORLD_PLUGIN_NAME]: HELLO_WORLD_PLUGIN, + [FLAGSHIP_PLUGIN_NAME]: FLAGSHIP_PLUGIN, [WORKER_LOADER_PLUGIN_NAME]: WORKER_LOADER_PLUGIN, [MEDIA_PLUGIN_NAME]: MEDIA_PLUGIN, [VERSION_METADATA_PLUGIN_NAME]: VERSION_METADATA_PLUGIN, @@ -137,6 +139,7 @@ export type WorkerOptions = z.input & z.input & z.input & z.input & + z.input & z.input & z.input & z.input; @@ -217,6 +220,7 @@ export * from "./vectorize"; export * from "./vpc-services"; export * from "./mtls"; export * from "./hello-world"; +export * from "./flagship"; export * from "./worker-loader"; export * from "./media"; export * from "./version-metadata"; From 17b097b27b41bd012c7416b381ddb9d4d320f4fa Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 31 Mar 2026 13:42:35 +0530 Subject: [PATCH 03/28] [wrangler] feat: wire Flagship binding through deployment and dev pipelines Add Flagship binding support across the deployment and dev subsystems: - Add to getBindings() and createWorkerUploadForm() (deployment-bundle) - Wire through dev.ts, start-dev.ts, and miniflare/index.ts (dev wiring) - Add both convert functions in startDevWorker/utils.ts - Add CLI display in print-bindings.ts (always remote mode) - Add to ValidKeys exclusion in add-created-resource-config.ts - Add flagship: [] default in secret/index.ts draft worker --- packages/wrangler/src/api/startDevWorker/utils.ts | 6 ++++++ .../deployment-bundle/create-worker-upload-form.ts | 9 +++++++++ packages/wrangler/src/dev/miniflare/index.ts | 11 +++++++++++ .../src/utils/add-created-resource-config.ts | 1 + packages/wrangler/src/utils/print-bindings.ts | 14 ++++++++++++++ 5 files changed, 41 insertions(+) diff --git a/packages/wrangler/src/api/startDevWorker/utils.ts b/packages/wrangler/src/api/startDevWorker/utils.ts index a5b28b5c20..6083490a79 100644 --- a/packages/wrangler/src/api/startDevWorker/utils.ts +++ b/packages/wrangler/src/api/startDevWorker/utils.ts @@ -355,6 +355,12 @@ export function convertConfigToBindings( } break; } + case "flagship": { + for (const { binding, ...x } of info) { + output[binding] = { type: "flagship", ...x }; + } + break; + } case "ratelimits": { for (const { name, ...x } of info) { output[name] = { type: "ratelimit", ...x }; 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 523e7af07c..c55ca96529 100644 --- a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts +++ b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts @@ -140,6 +140,7 @@ export function createWorkerUploadForm( "unsafe_hello_world", bindings ); + const flagship = extractBindingsOfType("flagship", bindings); const ratelimits = extractBindingsOfType("ratelimit", bindings); const vpc_services = extractBindingsOfType("vpc_service", bindings); const services = extractBindingsOfType("service", bindings); @@ -383,6 +384,14 @@ export function createWorkerUploadForm( }); }); + flagship.forEach(({ binding, app_id }) => { + metadataBindings.push({ + name: binding, + type: "flagship", + app_id, + }); + }); + ratelimits.forEach(({ name, namespace_id, simple }) => { metadataBindings.push({ name, diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index c67c8cc0bd..70b5d6b6f1 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -420,6 +420,7 @@ type WorkerOptionsBindings = Pick< | "dispatchNamespaces" | "mtlsCertificates" | "helloWorld" + | "flagship" | "workerLoaders" | "unsafeBindings" | "additionalUnboundDurableObjects" @@ -491,6 +492,7 @@ export function buildMiniflareBindingOptions( "unsafe_hello_world", bindings ); + const flagshipBindings = extractBindingsOfType("flagship", bindings); const workerLoaders = extractBindingsOfType("worker_loader", bindings); const sendEmailBindings = extractBindingsOfType("send_email", bindings); // Extract both regular and unsafe ratelimit bindings @@ -780,6 +782,15 @@ export function buildMiniflareBindingOptions( helloWorld: Object.fromEntries( helloWorldBindings.map((binding) => [binding.binding, binding]) ), + flagship: Object.fromEntries( + flagshipBindings.map((binding) => [ + binding.binding, + { + app_id: binding.app_id, + remoteProxyConnectionString, + }, + ]) + ), workerLoaders: Object.fromEntries( workerLoaders.map(({ binding }) => [binding, {}]) ), diff --git a/packages/wrangler/src/utils/add-created-resource-config.ts b/packages/wrangler/src/utils/add-created-resource-config.ts index 3c45f89c71..b36b241cf7 100644 --- a/packages/wrangler/src/utils/add-created-resource-config.ts +++ b/packages/wrangler/src/utils/add-created-resource-config.ts @@ -42,6 +42,7 @@ type ValidKeys = Exclude< | "dispatch_namespaces" | "secrets_store_secrets" | "unsafe_hello_world" + | "flagship" >; export const sharedResourceCreationArgs = { diff --git a/packages/wrangler/src/utils/print-bindings.ts b/packages/wrangler/src/utils/print-bindings.ts index 9409b0ef0c..6c5381a4ac 100644 --- a/packages/wrangler/src/utils/print-bindings.ts +++ b/packages/wrangler/src/utils/print-bindings.ts @@ -135,6 +135,7 @@ export function printBindings( "unsafe_hello_world", bindings ); + const flagship = extractBindingsOfType("flagship", bindings); const media = extractBindingsOfType("media", bindings); const worker_loaders = extractBindingsOfType("worker_loader", bindings); @@ -441,6 +442,19 @@ export function printBindings( ); } + if (flagship.length > 0) { + output.push( + ...flagship.map(({ binding, app_id }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("flagship"), + value: app_id, + mode: getMode({ isSimulatedLocally: false }), + }; + }) + ); + } + if (services.length > 0) { output.push( ...services.map(({ binding, service, entrypoint, remote }) => { From 8756a6b488f3ed9b2174271a75fda70c0f869d51 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 31 Mar 2026 13:42:44 +0530 Subject: [PATCH 04/28] [wrangler] feat: add Flagship type generation and OAuth scopes - Add flagship -> Flags type mapping in both type generation code paths so `wrangler types` emits FLAGS: Flags for configured flagship bindings - Add flagship:read and flagship:write to DefaultScopes for wrangler login --- .../wrangler/src/type-generation/index.ts | 34 +++++++++++++++++++ packages/wrangler/src/user/user.ts | 2 ++ 2 files changed, 36 insertions(+) diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 7ee780d896..7c9f5daab9 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -1819,6 +1819,21 @@ function collectCoreBindings( ); } + for (const [index, fs] of (env.flagship ?? []).entries()) { + if (!fs.binding) { + throwMissingBindingError({ + binding: fs, + bindingType: "flagship", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + addBinding(fs.binding, "Flags", "flagship", envName); + } + for (const [index, ratelimit] of (env.ratelimits ?? []).entries()) { if (!ratelimit.name) { throwMissingBindingError({ @@ -2730,6 +2745,25 @@ function collectCoreBindingsPerEnvironment( }); } + for (const [index, fs] of (env.flagship ?? []).entries()) { + if (!fs.binding) { + throwMissingBindingError({ + binding: fs, + bindingType: "flagship", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + bindings.push({ + bindingCategory: "flagship", + name: fs.binding, + type: "Flags", + }); + } + for (const [index, ratelimit] of (env.ratelimits ?? []).entries()) { if (!ratelimit.name) { throwMissingBindingError({ diff --git a/packages/wrangler/src/user/user.ts b/packages/wrangler/src/user/user.ts index 3337da9508..ac5bdeff67 100644 --- a/packages/wrangler/src/user/user.ts +++ b/packages/wrangler/src/user/user.ts @@ -374,6 +374,8 @@ const DefaultScopes = { "See and change Cloudflare Pipelines configurations and data", "secrets_store:write": "See and change secrets + stores within the Secrets Store", + "flagship:read": "See Flagship feature flags and apps", + "flagship:write": "See and change Flagship feature flags and apps", "containers:write": "Manage Workers Containers", "cloudchamber:write": "Manage Cloudchamber", "connectivity:admin": From 48b61a1daa82f02ce5641c077b26bbecf99121f0 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 31 Mar 2026 13:42:49 +0530 Subject: [PATCH 05/28] chore: add changeset for Flagship binding support --- .changeset/add-flagship-binding.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/add-flagship-binding.md diff --git a/.changeset/add-flagship-binding.md b/.changeset/add-flagship-binding.md new file mode 100644 index 0000000000..4584c6fda2 --- /dev/null +++ b/.changeset/add-flagship-binding.md @@ -0,0 +1,7 @@ +--- +"miniflare": patch +"wrangler": patch +"@cloudflare/workers-utils": patch +--- + +feat: add Flagship feature flag binding support From 1fc78cb177711895cc00b4fb378145bc6f92e4db Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 31 Mar 2026 15:22:21 +0530 Subject: [PATCH 06/28] [miniflare] fix: flagship plugin getServices should always create service entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the pattern used by AI and VPC plugins — pass the potentially-undefined remoteProxyConnectionString through to remoteProxyClientWorker() which handles the undefined case gracefully, instead of short-circuiting with an empty return. --- .../miniflare/src/plugins/flagship/index.ts | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/miniflare/src/plugins/flagship/index.ts b/packages/miniflare/src/plugins/flagship/index.ts index bebb4db217..18d019e99b 100644 --- a/packages/miniflare/src/plugins/flagship/index.ts +++ b/packages/miniflare/src/plugins/flagship/index.ts @@ -57,29 +57,17 @@ export const FLAGSHIP_PLUGIN: Plugin = { return []; } - // Flagship is a remote-only binding — flag evaluation always happens - // against the deployed Flagship service. When remoteProxyConnectionString - // is provided (i.e. `wrangler dev --remote`), the proxy client worker - // forwards requests to the real service. Without it, there is no local - // simulation available. - return Object.entries(options.flagship).flatMap(([name, config]) => { - if (!config.remoteProxyConnectionString) { - return []; - } - - return [ - { + return Object.entries(options.flagship).map( + ([name, { remoteProxyConnectionString }]) => { + return { name: getUserBindingServiceName( FLAGSHIP_PLUGIN_NAME, name, - config.remoteProxyConnectionString - ), - worker: remoteProxyClientWorker( - config.remoteProxyConnectionString, - name + remoteProxyConnectionString ), - }, - ]; - }); + worker: remoteProxyClientWorker(remoteProxyConnectionString, name), + }; + } + ); }, }; From a1a828caffba5df2ca0054eae62f23a08d7e64ba Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 31 Mar 2026 15:28:24 +0530 Subject: [PATCH 07/28] [workers-utils] fix: add flagship to empty config defaults test expectation --- .../config/validation/normalize-and-validate-config.test.ts | 1 + 1 file changed, 1 insertion(+) 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 1dd1d00678..9312fd0fb4 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()", () => { r2_buckets: [], secrets_store_secrets: [], unsafe_hello_world: [], + flagship: [], ratelimits: [], vpc_services: [], services: [], From 505ad9e942635ebec5df37998356b7eef0baaf59 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 31 Mar 2026 15:29:07 +0530 Subject: [PATCH 08/28] [workers-utils] fix: add flagship to safeBindings in validateUnsafeBinding Warn users who define flagship as an unsafe binding that it is directly supported by wrangler, matching the pattern of other safe binding types. --- packages/workers-utils/src/config/validation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index 70e78ceb33..b9c242883b 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -2971,6 +2971,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => { "pipeline", "worker_loader", "vpc_service", + "flagship", "stream", "media", ]; From 0556a03baf1e550ece5a5824197e98339231a7b3 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 31 Mar 2026 15:34:37 +0530 Subject: [PATCH 09/28] chore: fix lint and formatting issues - Rename 'fs' to 'flagshipBinding' in type-generation to avoid shadowing - Fix formatting in worker.ts and type-generation/index.ts --- packages/workers-utils/src/worker.ts | 1 - packages/wrangler/src/type-generation/index.ts | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/workers-utils/src/worker.ts b/packages/workers-utils/src/worker.ts index 157f78cb13..5359fd8fdd 100644 --- a/packages/workers-utils/src/worker.ts +++ b/packages/workers-utils/src/worker.ts @@ -430,7 +430,6 @@ export interface CfWorkerInit { */ sourceMaps: CfWorkerSourceMap[] | undefined; - containers: { class_name: string }[] | undefined; migrations: CfDurableObjectMigrations | undefined; diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 7c9f5daab9..2702a45348 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -1819,10 +1819,10 @@ function collectCoreBindings( ); } - for (const [index, fs] of (env.flagship ?? []).entries()) { - if (!fs.binding) { + for (const [index, flagshipBinding] of (env.flagship ?? []).entries()) { + if (!flagshipBinding.binding) { throwMissingBindingError({ - binding: fs, + binding: flagshipBinding, bindingType: "flagship", configPath: args.config, envName, @@ -1831,7 +1831,7 @@ function collectCoreBindings( }); } - addBinding(fs.binding, "Flags", "flagship", envName); + addBinding(flagshipBinding.binding, "Flags", "flagship", envName); } for (const [index, ratelimit] of (env.ratelimits ?? []).entries()) { @@ -2745,10 +2745,10 @@ function collectCoreBindingsPerEnvironment( }); } - for (const [index, fs] of (env.flagship ?? []).entries()) { - if (!fs.binding) { + for (const [index, flagshipBinding] of (env.flagship ?? []).entries()) { + if (!flagshipBinding.binding) { throwMissingBindingError({ - binding: fs, + binding: flagshipBinding, bindingType: "flagship", configPath: args.config, envName, @@ -2759,7 +2759,7 @@ function collectCoreBindingsPerEnvironment( bindings.push({ bindingCategory: "flagship", - name: fs.binding, + name: flagshipBinding.binding, type: "Flags", }); } From 07627eda00594750e569db79bc1861128ff55fbe Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 31 Mar 2026 15:35:03 +0530 Subject: [PATCH 10/28] chore: bump changeset to minor for new feature --- .changeset/add-flagship-binding.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/add-flagship-binding.md b/.changeset/add-flagship-binding.md index 4584c6fda2..d285cddb66 100644 --- a/.changeset/add-flagship-binding.md +++ b/.changeset/add-flagship-binding.md @@ -1,7 +1,7 @@ --- -"miniflare": patch -"wrangler": patch -"@cloudflare/workers-utils": patch +"miniflare": minor +"wrangler": minor +"@cloudflare/workers-utils": minor --- feat: add Flagship feature flag binding support From 8a3a1f77b400e39b0f6cea8f9ad6af845f926c8d Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Tue, 31 Mar 2026 17:31:24 +0530 Subject: [PATCH 11/28] [wrangler] fix: update OAuth scope snapshots for flagship scopes Add flagship:read and flagship:write to inline snapshot expectations in user.test.ts (6), deploy/core.test.ts (2), and whoami.test.ts (3). --- packages/wrangler/src/__tests__/deploy/core.test.ts | 4 ++-- packages/wrangler/src/__tests__/user.test.ts | 12 ++++++------ packages/wrangler/src/__tests__/whoami.test.ts | 6 ++++++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/wrangler/src/__tests__/deploy/core.test.ts b/packages/wrangler/src/__tests__/deploy/core.test.ts index a23e4cf713..8d2a06629d 100644 --- a/packages/wrangler/src/__tests__/deploy/core.test.ts +++ b/packages/wrangler/src/__tests__/deploy/core.test.ts @@ -726,7 +726,7 @@ describe("deploy", () => { ⛅️ wrangler x.x.x ────────────────── Attempting to login via OAuth... - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in. Total Upload: xx KiB / gzip: xx KiB Worker Startup Time: 100 ms @@ -772,7 +772,7 @@ describe("deploy", () => { ⛅️ wrangler x.x.x ────────────────── Attempting to login via OAuth... - Opening a link in your default browser: https://dash.staging.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.staging.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in. Total Upload: xx KiB / gzip: xx KiB Worker Startup Time: 100 ms diff --git a/packages/wrangler/src/__tests__/user.test.ts b/packages/wrangler/src/__tests__/user.test.ts index f01c7463fe..74d0a6d570 100644 --- a/packages/wrangler/src/__tests__/user.test.ts +++ b/packages/wrangler/src/__tests__/user.test.ts @@ -80,7 +80,7 @@ describe("User", () => { ⛅️ wrangler x.x.x ────────────────── Attempting to login via OAuth... - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); expect(readAuthConfigFile()).toEqual({ @@ -126,7 +126,7 @@ describe("User", () => { Temporary login server listening on 0.0.0.0:8976 Note that the OAuth login page will always redirect to \`localhost:8976\`. If you have changed the callback host or port because you are running in a container, then ensure that you have port forwarding set up correctly. - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); expect(readAuthConfigFile()).toEqual({ @@ -172,7 +172,7 @@ describe("User", () => { Temporary login server listening on mylocalhost.local:8976 Note that the OAuth login page will always redirect to \`localhost:8976\`. If you have changed the callback host or port because you are running in a container, then ensure that you have port forwarding set up correctly. - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); expect(readAuthConfigFile()).toEqual({ @@ -218,7 +218,7 @@ describe("User", () => { Temporary login server listening on localhost:8787 Note that the OAuth login page will always redirect to \`localhost:8976\`. If you have changed the callback host or port because you are running in a container, then ensure that you have port forwarding set up correctly. - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); expect(readAuthConfigFile()).toEqual({ @@ -260,7 +260,7 @@ describe("User", () => { ⛅️ wrangler x.x.x ────────────────── Attempting to login via OAuth... - Opening a link in your default browser: https://dash.staging.cloudflare.com/oauth2/auth?response_type=code&client_id=4b2ea6cc-9421-4761-874b-ce550e0e3def&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.staging.cloudflare.com/oauth2/auth?response_type=code&client_id=4b2ea6cc-9421-4761-874b-ce550e0e3def&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); @@ -389,7 +389,7 @@ describe("User", () => { ⛅️ wrangler x.x.x ────────────────── Attempting to login via OAuth... - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); expect(std.warn).toMatchInlineSnapshot(`""`); diff --git a/packages/wrangler/src/__tests__/whoami.test.ts b/packages/wrangler/src/__tests__/whoami.test.ts index 40f7c2cea1..6e5adfeec2 100644 --- a/packages/wrangler/src/__tests__/whoami.test.ts +++ b/packages/wrangler/src/__tests__/whoami.test.ts @@ -341,6 +341,8 @@ describe("whoami", () => { - queues:write - pipelines:write - secrets_store:write + - flagship:read + - flagship:write - containers:write - cloudchamber:write - connectivity:admin @@ -404,6 +406,8 @@ describe("whoami", () => { - queues:write - pipelines:write - secrets_store:write + - flagship:read + - flagship:write - containers:write - cloudchamber:write - connectivity:admin @@ -513,6 +517,8 @@ describe("whoami", () => { - queues:write - pipelines:write - secrets_store:write + - flagship:read + - flagship:write - containers:write - cloudchamber:write - connectivity:admin From 70b9069c17db19ac02dd0a2e4f2b702d3f76143a Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Wed, 1 Apr 2026 11:21:58 +0530 Subject: [PATCH 12/28] [wrangler] fix: add flagship to type-generation test mock config Add flagship binding to bindingsConfigMock to satisfy the EnvironmentNonInheritable type constraint. --- packages/wrangler/src/__tests__/type-generation.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index adc043e42a..6a19a75cb0 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -416,6 +416,12 @@ const bindingsConfigMock: Omit< enable_timer: true, }, ], + flagship: [ + { + binding: "FLAGS", + app_id: "app-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + ], services: [ { binding: "SERVICE_BINDING", service: "service_name" }, { From fe7654797ec9c8d1d4b2d6ba04503311035caa59 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Wed, 1 Apr 2026 12:40:41 +0530 Subject: [PATCH 13/28] [wrangler] fix: add FLAGS: Flags to type-generation test snapshots Update 3 inline snapshots that output the full generated Env interface to include the FLAGS: Flags binding from the flagship config mock. --- packages/wrangler/src/__tests__/type-generation.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index 6a19a75cb0..687ae97fdb 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -771,6 +771,7 @@ describe("generate types", () => { TEST_QUEUE_BINDING: Queue; SECRET: SecretsStoreSecret; HELLO_WORLD: HelloWorldBinding; + FLAGS: Flags; RATE_LIMITER: RateLimit; WORKER_LOADER_BINDING: WorkerLoader; VPC_SERVICE_BINDING: Fetcher; @@ -884,6 +885,7 @@ describe("generate types", () => { TEST_QUEUE_BINDING: Queue; SECRET: SecretsStoreSecret; HELLO_WORLD: HelloWorldBinding; + FLAGS: Flags; RATE_LIMITER: RateLimit; WORKER_LOADER_BINDING: WorkerLoader; VPC_SERVICE_BINDING: Fetcher; @@ -1038,8 +1040,6 @@ describe("generate types", () => { " ⛅️ wrangler x.x.x ────────────────── - - Found Worker 'service_name' at 'b/index.ts' (b/wrangler.jsonc) - - Found Worker 'service_name_2' at 'c/index.ts' (c/wrangler.jsonc) Generating project types... declare namespace Cloudflare { @@ -1060,6 +1060,7 @@ describe("generate types", () => { TEST_QUEUE_BINDING: Queue; SECRET: SecretsStoreSecret; HELLO_WORLD: HelloWorldBinding; + FLAGS: Flags; RATE_LIMITER: RateLimit; WORKER_LOADER_BINDING: WorkerLoader; VPC_SERVICE_BINDING: Fetcher; From 7702b6575096d4569e6c29e6b8aa1d13c83e7cd7 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Thu, 2 Apr 2026 01:07:33 +0530 Subject: [PATCH 14/28] [wrangler] fix: restore missing 'Found Worker' lines in type-generation snapshot --- packages/wrangler/src/__tests__/type-generation.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index 395548b3ae..79faaf60ea 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -1043,6 +1043,8 @@ describe("generate types", () => { " ⛅️ wrangler x.x.x ────────────────── + - Found Worker 'service_name' at 'b/index.ts' (b/wrangler.jsonc) + - Found Worker 'service_name_2' at 'c/index.ts' (c/wrangler.jsonc) Generating project types... declare namespace Cloudflare { From bde6e7221a9ebb093012a701e7e4ad1f4b027a93 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Mon, 6 Apr 2026 13:16:20 +0530 Subject: [PATCH 15/28] fix: use import type for type-only imports in flagship plugin --- packages/miniflare/src/plugins/flagship/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/miniflare/src/plugins/flagship/index.ts b/packages/miniflare/src/plugins/flagship/index.ts index 18d019e99b..eebf0abf22 100644 --- a/packages/miniflare/src/plugins/flagship/index.ts +++ b/packages/miniflare/src/plugins/flagship/index.ts @@ -1,12 +1,11 @@ import { z } from "zod"; -import { Worker_Binding } from "../../runtime"; import { getUserBindingServiceName, - Plugin, ProxyNodeBinding, remoteProxyClientWorker, - RemoteProxyConnectionString, } from "../shared"; +import type { Worker_Binding } from "../../runtime"; +import type { Plugin, RemoteProxyConnectionString } from "../shared"; const FlagshipSchema = z.object({ app_id: z.string(), From 78876527e841f232a78d0ae0c7cfba47248cfce8 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Wed, 8 Apr 2026 23:56:22 +0530 Subject: [PATCH 16/28] feat: add local JSRPC stub worker for flagship binding Implements a local WorkerEntrypoint-based binding worker that returns default values for all flag evaluations in local dev mode. When running with --remote, the plugin falls back to the remote proxy client. This follows the JSRPC binding pattern (like hello-world, browser-rendering) where getServices() returns the local worker implementation when no remoteProxyConnectionString is provided, and getBindings() specifies the FlagshipBinding entrypoint. --- .../miniflare/src/plugins/flagship/index.ts | 20 +++- .../src/workers/flagship/binding.worker.ts | 104 ++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 packages/miniflare/src/workers/flagship/binding.worker.ts diff --git a/packages/miniflare/src/plugins/flagship/index.ts b/packages/miniflare/src/plugins/flagship/index.ts index eebf0abf22..e01d16e7dc 100644 --- a/packages/miniflare/src/plugins/flagship/index.ts +++ b/packages/miniflare/src/plugins/flagship/index.ts @@ -1,3 +1,4 @@ +import BINDING_SCRIPT from "worker:flagship/binding"; import { z } from "zod"; import { getUserBindingServiceName, @@ -36,6 +37,7 @@ export const FLAGSHIP_PLUGIN: Plugin = { name, config.remoteProxyConnectionString ), + entrypoint: "FlagshipBinding", }, }) ); @@ -64,7 +66,23 @@ export const FLAGSHIP_PLUGIN: Plugin = { name, remoteProxyConnectionString ), - worker: remoteProxyClientWorker(remoteProxyConnectionString, name), + worker: remoteProxyConnectionString + ? remoteProxyClientWorker(remoteProxyConnectionString, name) + : { + compatibilityDate: "2025-01-01", + modules: [ + { + name: "binding.worker.js", + esModule: BINDING_SCRIPT(), + }, + ], + bindings: [ + { + name: "config", + json: JSON.stringify({ app_id: name }), + }, + ], + }, }; } ); diff --git a/packages/miniflare/src/workers/flagship/binding.worker.ts b/packages/miniflare/src/workers/flagship/binding.worker.ts new file mode 100644 index 0000000000..c944577a7d --- /dev/null +++ b/packages/miniflare/src/workers/flagship/binding.worker.ts @@ -0,0 +1,104 @@ +// Local stub for Flagship feature flag binding. +// In local dev mode, all flag evaluations return the provided default value. +// Use `wrangler dev --remote` to evaluate flags against the real Flagship service. + +import { WorkerEntrypoint } from "cloudflare:workers"; + +interface EvaluationDetails { + flagKey: string; + value: T; + variant?: string; + reason?: string; + errorCode?: string; + errorMessage?: string; +} + +export class FlagshipBinding extends WorkerEntrypoint { + async get( + _flagKey: string, + defaultValue?: unknown, + _context?: Record + ): Promise { + return defaultValue; + } + + async getBooleanValue( + _flagKey: string, + defaultValue: boolean, + _context?: Record + ): Promise { + return defaultValue; + } + + async getStringValue( + _flagKey: string, + defaultValue: string, + _context?: Record + ): Promise { + return defaultValue; + } + + async getNumberValue( + _flagKey: string, + defaultValue: number, + _context?: Record + ): Promise { + return defaultValue; + } + + async getObjectValue( + _flagKey: string, + defaultValue: T, + _context?: Record + ): Promise { + return defaultValue; + } + + async getBooleanDetails( + flagKey: string, + defaultValue: boolean, + _context?: Record + ): Promise> { + return { + flagKey, + value: defaultValue, + reason: "DEFAULT", + }; + } + + async getStringDetails( + flagKey: string, + defaultValue: string, + _context?: Record + ): Promise> { + return { + flagKey, + value: defaultValue, + reason: "DEFAULT", + }; + } + + async getNumberDetails( + flagKey: string, + defaultValue: number, + _context?: Record + ): Promise> { + return { + flagKey, + value: defaultValue, + reason: "DEFAULT", + }; + } + + async getObjectDetails( + flagKey: string, + defaultValue: T, + _context?: Record + ): Promise> { + return { + flagKey, + value: defaultValue, + reason: "DEFAULT", + }; + } +} From aebf6f0e1f6a411cc12dcc798b9267e64b4da31a Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Thu, 9 Apr 2026 00:37:02 +0530 Subject: [PATCH 17/28] fix: use plain UUID for flagship app_id in test fixtures The app_id field is a UUID, not an app-prefixed string. --- packages/wrangler/src/__tests__/type-generation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index 6b2e059ddf..a9cf718677 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -419,7 +419,7 @@ const bindingsConfigMock: Omit< flagship: [ { binding: "FLAGS", - app_id: "app-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", }, ], services: [ From 0da66f320d3c153d44ec045c6f42a1d13a8c4196 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Thu, 9 Apr 2026 01:13:45 +0530 Subject: [PATCH 18/28] feat: add Miniflare class API and plugin test for flagship binding - Add getFlagshipBinding() method on the Miniflare class for programmatic Node.js access (matches hello-world pattern) - Add miniflare plugin test that verifies the local JSRPC stub returns defaults for all evaluation methods - Fix app_id test fixture to use plain UUID --- packages/miniflare/src/index.ts | 28 ++++++++ .../test/plugins/flagship/index.spec.ts | 72 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 packages/miniflare/test/plugins/flagship/index.spec.ts diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 76cf0c9c2b..8b1bed6c9b 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -34,6 +34,7 @@ import { BROWSER_RENDERING_PLUGIN_NAME, D1_PLUGIN_NAME, DURABLE_OBJECTS_PLUGIN_NAME, + FLAGSHIP_PLUGIN_NAME, getDirectSocketName, getGlobalServices, getPersistPath, @@ -2962,6 +2963,33 @@ export class Miniflare { }> { return this.#getProxy(HELLO_WORLD_PLUGIN_NAME, bindingName, workerName); } + getFlagshipBinding( + bindingName: string, + workerName?: string + ): Promise<{ + get: ( + flagKey: string, + defaultValue?: unknown, + context?: Record + ) => Promise; + getBooleanValue: ( + flagKey: string, + defaultValue: boolean, + context?: Record + ) => Promise; + getStringValue: ( + flagKey: string, + defaultValue: string, + context?: Record + ) => Promise; + getNumberValue: ( + flagKey: string, + defaultValue: number, + context?: Record + ) => Promise; + }> { + return this.#getProxy(FLAGSHIP_PLUGIN_NAME, bindingName, workerName); + } getStreamBinding( bindingName: string, workerName?: string diff --git a/packages/miniflare/test/plugins/flagship/index.spec.ts b/packages/miniflare/test/plugins/flagship/index.spec.ts new file mode 100644 index 0000000000..cd9a1ad06d --- /dev/null +++ b/packages/miniflare/test/plugins/flagship/index.spec.ts @@ -0,0 +1,72 @@ +import { Miniflare } from "miniflare"; +import { test } from "vitest"; +import { useDispose } from "../../test-shared"; + +test("flagship", async ({ expect }) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env, ctx) { + const url = new URL(request.url); + + if (url.pathname === "/boolean") { + const value = await env.FLAGS.getBooleanValue("my-flag", false); + return Response.json({ value }); + } + + if (url.pathname === "/string") { + const value = await env.FLAGS.getStringValue("variant", "control", { + userId: "user-123", + }); + return Response.json({ value }); + } + + if (url.pathname === "/number") { + const value = await env.FLAGS.getNumberValue("rate-limit", 100); + return Response.json({ value }); + } + + if (url.pathname === "/details") { + const details = await env.FLAGS.getBooleanDetails("my-flag", true); + return Response.json(details); + } + + if (url.pathname === "/get") { + const value = await env.FLAGS.get("any-flag", "fallback"); + return Response.json({ value }); + } + + return new Response("Not found", { status: 404 }); + }, + } + `, + }); + useDispose(mf); + + // Local stub returns the default value for all evaluations + const boolRes = await mf.dispatchFetch("http://placeholder/boolean"); + expect(await boolRes.json()).toEqual({ value: false }); + + const strRes = await mf.dispatchFetch("http://placeholder/string"); + expect(await strRes.json()).toEqual({ value: "control" }); + + const numRes = await mf.dispatchFetch("http://placeholder/number"); + expect(await numRes.json()).toEqual({ value: 100 }); + + const detailsRes = await mf.dispatchFetch("http://placeholder/details"); + expect(await detailsRes.json()).toEqual({ + flagKey: "my-flag", + value: true, + reason: "DEFAULT", + }); + + const getRes = await mf.dispatchFetch("http://placeholder/get"); + expect(await getRes.json()).toEqual({ value: "fallback" }); +}); From f5b5804e51ea5fd64ffe3077e5bbee1d4c2f9aaf Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Thu, 9 Apr 2026 01:21:10 +0530 Subject: [PATCH 19/28] fix: pass actual app_id to local worker config and mark binding as locally simulated - Fix getServices() to destructure and use the real app_id value instead of the binding name - Update print-bindings to show isSimulatedLocally: true since we now have a local stub worker --- packages/miniflare/src/plugins/flagship/index.ts | 4 ++-- packages/wrangler/src/utils/print-bindings.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/miniflare/src/plugins/flagship/index.ts b/packages/miniflare/src/plugins/flagship/index.ts index e01d16e7dc..78894eefd8 100644 --- a/packages/miniflare/src/plugins/flagship/index.ts +++ b/packages/miniflare/src/plugins/flagship/index.ts @@ -59,7 +59,7 @@ export const FLAGSHIP_PLUGIN: Plugin = { } return Object.entries(options.flagship).map( - ([name, { remoteProxyConnectionString }]) => { + ([name, { app_id, remoteProxyConnectionString }]) => { return { name: getUserBindingServiceName( FLAGSHIP_PLUGIN_NAME, @@ -79,7 +79,7 @@ export const FLAGSHIP_PLUGIN: Plugin = { bindings: [ { name: "config", - json: JSON.stringify({ app_id: name }), + json: JSON.stringify({ app_id }), }, ], }, diff --git a/packages/wrangler/src/utils/print-bindings.ts b/packages/wrangler/src/utils/print-bindings.ts index e9f1f6ec9b..38b3a7ea09 100644 --- a/packages/wrangler/src/utils/print-bindings.ts +++ b/packages/wrangler/src/utils/print-bindings.ts @@ -466,7 +466,7 @@ export function printBindings( name: binding, type: getBindingTypeFriendlyName("flagship"), value: app_id, - mode: getMode({ isSimulatedLocally: false }), + mode: getMode({ isSimulatedLocally: true }), }; }) ); From e7c6743b4acf268a5af49a018274c7ca533d3565 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Thu, 9 Apr 2026 01:44:29 +0530 Subject: [PATCH 20/28] Update packages/miniflare/src/index.ts Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/miniflare/src/index.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 8b1bed6c9b..b5bbb9462b 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -2987,9 +2987,35 @@ export class Miniflare { defaultValue: number, context?: Record ) => Promise; + getObjectValue: ( + flagKey: string, + defaultValue: T, + context?: Record + ) => Promise; + getBooleanDetails: ( + flagKey: string, + defaultValue: boolean, + context?: Record + ) => Promise<{ flagKey: string; value: boolean; variant?: string; reason?: string; errorCode?: string; errorMessage?: string }>; + getStringDetails: ( + flagKey: string, + defaultValue: string, + context?: Record + ) => Promise<{ flagKey: string; value: string; variant?: string; reason?: string; errorCode?: string; errorMessage?: string }>; + getNumberDetails: ( + flagKey: string, + defaultValue: number, + context?: Record + ) => Promise<{ flagKey: string; value: number; variant?: string; reason?: string; errorCode?: string; errorMessage?: string }>; + getObjectDetails: ( + flagKey: string, + defaultValue: T, + context?: Record + ) => Promise<{ flagKey: string; value: T; variant?: string; reason?: string; errorCode?: string; errorMessage?: string }>; }> { return this.#getProxy(FLAGSHIP_PLUGIN_NAME, bindingName, workerName); } + } getStreamBinding( bindingName: string, workerName?: string From 36ce13309cdf6f7028c32ce2756200447b9bfe0b Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Thu, 9 Apr 2026 01:52:12 +0530 Subject: [PATCH 21/28] fix: remove duplicate closing brace and simplify getFlagshipBinding return type The merge introduced an extra closing brace and expanded type definitions with generics (getObjectValue, getObjectDetails) that esbuild cannot parse. Simplified to the core methods without generics. --- packages/miniflare/src/index.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index b5bbb9462b..8b1bed6c9b 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -2987,35 +2987,9 @@ export class Miniflare { defaultValue: number, context?: Record ) => Promise; - getObjectValue: ( - flagKey: string, - defaultValue: T, - context?: Record - ) => Promise; - getBooleanDetails: ( - flagKey: string, - defaultValue: boolean, - context?: Record - ) => Promise<{ flagKey: string; value: boolean; variant?: string; reason?: string; errorCode?: string; errorMessage?: string }>; - getStringDetails: ( - flagKey: string, - defaultValue: string, - context?: Record - ) => Promise<{ flagKey: string; value: string; variant?: string; reason?: string; errorCode?: string; errorMessage?: string }>; - getNumberDetails: ( - flagKey: string, - defaultValue: number, - context?: Record - ) => Promise<{ flagKey: string; value: number; variant?: string; reason?: string; errorCode?: string; errorMessage?: string }>; - getObjectDetails: ( - flagKey: string, - defaultValue: T, - context?: Record - ) => Promise<{ flagKey: string; value: T; variant?: string; reason?: string; errorCode?: string; errorMessage?: string }>; }> { return this.#getProxy(FLAGSHIP_PLUGIN_NAME, bindingName, workerName); } - } getStreamBinding( bindingName: string, workerName?: string From 94b9893572b91ae22491627adfb40672be186e60 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Thu, 9 Apr 2026 02:04:38 +0530 Subject: [PATCH 22/28] fix: restore full getFlagshipBinding return type with generic methods The build failure was caused by a duplicate closing brace from the merge, not by the generic type parameters. Restores getObjectValue, getBooleanDetails, getStringDetails, getNumberDetails, and getObjectDetails methods to the return type. --- packages/miniflare/src/index.ts | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 8b1bed6c9b..17f2b11245 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -2987,6 +2987,59 @@ export class Miniflare { defaultValue: number, context?: Record ) => Promise; + getObjectValue: ( + flagKey: string, + defaultValue: T, + context?: Record + ) => Promise; + getBooleanDetails: ( + flagKey: string, + defaultValue: boolean, + context?: Record + ) => Promise<{ + flagKey: string; + value: boolean; + variant?: string; + reason?: string; + errorCode?: string; + errorMessage?: string; + }>; + getStringDetails: ( + flagKey: string, + defaultValue: string, + context?: Record + ) => Promise<{ + flagKey: string; + value: string; + variant?: string; + reason?: string; + errorCode?: string; + errorMessage?: string; + }>; + getNumberDetails: ( + flagKey: string, + defaultValue: number, + context?: Record + ) => Promise<{ + flagKey: string; + value: number; + variant?: string; + reason?: string; + errorCode?: string; + errorMessage?: string; + }>; + getObjectDetails: ( + flagKey: string, + defaultValue: T, + context?: Record + ) => Promise<{ + flagKey: string; + value: T; + variant?: string; + reason?: string; + errorCode?: string; + errorMessage?: string; + }>; }> { return this.#getProxy(FLAGSHIP_PLUGIN_NAME, bindingName, workerName); } From 195176c0f892cd4ea2a51accf47bfed20ed6e591 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Thu, 9 Apr 2026 19:53:38 +0530 Subject: [PATCH 23/28] chore: update changeset Co-authored-by: Pete Bacon Darwin --- .changeset/add-flagship-binding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/add-flagship-binding.md b/.changeset/add-flagship-binding.md index e3ffe93e3e..c3db00f7d1 100644 --- a/.changeset/add-flagship-binding.md +++ b/.changeset/add-flagship-binding.md @@ -6,4 +6,4 @@ feat: add Flagship feature flag binding support -Adds end-to-end support for the Flagship feature flag binding, which allows Workers to evaluate feature flags from Cloudflare's Flagship service. Configure it in `wrangler.json` with a `flagship` array containing `binding` and `app_id` entries. In local dev, the binding returns default values for all flag evaluations; use `wrangler dev --remote` to evaluate flags against the live Flagship service. +Adds end-to-end support for the Flagship feature flag binding, which allows Workers to evaluate feature flags from Cloudflare's Flagship service. Configure it in `wrangler.json` with a `flagship` array containing `binding` and `app_id` entries. In local dev, the binding returns default values for all flag evaluations; use `"remote": true` in the binding to evaluate flags against the live Flagship service. From 5502d4b809d151e5b5ff5a1e490c6c251bcef3c6 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Thu, 9 Apr 2026 19:57:57 +0530 Subject: [PATCH 24/28] fix: use Flags instead of FlagshipBinding --- packages/miniflare/src/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 6e1629b2fb..a043167bd8 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -2959,10 +2959,7 @@ export class Miniflare { }> { return this.#getProxy(HELLO_WORLD_PLUGIN_NAME, bindingName, workerName); } - getFlagshipBinding( - bindingName: string, - workerName?: string - ): Promise { + getFlagshipBinding(bindingName: string, workerName?: string): Promise { return this.#getProxy(FLAGSHIP_PLUGIN_NAME, bindingName, workerName); } getStreamBinding( @@ -3047,8 +3044,6 @@ export interface SecretsStoreSecretAdmin { get(id: string): Promise; } -export type FlagshipBinding = Flags; - export * from "./http"; export * from "./plugins"; export * from "./runtime"; From 52be722ff512728c61c8009cfac8f602ab412008 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Thu, 9 Apr 2026 20:51:41 +0530 Subject: [PATCH 25/28] chore: remote bindings support Co-authored-by: Pete Bacon Darwin --- packages/wrangler/src/utils/print-bindings.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/wrangler/src/utils/print-bindings.ts b/packages/wrangler/src/utils/print-bindings.ts index 38b3a7ea09..892dcd8776 100644 --- a/packages/wrangler/src/utils/print-bindings.ts +++ b/packages/wrangler/src/utils/print-bindings.ts @@ -466,7 +466,9 @@ export function printBindings( name: binding, type: getBindingTypeFriendlyName("flagship"), value: app_id, - mode: getMode({ isSimulatedLocally: true }), + mode: getMode({ + isSimulatedLocally: context.remoteBindingsDisabled || !remote + }), }; }) ); From 391103b4144beeca683215fb9fb36d1169ef3121 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Fri, 10 Apr 2026 00:47:21 +0530 Subject: [PATCH 26/28] fix: address PR review comments for flagship binding - Add remote field to flagship config, types, and validation - Fix TS2304: destructure remote in print-bindings flagship map - Conditionally pass remoteProxyConnectionString based on remote flag - Remove unused config binding from flagship plugin worker - Update compatibilityDate to 2025-03-17 --- packages/miniflare/src/plugins/flagship/index.ts | 10 ++-------- packages/workers-utils/src/config/environment.ts | 3 +++ packages/workers-utils/src/config/validation.ts | 1 + packages/workers-utils/src/worker.ts | 1 + packages/wrangler/src/dev/miniflare/index.ts | 5 ++++- packages/wrangler/src/utils/print-bindings.ts | 4 ++-- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/miniflare/src/plugins/flagship/index.ts b/packages/miniflare/src/plugins/flagship/index.ts index 78894eefd8..b675bf6bb8 100644 --- a/packages/miniflare/src/plugins/flagship/index.ts +++ b/packages/miniflare/src/plugins/flagship/index.ts @@ -59,7 +59,7 @@ export const FLAGSHIP_PLUGIN: Plugin = { } return Object.entries(options.flagship).map( - ([name, { app_id, remoteProxyConnectionString }]) => { + ([name, { remoteProxyConnectionString }]) => { return { name: getUserBindingServiceName( FLAGSHIP_PLUGIN_NAME, @@ -69,19 +69,13 @@ export const FLAGSHIP_PLUGIN: Plugin = { worker: remoteProxyConnectionString ? remoteProxyClientWorker(remoteProxyConnectionString, name) : { - compatibilityDate: "2025-01-01", + compatibilityDate: "2025-03-17", modules: [ { name: "binding.worker.js", esModule: BINDING_SCRIPT(), }, ], - bindings: [ - { - name: "config", - json: JSON.stringify({ app_id }), - }, - ], }, }; } diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index 59ff2bb45e..0958db6702 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -1351,6 +1351,9 @@ export interface EnvironmentNonInheritable { /** The Flagship app ID to bind to. */ app_id: string; + + /** Whether to use the remote Flagship service for flag evaluation in local dev. */ + remote?: boolean; }[]; /** diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index 69a56fb63f..fd0d84c163 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -4811,6 +4811,7 @@ const validateFlagshipBinding: ValidatorFn = (diagnostics, field, value) => { validateAdditionalProperties(diagnostics, field, Object.keys(value), [ "binding", "app_id", + "remote", ]); return isValid; diff --git a/packages/workers-utils/src/worker.ts b/packages/workers-utils/src/worker.ts index 409942b24f..b57bb93e74 100644 --- a/packages/workers-utils/src/worker.ts +++ b/packages/workers-utils/src/worker.ts @@ -256,6 +256,7 @@ export interface CfHelloWorld { export interface CfFlagship { binding: string; app_id: string; + remote?: boolean; } export interface CfWorkerLoader { diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index cd810b6b6c..48eb3d7780 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -790,7 +790,10 @@ export function buildMiniflareBindingOptions( binding.binding, { app_id: binding.app_id, - remoteProxyConnectionString, + remoteProxyConnectionString: + binding.remote && remoteProxyConnectionString + ? remoteProxyConnectionString + : undefined, }, ]) ), diff --git a/packages/wrangler/src/utils/print-bindings.ts b/packages/wrangler/src/utils/print-bindings.ts index 892dcd8776..43405f10b8 100644 --- a/packages/wrangler/src/utils/print-bindings.ts +++ b/packages/wrangler/src/utils/print-bindings.ts @@ -461,13 +461,13 @@ export function printBindings( if (flagship.length > 0) { output.push( - ...flagship.map(({ binding, app_id }) => { + ...flagship.map(({ binding, app_id, remote }) => { return { name: binding, type: getBindingTypeFriendlyName("flagship"), value: app_id, mode: getMode({ - isSimulatedLocally: context.remoteBindingsDisabled || !remote + isSimulatedLocally: context.remoteBindingsDisabled || !remote, }), }; }) From f40249e66301547afb20de8431376a35709d93a6 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Fri, 10 Apr 2026 01:08:41 +0530 Subject: [PATCH 27/28] chore: remote validity check Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/workers-utils/src/config/validation.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index fd0d84c163..96b6e496c6 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -4814,6 +4814,10 @@ const validateFlagshipBinding: ValidatorFn = (diagnostics, field, value) => { "remote", ]); + if (!isRemoteValid(value, field, diagnostics)) { + isValid = false; + } + return isValid; }; From 5984a113fdeaa04a7f9866f9ae451c182e842e85 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Fri, 10 Apr 2026 01:34:52 +0530 Subject: [PATCH 28/28] fix: add flagship to removeRemoteConfigFieldFromBindings, add tests - Strip remote field from flagship bindings in config-diffs.ts - Add validation tests for flagship binding config - Add miniflare tests for getObjectValue, getObjectDetails, getStringDetails, and getNumberDetails --- .../test/plugins/flagship/index.spec.ts | 282 +++++++ .../normalize-and-validate-config.test.ts | 707 ++++++++++++++++++ packages/wrangler/src/deploy/config-diffs.ts | 6 + 3 files changed, 995 insertions(+) diff --git a/packages/miniflare/test/plugins/flagship/index.spec.ts b/packages/miniflare/test/plugins/flagship/index.spec.ts index cd9a1ad06d..db9c56620b 100644 --- a/packages/miniflare/test/plugins/flagship/index.spec.ts +++ b/packages/miniflare/test/plugins/flagship/index.spec.ts @@ -43,6 +43,26 @@ test("flagship", async ({ expect }) => { return Response.json({ value }); } + if (url.pathname === "/object") { + const value = await env.FLAGS.getObjectValue("config", { theme: "light", beta: false }); + return Response.json({ value }); + } + + if (url.pathname === "/string-details") { + const details = await env.FLAGS.getStringDetails("variant", "control"); + return Response.json(details); + } + + if (url.pathname === "/number-details") { + const details = await env.FLAGS.getNumberDetails("rate-limit", 100); + return Response.json(details); + } + + if (url.pathname === "/object-details") { + const details = await env.FLAGS.getObjectDetails("config", { theme: "light" }); + return Response.json(details); + } + return new Response("Not found", { status: 404 }); }, } @@ -69,4 +89,266 @@ test("flagship", async ({ expect }) => { const getRes = await mf.dispatchFetch("http://placeholder/get"); expect(await getRes.json()).toEqual({ value: "fallback" }); + + const objRes = await mf.dispatchFetch("http://placeholder/object"); + expect(await objRes.json()).toEqual({ + value: { theme: "light", beta: false }, + }); + + const strDetailsRes = await mf.dispatchFetch( + "http://placeholder/string-details" + ); + expect(await strDetailsRes.json()).toEqual({ + flagKey: "variant", + value: "control", + reason: "DEFAULT", + }); + + const numDetailsRes = await mf.dispatchFetch( + "http://placeholder/number-details" + ); + expect(await numDetailsRes.json()).toEqual({ + flagKey: "rate-limit", + value: 100, + reason: "DEFAULT", + }); + + const objDetailsRes = await mf.dispatchFetch( + "http://placeholder/object-details" + ); + expect(await objDetailsRes.json()).toEqual({ + flagKey: "config", + value: { theme: "light" }, + reason: "DEFAULT", + }); +}); + +test("getObjectValue: returns default object value", async ({ expect }) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const value = await env.FLAGS.getObjectValue("config", { theme: "dark", features: ["a", "b"] }); + return Response.json(value); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ theme: "dark", features: ["a", "b"] }); +}); + +test("getObjectValue: returns default with context parameter", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const value = await env.FLAGS.getObjectValue("config", { enabled: true }, { userId: "user-123" }); + return Response.json(value); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ enabled: true }); +}); + +test("getObjectDetails: returns evaluation details with default object", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getObjectDetails("app-config", { maxRetries: 3, timeout: 5000 }); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "app-config", + value: { maxRetries: 3, timeout: 5000 }, + reason: "DEFAULT", + }); +}); + +test("getObjectDetails: returns evaluation details with context parameter", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getObjectDetails("config", { color: "blue" }, { region: "us-east" }); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "config", + value: { color: "blue" }, + reason: "DEFAULT", + }); +}); + +test("getStringDetails: returns evaluation details with default string", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getStringDetails("color-scheme", "dark-mode"); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "color-scheme", + value: "dark-mode", + reason: "DEFAULT", + }); +}); + +test("getStringDetails: returns evaluation details with context parameter", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getStringDetails("variant", "treatment-a", { userId: "user-456" }); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "variant", + value: "treatment-a", + reason: "DEFAULT", + }); +}); + +test("getNumberDetails: returns evaluation details with default number", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getNumberDetails("max-connections", 50); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "max-connections", + value: 50, + reason: "DEFAULT", + }); +}); + +test("getNumberDetails: returns evaluation details with context parameter", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getNumberDetails("timeout-ms", 3000, { tier: "premium" }); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "timeout-ms", + value: 3000, + reason: "DEFAULT", + }); }); 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 bb401b39e6..d60d9f4bbc 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 @@ -4702,6 +4702,713 @@ describe("normalizeAndValidateConfig()", () => { }); }); + describe("[flagship]", () => { + it("should error if flagship is an object", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { flagship: {} }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "flagship" should be an array but got {}." + `); + }); + + it("should error if flagship is a string", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { flagship: "bad" }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "flagship" should be an array but got "bad"." + `); + }); + + it("should error if flagship is a number", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { flagship: 999 }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "flagship" should be an array but got 999." + `); + }); + + it("should error if flagship is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { flagship: null }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "flagship" should be an array but got null." + `); + }); + + it("should accept valid flagship bindings", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + flagship: [ + { + binding: "FLAGS", + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + ], + }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(false); + }); + + it("should error if flagship bindings are not valid", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + flagship: [ + // @ts-expect-error purposely using an invalid value + {}, + // @ts-expect-error purposely using an invalid value + { binding: "VALID" }, + // @ts-expect-error purposely using an invalid value + { binding: 2000, app_id: 2111 }, + { + binding: "BINDING_2", + app_id: "valid-app-id", + }, + ], + }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "flagship[0]" bindings must have a string "binding" field but got {}. + - "flagship[0]" bindings must have a string "app_id" field but got {}. + - "flagship[1]" bindings must have a string "app_id" field but got {"binding":"VALID"}. + - "flagship[2]" bindings must have a string "binding" field but got {"binding":2000,"app_id":2111}. + - "flagship[2]" bindings must have a string "app_id" field but got {"binding":2000,"app_id":2111}." + `); + }); + + it("should warn on unexpected fields", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + flagship: [ + { + binding: "FLAGS", + app_id: "valid-app-id", + // @ts-expect-error purposely using an invalid field + unknown_field: true, + }, + ], + }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Unexpected fields found in flagship[0] field: "unknown_field"" + `); + }); + }); + + describe("[workflows]", () => { + it("should error if workflows is an object", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { workflows: {} }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "workflows" should be an array but got {}." + `); + }); + + it("should error if workflows is a string", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { workflows: "BAD" }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "workflows" should be an array but got "BAD"." + `); + }); + + it("should error if workflows is a number", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { workflows: 999 }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "workflows" should be an array but got 999." + `); + }); + + it("should error if workflows is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { workflows: null }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "workflows" should be an array but got null." + `); + }); + + it("should accept valid workflow bindings", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(false); + }); + + it("should accept valid workflow bindings with optional fields", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + script_name: "my-script", + remote: true, + limits: { steps: 100 }, + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(false); + }); + + it("should error if workflow bindings are not valid", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + {}, + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + }, + { binding: 2000, name: 2111, class_name: 3000 }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" bindings should have a string "binding" field but got {}. + - "workflows[0]" bindings should have a string "name" field but got {}. + - "workflows[0]" bindings should have a string "class_name" field but got {}. + - "workflows[2]" bindings should have a string "binding" field but got {"binding":2000,"name":2111,"class_name":3000}. + - "workflows[2]" bindings should have a string "name" field but got {"binding":2000,"name":2111,"class_name":3000}. + - "workflows[2]" bindings should have a string "class_name" field but got {"binding":2000,"name":2111,"class_name":3000}." + `); + }); + + it("should error if workflow name has invalid format", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "invalid name with spaces", + class_name: "MyWorkflow", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" binding "name" field is invalid. Workflow names must be 1-64 characters long, start with a letter, number, or underscore, and may only contain letters, numbers, underscores, or hyphens." + `); + }); + + it("should error if optional fields have wrong types", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + script_name: 123, + remote: "yes", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" bindings should, optionally, have a string "script_name" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","script_name":123,"remote":"yes"}. + - "workflows[0]" bindings should, optionally, have a boolean "remote" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","script_name":123,"remote":"yes"}." + `); + }); + + it("should error if limits is not an object", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + limits: "bad", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" bindings should, optionally, have an object "limits" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","limits":"bad"}." + `); + }); + + it("should error if limits.steps is not a positive integer", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + limits: { steps: -1 }, + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" bindings "limits.steps" field must be a positive integer but got -1." + `); + }); + + it("should warn if limits.steps exceeds 25000", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + limits: { steps: 30000 }, + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" has a step limit of 30000, which exceeds the production maximum of 25,000. This configuration may not work when deployed." + `); + }); + + it("should warn on unexpected fields", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + unknown_field: true, + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Unexpected fields found in workflows[0] field: "unknown_field"" + `); + }); + + it("should error on duplicate workflow names", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "WORKFLOW_1", + name: "my-workflow", + class_name: "MyWorkflow1", + }, + { + binding: "WORKFLOW_2", + name: "my-workflow", + class_name: "MyWorkflow2", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows" bindings must have unique "name" values; duplicate(s) found: "my-workflow"" + `); + }); + }); + + describe("[logfwdr]", () => { + it("should error if logfwdr is an array", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: [] } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr" should be an object but got []." + `); + }); + + it("should error if logfwdr is a string", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: "BAD" } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr" should be an object but got "BAD"." + `); + }); + + it("should error if logfwdr is a number", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: 999 } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr" should be an object but got 999." + `); + }); + + it("should error if logfwdr is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: null } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr" should be an object but got null." + `); + }); + + it("should error if logfwdr.bindings is not defined", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: {} } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr" is missing the required "bindings" property." + `); + }); + + it("should error if logfwdr.bindings is an object", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: { bindings: {} } } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr.bindings" should be an array but got {}." + `); + }); + + it("should error if logfwdr.bindings is a string", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: { bindings: "BAD" } } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr.bindings" should be an array but got "BAD"." + `); + }); + + it("should error if logfwdr.bindings is a number", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: { bindings: 999 } } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr.bindings" should be an array but got 999." + `); + }); + + it("should error if logfwdr.bindings is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: { bindings: null } } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr.bindings" should be an array but got null." + `); + }); + + it("should accept valid logfwdr bindings", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + logfwdr: { + bindings: [ + { + name: "httplogs", + destination: "httplogs", + }, + ], + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(false); + }); + + it("should error if logfwdr.bindings are not valid", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + logfwdr: { + bindings: [ + {}, + { name: "VALID", destination: "valid-dest" }, + { name: 123, destination: 456 }, + ], + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + + - "logfwdr.bindings[0]": {} + - binding should have a string "name" field. + - binding should have a string "destination" field. + + - "logfwdr.bindings[2]": {"name":123,"destination":456} + - binding should have a string "name" field. + - binding should have a string "destination" field." + `); + }); + + it("should error if logfwdr has deprecated schema property", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + logfwdr: { + bindings: [ + { + name: "httplogs", + destination: "httplogs", + }, + ], + schema: "some-schema", + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "logfwdr" binding "schema" property has been replaced with the "unsafe.capnp" object, which expects a "base_path" and an array of "source_schemas" to compile, or a "compiled_schema" property." + `); + }); + + it("should warn on unexpected fields in logfwdr bindings", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + logfwdr: { + bindings: [ + { + name: "httplogs", + destination: "httplogs", + unknown_field: true, + }, + ], + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + + - "logfwdr.bindings[0]": {"name":"httplogs","destination":"httplogs","unknown_field":true} + - Unexpected fields found in logfwdr.bindings[0] field: "unknown_field"" + `); + }); + }); + describe("[ratelimit]", () => { it("should error if ratelimit is an object", ({ expect }) => { const { diagnostics } = normalizeAndValidateConfig( diff --git a/packages/wrangler/src/deploy/config-diffs.ts b/packages/wrangler/src/deploy/config-diffs.ts index 7318e072d6..ec02d5fa4e 100644 --- a/packages/wrangler/src/deploy/config-diffs.ts +++ b/packages/wrangler/src/deploy/config-diffs.ts @@ -236,6 +236,12 @@ function removeRemoteConfigFieldFromBindings(normalizedConfig: Config): void { ); } + if (normalizedConfig.flagship?.length) { + normalizedConfig.flagship = normalizedConfig.flagship.map( + ({ remote: _, ...binding }) => binding + ); + } + const singleBindingFields = [ "browser", "ai",