From 7fb04c490ba58486ec9726ecdcda0c4b25acdd60 Mon Sep 17 00:00:00 2001 From: Elias Hawa Date: Tue, 2 Jun 2026 10:00:53 -0400 Subject: [PATCH 1/6] Extend ExtensionInstance.deployConfig to include app config info --- .../extensions/extension-instance.test.ts | 24 +++++++++++++++++++ .../models/extensions/extension-instance.ts | 10 +++++++- .../cli/models/extensions/specification.ts | 5 ++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/app/src/cli/models/extensions/extension-instance.test.ts b/packages/app/src/cli/models/extensions/extension-instance.test.ts index 3fea6b4a055..bcf547c7939 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.test.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.test.ts @@ -218,6 +218,30 @@ describe('deployConfig', async () => { expect(got).toMatchObject({theme_extension: {files: {}}}) }) + test('passes app configuration context to deployConfig', async () => { + const extensionInstance = await testThemeExtensions() + const originalDeployConfig = extensionInstance.specification.deployConfig + const deployConfig = vi.fn().mockResolvedValue({theme_extension: {files: {}}}) + extensionInstance.specification.deployConfig = deployConfig + + try { + await extensionInstance.deployConfig({ + apiKey: 'apiKey', + appConfiguration: placeholderAppConfiguration, + }) + + expect(deployConfig).toHaveBeenCalledWith( + extensionInstance.configuration, + extensionInstance.directory, + 'apiKey', + undefined, + {appConfiguration: placeholderAppConfiguration}, + ) + } finally { + extensionInstance.specification.deployConfig = originalDeployConfig + } + }) + test('returns transformed config when defined', async () => { const extensionInstance = await testAppConfigExtensions() diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index 3554617eb81..348e25f671a 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -199,7 +199,15 @@ export class ExtensionInstance { - const deployConfig = await this.specification.deployConfig?.(this.configuration, this.directory, apiKey, undefined) + const deployConfig = await this.specification.deployConfig?.( + this.configuration, + this.directory, + apiKey, + undefined, + { + appConfiguration, + }, + ) const transformedConfig = this.specification.transformLocalToRemote?.(this.configuration, appConfiguration) as | {[key: string]: unknown} | undefined diff --git a/packages/app/src/cli/models/extensions/specification.ts b/packages/app/src/cli/models/extensions/specification.ts index bfbabb2af51..0dc1825a0a1 100644 --- a/packages/app/src/cli/models/extensions/specification.ts +++ b/packages/app/src/cli/models/extensions/specification.ts @@ -57,6 +57,10 @@ export interface BuildAsset { static?: boolean } +interface ExtensionDeployConfigContext { + appConfiguration: AppConfiguration +} + /** * Extension specification with all the needed properties and methods to load an extension. */ @@ -80,6 +84,7 @@ export interface ExtensionSpecification Promise | undefined> validate?: (config: TConfiguration, configPath: string, directory: string) => Promise> preDeployValidation?: (extension: ExtensionInstance) => Promise From 487d5bd1fc2037262ff0a703d9973d53da79ccda Mon Sep 17 00:00:00 2001 From: Elias Hawa Date: Fri, 29 May 2026 16:44:46 -0400 Subject: [PATCH 2/6] Allow for relative urls in Flow action extensions during development --- .../specifications/flow_action.test.ts | 117 ++++++++++++++++++ .../extensions/specifications/flow_action.ts | 29 ++++- 2 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 packages/app/src/cli/models/extensions/specifications/flow_action.test.ts diff --git a/packages/app/src/cli/models/extensions/specifications/flow_action.test.ts b/packages/app/src/cli/models/extensions/specifications/flow_action.test.ts new file mode 100644 index 00000000000..a32d000834f --- /dev/null +++ b/packages/app/src/cli/models/extensions/specifications/flow_action.test.ts @@ -0,0 +1,117 @@ +import {placeholderAppConfiguration, testFlowActionExtension} from '../../app/app.test-data.js' +import {ExtensionInstance} from '../extension-instance.js' +import {BaseConfigType} from '../schemas.js' +import {ApplicationURLs} from '../../../services/dev/urls.js' +import {beforeEach, describe, expect, test} from 'vitest' + +type FlowActionConfig = BaseConfigType & { + type: 'flow_action' + handle: string + name: string + runtime_url: string + validation_url?: string + config_page_url?: string + config_page_preview_url?: string +} + +const tunnelUrls: ApplicationURLs = { + applicationUrl: 'https://my-tunnel.example.com', + redirectUrlWhitelist: [], +} + +describe('FlowActionExtension', () => { + let extension: ExtensionInstance + + const config: FlowActionConfig = { + type: 'flow_action', + handle: 'place-bid', + name: 'Place auction bid', + description: 'Place a bid on an auction', + runtime_url: '/api/execute', + validation_url: '/api/validate', + config_page_url: '/config', + config_page_preview_url: '/config/preview', + } + + beforeEach(async () => { + extension = (await testFlowActionExtension()) as ExtensionInstance + extension.configuration = {...config} + }) + + test('accepts an absolute https runtime_url', () => { + // When + const parsed = extension.specification.parseConfigurationObject({ + ...config, + runtime_url: 'https://example.com/api/execute', + }) + + // Then + expect(parsed.state).toBe('ok') + }) + + test('accepts a relative runtime_url starting with /', () => { + // When + const parsed = extension.specification.parseConfigurationObject(config) + + // Then + expect(parsed.state).toBe('ok') + }) + + test('rejects a non-https absolute runtime_url', () => { + // When + const parsed = extension.specification.parseConfigurationObject({ + ...config, + runtime_url: 'http://example.com/api/execute', + }) + + // Then + expect(parsed.state).toBe('error') + }) + + test('handles the deploy configuration', async () => { + // When + const got = await extension.deployConfig({ + apiKey: 'api-key', + appConfiguration: placeholderAppConfiguration, + }) + + // Then + expect(got).toEqual({ + title: extension.configuration.name, + description: extension.configuration.description, + url: extension.configuration.runtime_url, + fields: [], + validation_url: extension.configuration.validation_url, + custom_configuration_page_url: extension.configuration.config_page_url, + custom_configuration_page_preview_url: extension.configuration.config_page_preview_url, + schema_patch: '', + return_type_ref: undefined, + }) + }) + + test('prepends the dev application URL to relative URL fields', () => { + // When + extension.patchWithAppDevURLs(tunnelUrls) + + // Then + expect(extension.configuration.runtime_url).toBe('https://my-tunnel.example.com/api/execute') + expect(extension.configuration.validation_url).toBe('https://my-tunnel.example.com/api/validate') + expect(extension.configuration.config_page_url).toBe('https://my-tunnel.example.com/config') + expect(extension.configuration.config_page_preview_url).toBe('https://my-tunnel.example.com/config/preview') + }) + + test('leaves absolute dev URLs untouched', () => { + // Given + extension.configuration.runtime_url = 'https://my-prod-host.example.com/api/execute' + extension.configuration.validation_url = undefined + extension.configuration.config_page_url = undefined + extension.configuration.config_page_preview_url = undefined + + // When + extension.patchWithAppDevURLs(tunnelUrls) + + // Then + expect(extension.configuration.runtime_url).toBe('https://my-prod-host.example.com/api/execute') + expect(extension.configuration.validation_url).toBeUndefined() + }) +}) diff --git a/packages/app/src/cli/models/extensions/specifications/flow_action.ts b/packages/app/src/cli/models/extensions/specifications/flow_action.ts index 0df7fffb731..4b76cf867dc 100644 --- a/packages/app/src/cli/models/extensions/specifications/flow_action.ts +++ b/packages/app/src/cli/models/extensions/specifications/flow_action.ts @@ -1,8 +1,9 @@ +import {prependApplicationUrl} from './validation/url_prepender.js' import {BaseSchemaWithHandle} from '../schemas.js' import {createExtensionSpecification} from '../specification.js' +import {validateRelativeUrl} from '../../app/validation/common.js' import { validateFieldShape, - startsWithHttps, validateCustomConfigurationPageConfig, validateReturnTypeConfig, } from '../../../services/flow/validation.js' @@ -10,13 +11,15 @@ import {serializeFields} from '../../../services/flow/serialize-fields.js' import {loadSchemaFromPath} from '../../../services/flow/utils.js' import {zod} from '@shopify/cli-kit/node/schema' +const RELATIVE_URL_FIELDS = ['runtime_url', 'validation_url', 'config_page_url', 'config_page_preview_url'] as const + const FlowActionExtensionSchema = BaseSchemaWithHandle.extend({ type: zod.literal('flow_action'), name: zod.string(), - runtime_url: zod.string().url().refine(startsWithHttps), - validation_url: zod.string().url().refine(startsWithHttps).optional(), - config_page_url: zod.string().url().refine(startsWithHttps).optional(), - config_page_preview_url: zod.string().url().refine(startsWithHttps).optional(), + runtime_url: validateRelativeUrl(zod.string({invalid_type_error: 'Value must be string'})), + validation_url: validateRelativeUrl(zod.string({invalid_type_error: 'Value must be string'})).optional(), + config_page_url: validateRelativeUrl(zod.string({invalid_type_error: 'Value must be string'})).optional(), + config_page_preview_url: validateRelativeUrl(zod.string({invalid_type_error: 'Value must be string'})).optional(), schema: zod.string().optional(), return_type_ref: zod.string().optional(), }).refine((config) => { @@ -45,6 +48,22 @@ const flowActionSpecification = createExtensionSpecification({ // https://github.com/Shopify/cli/blob/73ac91c0f40be0a57d1b18cb34254b12d3a071af/packages/app/src/cli/services/deploy.ts#L107 // Should be removed after unified deployment is 100% rolled out appModuleFeatures: (_) => [], + /** + * During `app dev`, swap any relative URLs (starting with `/`) for the dev + * tunnel URL the CLI assigned. This lets developers write + * `runtime_url = "/api/execute"` in their TOML and have it resolved against + * the tunnel automatically — the same pattern app_proxy, webhooks, and + * events subscriptions already use. + * + */ + patchWithAppDevURLs: (config, urls) => { + for (const key of RELATIVE_URL_FIELDS) { + const value = config[key] + if (typeof value === 'string' && value.startsWith('/')) { + config[key] = prependApplicationUrl(value, urls.applicationUrl) + } + } + }, deployConfig: async (config, extensionPath) => { return { title: config.name, From 67fd91dc829ebcbaa4f0be2f2a265ec99f138cd2 Mon Sep 17 00:00:00 2001 From: Elias Hawa Date: Thu, 4 Jun 2026 10:52:02 -0400 Subject: [PATCH 3/6] Allow flow action deployments to use relative urls --- .../extensions/specifications/flow_action.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/app/src/cli/models/extensions/specifications/flow_action.ts b/packages/app/src/cli/models/extensions/specifications/flow_action.ts index 4b76cf867dc..46f5388b601 100644 --- a/packages/app/src/cli/models/extensions/specifications/flow_action.ts +++ b/packages/app/src/cli/models/extensions/specifications/flow_action.ts @@ -64,15 +64,22 @@ const flowActionSpecification = createExtensionSpecification({ } } }, - deployConfig: async (config, extensionPath) => { + deployConfig: async (config, extensionPath, _apiKey, _moduleId, context) => { + const appConfiguration = context?.appConfiguration + const appUrl = typeof appConfiguration?.application_url === 'string' ? appConfiguration.application_url : undefined + return { title: config.name, description: config.description, - url: config.runtime_url, + url: prependApplicationUrl(config.runtime_url, appUrl), fields: serializeFields('flow_action', config.settings?.fields), - validation_url: config.validation_url, - custom_configuration_page_url: config.config_page_url, - custom_configuration_page_preview_url: config.config_page_preview_url, + validation_url: config.validation_url ? prependApplicationUrl(config.validation_url, appUrl) : undefined, + custom_configuration_page_url: config.config_page_url + ? prependApplicationUrl(config.config_page_url, appUrl) + : undefined, + custom_configuration_page_preview_url: config.config_page_preview_url + ? prependApplicationUrl(config.config_page_preview_url, appUrl) + : undefined, schema_patch: await loadSchemaFromPath(extensionPath, config.schema), return_type_ref: config.return_type_ref, } From 563c8dd8a06d7cf5ee0799dbd7e80fc0ab5baedc Mon Sep 17 00:00:00 2001 From: Elias Hawa Date: Thu, 4 Jun 2026 13:12:50 -0400 Subject: [PATCH 4/6] Have runtime urls be properly resolved with app url at dev and deploy time --- .../specifications/flow_action.test.ts | 39 ++++++++++++++++--- .../extensions/specifications/flow_action.ts | 22 +++++------ .../app/src/cli/services/flow/utils.test.ts | 38 +++++++++++++++++- packages/app/src/cli/services/flow/utils.ts | 23 +++++++++++ 4 files changed, 104 insertions(+), 18 deletions(-) diff --git a/packages/app/src/cli/models/extensions/specifications/flow_action.test.ts b/packages/app/src/cli/models/extensions/specifications/flow_action.test.ts index a32d000834f..49c10fdd169 100644 --- a/packages/app/src/cli/models/extensions/specifications/flow_action.test.ts +++ b/packages/app/src/cli/models/extensions/specifications/flow_action.test.ts @@ -19,6 +19,8 @@ const tunnelUrls: ApplicationURLs = { redirectUrlWhitelist: [], } +const urlFields = ['runtime_url', 'validation_url', 'config_page_url', 'config_page_preview_url'] as const + describe('FlowActionExtension', () => { let extension: ExtensionInstance @@ -68,27 +70,52 @@ describe('FlowActionExtension', () => { expect(parsed.state).toBe('error') }) - test('handles the deploy configuration', async () => { + test('prepends the app URL to relative URL fields in the deploy configuration', async () => { // When const got = await extension.deployConfig({ apiKey: 'api-key', - appConfiguration: placeholderAppConfiguration, + appConfiguration: { + ...placeholderAppConfiguration, + application_url: 'https://my-app.example.com', + }, }) // Then expect(got).toEqual({ title: extension.configuration.name, description: extension.configuration.description, - url: extension.configuration.runtime_url, + url: 'https://my-app.example.com/api/execute', fields: [], - validation_url: extension.configuration.validation_url, - custom_configuration_page_url: extension.configuration.config_page_url, - custom_configuration_page_preview_url: extension.configuration.config_page_preview_url, + validation_url: 'https://my-app.example.com/api/validate', + custom_configuration_page_url: 'https://my-app.example.com/config', + custom_configuration_page_preview_url: 'https://my-app.example.com/config/preview', schema_patch: '', return_type_ref: undefined, }) }) + test.each(urlFields)('throws when deploying a relative %s without an app URL', async (field) => { + // Given + extension.configuration = { + ...extension.configuration, + runtime_url: 'https://my-prod-host.example.com/api/execute', + validation_url: 'https://my-prod-host.example.com/api/validate', + config_page_url: 'https://my-prod-host.example.com/config', + config_page_preview_url: 'https://my-prod-host.example.com/config/preview', + } + extension.configuration[field] = `/${field}` + + // When/Then + await expect( + extension.deployConfig({ + apiKey: 'api-key', + appConfiguration: placeholderAppConfiguration, + }), + ).rejects.toThrow( + `Flow action ${field} is a relative URL, but no application_url is configured. Set application_url in your app configuration or use an absolute HTTPS URL.`, + ) + }) + test('prepends the dev application URL to relative URL fields', () => { // When extension.patchWithAppDevURLs(tunnelUrls) diff --git a/packages/app/src/cli/models/extensions/specifications/flow_action.ts b/packages/app/src/cli/models/extensions/specifications/flow_action.ts index 46f5388b601..85ca7bfa962 100644 --- a/packages/app/src/cli/models/extensions/specifications/flow_action.ts +++ b/packages/app/src/cli/models/extensions/specifications/flow_action.ts @@ -1,4 +1,4 @@ -import {prependApplicationUrl} from './validation/url_prepender.js' +// import {prependApplicationUrl} from './validation/url_prepender.js' import {BaseSchemaWithHandle} from '../schemas.js' import {createExtensionSpecification} from '../specification.js' import {validateRelativeUrl} from '../../app/validation/common.js' @@ -8,7 +8,7 @@ import { validateReturnTypeConfig, } from '../../../services/flow/validation.js' import {serializeFields} from '../../../services/flow/serialize-fields.js' -import {loadSchemaFromPath} from '../../../services/flow/utils.js' +import {loadSchemaFromPath, resolveFlowActionUrl} from '../../../services/flow/utils.js' import {zod} from '@shopify/cli-kit/node/schema' const RELATIVE_URL_FIELDS = ['runtime_url', 'validation_url', 'config_page_url', 'config_page_preview_url'] as const @@ -60,7 +60,7 @@ const flowActionSpecification = createExtensionSpecification({ for (const key of RELATIVE_URL_FIELDS) { const value = config[key] if (typeof value === 'string' && value.startsWith('/')) { - config[key] = prependApplicationUrl(value, urls.applicationUrl) + config[key] = resolveFlowActionUrl(key, value, urls.applicationUrl) ?? '' } } }, @@ -71,15 +71,15 @@ const flowActionSpecification = createExtensionSpecification({ return { title: config.name, description: config.description, - url: prependApplicationUrl(config.runtime_url, appUrl), + url: resolveFlowActionUrl('runtime_url', config.runtime_url, appUrl), fields: serializeFields('flow_action', config.settings?.fields), - validation_url: config.validation_url ? prependApplicationUrl(config.validation_url, appUrl) : undefined, - custom_configuration_page_url: config.config_page_url - ? prependApplicationUrl(config.config_page_url, appUrl) - : undefined, - custom_configuration_page_preview_url: config.config_page_preview_url - ? prependApplicationUrl(config.config_page_preview_url, appUrl) - : undefined, + validation_url: resolveFlowActionUrl('validation_url', config.validation_url, appUrl), + custom_configuration_page_url: resolveFlowActionUrl('config_page_url', config.config_page_url, appUrl), + custom_configuration_page_preview_url: resolveFlowActionUrl( + 'config_page_preview_url', + config.config_page_preview_url, + appUrl, + ), schema_patch: await loadSchemaFromPath(extensionPath, config.schema), return_type_ref: config.return_type_ref, } diff --git a/packages/app/src/cli/services/flow/utils.test.ts b/packages/app/src/cli/services/flow/utils.test.ts index d0582058c5c..d5bd9bc3050 100644 --- a/packages/app/src/cli/services/flow/utils.test.ts +++ b/packages/app/src/cli/services/flow/utils.test.ts @@ -1,8 +1,44 @@ -import {loadSchemaFromPath} from './utils.js' +import {loadSchemaFromPath, resolveFlowActionUrl} from './utils.js' import {describe, expect, test} from 'vitest' import {readFile} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' +describe('resolveFlowActionUrl', () => { + test('returns undefined when the URL is not configured', () => { + expect(resolveFlowActionUrl('validation_url', undefined, 'https://my-app.example.com')).toBeUndefined() + }) + + test('returns absolute URLs unchanged', () => { + expect( + resolveFlowActionUrl('runtime_url', 'https://my-prod-host.example.com/api/execute', 'https://my-app.example.com'), + ).toBe('https://my-prod-host.example.com/api/execute') + }) + + test('prepends the app URL to relative URLs', () => { + expect(resolveFlowActionUrl('runtime_url', '/api/execute', 'https://my-app.example.com/')).toBe( + 'https://my-app.example.com/api/execute', + ) + }) + + test('throws when a relative URL cannot be resolved without an app URL', () => { + expect(() => resolveFlowActionUrl('runtime_url', '/api/execute', undefined)).toThrow( + 'Flow action runtime_url is a relative URL, but no application_url is configured. Set application_url in your app configuration or use an absolute HTTPS URL.', + ) + }) + + test('throws when an absolute URL is not HTTPS', () => { + expect(() => resolveFlowActionUrl('runtime_url', 'http://my-prod-host.example.com/api/execute', undefined)).toThrow( + 'Flow action runtime_url must resolve to an HTTPS URL. Set application_url to an HTTPS URL or use an absolute HTTPS URL.', + ) + }) + + test('throws when a relative URL resolves against a non-HTTPS app URL', () => { + expect(() => resolveFlowActionUrl('runtime_url', '/api/execute', 'http://my-app.example.com')).toThrow( + 'Flow action runtime_url must resolve to an HTTPS URL. Set application_url to an HTTPS URL or use an absolute HTTPS URL.', + ) + }) +}) + describe('loadSchemaFromPath', () => { test('loading schema from valid file path should return file contents', async () => { const extensionPath = __dirname.concat('/fixtures') diff --git a/packages/app/src/cli/services/flow/utils.ts b/packages/app/src/cli/services/flow/utils.ts index cdc71c0575f..ce13859891e 100644 --- a/packages/app/src/cli/services/flow/utils.ts +++ b/packages/app/src/cli/services/flow/utils.ts @@ -1,9 +1,32 @@ +import {prependApplicationUrl} from '../../models/extensions/specifications/validation/url_prepender.js' import {joinPath} from '@shopify/cli-kit/node/path' import {glob, readFile} from '@shopify/cli-kit/node/fs' +import {AbortError} from '@shopify/cli-kit/node/error' /** * Loads the schema from the partner defined file. */ +export const resolveFlowActionUrl = (fieldName: string, url: string | undefined, appUrl: string | undefined) => { + if (!url) return undefined + + const resolvedUrl = prependApplicationUrl(url, appUrl) + if (resolvedUrl.startsWith('/')) { + throw new AbortError( + `Flow action ${fieldName} is a relative URL, but no application_url is configured. ` + + 'Set application_url in your app configuration or use an absolute HTTPS URL.', + ) + } + + if (!resolvedUrl.startsWith('https://')) { + throw new AbortError( + `Flow action ${fieldName} must resolve to an HTTPS URL. ` + + 'Set application_url to an HTTPS URL or use an absolute HTTPS URL.', + ) + } + + return resolvedUrl +} + export const loadSchemaFromPath = async (extensionPath: string, patchPath: string | undefined) => { if (!patchPath) { return '' From 26c25d20c3e6d624e7401a89bcc0129511b60255 Mon Sep 17 00:00:00 2001 From: Elias Hawa Date: Thu, 4 Jun 2026 13:13:06 -0400 Subject: [PATCH 5/6] Remove unused validation function --- packages/app/src/cli/services/flow/validation.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/app/src/cli/services/flow/validation.ts b/packages/app/src/cli/services/flow/validation.ts index e69fa1be5c7..20b6bec3670 100644 --- a/packages/app/src/cli/services/flow/validation.ts +++ b/packages/app/src/cli/services/flow/validation.ts @@ -53,8 +53,6 @@ export const validateFieldShape = ( return baseFieldSchema.parse(configField) } -export const startsWithHttps = (url: string) => url.startsWith('https://') - export const isSchemaTypeReference = (type: string) => type.startsWith('schema.') export const validateCustomConfigurationPageConfig = ( From 3b294505435d0f39ef7b43431628468ed5d43c89 Mon Sep 17 00:00:00 2001 From: Elias Hawa Date: Thu, 4 Jun 2026 14:09:11 -0400 Subject: [PATCH 6/6] Expand deploy config url resolution test --- .../extensions/specifications/flow_action.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/app/src/cli/models/extensions/specifications/flow_action.test.ts b/packages/app/src/cli/models/extensions/specifications/flow_action.test.ts index 49c10fdd169..555bd097754 100644 --- a/packages/app/src/cli/models/extensions/specifications/flow_action.test.ts +++ b/packages/app/src/cli/models/extensions/specifications/flow_action.test.ts @@ -70,7 +70,16 @@ describe('FlowActionExtension', () => { expect(parsed.state).toBe('error') }) - test('prepends the app URL to relative URL fields in the deploy configuration', async () => { + test('preserves absolute URLs and prepends the app URL to relative URLs in the deploy configuration', async () => { + // Given + extension.configuration = { + ...extension.configuration, + runtime_url: '/api/execute', + validation_url: 'https://my-app.example.com/api/validate', + config_page_url: '/config', + config_page_preview_url: 'https://my-app.example.com/config/preview', + } + // When const got = await extension.deployConfig({ apiKey: 'api-key',