diff --git a/.changeset/quiet-workers-register.md b/.changeset/quiet-workers-register.md new file mode 100644 index 00000000000..6dd7a73689e --- /dev/null +++ b/.changeset/quiet-workers-register.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Add per-worker control over dev registry registration + +Miniflare workers must now opt in to the dev registry with `unsafeRegisterWorker`. Wrangler and the Cloudflare Vite plugin use this option to advertise user workers without exposing internal or external workers. diff --git a/fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts b/fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts index 33b8c076e37..03430040f3e 100644 --- a/fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts +++ b/fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts @@ -890,6 +890,7 @@ describe("entrypoints", () => { const boundWorker = new Miniflare({ name: "bound", unsafeDevRegistryPath: isolatedDevRegistryPath, + unsafeRegisterWorker: true, compatibilityFlags: ["experimental"], modules: true, https: true, diff --git a/packages/miniflare/README.md b/packages/miniflare/README.md index 58c157c26ff..0c2c909eb7f 100644 --- a/packages/miniflare/README.md +++ b/packages/miniflare/README.md @@ -218,6 +218,11 @@ parameter in module format Workers. Unique name for this worker. Only required if multiple `workers` are specified. +- `unsafeRegisterWorker?: boolean` + + If `true`, advertises this Worker in the dev registry configured by + `unsafeDevRegistryPath`. Defaults to `false`. + - `rootPath?: string` Path against which all other path options for this Worker are resolved @@ -597,7 +602,9 @@ Options shared between all Workers/"nanoservices". - `unsafeDevRegistryPath?: string` Path to the dev registry directory. This allows Miniflare to automatically - discover external services and Durable Objects running on another miniflare instance and connect them. + discover external services and Durable Objects running on another Miniflare + instance and connect them. Workers must opt in to being advertised by setting + `unsafeRegisterWorker` to `true`. - `unsafeDevRegistryDurableObjectProxy?: boolean` diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 87314a6b275..262b8298c88 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -2695,7 +2695,7 @@ export class Miniflare { const entries: [string, WorkerDefinition][] = []; for (const workerOpts of this.#workerOpts) { - if (!workerOpts.core.name) { + if (!workerOpts.core.name || !workerOpts.core.unsafeRegisterWorker) { continue; } diff --git a/packages/miniflare/src/plugins/core/index.ts b/packages/miniflare/src/plugins/core/index.ts index 53569af0075..6f5907d1fab 100644 --- a/packages/miniflare/src/plugins/core/index.ts +++ b/packages/miniflare/src/plugins/core/index.ts @@ -189,6 +189,8 @@ const CoreOptionsSchemaInput = z.intersection( unsafeEvalBinding: z.string().optional(), unsafeUseModuleFallbackService: z.boolean().optional(), + /** Whether this Worker should be advertised in the dev registry. Defaults to `false`. */ + unsafeRegisterWorker: z.boolean().optional(), /** Used to set the vitest pool worker SELF binding to point to the Router Worker if there are assets. (If there are assets but we're not using vitest, the miniflare entry worker can point directly to diff --git a/packages/miniflare/src/shared/DEV_REGISTRY.md b/packages/miniflare/src/shared/DEV_REGISTRY.md index 5d226361b79..96864074a47 100644 --- a/packages/miniflare/src/shared/DEV_REGISTRY.md +++ b/packages/miniflare/src/shared/DEV_REGISTRY.md @@ -4,7 +4,7 @@ The dev registry enables cross-process communication between multiple `wrangler ## Overview -Each `wrangler dev` process writes its worker's connection info to a shared filesystem directory. When Worker A needs to talk to Worker B, a proxy worker inside A's workerd process reads B's debug port address from the registry and connects via Cap'n Proto RPC. +Each `wrangler dev` process writes its user worker's connection info to a shared filesystem directory. Internal and external workers are not advertised. When Worker A needs to talk to Worker B, a proxy worker inside A's workerd process reads B's debug port address from the registry and connects via Cap'n Proto RPC. ```mermaid graph TB @@ -55,6 +55,7 @@ type WorkerDefinition = { ``` - **Heartbeat**: Every 30s, the file's mtime is touched to signal that the Worker is still running. +- **Registration**: Only workers with `unsafeRegisterWorker: true` are advertised. - **Stale cleanup**: On every read, files older than 5 minutes are deleted (5 minutes is much longer than 30s just to provide a safe buffer) - **Change detection**: Chokidar watches the registry directory. When a file changes, `refresh()` compares the new state against the previous JSON snapshot and fires `onUpdate` only if a watched external service actually changed. diff --git a/packages/miniflare/test/dev-registry.spec.ts b/packages/miniflare/test/dev-registry.spec.ts index 5f7714e07f6..6e9135d7ff3 100644 --- a/packages/miniflare/test/dev-registry.spec.ts +++ b/packages/miniflare/test/dev-registry.spec.ts @@ -4,10 +4,30 @@ import { useDispose, useTmp } from "./test-shared"; import type { MiniflareOptions, WorkerRegistry } from "miniflare"; describe.sequential("DevRegistry", () => { + test("only registers workers that opt in", async ({ expect }) => { + const unsafeDevRegistryPath = await useTmp(); + const workerOptions = { + name: "worker", + unsafeDevRegistryPath, + compatibilityFlags: ["experimental"], + modules: true, + script: `export default { fetch() { return new Response("ok"); } };`, + } satisfies MiniflareOptions; + const mf = new Miniflare(workerOptions); + useDispose(mf); + await mf.ready; + + expect(getWorkerRegistry(unsafeDevRegistryPath)).toEqual({}); + + await mf.setOptions({ ...workerOptions, unsafeRegisterWorker: true }); + expect(getWorkerRegistry(unsafeDevRegistryPath)["worker"]).toBeDefined(); + }); + test("fetch to service worker", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], script: `addEventListener("fetch", (event) => { @@ -92,6 +112,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -169,6 +190,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -245,6 +267,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -316,6 +339,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -356,6 +380,7 @@ describe.sequential("DevRegistry", () => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -444,6 +469,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -527,6 +553,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -568,6 +595,7 @@ describe.sequential("DevRegistry", () => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], @@ -635,6 +663,7 @@ describe.sequential("DevRegistry", () => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], @@ -735,6 +764,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], @@ -813,6 +843,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], @@ -873,6 +904,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, workflows: { @@ -916,6 +948,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], durableObjects: { @@ -979,6 +1012,7 @@ describe.sequential("DevRegistry", () => { // Restart remote — gets a new debug port, registry file updates await remote.setOptions({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], durableObjects: { @@ -1017,6 +1051,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -1066,6 +1101,7 @@ describe.sequential("DevRegistry", () => { // Restart remote — gets a new debug port, registry file updates await remote.setOptions({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -1093,6 +1129,7 @@ describe.sequential("DevRegistry", () => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, unsafeTriggerHandlers: true, compatibilityFlags: ["experimental"], @@ -1155,6 +1192,7 @@ describe.sequential("DevRegistry", () => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -1264,6 +1302,7 @@ describe.sequential("DevRegistry", () => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -1366,6 +1405,7 @@ describe.sequential("DevRegistry", () => { }; const remoteOptions: MiniflareOptions = { name: "remote-worker", + unsafeRegisterWorker: true, compatibilityFlags: ["experimental"], modules: true, script: ` @@ -1471,6 +1511,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -1538,6 +1579,7 @@ describe.sequential("DevRegistry", () => { const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, https: true, @@ -1622,6 +1664,7 @@ describe.sequential("DevRegistry", () => { const unrelated = new Miniflare({ name: "unrelated-worker", unsafeDevRegistryPath, + unsafeRegisterWorker: true, compatibilityFlags: ["experimental"], modules: true, script: ` @@ -1643,6 +1686,7 @@ describe.sequential("DevRegistry", () => { // Create remote worker (one we're actually bound to) - this should trigger the callback const remote = new Miniflare({ name: "remote-worker", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, @@ -1746,6 +1790,7 @@ describe.sequential("DevRegistry", () => { const sharedOptions = { name: "consumer-worker", unsafeDevRegistryPath, + unsafeRegisterWorker: true, compatibilityFlags: ["experimental"], modules: true, } satisfies Partial; diff --git a/packages/miniflare/test/plugins/local-explorer/aggregation.spec.ts b/packages/miniflare/test/plugins/local-explorer/aggregation.spec.ts index 32c677a819f..7f9b50f27e9 100644 --- a/packages/miniflare/test/plugins/local-explorer/aggregation.spec.ts +++ b/packages/miniflare/test/plugins/local-explorer/aggregation.spec.ts @@ -39,6 +39,7 @@ describe("Cross-process aggregation", () => { instanceA = new Miniflare({ name: "worker-a", + unsafeRegisterWorker: true, inspectorPort: 0, compatibilityDate: "2025-01-01", modules: true, @@ -68,6 +69,7 @@ describe("Cross-process aggregation", () => { instanceB = new Miniflare({ name: "worker-b", + unsafeRegisterWorker: true, inspectorPort: 0, compatibilityDate: "2025-01-01", modules: true, @@ -406,6 +408,7 @@ describe("Multi-worker peer deduplication", () => { instanceA = new Miniflare({ name: "worker-a", + unsafeRegisterWorker: true, inspectorPort: 0, compatibilityDate: "2025-01-01", modules: true, @@ -428,6 +431,7 @@ describe("Multi-worker peer deduplication", () => { workers: [ { name: "worker-b1", + unsafeRegisterWorker: true, modules: true, script: `export default { fetch() { return new Response("Worker B1"); } }`, kvNamespaces: { @@ -436,6 +440,7 @@ describe("Multi-worker peer deduplication", () => { }, { name: "worker-b2", + unsafeRegisterWorker: true, modules: true, script: `export default { fetch() { return new Response("Worker B2"); } }`, kvNamespaces: { @@ -510,6 +515,7 @@ describe("Same ID across multiple instances with different persistence directori // ties it to a specific instance. instanceA = new Miniflare({ name: "worker-a", + unsafeRegisterWorker: true, inspectorPort: 0, compatibilityDate: "2025-01-01", modules: true, @@ -531,6 +537,7 @@ describe("Same ID across multiple instances with different persistence directori instanceB = new Miniflare({ name: "worker-b", + unsafeRegisterWorker: true, inspectorPort: 0, compatibilityDate: "2025-01-01", modules: true, @@ -627,6 +634,7 @@ describe("Same ID across multiple instances with same persistence directories", // ties it to a specific instance. instanceA = new Miniflare({ name: "worker-a", + unsafeRegisterWorker: true, inspectorPort: 0, compatibilityDate: "2025-01-01", modules: true, @@ -646,6 +654,7 @@ describe("Same ID across multiple instances with same persistence directories", instanceB = new Miniflare({ name: "worker-b", + unsafeRegisterWorker: true, inspectorPort: 0, compatibilityDate: "2025-01-01", modules: true, diff --git a/packages/miniflare/test/plugins/local-explorer/index.spec.ts b/packages/miniflare/test/plugins/local-explorer/index.spec.ts index 8fa26ece0c2..fb784b512f7 100644 --- a/packages/miniflare/test/plugins/local-explorer/index.spec.ts +++ b/packages/miniflare/test/plugins/local-explorer/index.spec.ts @@ -605,6 +605,7 @@ describe("Local Explorer /api/local/workers endpoint", () => { workers: [ { name: "worker-a1", + unsafeRegisterWorker: true, modules: true, script: ` export class TestDO { @@ -628,6 +629,7 @@ describe("Local Explorer /api/local/workers endpoint", () => { }, { name: "worker-a2", + unsafeRegisterWorker: true, modules: true, script: `export default { fetch() { return new Response("Worker A2"); } }`, kvNamespaces: { @@ -640,6 +642,7 @@ describe("Local Explorer /api/local/workers endpoint", () => { // Instance B has one worker instanceB = new Miniflare({ name: "worker-b", + unsafeRegisterWorker: true, inspectorPort: 0, compatibilityDate: "2025-01-01", modules: true, diff --git a/packages/miniflare/test/plugins/queues/cross-process.spec.ts b/packages/miniflare/test/plugins/queues/cross-process.spec.ts index f97117e6ae5..2167711a255 100644 --- a/packages/miniflare/test/plugins/queues/cross-process.spec.ts +++ b/packages/miniflare/test/plugins/queues/cross-process.spec.ts @@ -13,6 +13,7 @@ function createConsumer( ): Miniflare { return new Miniflare({ name, + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], queueConsumers: { @@ -171,6 +172,7 @@ describe.sequential("cross-process queues", () => { // message moves to "my-dlq", whose consumer lives in the other process. const failingConsumer = new Miniflare({ name: "failing-consumer", + unsafeRegisterWorker: true, unsafeDevRegistryPath, compatibilityFlags: ["experimental"], queueProducers: { QUEUE: { queueName: "my-queue" } }, diff --git a/packages/vite-plugin-cloudflare/src/miniflare-options.ts b/packages/vite-plugin-cloudflare/src/miniflare-options.ts index 46ceb863a6f..d99df9ecb61 100644 --- a/packages/vite-plugin-cloudflare/src/miniflare-options.ts +++ b/packages/vite-plugin-cloudflare/src/miniflare-options.ts @@ -442,6 +442,7 @@ export async function getDevMiniflareOptions( worker: { ...workerOptions, name: worker.config.name, + unsafeRegisterWorker: true, modulesRoot: miniflareModulesRoot, modules: [ { @@ -835,6 +836,7 @@ export async function getPreviewMiniflareOptions( ...workerOptions, name: workerOptions.name ?? workerConfig.name, unsafeInspectorProxy: inputInspectorPort !== false, + unsafeRegisterWorker: true, ...(previewWorker.source === "build-output" && previewWorker.bundle ? getModulesFromManifest(previewWorker.bundle) : miniflareWorkerOptions.main diff --git a/packages/wrangler/src/api/integrations/platform/index.ts b/packages/wrangler/src/api/integrations/platform/index.ts index 522193935c9..749a9cf3885 100644 --- a/packages/wrangler/src/api/integrations/platform/index.ts +++ b/packages/wrangler/src/api/integrations/platform/index.ts @@ -331,6 +331,7 @@ async function getMiniflareOptionsFromConfig(args: { script: "", modules: true, name: config.name, + unsafeRegisterWorker: true, zone: getZoneFromConfig(config), ...bindingOptions, ...assetOptions, diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index 110f4c72e52..da85063b9c2 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -1200,6 +1200,7 @@ export async function buildMiniflareOptions( workers: [ { name: getName(config), + unsafeRegisterWorker: true, compatibilityDate: config.compatibilityDate, compatibilityFlags: config.compatibilityFlags,