From 68743e7efaf0f233277a343061daa7bd61943c9c Mon Sep 17 00:00:00 2001 From: Elana Kopelevich Date: Thu, 9 Apr 2026 17:06:56 -0600 Subject: [PATCH] Expose allowed_domains on the app --- packages/app/src/cli/models/app/app.ts | 7 +++ .../models/extensions/specifications/admin.ts | 5 +- .../app/src/cli/services/dev/extension.ts | 5 ++ .../services/dev/extension/payload/models.ts | 1 + .../dev/extension/payload/store.test.ts | 46 +++++++++++++++++++ .../services/dev/extension/payload/store.ts | 5 ++ .../dev/processes/previewable-extension.ts | 3 ++ .../dev/processes/setup-dev-processes.ts | 1 + 8 files changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/app/src/cli/models/app/app.ts b/packages/app/src/cli/models/app/app.ts index c557afbc62b..1f238f1c137 100644 --- a/packages/app/src/cli/models/app/app.ts +++ b/packages/app/src/cli/models/app/app.ts @@ -3,6 +3,7 @@ import {ensurePathStartsWithSlash} from './validation/common.js' import {Identifiers} from './identifiers.js' import {ExtensionInstance} from '../extensions/extension-instance.js' import {FunctionConfigType} from '../extensions/specifications/function.js' +import {AdminConfigType} from '../extensions/specifications/admin.js' import {ExtensionSpecification, RemoteAwareExtensionSpecification} from '../extensions/specification.js' import {AppConfigurationUsedByCli} from '../extensions/specifications/types/app_config.js' import {EditorExtensionCollectionType} from '../extensions/specifications/editor_extension_collection.js' @@ -230,6 +231,7 @@ export interface AppInterface< nonConfigExtensions: ExtensionInstance[] draftableExtensions: ExtensionInstance[] appAssetsConfigs: Record | undefined + allowedDomains: string[] | undefined errors: AppErrors hiddenConfig: AppHiddenConfig includeConfigOnDeploy: boolean | undefined @@ -344,6 +346,11 @@ export class App< }, {}) } + get allowedDomains(): string[] | undefined { + const adminExt = this.realExtensions.find((ext) => ext.specification.identifier === 'admin') + return (adminExt?.configuration as AdminConfigType | undefined)?.admin?.allowed_domains + } + setDevApplicationURLs(devApplicationURLs: ApplicationURLs) { this.patchAppConfiguration(devApplicationURLs) this.realExtensions.forEach((ext) => ext.patchWithAppDevURLs(devApplicationURLs)) diff --git a/packages/app/src/cli/models/extensions/specifications/admin.ts b/packages/app/src/cli/models/extensions/specifications/admin.ts index e166eb5d35e..52aacf7abab 100644 --- a/packages/app/src/cli/models/extensions/specifications/admin.ts +++ b/packages/app/src/cli/models/extensions/specifications/admin.ts @@ -7,11 +7,12 @@ const AdminSchema = zod.object({ admin: zod .object({ static_root: zod.string().optional(), + allowed_domains: zod.array(zod.string()).optional(), }) .optional(), }) -type AdminConfigType = zod.infer & BaseConfigType +export type AdminConfigType = zod.infer & BaseConfigType const adminSpecificationSpec = createExtensionSpecification({ identifier: 'admin', @@ -33,6 +34,8 @@ const adminSpecificationSpec = createExtensionSpecification({ admin: { // eslint-disable-next-line @typescript-eslint/no-explicit-any static_root: (remoteContent as any).admin.static_root, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + allowed_domains: (remoteContent as any).admin.allowed_domains, }, } }, diff --git a/packages/app/src/cli/services/dev/extension.ts b/packages/app/src/cli/services/dev/extension.ts index b1e1929f732..635f4c51036 100644 --- a/packages/app/src/cli/services/dev/extension.ts +++ b/packages/app/src/cli/services/dev/extension.ts @@ -118,6 +118,11 @@ export interface ExtensionDevOptions { * Map of asset key to absolute directory path for app-level assets (e.g., admin static_root) */ appAssets?: Record + + /** + * Allowed domains from the admin module config + */ + allowedDomains?: string[] } export async function devUIExtensions(options: ExtensionDevOptions): Promise { diff --git a/packages/app/src/cli/services/dev/extension/payload/models.ts b/packages/app/src/cli/services/dev/extension/payload/models.ts index 9495c31aab6..8ed0b966df6 100644 --- a/packages/app/src/cli/services/dev/extension/payload/models.ts +++ b/packages/app/src/cli/services/dev/extension/payload/models.ts @@ -14,6 +14,7 @@ interface ExtensionsPayloadInterface { lastUpdated: number } } + allowedDomains?: string[] } appId?: string store: string diff --git a/packages/app/src/cli/services/dev/extension/payload/store.test.ts b/packages/app/src/cli/services/dev/extension/payload/store.test.ts index cd9a5229da8..8972af60eaf 100644 --- a/packages/app/src/cli/services/dev/extension/payload/store.test.ts +++ b/packages/app/src/cli/services/dev/extension/payload/store.test.ts @@ -52,6 +52,52 @@ describe('getExtensionsPayloadStoreRawPayload()', () => { extensions: [{mock: 'extension-payload'}, {mock: 'extension-payload'}, {mock: 'extension-payload'}], }) }) + test('includes allowedDomains in app when provided', async () => { + // Given + vi.spyOn(payload, 'getUIExtensionPayload').mockResolvedValue({ + mock: 'extension-payload', + } as unknown as UIExtensionPayload) + + const options = { + apiKey: 'mock-api-key', + appName: 'mock-app-name', + url: 'https://mock-url.com', + websocketURL: 'wss://mock-websocket-url.com', + extensions: [], + storeFqdn: 'mock-store-fqdn.myshopify.com', + manifestVersion: '3', + allowedDomains: ['example.com', 'cdn.example.com'], + } as unknown as ExtensionsPayloadStoreOptions + + // When + const rawPayload = await getExtensionsPayloadStoreRawPayload(options, 'mock-bundle-path') + + // Then + expect(rawPayload.app.allowedDomains).toEqual(['example.com', 'cdn.example.com']) + }) + + test('does not include allowedDomains in app when not provided', async () => { + // Given + vi.spyOn(payload, 'getUIExtensionPayload').mockResolvedValue({ + mock: 'extension-payload', + } as unknown as UIExtensionPayload) + + const options = { + apiKey: 'mock-api-key', + appName: 'mock-app-name', + url: 'https://mock-url.com', + websocketURL: 'wss://mock-websocket-url.com', + extensions: [], + storeFqdn: 'mock-store-fqdn.myshopify.com', + manifestVersion: '3', + } as unknown as ExtensionsPayloadStoreOptions + + // When + const rawPayload = await getExtensionsPayloadStoreRawPayload(options, 'mock-bundle-path') + + // Then + expect(rawPayload.app.allowedDomains).toBeUndefined() + }) }) describe('ExtensionsPayloadStore()', () => { diff --git a/packages/app/src/cli/services/dev/extension/payload/store.ts b/packages/app/src/cli/services/dev/extension/payload/store.ts index ef2676178ea..35f6a2e9d7c 100644 --- a/packages/app/src/cli/services/dev/extension/payload/store.ts +++ b/packages/app/src/cli/services/dev/extension/payload/store.ts @@ -52,6 +52,11 @@ export async function getExtensionsPayloadStoreRawPayload( } payload.app.assets = assets } + + if (options.allowedDomains) { + payload.app.allowedDomains = options.allowedDomains + } + return payload } diff --git a/packages/app/src/cli/services/dev/processes/previewable-extension.ts b/packages/app/src/cli/services/dev/processes/previewable-extension.ts index 935fbc948d4..f291494a10e 100644 --- a/packages/app/src/cli/services/dev/processes/previewable-extension.ts +++ b/packages/app/src/cli/services/dev/processes/previewable-extension.ts @@ -25,6 +25,7 @@ interface PreviewableExtensionOptions { previewableExtensions: ExtensionInstance[] appWatcher: AppEventWatcher appAssetsConfigs: Record | undefined + allowedDomains?: string[] } export interface PreviewableExtensionProcess extends BaseProcess { @@ -49,6 +50,7 @@ export const launchPreviewableExtensionProcess: DevProcessFunction { await devUIExtensions({ @@ -71,6 +73,7 @@ export const launchPreviewableExtensionProcess: DevProcessFunction