From 18a5adb9d29f7a9ff2d422225a63f8a5ad6b27a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Thu, 9 Jul 2026 15:44:57 +0200 Subject: [PATCH 1/2] Fix storeByDomain connecting to the wrong store on substring domain match AppManagementClient.storeByDomain passed the domain as a fuzzy `accessibleShops(search:)` argument and then returned `stores[0]`, so a domain that is a substring of another store's domain (e.g. `example.myshopify.com` vs `turbo-example.myshopify.com`) could resolve to the wrong store. That store then gets cached to `dev_store_url` and self-reinforces. Match the normalized domain exactly instead of taking the first fuzzy hit, returning undefined when nothing matches so the existing "could not find store" error path is used. Adds tests for the multi-hit and no-exact-match cases. Co-authored-by: Isaac Roldan --- .../app-management-client.test.ts | 96 +++++++++++++++++++ .../app-management-client.ts | 8 +- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts index ce225c38957..81eaf08cf7e 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts @@ -1609,6 +1609,102 @@ describe('storeByDomain', () => { }, }) }) + + test('returns the exact-domain match when the fuzzy search also returns substring matches', async () => { + // Given + // The Business Platform `accessibleShops` query treats `domain` as a fuzzy `search` argument, so + // a request for `example.myshopify.com` also returns `turbo-example.myshopify.com`, and the + // backend may rank the wrong (substring superset) store first. + const orgGid = 'gid://shopify/Organization/123' + const shopDomain = 'example.myshopify.com' + const token = 'business-platform-token' + const response: FetchStoreByDomainQuery = { + organization: { + id: orgGid, + name: 'Org 123', + accessibleShops: { + edges: [ + { + node: { + id: 'gid://BusinessPlatform/Shop/1', + externalId: encodedGidFromShopId('1'), + name: 'Turbo Example', + storeType: 'APP_DEVELOPMENT', + primaryDomain: 'turbo-example.myshopify.com', + shortName: 'turbo-example', + url: 'https://turbo-example.myshopify.com', + }, + }, + { + node: { + id: 'gid://BusinessPlatform/Shop/2', + externalId: encodedGidFromShopId('2'), + name: 'Example', + storeType: 'APP_DEVELOPMENT', + primaryDomain: shopDomain, + shortName: 'example', + url: `https://${shopDomain}`, + }, + }, + ], + }, + currentUser: {organizationPermissions: ['ondemand_access_to_stores']}, + }, + } + vi.mocked(businessPlatformOrganizationsRequestDoc).mockResolvedValueOnce(response) + + const client = AppManagementClient.getInstance() + client.businessPlatformToken = () => Promise.resolve(token) + + // When + const result = await client.storeByDomain(orgGid, shopDomain, ['APP_DEVELOPMENT']) + + // Then + expect(result).toMatchObject({ + shopDomain, + shopName: 'Example', + storeType: 'APP_DEVELOPMENT', + }) + }) + + test('returns undefined when the fuzzy search only returns substring (non-exact) matches', async () => { + // Given + const orgGid = 'gid://shopify/Organization/123' + const shopDomain = 'example.myshopify.com' + const token = 'business-platform-token' + const response: FetchStoreByDomainQuery = { + organization: { + id: orgGid, + name: 'Org 123', + accessibleShops: { + edges: [ + { + node: { + id: 'gid://BusinessPlatform/Shop/1', + externalId: encodedGidFromShopId('1'), + name: 'Turbo Example', + storeType: 'APP_DEVELOPMENT', + primaryDomain: 'turbo-example.myshopify.com', + shortName: 'turbo-example', + url: 'https://turbo-example.myshopify.com', + }, + }, + ], + }, + currentUser: {organizationPermissions: []}, + }, + } + vi.mocked(businessPlatformOrganizationsRequestDoc).mockResolvedValueOnce(response) + + const client = AppManagementClient.getInstance() + client.businessPlatformToken = () => Promise.resolve(token) + + // When + const result = await client.storeByDomain(orgGid, shopDomain, ['APP_DEVELOPMENT']) + + // Then + expect(result).toBeUndefined() + }) }) describe('ensureUserAccessToStore', () => { diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts index 07b32c020bd..32457c5c47f 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts @@ -863,7 +863,13 @@ export class AppManagementClient implements DeveloperPlatformClient { const provisionable = isStoreProvisionable(org.currentUser?.organizationPermissions ?? []) return mapBusinessPlatformStoresToOrganizationStores(nodes, provisionable) }) - return stores[0] + + // `accessibleShops` treats `domain` as a fuzzy, free-text `search` argument, so a domain that is + // a substring of another store's domain (e.g. `example.myshopify.com` vs + // `turbo-example.myshopify.com`) can return multiple hits. Return only the store whose domain + // matches exactly, otherwise we risk connecting to the wrong store. + const normalizedDomain = normalizeStoreFqdn(shopDomain) + return stores.find((store) => store.shopDomain === normalizedDomain) } async ensureUserAccessToStore(orgId: string, store: OrganizationStore): Promise { From 57eff800a47ebc4936663809f534886ec22013ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Thu, 9 Jul 2026 15:44:57 +0200 Subject: [PATCH 2/2] Add changeset for exact domain store lookup Assisted-By: devx/782ef931-ac0a-4cc4-bff1-a2511c379664 --- .changeset/exact-domain-store-lookup.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/exact-domain-store-lookup.md diff --git a/.changeset/exact-domain-store-lookup.md b/.changeset/exact-domain-store-lookup.md new file mode 100644 index 00000000000..b8aaa62238b --- /dev/null +++ b/.changeset/exact-domain-store-lookup.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Fix store lookup by domain to require an exact domain match.