From 5ce7fc5bdea106363e3877e93defab2aa4a7e7f3 Mon Sep 17 00:00:00 2001 From: Enyinna Ogwe Date: Wed, 22 Jul 2026 11:57:07 +0100 Subject: [PATCH 1/6] [wrangler] Add --jurisdiction flag to kv namespace create --- .../kv-namespace-create-jurisdiction.md | 7 +++++ .../src/__tests__/kv/namespace.test.ts | 26 +++++++++++++++++++ packages/wrangler/src/kv/index.ts | 22 +++++++++++++--- 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 .changeset/kv-namespace-create-jurisdiction.md diff --git a/.changeset/kv-namespace-create-jurisdiction.md b/.changeset/kv-namespace-create-jurisdiction.md new file mode 100644 index 0000000000..e9a8aed0ac --- /dev/null +++ b/.changeset/kv-namespace-create-jurisdiction.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +Add an experimental `--jurisdiction` option to `wrangler kv namespace create` + +This option creates a KV namespace within a specific jurisdiction (for example `us`, `eu`, or `fedramp`), backing it with jurisdiction-scoped storage. It is experimental and currently gated to allow-listed accounts, so it is hidden from `--help` until the feature is generally available. diff --git a/packages/wrangler/src/__tests__/kv/namespace.test.ts b/packages/wrangler/src/__tests__/kv/namespace.test.ts index 61da7934bf..c78d0dbf59 100644 --- a/packages/wrangler/src/__tests__/kv/namespace.test.ts +++ b/packages/wrangler/src/__tests__/kv/namespace.test.ts @@ -165,6 +165,32 @@ describe("kv", () => { `); }); + it("should create a namespace in a jurisdiction", async ({ expect }) => { + msw.use( + http.post( + "*/accounts/:accountId/storage/kv/namespaces", + async ({ request, params }) => { + expect(params.accountId).toEqual("some-account-id"); + const body = (await request.json()) as Record; + expect(body.title).toEqual("UnitTestNamespace"); + expect(body.jurisdiction).toEqual("eu"); + return HttpResponse.json( + createFetchResult({ id: "some-namespace-id" }), + { status: 200 } + ); + }, + { once: true } + ) + ); + + await runWrangler( + "kv namespace create UnitTestNamespace --binding MY_NS --jurisdiction eu" + ); + expect(std.out).toContain( + 'Creating namespace with title "UnitTestNamespace" (jurisdiction: eu)' + ); + }); + describe.each(["wrangler.json", "wrangler.toml"])("%s", (configPath) => { it("should create a namespace", async ({ expect }) => { writeWranglerConfig({ name: "worker" }, configPath); diff --git a/packages/wrangler/src/kv/index.ts b/packages/wrangler/src/kv/index.ts index ea31f7babd..404df2d2b1 100644 --- a/packages/wrangler/src/kv/index.ts +++ b/packages/wrangler/src/kv/index.ts @@ -96,6 +96,13 @@ export const kvNamespaceCreateCommand = createCommand({ type: "boolean", describe: "Interact with a preview namespace", }, + jurisdiction: { + type: "string", + describe: + 'The jurisdiction where the new namespace will be created (e.g. "us", "eu", "fedramp")', + requiresArg: true, + hidden: true, + }, ...sharedResourceCreationArgs, }, positionalArgs: ["namespace"], @@ -104,18 +111,27 @@ export const kvNamespaceCreateCommand = createCommand({ const environment = args.env ? `${args.env}-` : ""; const preview = args.preview ? "_preview" : ""; const title = `${environment}${args.namespace}${preview}`; + const { jurisdiction } = args; const accountId = await requireAuth(config); printResourceLocation("remote"); // TODO: generate a binding name stripping non alphanumeric chars - logger.log(`🌀 Creating namespace with title "${title}"`); + logger.log( + `🌀 Creating namespace with title "${title}"${ + jurisdiction ? ` (jurisdiction: ${jurisdiction})` : "" + }` + ); let namespaceId: string; try { - const result = await sdk.kv.namespaces.create({ + const createParams: Cloudflare.KV.Namespaces.NamespaceCreateParams & { + jurisdiction?: string; + } = { account_id: accountId, title, - }); + jurisdiction, + }; + const result = await sdk.kv.namespaces.create(createParams); namespaceId = result.id; } catch (e) { if ( From 0f42bfe9517d4ec3c2080399f5be8f09b6ca03b9 Mon Sep 17 00:00:00 2001 From: oOPa <5878650+oOPa@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:54:55 +0100 Subject: [PATCH 2/6] Update .changeset/kv-namespace-create-jurisdiction.md to denote that the jurisdiction flag is hidden rather than experimental Co-authored-by: Edmund Hung --- .changeset/kv-namespace-create-jurisdiction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/kv-namespace-create-jurisdiction.md b/.changeset/kv-namespace-create-jurisdiction.md index e9a8aed0ac..bcaaa2d4d0 100644 --- a/.changeset/kv-namespace-create-jurisdiction.md +++ b/.changeset/kv-namespace-create-jurisdiction.md @@ -2,6 +2,6 @@ "wrangler": minor --- -Add an experimental `--jurisdiction` option to `wrangler kv namespace create` +Add hidden `--jurisdiction` option to `wrangler kv namespace create` for internal testing This option creates a KV namespace within a specific jurisdiction (for example `us`, `eu`, or `fedramp`), backing it with jurisdiction-scoped storage. It is experimental and currently gated to allow-listed accounts, so it is hidden from `--help` until the feature is generally available. From 5a77aafd664c362f9699df1c144550071d3a94da Mon Sep 17 00:00:00 2001 From: Enyinna Ogwe Date: Thu, 23 Jul 2026 14:00:58 +0100 Subject: [PATCH 3/6] Assert that output contains success message --- packages/wrangler/src/__tests__/kv/namespace.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wrangler/src/__tests__/kv/namespace.test.ts b/packages/wrangler/src/__tests__/kv/namespace.test.ts index c78d0dbf59..880a44cfec 100644 --- a/packages/wrangler/src/__tests__/kv/namespace.test.ts +++ b/packages/wrangler/src/__tests__/kv/namespace.test.ts @@ -189,6 +189,7 @@ describe("kv", () => { expect(std.out).toContain( 'Creating namespace with title "UnitTestNamespace" (jurisdiction: eu)' ); + expect(std.out).toContain("✨ Success!"); }); describe.each(["wrangler.json", "wrangler.toml"])("%s", (configPath) => { From 706aa6d2943a4dcbccff49ac75e7b68dfe84e351 Mon Sep 17 00:00:00 2001 From: Enyinna Ogwe Date: Fri, 24 Jul 2026 16:28:32 +0100 Subject: [PATCH 4/6] Support a jurisdiction field on kv_namespaces bindings for provisioning namespaces during wrangler deploy --- .../kv-namespace-provision-jurisdiction.md | 13 ++++ .../src/deploy/helpers/provision-bindings.ts | 11 ++- .../workers-utils/src/config/environment.ts | 5 ++ .../workers-utils/src/config/validation.ts | 9 +++ packages/workers-utils/src/worker.ts | 1 + .../normalize-and-validate-config.test.ts | 31 ++++++++ .../wrangler/src/__tests__/helpers/mock-kv.ts | 10 ++- .../wrangler/src/__tests__/provision.test.ts | 76 +++++++++++++++++++ packages/wrangler/src/kv/helpers.ts | 4 +- 9 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 .changeset/kv-namespace-provision-jurisdiction.md diff --git a/.changeset/kv-namespace-provision-jurisdiction.md b/.changeset/kv-namespace-provision-jurisdiction.md new file mode 100644 index 0000000000..5d6477c426 --- /dev/null +++ b/.changeset/kv-namespace-provision-jurisdiction.md @@ -0,0 +1,13 @@ +--- +"wrangler": minor +--- + +Support an optional `jurisdiction` field on `kv_namespaces` bindings + + The field is only read when the namespace is first provisioned; once an `id` exists the namespace is addressed by that `id` and the `jurisdiction` field is ignored. + +```jsonc +{ + "kv_namespaces": [{ "binding": "MY_KV", "jurisdiction": "eu" }] +} +``` diff --git a/packages/deploy-helpers/src/deploy/helpers/provision-bindings.ts b/packages/deploy-helpers/src/deploy/helpers/provision-bindings.ts index ab2a123046..f715ab0ef5 100644 --- a/packages/deploy-helpers/src/deploy/helpers/provision-bindings.ts +++ b/packages/deploy-helpers/src/deploy/helpers/provision-bindings.ts @@ -576,7 +576,12 @@ class KVHandler extends ProvisionResourceHandler< return undefined; } async create(name: string) { - return await createKVNamespace(this.complianceConfig, this.accountId, name); + return await createKVNamespace( + this.complianceConfig, + this.accountId, + name, + this.binding.jurisdiction + ); } constructor( bindingName: string, @@ -1358,7 +1363,8 @@ function autoProvisionedResourceName( async function createKVNamespace( complianceConfig: ComplianceConfig, accountId: string, - title: string + title: string, + jurisdiction?: string ): Promise { const response = await fetchResult<{ id: string }>( complianceConfig, @@ -1370,6 +1376,7 @@ async function createKVNamespace( }, body: JSON.stringify({ title, + jurisdiction, }), } ); diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index f11298c460..437245be3f 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -937,6 +937,11 @@ export interface EnvironmentNonInheritable { id?: string; /** The ID of the KV namespace used during `wrangler dev` */ preview_id?: string; + /** + * The jurisdiction to create the KV namespace in when provisioning it + * during `wrangler deploy`. Only read when the namespace does not yet exist. Ignored once the namespace has been created. + */ + jurisdiction?: string; /** Whether the KV namespace should be remote or not in local development */ remote?: boolean; }[]; diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index eac1c31443..1e9838eb28 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -3903,6 +3903,14 @@ const validateKVBinding: ValidatorFn = (diagnostics, field, value) => { ); isValid = false; } + if (!isOptionalProperty(value, "jurisdiction", "string")) { + diagnostics.errors.push( + `"${field}" bindings should, optionally, have a string "jurisdiction" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } if (!isRemoteValid(value, field, diagnostics)) { isValid = false; } @@ -3911,6 +3919,7 @@ const validateKVBinding: ValidatorFn = (diagnostics, field, value) => { "binding", "id", "preview_id", + "jurisdiction", "remote", ]); diff --git a/packages/workers-utils/src/worker.ts b/packages/workers-utils/src/worker.ts index 4ec07328f3..d93e63ed4b 100644 --- a/packages/workers-utils/src/worker.ts +++ b/packages/workers-utils/src/worker.ts @@ -83,6 +83,7 @@ export interface CfVars { export interface CfKvNamespace { binding: string; id?: string | typeof INHERIT_SYMBOL; + jurisdiction?: string; remote?: boolean; raw?: boolean; } 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 1dca66f37e..6d194acc5e 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 @@ -4179,6 +4179,37 @@ describe("normalizeAndValidateConfig()", () => { expect(diagnostics.hasWarnings()).toBe(false); expect(diagnostics.hasErrors()).toBe(false); }); + + it("should allow an optional jurisdiction field", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + kv_namespaces: [{ binding: "VALID", jurisdiction: "eu" }], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(false); + }); + + it("should error if jurisdiction is not a string", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + kv_namespaces: [{ binding: "VALID", jurisdiction: 2000 }], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "kv_namespaces[0]" bindings should, optionally, have a string "jurisdiction" field but got {"binding":"VALID","jurisdiction":2000}." + `); + }); }); it("should error if send_email.bindings are not valid", ({ expect }) => { diff --git a/packages/wrangler/src/__tests__/helpers/mock-kv.ts b/packages/wrangler/src/__tests__/helpers/mock-kv.ts index 01e8b2bc87..f897110908 100644 --- a/packages/wrangler/src/__tests__/helpers/mock-kv.ts +++ b/packages/wrangler/src/__tests__/helpers/mock-kv.ts @@ -68,15 +68,21 @@ export function mockCreateKVNamespace( options: { resultId?: string; assertTitle?: string; + assertJurisdiction?: string; } = {} ) { msw.use( http.post( "*/accounts/:accountId/storage/kv/namespaces", async ({ request }) => { + const requestBody = await request.json(); if (options.assertTitle) { - const requestBody = await request.json(); - expect(requestBody).toEqual({ title: options.assertTitle }); + expect(requestBody).toMatchObject({ title: options.assertTitle }); + } + if (options.assertJurisdiction) { + expect(requestBody).toMatchObject({ + jurisdiction: options.assertJurisdiction, + }); } return HttpResponse.json( diff --git a/packages/wrangler/src/__tests__/provision.test.ts b/packages/wrangler/src/__tests__/provision.test.ts index e050311a3a..51e63d0ae1 100644 --- a/packages/wrangler/src/__tests__/provision.test.ts +++ b/packages/wrangler/src/__tests__/provision.test.ts @@ -826,6 +826,82 @@ describe("resource provisioning", () => { `); }); + it("provisions a KV namespace in the jurisdiction specified in config", async ({ + expect, + }) => { + writeWranglerConfig({ + main: "index.js", + kv_namespaces: [ + { + binding: "KV", + jurisdiction: "eu", + }, + ], + }); + mockGetSettings(); + mockListKVNamespacesRequest(expect, { + title: "test-kv", + id: "existing-kv-id", + }); + + mockSelect({ + text: "Would you like to connect an existing KV Namespace or create a new one?", + result: "__WRANGLER_INTERNAL_NEW", + }); + mockPrompt({ + text: "Enter a name for your new KV Namespace", + result: "new-kv", + }); + mockCreateKVNamespace(expect, { + assertTitle: "new-kv", + assertJurisdiction: "eu", + resultId: "new-kv-id", + }); + + mockUploadWorkerRequest({ + expectedBindings: [ + { + name: "KV", + type: "kv_namespace", + namespace_id: "new-kv-id", + }, + ], + }); + + await runWrangler("deploy --x-auto-create=false"); + + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Total Upload: xx KiB / gzip: xx KiB + + The following bindings need to be provisioned: + Binding Resource + env.KV KV Namespace + + + Provisioning KV (KV Namespace)... + 🌀 Creating new KV Namespace "new-kv"... + ✨ KV provisioned 🎉 + + Your Worker was deployed with provisioned resources. We've written the IDs of these resources to your config file, which you can choose to save or discard. Either way future deploys will continue to work. + 🎉 All resources provisioned, continuing with deployment... + + Worker Startup Time: 100 ms + Your Worker has access to the following bindings: + Binding Resource + env.KV (new-kv-id) KV Namespace + + Uploaded test-name (TIMINGS) + Deployed test-name triggers (TIMINGS) + https://test-name.test-sub-domain.workers.dev + Current Version ID: Galaxy-Class" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); + it("can provision KV, R2 and D1 bindings with new resources w/ redirected config", async ({ expect, }) => { diff --git a/packages/wrangler/src/kv/helpers.ts b/packages/wrangler/src/kv/helpers.ts index cd01bff4b3..7900b881bc 100644 --- a/packages/wrangler/src/kv/helpers.ts +++ b/packages/wrangler/src/kv/helpers.ts @@ -46,7 +46,8 @@ type KvArgs = { export async function createKVNamespace( complianceConfig: ComplianceConfig, accountId: string, - title: string + title: string, + jurisdiction?: string ): Promise { const response = await fetchResult<{ id: string }>( complianceConfig, @@ -58,6 +59,7 @@ export async function createKVNamespace( }, body: JSON.stringify({ title, + jurisdiction, }), } ); From 68b0cae6c19474507749f166735ab0f143653024 Mon Sep 17 00:00:00 2001 From: Enyinna Ogwe Date: Fri, 24 Jul 2026 20:58:33 +0100 Subject: [PATCH 5/6] Run pnpm prettify --- .changeset/kv-namespace-provision-jurisdiction.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/kv-namespace-provision-jurisdiction.md b/.changeset/kv-namespace-provision-jurisdiction.md index 5d6477c426..a44955ea99 100644 --- a/.changeset/kv-namespace-provision-jurisdiction.md +++ b/.changeset/kv-namespace-provision-jurisdiction.md @@ -4,10 +4,10 @@ Support an optional `jurisdiction` field on `kv_namespaces` bindings - The field is only read when the namespace is first provisioned; once an `id` exists the namespace is addressed by that `id` and the `jurisdiction` field is ignored. +The field is only read when the namespace is first provisioned; once an `id` exists the namespace is addressed by that `id` and the `jurisdiction` field is ignored. ```jsonc { - "kv_namespaces": [{ "binding": "MY_KV", "jurisdiction": "eu" }] + "kv_namespaces": [{ "binding": "MY_KV", "jurisdiction": "eu" }], } ``` From 92c6c397cf79ca5ce99d04e9aa3d884dcf42ba76 Mon Sep 17 00:00:00 2001 From: oOPa <5878650+oOPa@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:19:16 +0100 Subject: [PATCH 6/6] Update .changeset/kv-namespace-provision-jurisdiction.md to denote that the jurisdiction flag is experimental and gated to allowed accounts only Co-authored-by: Edmund Hung --- .changeset/kv-namespace-provision-jurisdiction.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/kv-namespace-provision-jurisdiction.md b/.changeset/kv-namespace-provision-jurisdiction.md index a44955ea99..f7303516c2 100644 --- a/.changeset/kv-namespace-provision-jurisdiction.md +++ b/.changeset/kv-namespace-provision-jurisdiction.md @@ -6,6 +6,8 @@ Support an optional `jurisdiction` field on `kv_namespaces` bindings The field is only read when the namespace is first provisioned; once an `id` exists the namespace is addressed by that `id` and the `jurisdiction` field is ignored. +This is experimental and currently gated to accounts allowed only. + ```jsonc { "kv_namespaces": [{ "binding": "MY_KV", "jurisdiction": "eu" }],