Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 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
dfeef2d
Merge remote-tracking branch 'upstream/main' into roerohan/flagship
roerohan Apr 6, 2026
bde6e72
fix: use import type for type-only imports in flagship plugin
roerohan Apr 6, 2026
7887652
feat: add local JSRPC stub worker for flagship binding
roerohan Apr 8, 2026
aebf6f0
fix: use plain UUID for flagship app_id in test fixtures
roerohan Apr 8, 2026
0da66f3
feat: add Miniflare class API and plugin test for flagship binding
roerohan Apr 8, 2026
f5b5804
fix: pass actual app_id to local worker config and mark binding as lo…
roerohan Apr 8, 2026
3f637dd
Merge branch 'main' into roerohan/flagship
roerohan Apr 8, 2026
e7c6743
Update packages/miniflare/src/index.ts
roerohan Apr 8, 2026
36ce133
fix: remove duplicate closing brace and simplify getFlagshipBinding r…
roerohan Apr 8, 2026
94b9893
fix: restore full getFlagshipBinding return type with generic methods
roerohan Apr 8, 2026
5bdd92d
Merge branch 'main' into roerohan/flagship
roerohan Apr 9, 2026
79a9d9c
Merge branch 'main' into roerohan/flagship
roerohan Apr 9, 2026
195176c
chore: update changeset
roerohan Apr 9, 2026
5502d4b
fix: use Flags instead of FlagshipBinding
roerohan Apr 9, 2026
a307247
Merge branch 'main' into roerohan/flagship
roerohan Apr 9, 2026
52be722
chore: remote bindings support
roerohan Apr 9, 2026
fef1869
Merge branch 'main' into roerohan/flagship
petebacondarwin Apr 9, 2026
391103b
fix: address PR review comments for flagship binding
roerohan Apr 9, 2026
f40249e
chore: remote validity check
roerohan Apr 9, 2026
5984a11
fix: add flagship to removeRemoteConfigFieldFromBindings, add tests
roerohan Apr 9, 2026
5ce25e1
Merge branch 'main' into roerohan/flagship
roerohan Apr 9, 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
9 changes: 9 additions & 0 deletions .changeset/add-flagship-binding.md
Comment thread
roerohan marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"miniflare": minor
"wrangler": minor
"@cloudflare/workers-utils": minor
---

feat: add Flagship feature flag binding support
Comment thread
roerohan marked this conversation as resolved.

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.
5 changes: 5 additions & 0 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
BROWSER_RENDERING_PLUGIN_NAME,
D1_PLUGIN_NAME,
DURABLE_OBJECTS_PLUGIN_NAME,
FLAGSHIP_PLUGIN_NAME,
getDirectSocketName,
getGlobalServices,
getPersistPath,
Expand Down Expand Up @@ -148,6 +149,7 @@ import type {
D1Database,
DurableObjectNamespace,
Fetcher,
Flags,
ImagesBinding,
KVNamespace,
KVNamespaceListKey,
Expand Down Expand Up @@ -2957,6 +2959,9 @@ export class Miniflare {
}> {
return this.#getProxy(HELLO_WORLD_PLUGIN_NAME, bindingName, workerName);
}
getFlagshipBinding(bindingName: string, workerName?: string): Promise<Flags> {
return this.#getProxy(FLAGSHIP_PLUGIN_NAME, bindingName, workerName);
}
getStreamBinding(
bindingName: string,
workerName?: string
Expand Down
84 changes: 84 additions & 0 deletions packages/miniflare/src/plugins/flagship/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import BINDING_SCRIPT from "worker:flagship/binding";
import { z } from "zod";
import {
getUserBindingServiceName,
ProxyNodeBinding,
remoteProxyClientWorker,
} from "../shared";
import type { Worker_Binding } from "../../runtime";
import type { Plugin, 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
),
entrypoint: "FlagshipBinding",
},
})
);
},
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: remoteProxyConnectionString
? remoteProxyClientWorker(remoteProxyConnectionString, name)
: {
compatibilityDate: "2025-03-17",
modules: [
{
name: "binding.worker.js",
esModule: BINDING_SCRIPT(),
},
],
},
};
}
);
},
};
4 changes: 4 additions & 0 deletions packages/miniflare/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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 @@ -74,6 +75,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 @@ -141,6 +143,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 @@ -230,6 +233,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";
104 changes: 104 additions & 0 deletions packages/miniflare/src/workers/flagship/binding.worker.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
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<string, string | number | boolean>
): Promise<unknown> {
return defaultValue;
}

async getBooleanValue(
_flagKey: string,
defaultValue: boolean,
_context?: Record<string, string | number | boolean>
): Promise<boolean> {
return defaultValue;
}

async getStringValue(
_flagKey: string,
defaultValue: string,
_context?: Record<string, string | number | boolean>
): Promise<string> {
return defaultValue;
}

async getNumberValue(
_flagKey: string,
defaultValue: number,
_context?: Record<string, string | number | boolean>
): Promise<number> {
return defaultValue;
}

async getObjectValue<T extends object>(
_flagKey: string,
defaultValue: T,
_context?: Record<string, string | number | boolean>
): Promise<T> {
return defaultValue;
}

async getBooleanDetails(
flagKey: string,
defaultValue: boolean,
_context?: Record<string, string | number | boolean>
): Promise<EvaluationDetails<boolean>> {
return {
flagKey,
value: defaultValue,
reason: "DEFAULT",
};
}

async getStringDetails(
flagKey: string,
defaultValue: string,
_context?: Record<string, string | number | boolean>
): Promise<EvaluationDetails<string>> {
return {
flagKey,
value: defaultValue,
reason: "DEFAULT",
};
}

async getNumberDetails(
flagKey: string,
defaultValue: number,
_context?: Record<string, string | number | boolean>
): Promise<EvaluationDetails<number>> {
return {
flagKey,
value: defaultValue,
reason: "DEFAULT",
};
}

async getObjectDetails<T extends object>(
flagKey: string,
defaultValue: T,
_context?: Record<string, string | number | boolean>
): Promise<EvaluationDetails<T>> {
return {
flagKey,
value: defaultValue,
reason: "DEFAULT",
};
}
}
Loading
Loading