diff --git a/.changeset/add-flagship-binding.md b/.changeset/add-flagship-binding.md new file mode 100644 index 0000000000..d285cddb66 --- /dev/null +++ b/.changeset/add-flagship-binding.md @@ -0,0 +1,7 @@ +--- +"miniflare": minor +"wrangler": minor +"@cloudflare/workers-utils": minor +--- + +feat: add Flagship feature flag binding support diff --git a/packages/miniflare/src/plugins/flagship/index.ts b/packages/miniflare/src/plugins/flagship/index.ts new file mode 100644 index 0000000000..18d019e99b --- /dev/null +++ b/packages/miniflare/src/plugins/flagship/index.ts @@ -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() + .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 []; + } + + return Object.entries(options.flagship).map( + ([name, { remoteProxyConnectionString }]) => { + return { + name: getUserBindingServiceName( + FLAGSHIP_PLUGIN_NAME, + name, + remoteProxyConnectionString + ), + worker: remoteProxyClientWorker(remoteProxyConnectionString, name), + }; + } + ); + }, +}; diff --git a/packages/miniflare/src/plugins/index.ts b/packages/miniflare/src/plugins/index.ts index 168ca2a3e9..f01e021ccf 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"; @@ -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, @@ -140,6 +142,7 @@ export type WorkerOptions = z.input & z.input & z.input & z.input & + z.input & z.input & z.input & z.input; @@ -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"; diff --git a/packages/workers-utils/src/config/config.ts b/packages/workers-utils/src/config/config.ts index a5daf53431..98b586db46 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 cc3ed4b365..59ff2bb45e 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 c0e95b9f8f..69a56fb63f 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" | "vpc_networks"; @@ -144,6 +145,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", vpc_networks: "VPC Network", @@ -186,6 +188,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", @@ -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, @@ -2971,6 +2984,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => { "pipeline", "worker_loader", "vpc_service", + "flagship", "vpc_network", "stream", "media", @@ -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, diff --git a/packages/workers-utils/src/map-worker-metadata-bindings.ts b/packages/workers-utils/src/map-worker-metadata-bindings.ts index ccd7462f41..2a3d35f5ce 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 143ec41b54..29fbf67289 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, @@ -159,6 +160,11 @@ export type WorkerMetadataBinding = name: string; enable_timer?: boolean; } + | { + type: "flagship"; + name: string; + app_id: string; + } | { type: "ratelimit"; name: string; @@ -332,6 +338,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 f7cc525212..409942b24f 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; } 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 c60f5e6d8c..bb401b39e6 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: [], vpc_networks: [], 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__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index 272f665ee8..bc607feee6 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" }, { @@ -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; @@ -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; @@ -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; 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 diff --git a/packages/wrangler/src/api/startDevWorker/utils.ts b/packages/wrangler/src/api/startDevWorker/utils.ts index bf195fceb4..66e8385aa0 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/deploy/config-diffs.ts b/packages/wrangler/src/deploy/config-diffs.ts index d5e636471e..736df54f77 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, vpc_networks: true, 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 ae704c8708..1fa2be7b2d 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 vpc_networks = extractBindingsOfType("vpc_network", bindings); @@ -384,6 +385,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 d0d9e8d4b7..cd810b6b6c 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -422,6 +422,7 @@ type WorkerOptionsBindings = Pick< | "dispatchNamespaces" | "mtlsCertificates" | "helloWorld" + | "flagship" | "workerLoaders" | "unsafeBindings" | "additionalUnboundDurableObjects" @@ -494,6 +495,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 @@ -783,6 +785,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/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 71d09edd04..2a8b8a8dc4 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, flagshipBinding] of (env.flagship ?? []).entries()) { + if (!flagshipBinding.binding) { + throwMissingBindingError({ + binding: flagshipBinding, + bindingType: "flagship", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + addBinding(flagshipBinding.binding, "Flags", "flagship", envName); + } + for (const [index, ratelimit] of (env.ratelimits ?? []).entries()) { if (!ratelimit.name) { throwMissingBindingError({ @@ -2782,6 +2797,25 @@ function collectCoreBindingsPerEnvironment( }); } + for (const [index, flagshipBinding] of (env.flagship ?? []).entries()) { + if (!flagshipBinding.binding) { + throwMissingBindingError({ + binding: flagshipBinding, + bindingType: "flagship", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + bindings.push({ + bindingCategory: "flagship", + name: flagshipBinding.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": 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 659cbbd877..e9f1f6ec9b 100644 --- a/packages/wrangler/src/utils/print-bindings.ts +++ b/packages/wrangler/src/utils/print-bindings.ts @@ -136,6 +136,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); @@ -458,6 +459,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 }) => {