Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions .changeset/add-web-search-binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"miniflare": minor
"wrangler": minor
"@cloudflare/workers-utils": minor
---

Add support for the new `web_search` binding kind.

Cloudflare Web Search is a managed, zero-setup web discovery primitive for agents and Workers. Declare the binding as a single object in `wrangler.jsonc`:

```jsonc
{
"web_search": { "binding": "WEBSEARCH" },
}
```

There is exactly one shared web corpus, so there is no namespace, instance, or other field to specify -- only the variable name. The binding exposes a single `search()` method that returns URLs and catalog metadata for a query. Web Search is discovery-only -- to read a result's content the caller invokes the global `fetch()` API against the result's `url`.

The binding is **always remote** in local development: Miniflare proxies to the production Web Search service via the remote-bindings transport. Adds the `websearch.run` OAuth scope to `wrangler login`.

Also adds a `wrangler websearch search` command for running ad-hoc queries from the CLI:

```sh
npx wrangler websearch search "cloudflare workers"
npx wrangler websearch search "cloudflare workers" --limit 5
npx wrangler websearch search "cloudflare workers" --json
```

`--limit` is optional (defaults to 10, capped at 20). `--json` prints the raw response; without it the results render as a pretty table.
4 changes: 4 additions & 0 deletions packages/miniflare/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
} from "./version-metadata";
import { VPC_NETWORKS_PLUGIN, VPC_NETWORKS_PLUGIN_NAME } from "./vpc-networks";
import { VPC_SERVICES_PLUGIN, VPC_SERVICES_PLUGIN_NAME } from "./vpc-services";
import { WEB_SEARCH_PLUGIN, WEB_SEARCH_PLUGIN_NAME } from "./web-search";
import {
WORKER_LOADER_PLUGIN,
WORKER_LOADER_PLUGIN_NAME,
Expand Down Expand Up @@ -67,6 +68,7 @@ export const PLUGINS = {
[ANALYTICS_ENGINE_PLUGIN_NAME]: ANALYTICS_ENGINE_PLUGIN,
[AI_PLUGIN_NAME]: AI_PLUGIN,
[AI_SEARCH_PLUGIN_NAME]: AI_SEARCH_PLUGIN,
[WEB_SEARCH_PLUGIN_NAME]: WEB_SEARCH_PLUGIN,
[BROWSER_RENDERING_PLUGIN_NAME]: BROWSER_RENDERING_PLUGIN,
[DISPATCH_NAMESPACE_PLUGIN_NAME]: DISPATCH_NAMESPACE_PLUGIN,
[IMAGES_PLUGIN_NAME]: IMAGES_PLUGIN,
Expand Down Expand Up @@ -136,6 +138,7 @@ export type WorkerOptions = z.input<typeof CORE_PLUGIN.options> &
z.input<typeof ANALYTICS_ENGINE_PLUGIN.options> &
z.input<typeof AI_PLUGIN.options> &
z.input<typeof AI_SEARCH_PLUGIN.options> &
z.input<typeof WEB_SEARCH_PLUGIN.options> &
z.input<typeof BROWSER_RENDERING_PLUGIN.options> &
z.input<typeof DISPATCH_NAMESPACE_PLUGIN.options> &
z.input<typeof IMAGES_PLUGIN.options> &
Expand Down Expand Up @@ -227,6 +230,7 @@ export * from "./email";
export * from "./analytics-engine";
export * from "./ai";
export * from "./ai-search";
export * from "./web-search";
export * from "./browser-rendering";
export * from "./dispatch-namespace";
export * from "./images";
Expand Down
81 changes: 81 additions & 0 deletions packages/miniflare/src/plugins/web-search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { z } from "zod";
import {
getUserBindingServiceName,
ProxyNodeBinding,
remoteProxyClientWorker,
} from "../shared";
import type { Plugin, RemoteProxyConnectionString } from "../shared";

const WebSearchEntrySchema = z.object({
remoteProxyConnectionString: z
.custom<RemoteProxyConnectionString>()
.optional(),
});

export const WebSearchOptionsSchema = z.object({
webSearch: z.record(WebSearchEntrySchema).optional(),
});

export const WEB_SEARCH_PLUGIN_NAME = "web-search";

const WEB_SEARCH_SCOPE = "web-search";

export const WEB_SEARCH_PLUGIN: Plugin<typeof WebSearchOptionsSchema> = {
options: WebSearchOptionsSchema,
async getBindings(options) {
const bindings: {
name: string;
service: { name: string };
}[] = [];

for (const [bindingName, entry] of Object.entries(
options.webSearch ?? {}
)) {
bindings.push({
name: bindingName,
service: {
name: getUserBindingServiceName(
WEB_SEARCH_SCOPE,
bindingName,
entry.remoteProxyConnectionString
),
},
});
}

return bindings;
},
getNodeBindings(options: z.infer<typeof WebSearchOptionsSchema>) {
const nodeBindings: Record<string, ProxyNodeBinding> = {};

for (const bindingName of Object.keys(options.webSearch ?? {})) {
nodeBindings[bindingName] = new ProxyNodeBinding();
}

return nodeBindings;
},
async getServices({ options }) {
const services: {
name: string;
worker: ReturnType<typeof remoteProxyClientWorker>;
}[] = [];

for (const [bindingName, entry] of Object.entries(
options.webSearch ?? {}
)) {
services.push({
name: getUserBindingServiceName(
WEB_SEARCH_SCOPE,
bindingName,
entry.remoteProxyConnectionString
),
worker: remoteProxyClientWorker(
entry.remoteProxyConnectionString,
bindingName
),
});
}

return services;
},
};
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 @@ -336,6 +336,7 @@ export const defaultWranglerConfig: Config = {
vectorize: [],
ai_search_namespaces: [],
ai_search: [],
web_search: undefined,
hyperdrive: [],
workflows: [],
secrets_store_secrets: [],
Expand Down
20 changes: 20 additions & 0 deletions packages/workers-utils/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,26 @@ export interface EnvironmentNonInheritable {
remote?: boolean;
}[];

/**
* Cloudflare Web Search binding. There is exactly one shared web corpus, so the
* binding is zero-config -- only the variable name is required, declared as a
* single object (not an array).
*
* NOTE: This field is not automatically inherited from the top level environment,
* and so must be specified in every named environment.
*
* @default {}
* @nonInheritable
*/
web_search:
| {
/** The binding name used to refer to Web Search in the Worker. */
binding: string;
/** Whether the Web Search binding should be remote or not in local development */
remote?: boolean;
}
| undefined;

/**
* Specifies Hyperdrive configs that are bound to this Worker environment.
*
Expand Down
14 changes: 14 additions & 0 deletions packages/workers-utils/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export type ConfigBindingFieldName =
| "vectorize"
| "ai_search_namespaces"
| "ai_search"
| "web_search"
| "hyperdrive"
| "r2_buckets"
| "logfwdr"
Expand Down Expand Up @@ -124,6 +125,7 @@ export const friendlyBindingNames: Record<ConfigBindingFieldName, string> = {
vectorize: "Vectorize Index",
ai_search_namespaces: "AI Search Namespace",
ai_search: "AI Search Instance",
web_search: "Web Search",
hyperdrive: "Hyperdrive Config",
r2_buckets: "R2 Bucket",
logfwdr: "logfwdr",
Expand Down Expand Up @@ -181,6 +183,7 @@ const bindingTypeFriendlyNames: Record<Binding["type"], string> = {
vectorize: "Vectorize Index",
ai_search_namespace: "AI Search Namespace",
ai_search: "AI Search Instance",
web_search: "Web Search",
hyperdrive: "Hyperdrive Config",
service: "Worker",
fetcher: "Service Binding",
Expand Down Expand Up @@ -1756,6 +1759,16 @@ function normalizeAndValidateEnvironment(
validateBindingArray(envName, validateAISearchBinding),
[]
),
web_search: notInheritable(
diagnostics,
topLevelEnv,
rawConfig,
rawEnv,
envName,
"web_search",
validateNamedSimpleBinding(envName),
undefined
),
hyperdrive: notInheritable(
diagnostics,
topLevelEnv,
Expand Down Expand Up @@ -3024,6 +3037,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
"ai",
"ai_search_namespace",
"ai_search",
"web_search",
"kv_namespace",
"durable_object_namespace",
"d1_database",
Expand Down
7 changes: 7 additions & 0 deletions packages/workers-utils/src/map-worker-metadata-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,13 @@ export function mapWorkerMetadataBindings(
},
];
break;
case "web_search":
{
configObj.web_search = {
binding: binding.name,
};
}
break;
case "hyperdrive":
configObj.hyperdrive = [
...(configObj.hyperdrive ?? []),
Expand Down
3 changes: 3 additions & 0 deletions packages/workers-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type {
CfVectorize,
CfVpcNetwork,
CfVpcService,
CfWebSearch,
CfWorkerLoader,
CfWorkflow,
} from "./worker";
Expand Down Expand Up @@ -73,6 +74,7 @@ export type WorkerMetadataBinding =
| { type: "data_blob"; name: string; part: string }
| { type: "ai_search_namespace"; name: string; namespace: string }
| { type: "ai_search"; name: string; instance_name: string }
| { type: "web_search"; name: string }
| { type: "kv_namespace"; name: string; namespace_id: string; raw?: boolean }
| { type: "media"; name: string }
| {
Expand Down Expand Up @@ -334,6 +336,7 @@ export type Binding =
| ({ type: "vectorize" } & BindingOmit<CfVectorize>)
| ({ type: "ai_search_namespace" } & BindingOmit<CfAISearchNamespace>)
| ({ type: "ai_search" } & BindingOmit<CfAISearch>)
| ({ type: "web_search" } & BindingOmit<CfWebSearch>)
| ({ type: "hyperdrive" } & BindingOmit<CfHyperdrive>)
| ({ type: "service" } & BindingOmit<CfService>)
| { type: "fetcher"; fetcher: ServiceFetch }
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 @@ -242,6 +242,11 @@ export interface CfAISearch {
remote?: boolean;
}

export interface CfWebSearch {
binding: string;
remote?: boolean;
}

export interface CfSecretsStoreSecrets {
binding: string;
store_id: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ describe("normalizeAndValidateConfig()", () => {
text_blobs: undefined,
browser: undefined,
ai: undefined,
web_search: undefined,
version_metadata: undefined,
triggers: {
crons: undefined,
Expand Down Expand Up @@ -2019,6 +2020,50 @@ describe("normalizeAndValidateConfig()", () => {
});
});

describe("[web_search]", () => {
it("should accept a valid web_search binding", ({ expect }) => {
const { diagnostics } = normalizeAndValidateConfig(
{ web_search: { binding: "WEBSEARCH" } } as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasErrors()).toBe(false);
expect(diagnostics.hasWarnings()).toBe(false);
});

it("should error if web_search is an array", ({ expect }) => {
const { diagnostics } = normalizeAndValidateConfig(
{ web_search: [] } as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasWarnings()).toBe(false);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- The field "web_search" should be an object but got []."
`);
});

it("should error if web_search has no binding name", ({ expect }) => {
const { diagnostics } = normalizeAndValidateConfig(
{ web_search: {} } as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasWarnings()).toBe(false);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- binding should have a string "binding" field."
`);
});
});

// Vectorize
describe("[vectorize]", () => {
it("should error if vectorize is an object", ({ expect }) => {
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 @@ -761,7 +761,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%20artifacts%3Awrite%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20browser%3Awrite%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%20websearch.run%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20artifacts%3Awrite%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20browser%3Awrite%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 @@ -807,7 +807,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%20artifacts%3Awrite%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20browser%3Awrite%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%20websearch.run%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20artifacts%3Awrite%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20browser%3Awrite%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
2 changes: 2 additions & 0 deletions packages/wrangler/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ describe("wrangler", () => {
wrangler types [path] 📝 Generate types from your Worker configuration
wrangler versions 🫧 List, view, upload and deploy Versions of your Worker to Cloudflare
wrangler vpc 🌐 Manage VPC [open beta]
wrangler websearch 🔎 Run queries against Cloudflare Web Search [open beta]
wrangler workflows 🔁 Manage Workflows

STORAGE & DATABASES
Expand Down Expand Up @@ -149,6 +150,7 @@ describe("wrangler", () => {
wrangler types [path] 📝 Generate types from your Worker configuration
wrangler versions 🫧 List, view, upload and deploy Versions of your Worker to Cloudflare
wrangler vpc 🌐 Manage VPC [open beta]
wrangler websearch 🔎 Run queries against Cloudflare Web Search [open beta]
wrangler workflows 🔁 Manage Workflows

STORAGE & DATABASES
Expand Down
Loading
Loading