From 6f517d76c51826f2fc215e88421f312b55b8d7d0 Mon Sep 17 00:00:00 2001 From: Liz Kenyon Date: Mon, 30 Mar 2026 16:55:01 -0500 Subject: [PATCH 1/6] Fix: guard rewriteConfiguration against non-array and nullish config values Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/chilly-glasses-fetch.md | 5 ++++ .../app/write-app-configuration-file.test.ts | 29 ++++++++++++++++++- .../app/write-app-configuration-file.ts | 4 ++- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 .changeset/chilly-glasses-fetch.md diff --git a/.changeset/chilly-glasses-fetch.md b/.changeset/chilly-glasses-fetch.md new file mode 100644 index 00000000000..7e1b85c8f79 --- /dev/null +++ b/.changeset/chilly-glasses-fetch.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Fix crash "config2.map is not a function" in `rewriteConfiguration` when writing app configuration with unvalidated data (e.g., from third-party templates without `client_id`) diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts index 685e53f9596..0538a7c0eca 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts @@ -1,8 +1,9 @@ -import {writeAppConfigurationFile} from './write-app-configuration-file.js' +import {rewriteConfiguration, writeAppConfigurationFile} from './write-app-configuration-file.js' import {DEFAULT_CONFIG, buildVersionedAppSchema} from '../../models/app/app.test-data.js' import {CurrentAppConfiguration} from '../../models/app/app.js' import {inTemporaryDirectory, readFile} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' +import {zod} from '@shopify/cli-kit/node/schema' import {describe, expect, test} from 'vitest' const FULL_CONFIGURATION = { @@ -151,3 +152,29 @@ url = "https://example.com/prefs" }) }) }) + +describe('rewriteConfiguration', () => { + test('handles undefined config for an optional array schema wrapped in effects', () => { + const schema = zod.array(zod.string()).optional().transform((val) => val) + + expect(rewriteConfiguration(schema, undefined)).toBeUndefined() + }) + + test('handles null config for an array schema', () => { + const schema = zod.array(zod.string()) + + expect(rewriteConfiguration(schema, null)).toBeUndefined() + }) + + test('handles undefined config for an object schema', () => { + const schema = zod.object({name: zod.string()}).optional() + + expect(rewriteConfiguration(schema, undefined)).toBeUndefined() + }) + + test('passes through non-array value when schema expects an array', () => { + const schema = zod.array(zod.string()) + + expect(rewriteConfiguration(schema, 'not-an-array')).toBe('not-an-array') + }) +}) diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.ts b/packages/app/src/cli/services/app/write-app-configuration-file.ts index c2f82012401..80e8091480a 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.ts @@ -27,10 +27,12 @@ export async function writeAppConfigurationFile( export const rewriteConfiguration = (schema: T, config: unknown): unknown => { if (schema === null || schema === undefined) return null + if (config === null || config === undefined) return undefined if (schema instanceof zod.ZodNullable || schema instanceof zod.ZodOptional) return rewriteConfiguration(schema.unwrap(), config) if (schema instanceof zod.ZodArray) { - return (config as unknown[]).map((item) => rewriteConfiguration(schema.element, item)) + if (!Array.isArray(config)) return config + return config.map((item) => rewriteConfiguration(schema.element, item)) } if (schema instanceof zod.ZodEffects) { return rewriteConfiguration(schema._def.schema, config) From b460b6124ec7ae032789a570b75c91851a4649c4 Mon Sep 17 00:00:00 2001 From: Liz Kenyon Date: Mon, 30 Mar 2026 16:57:37 -0500 Subject: [PATCH 2/6] Add type guard to ZodObject branch for non-object config values --- .../cli/services/app/write-app-configuration-file.test.ts | 6 ++++++ .../src/cli/services/app/write-app-configuration-file.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts index 0538a7c0eca..e9285319c8a 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts @@ -177,4 +177,10 @@ describe('rewriteConfiguration', () => { expect(rewriteConfiguration(schema, 'not-an-array')).toBe('not-an-array') }) + + test('passes through non-object value when schema expects an object', () => { + const schema = zod.object({name: zod.string()}) + + expect(rewriteConfiguration(schema, 'not-an-object')).toBe('not-an-object') + }) }) diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.ts b/packages/app/src/cli/services/app/write-app-configuration-file.ts index 80e8091480a..9f3c6c4089a 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.ts @@ -38,6 +38,7 @@ export const rewriteConfiguration = (schema: T, config return rewriteConfiguration(schema._def.schema, config) } if (schema instanceof zod.ZodObject) { + if (typeof config !== 'object' || Array.isArray(config)) return config const entries = Object.entries(schema.shape) const confObj = config as {[key: string]: unknown} let result: {[key: string]: unknown} = {} From 9e0ac1c50fd209aff7abd4a442104a8725c2ae0e Mon Sep 17 00:00:00 2001 From: Liz Kenyon Date: Mon, 30 Mar 2026 17:07:25 -0500 Subject: [PATCH 3/6] Add integration test with real app schema and type-mismatched config --- .../services/app/write-app-configuration-file.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts index e9285319c8a..3c2f828b60c 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts @@ -183,4 +183,15 @@ describe('rewriteConfiguration', () => { expect(rewriteConfiguration(schema, 'not-an-object')).toBe('not-an-object') }) + + test('does not crash with type-mismatched config against the real app schema', async () => { + const {schema} = await buildVersionedAppSchema() + const malformedConfig = { + ...DEFAULT_CONFIG, + auth: {redirect_urls: 'not-an-array'}, + webhooks: {api_version: '2023-07', subscriptions: 'also-not-an-array'}, + } + + expect(() => rewriteConfiguration(schema, malformedConfig)).not.toThrow() + }) }) From 8e7e67854131608a0b3cfd9909ac47f15f4e4b4a Mon Sep 17 00:00:00 2001 From: Liz Kenyon Date: Mon, 30 Mar 2026 17:08:19 -0500 Subject: [PATCH 4/6] Add test for array value against ZodObject schema guard --- .../cli/services/app/write-app-configuration-file.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts index 3c2f828b60c..c1ce501ad96 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts @@ -184,6 +184,13 @@ describe('rewriteConfiguration', () => { expect(rewriteConfiguration(schema, 'not-an-object')).toBe('not-an-object') }) + test('passes through array value when schema expects an object', () => { + const schema = zod.object({name: zod.string()}) + + const input = ['a', 'b'] + expect(rewriteConfiguration(schema, input)).toBe(input) + }) + test('does not crash with type-mismatched config against the real app schema', async () => { const {schema} = await buildVersionedAppSchema() const malformedConfig = { From 3182864a966b6ccfc34ca44f98f9ebb02675d761 Mon Sep 17 00:00:00 2001 From: Liz Kenyon Date: Mon, 30 Mar 2026 17:09:43 -0500 Subject: [PATCH 5/6] Return config as-is for null/undefined instead of coercing to undefined --- .../src/cli/services/app/write-app-configuration-file.test.ts | 2 +- .../app/src/cli/services/app/write-app-configuration-file.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts index c1ce501ad96..d10fae545c8 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts @@ -163,7 +163,7 @@ describe('rewriteConfiguration', () => { test('handles null config for an array schema', () => { const schema = zod.array(zod.string()) - expect(rewriteConfiguration(schema, null)).toBeUndefined() + expect(rewriteConfiguration(schema, null)).toBeNull() }) test('handles undefined config for an object schema', () => { diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.ts b/packages/app/src/cli/services/app/write-app-configuration-file.ts index 9f3c6c4089a..7dbcc532367 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.ts @@ -27,7 +27,7 @@ export async function writeAppConfigurationFile( export const rewriteConfiguration = (schema: T, config: unknown): unknown => { if (schema === null || schema === undefined) return null - if (config === null || config === undefined) return undefined + if (config === null || config === undefined) return config if (schema instanceof zod.ZodNullable || schema instanceof zod.ZodOptional) return rewriteConfiguration(schema.unwrap(), config) if (schema instanceof zod.ZodArray) { From 1efd2f7f030bf9467617ff08e28265ce5144d533 Mon Sep 17 00:00:00 2001 From: Liz Kenyon Date: Mon, 30 Mar 2026 17:33:13 -0500 Subject: [PATCH 6/6] Add test for null config with ZodNullable-wrapped object schema --- .../cli/services/app/write-app-configuration-file.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts index d10fae545c8..44f129c0735 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts @@ -172,6 +172,12 @@ describe('rewriteConfiguration', () => { expect(rewriteConfiguration(schema, undefined)).toBeUndefined() }) + test('handles null config for a nullable object schema', () => { + const schema = zod.object({name: zod.string()}).nullable() + + expect(rewriteConfiguration(schema, null)).toBeNull() + }) + test('passes through non-array value when schema expects an array', () => { const schema = zod.array(zod.string())