Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1412c56
[workers-utils] feat: add Flagship feature flag binding types and config
roerohan Mar 31, 2026
1c08e92
[miniflare] feat: add Flagship remote-only plugin
roerohan Mar 31, 2026
17b097b
[wrangler] feat: wire Flagship binding through deployment and dev pip…
roerohan Mar 31, 2026
8756a6b
[wrangler] feat: add Flagship type generation and OAuth scopes
roerohan Mar 31, 2026
48b61a1
chore: add changeset for Flagship binding support
roerohan Mar 31, 2026
f4b948a
Merge branch 'main' into roerohan/flagship
roerohan Mar 31, 2026
1fc78cb
[miniflare] fix: flagship plugin getServices should always create ser…
roerohan Mar 31, 2026
a1a828c
[workers-utils] fix: add flagship to empty config defaults test expec…
roerohan Mar 31, 2026
505ad9e
[workers-utils] fix: add flagship to safeBindings in validateUnsafeBi…
roerohan Mar 31, 2026
0556a03
chore: fix lint and formatting issues
roerohan Mar 31, 2026
07627ed
chore: bump changeset to minor for new feature
roerohan Mar 31, 2026
8a3a1f7
[wrangler] fix: update OAuth scope snapshots for flagship scopes
roerohan Mar 31, 2026
2c8a288
Merge branch 'main' into roerohan/flagship
roerohan Mar 31, 2026
70b9069
[wrangler] fix: add flagship to type-generation test mock config
roerohan Apr 1, 2026
fe76547
[wrangler] fix: add FLAGS: Flags to type-generation test snapshots
roerohan Apr 1, 2026
84f2895
Merge branch 'main' into roerohan/flagship
roerohan Apr 1, 2026
7702b65
[wrangler] fix: restore missing 'Found Worker' lines in type-generati…
roerohan Apr 1, 2026
b47e65c
Merge branch 'main' into roerohan/flagship
roerohan Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/add-flagship-binding.md
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 No tests included for flagship binding integration

Per the AGENTS.md and CONTRIBUTING.md guidelines, every non-trivial change should include tests. This PR adds a new binding type across multiple packages (miniflare plugin, config validation, type generation, deployment form, print bindings) but includes no test files. Existing bindings like Hello World have tests in packages/miniflare/test/plugins/. Config validation changes typically have tests in packages/workers-utils/src/config/__tests__/. The type generation changes have tests in packages/wrangler/src/__tests__/type-generation/. At minimum, the miniflare plugin, config validation, and type generation should have corresponding tests.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"miniflare": minor
"wrangler": minor
"@cloudflare/workers-utils": minor
---

feat: add Flagship feature flag binding support
73 changes: 73 additions & 0 deletions packages/miniflare/src/plugins/flagship/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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<RemoteProxyConnectionString>()
.optional(),
});

export const FlagshipOptionsSchema = z.object({
flagship: z.record(FlagshipSchema).optional(),
});

export const FLAGSHIP_PLUGIN_NAME = "flagship";

export const FLAGSHIP_PLUGIN: Plugin<typeof FlagshipOptionsSchema> = {
options: FlagshipOptionsSchema,
async getBindings(options) {
if (!options.flagship) {
return [];
}

return Object.entries(options.flagship).map<Worker_Binding>(
([name, config]) => ({
name,
service: {
name: getUserBindingServiceName(
FLAGSHIP_PLUGIN_NAME,
name,
config.remoteProxyConnectionString
),
},
})
);
},
getNodeBindings(options: z.infer<typeof FlagshipOptionsSchema>) {
if (!options.flagship) {
return {};
}
return Object.fromEntries(
Object.keys(options.flagship).map((name) => [
name,
new ProxyNodeBinding(),
])
);
},
async getServices({ options }) {
if (!options.flagship) {
return [];
}

return Object.entries(options.flagship).map(
([name, { remoteProxyConnectionString }]) => {
return {
name: getUserBindingServiceName(
FLAGSHIP_PLUGIN_NAME,
name,
remoteProxyConnectionString
),
worker: remoteProxyClientWorker(remoteProxyConnectionString, name),
};
}
);
},
};
4 changes: 4 additions & 0 deletions packages/miniflare/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -73,6 +74,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,
Expand Down Expand Up @@ -140,6 +142,7 @@ export type WorkerOptions = z.input<typeof CORE_PLUGIN.options> &
z.input<typeof VPC_SERVICES_PLUGIN.options> &
z.input<typeof MTLS_PLUGIN.options> &
z.input<typeof HELLO_WORLD_PLUGIN.options> &
z.input<typeof FLAGSHIP_PLUGIN.options> &
z.input<typeof WORKER_LOADER_PLUGIN.options> &
z.input<typeof MEDIA_PLUGIN.options> &
z.input<typeof VERSION_METADATA_PLUGIN.options>;
Expand Down Expand Up @@ -221,6 +224,7 @@ export * from "./vpc-networks";
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";
1 change: 1 addition & 0 deletions packages/workers-utils/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ export const defaultWranglerConfig: Config = {
media: undefined,
version_metadata: undefined,
unsafe_hello_world: [],
flagship: [],
ratelimits: [],
worker_loaders: [],

Expand Down
17 changes: 17 additions & 0 deletions packages/workers-utils/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
47 changes: 47 additions & 0 deletions packages/workers-utils/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export type ConfigBindingFieldName =
| "ratelimits"
| "assets"
| "unsafe_hello_world"
| "flagship"
| "worker_loaders"
| "vpc_services"
| "vpc_networks";
Expand Down Expand Up @@ -144,6 +145,7 @@ export const friendlyBindingNames: Record<ConfigBindingFieldName, string> = {
ratelimits: "Rate Limit",
assets: "Assets",
unsafe_hello_world: "Hello World",
flagship: "Flagship",
worker_loaders: "Worker Loader",
vpc_services: "VPC Service",
vpc_networks: "VPC Network",
Expand Down Expand Up @@ -186,6 +188,7 @@ const bindingTypeFriendlyNames: Record<Binding["type"], string> = {
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",
Expand Down Expand Up @@ -1884,6 +1887,16 @@ function normalizeAndValidateEnvironment(
validateBindingArray(envName, validateHelloWorldBinding),
[]
),
flagship: notInheritable(
diagnostics,
topLevelEnv,
rawConfig,
rawEnv,
envName,
"flagship",
validateBindingArray(envName, validateFlagshipBinding),
[]
),
worker_loaders: notInheritable(
diagnostics,
topLevelEnv,
Expand Down Expand Up @@ -2971,6 +2984,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
"pipeline",
"worker_loader",
"vpc_service",
"flagship",
"vpc_network",
"stream",
"media",
Expand Down Expand Up @@ -4769,6 +4783,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,
Expand Down
10 changes: 10 additions & 0 deletions packages/workers-utils/src/map-worker-metadata-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
7 changes: 7 additions & 0 deletions packages/workers-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
CfDispatchNamespace,
CfDurableObject,
CfDurableObjectMigrations,
CfFlagship,
CfHelloWorld,
CfHyperdrive,
CfImagesBinding,
Expand Down Expand Up @@ -159,6 +160,11 @@ export type WorkerMetadataBinding =
name: string;
enable_timer?: boolean;
}
| {
type: "flagship";
name: string;
app_id: string;
}
| {
type: "ratelimit";
name: string;
Expand Down Expand Up @@ -332,6 +338,7 @@ export type Binding =
| ({ type: "secrets_store_secret" } & BindingOmit<CfSecretsStoreSecrets>)
| ({ type: "logfwdr" } & NameOmit<CfLogfwdrBinding>)
| ({ type: "unsafe_hello_world" } & BindingOmit<CfHelloWorld>)
| ({ type: "flagship" } & BindingOmit<CfFlagship>)
| ({ type: "ratelimit" } & NameOmit<CfRateLimit>)
| ({ type: "worker_loader" } & BindingOmit<CfWorkerLoader>)
| ({ type: "vpc_service" } & BindingOmit<CfVpcService>)
Expand Down
5 changes: 5 additions & 0 deletions packages/workers-utils/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ export interface CfHelloWorld {
enable_timer?: boolean;
}

export interface CfFlagship {
binding: string;
app_id: string;
}

export interface CfWorkerLoader {
binding: string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe("normalizeAndValidateConfig()", () => {
r2_buckets: [],
secrets_store_secrets: [],
unsafe_hello_world: [],
flagship: [],
ratelimits: [],
vpc_services: [],
vpc_networks: [],
Expand Down
4 changes: 2 additions & 2 deletions packages/wrangler/src/__tests__/deploy/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions packages/wrangler/src/__tests__/type-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
{
Expand Down Expand Up @@ -765,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;
Expand Down Expand Up @@ -880,6 +887,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;
Expand Down Expand Up @@ -1058,6 +1066,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;
Expand Down
Loading
Loading