From 4c7f7ae51eb7a7359ecde95d0835f6f18aa8b154 Mon Sep 17 00:00:00 2001 From: David Peacock Date: Mon, 15 Jun 2026 15:04:07 -0400 Subject: [PATCH] Allow kafka:// URIs in webhook subscription TOML validation The webhook subscription URI validator in @shopify/app previously rejected kafka:// URIs, which blocked internal Shopify apps (Email, Shop, Flow, Collabs, Stocky, Marketplace, Collective, etc.) from declaring their webhook subscriptions in the app TOML. Adds a kafka:// regex mirroring the server-side topic character set ([a-zA-Z0-9_.-]+) so the CLI rejects exactly what the platform rejects for shape. Authorization (internal-app-only) is enforced by the platform at subscription creation time via api_client.internal_graphql_access?; the CLI intentionally validates URI shape only, matching how pubsub:// and Eventbridge ARNs are handled today. Updates the validation error message to mention kafka and adds tests for accepting a well-formed kafka URI, rejecting invalid topic characters, and including kafka in the combined-URI subscriptions test (which expands and sorts alphabetically by URI). Resolves shop/issues-event-foundations#252. --- .../add-kafka-webhook-uri-validation.md | 5 ++ .../app/src/cli/models/app/loader.test.ts | 47 +++++++++++++++++-- .../specifications/validation/common.ts | 6 ++- 3 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 .changeset/add-kafka-webhook-uri-validation.md diff --git a/.changeset/add-kafka-webhook-uri-validation.md b/.changeset/add-kafka-webhook-uri-validation.md new file mode 100644 index 00000000000..30a8ab8d1f1 --- /dev/null +++ b/.changeset/add-kafka-webhook-uri-validation.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Accept `kafka://{topic}` URIs in `[[webhooks.subscriptions]]` TOML validation. Kafka delivery is restricted to internal Shopify apps; the platform enforces authorization at subscription time. diff --git a/packages/app/src/cli/models/app/loader.test.ts b/packages/app/src/cli/models/app/loader.test.ts index 4f35ca534b0..1965cfd3227 100644 --- a/packages/app/src/cli/models/app/loader.test.ts +++ b/packages/app/src/cli/models/app/loader.test.ts @@ -2815,7 +2815,7 @@ describe('WebhooksSchema', () => { const errorObj = { code: zod.ZodIssueCode.custom, message: - "URI format isn't correct. Valid formats include: relative path starting with a slash, HTTPS URL, pubsub://{project-id}:{topic-id} or Eventbridge ARN", + "URI format isn't correct. Valid formats include: relative path starting with a slash, HTTPS URL, pubsub://{project-id}:{topic-id}, kafka://{topic-id} or Eventbridge ARN", path: ['webhooks', 'subscriptions', 0, 'uri'], } @@ -2835,7 +2835,7 @@ describe('WebhooksSchema', () => { expect(result.parsedConfiguration.webhooks).toMatchObject(webhookConfig) }) - test('throws an error if uri is not a valid https URL, pubsub URI, or Eventbridge ARN', async () => { + test('throws an error if uri is not a valid https URL, pubsub URI, kafka URI, or Eventbridge ARN', async () => { const webhookConfig: WebhooksConfig = { api_version: '2021-07', subscriptions: [{uri: 'my::URI-thing::Shopify::123', topics: ['products/create']}], @@ -2843,7 +2843,7 @@ describe('WebhooksSchema', () => { const errorObj = { code: zod.ZodIssueCode.custom, message: - "URI format isn't correct. Valid formats include: relative path starting with a slash, HTTPS URL, pubsub://{project-id}:{topic-id} or Eventbridge ARN", + "URI format isn't correct. Valid formats include: relative path starting with a slash, HTTPS URL, pubsub://{project-id}:{topic-id}, kafka://{topic-id} or Eventbridge ARN", path: ['webhooks', 'subscriptions', 0, 'uri'], } @@ -2889,6 +2889,33 @@ describe('WebhooksSchema', () => { expect(result.parsedConfiguration.webhooks).toMatchObject(webhookConfig) }) + test('accepts a kafka uri', async () => { + const webhookConfig: WebhooksConfig = { + api_version: '2021-07', + subscriptions: [{uri: 'kafka://my_topic.name-1', topics: ['products/create']}], + } + + const result = await setupParsing({}, webhookConfig) + expect(result.threw).toBe(false) + expect(result.parsedConfiguration.webhooks).toMatchObject(webhookConfig) + }) + + test('throws an error if kafka topic contains invalid characters', async () => { + const webhookConfig: WebhooksConfig = { + api_version: '2021-07', + subscriptions: [{uri: 'kafka://invalid topic!', topics: ['products/create']}], + } + const errorObj = { + code: zod.ZodIssueCode.custom, + message: + "URI format isn't correct. Valid formats include: relative path starting with a slash, HTTPS URL, pubsub://{project-id}:{topic-id}, kafka://{topic-id} or Eventbridge ARN", + path: ['webhooks', 'subscriptions', 0, 'uri'], + } + + const result = await setupParsing(errorObj, webhookConfig) + expect(result.threw).toBe(true) + }) + test('accepts combination of uris', async () => { const webhookConfig: WebhooksConfig = { api_version: '2021-07', @@ -2905,6 +2932,10 @@ describe('WebhooksSchema', () => { uri: 'pubsub://my-project-123:my-topic', topics: ['products/create', 'products/update'], }, + { + uri: 'kafka://my-topic', + topics: ['products/create', 'products/update'], + }, ], } @@ -2927,6 +2958,14 @@ describe('WebhooksSchema', () => { uri: 'https://example.com', topics: ['products/update'], }, + { + uri: 'kafka://my-topic', + topics: ['products/create'], + }, + { + uri: 'kafka://my-topic', + topics: ['products/update'], + }, { uri: 'pubsub://my-project-123:my-topic', topics: ['products/create'], @@ -3010,7 +3049,7 @@ describe('WebhooksSchema', () => { const errorObj = { code: zod.ZodIssueCode.custom, message: - "URI format isn't correct. Valid formats include: relative path starting with a slash, HTTPS URL, pubsub://{project-id}:{topic-id} or Eventbridge ARN", + "URI format isn't correct. Valid formats include: relative path starting with a slash, HTTPS URL, pubsub://{project-id}:{topic-id}, kafka://{topic-id} or Eventbridge ARN", path: ['webhooks', 'subscriptions', 0, 'uri'], } diff --git a/packages/app/src/cli/models/extensions/specifications/validation/common.ts b/packages/app/src/cli/models/extensions/specifications/validation/common.ts index 8c6faf91057..96b932e8b37 100644 --- a/packages/app/src/cli/models/extensions/specifications/validation/common.ts +++ b/packages/app/src/cli/models/extensions/specifications/validation/common.ts @@ -6,6 +6,8 @@ const pubSubRegex = /^pubsub:\/\/(?[^:]+):(?.+)$/ // example Eventbridge ARN - arn:aws:events:{region}::event-source/aws.partner/shopify.com/{app_id}/{path} const arnRegex = /^arn:aws:events:(?[a-z]{2}-[a-z]+-[0-9]+)::event-source\/aws\.partner\/shopify\.com(\.test)?\/(?\d+)\/(?.+)$/ +// example Kafka URI - kafka://{topic}. Restricted to internal apps; the platform enforces authorization. +const kafkaRegex = /^kafka:\/\/(?[a-zA-Z0-9_.-]+)$/ export function removeTrailingSlash(arg: string): string export function removeTrailingSlash(arg: unknown): unknown { @@ -16,10 +18,10 @@ export const WebhookSubscriptionUriValidation = zod.string({invalid_type_error: (uri) => { if (uri.startsWith('/')) return true - return httpsRegex.test(uri) || pubSubRegex.test(uri) || arnRegex.test(uri) + return httpsRegex.test(uri) || pubSubRegex.test(uri) || arnRegex.test(uri) || kafkaRegex.test(uri) }, { message: - "URI format isn't correct. Valid formats include: relative path starting with a slash, HTTPS URL, pubsub://{project-id}:{topic-id} or Eventbridge ARN", + "URI format isn't correct. Valid formats include: relative path starting with a slash, HTTPS URL, pubsub://{project-id}:{topic-id}, kafka://{topic-id} or Eventbridge ARN", }, )