From 3bbda172b49b45f9f382bdf77bc5f73d0aa4c7d1 Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Wed, 1 Apr 2026 17:27:36 -0400 Subject: [PATCH] Adjust store auth warning for non-canonical store domain --- packages/cli/src/cli/services/store/auth.test.ts | 8 ++++++-- packages/cli/src/cli/services/store/auth.ts | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/cli/services/store/auth.test.ts b/packages/cli/src/cli/services/store/auth.test.ts index 1c754ae900c..708416cc7e6 100644 --- a/packages/cli/src/cli/services/store/auth.test.ts +++ b/packages/cli/src/cli/services/store/auth.test.ts @@ -156,7 +156,7 @@ describe('store auth service', () => { ).rejects.toThrow('OAuth callback state does not match the original request.') }) - test('waitForStoreAuthCode rejects when callback store does not match', async () => { + test('waitForStoreAuthCode rejects when callback store does not match and suggests the returned permanent domain', async () => { const port = await getAvailablePort() const params = callbackParams({shop: 'other-shop.myshopify.com'}) @@ -172,7 +172,11 @@ describe('store auth service', () => { await response.text() }, }), - ).rejects.toThrow('OAuth callback store does not match the requested store.') + ).rejects.toMatchObject({ + message: 'OAuth callback store does not match the requested store.', + tryMessage: + 'Shopify returned other-shop.myshopify.com during authentication. Re-run shopify store auth --store other-shop.myshopify.com --scopes using the permanent store domain instead of an alias or vanity domain.', + }) }) test('waitForStoreAuthCode rejects when Shopify returns an OAuth error', async () => { diff --git a/packages/cli/src/cli/services/store/auth.ts b/packages/cli/src/cli/services/store/auth.ts index 28853de1c9b..e6bdeb8234f 100644 --- a/packages/cli/src/cli/services/store/auth.ts +++ b/packages/cli/src/cli/services/store/auth.ts @@ -190,22 +190,31 @@ export async function waitForStoreAuthCode({ const {searchParams} = requestUrl - const fail = (message: string) => { + const fail = (message: string, tryMessage?: string) => { res.statusCode = 400 res.setHeader('Content-Type', 'text/html') res.setHeader('Connection', 'close') - res.once('finish', () => settleWithError(new AbortError(message))) + res.once('finish', () => settleWithError(new AbortError(message, tryMessage))) res.end(renderAuthCallbackPage('Authentication failed', message)) } const returnedStore = searchParams.get('shop') outputDebug(outputContent`Received OAuth callback for shop ${outputToken.raw(returnedStore ?? 'unknown')}`) - if (!returnedStore || normalizeStoreFqdn(returnedStore) !== normalizedStore) { + if (!returnedStore) { fail('OAuth callback store does not match the requested store.') return } + const normalizedReturnedStore = normalizeStoreFqdn(returnedStore) + if (normalizedReturnedStore !== normalizedStore) { + fail( + 'OAuth callback store does not match the requested store.', + `Shopify returned ${normalizedReturnedStore} during authentication. Re-run ${outputToken.genericShellCommand(`shopify store auth --store ${normalizedReturnedStore} --scopes `).value} using the permanent store domain instead of an alias or vanity domain.`, + ) + return + } + const returnedState = searchParams.get('state') if (!returnedState || !constantTimeEqual(returnedState, state)) { fail('OAuth callback state does not match the original request.')