diff --git a/packages/cli-kit/src/public/node/session.test.ts b/packages/cli-kit/src/public/node/session.test.ts index 13406de7133..31d90f7dcf6 100644 --- a/packages/cli-kit/src/public/node/session.test.ts +++ b/packages/cli-kit/src/public/node/session.test.ts @@ -7,6 +7,7 @@ import { ensureAuthenticatedStorefront, ensureAuthenticatedThemes, ensureNoServiceAccountToken, + isServiceAccountTokenSet, findSessionIdByAlias, setCurrentSessionAlias, setLastSeenUserId, @@ -72,6 +73,23 @@ describe('ensureNoServiceAccountToken', () => { ) }) +describe('isServiceAccountTokenSet', () => { + test('is true while SHOPIFY_SERVICE_ACCOUNT_TOKEN is the selected variable', () => { + vi.mocked(automationTokenVariable).mockReturnValue('SHOPIFY_SERVICE_ACCOUNT_TOKEN') + + expect(isServiceAccountTokenSet()).toBe(true) + }) + + test.each([undefined, 'SHOPIFY_APP_AUTOMATION_TOKEN', 'SHOPIFY_CLI_PARTNERS_TOKEN'])( + 'is false when the automation token variable is %s', + (variable) => { + vi.mocked(automationTokenVariable).mockReturnValue(variable) + + expect(isServiceAccountTokenSet()).toBe(false) + }, + ) +}) + describe('findSessionIdByAlias', () => { test('returns the matching session ID without selecting it', async () => { // Given diff --git a/packages/cli-kit/src/public/node/session.ts b/packages/cli-kit/src/public/node/session.ts index 2c3c78f03ee..e71edd0416c 100644 --- a/packages/cli-kit/src/public/node/session.ts +++ b/packages/cli-kit/src/public/node/session.ts @@ -343,6 +343,18 @@ ${outputToken.json(scopes)} return tokens.businessPlatform } +/** + * Whether SHOPIFY_SERVICE_ACCOUNT_TOKEN is set, which makes it the automation token the CLI authenticates with. + * + * True even when it's empty or set together with another automation token variable. Commands use this to skip logins + * saved on this machine, and authentication then reports any problem with the variables. + * + * @returns True when SHOPIFY_SERVICE_ACCOUNT_TOKEN is set. + */ +export function isServiceAccountTokenSet(): boolean { + return automationTokenVariable() === environmentVariables.serviceAccountToken +} + /** * Fails when SHOPIFY_SERVICE_ACCOUNT_TOKEN is set, for commands that can't run with that token. * @@ -353,8 +365,8 @@ ${outputToken.json(scopes)} * @throws AbortError when SHOPIFY_SERVICE_ACCOUNT_TOKEN is set. */ export function ensureNoServiceAccountToken(): void { + if (!isServiceAccountTokenSet()) return const variable = environmentVariables.serviceAccountToken - if (automationTokenVariable() !== variable) return throw new AbortError(`This command can't use ${variable}.`, `Unset ${variable} to run it with your Shopify account.`) } diff --git a/packages/store/src/cli/services/store/info/index.test.ts b/packages/store/src/cli/services/store/info/index.test.ts index dfa0c1e20fb..7680fa414d1 100644 --- a/packages/store/src/cli/services/store/info/index.test.ts +++ b/packages/store/src/cli/services/store/info/index.test.ts @@ -10,7 +10,7 @@ import {clearStoredStoreAppSession, getCurrentStoredStoreAppSession} from '@shop import {AbortError, BugError} from '@shopify/cli-kit/node/error' import {adminUrl} from '@shopify/cli-kit/node/api/admin' import {graphqlRequest} from '@shopify/cli-kit/node/api/graphql' -import {setLastSeenUserId} from '@shopify/cli-kit/node/session' +import {setLastSeenUserId, isServiceAccountTokenSet} from '@shopify/cli-kit/node/session' import {beforeEach, describe, test, expect, vi} from 'vitest' import type {OrganizationShopFields} from '../../../utilities/store-lookup/types.js' import type {Store} from '../../../api/graphql/business-platform-organizations/generated/types.js' @@ -417,6 +417,41 @@ The CLI is currently unable to prompt for reauthentication.`) expect(loadStoredStoreSession).not.toHaveBeenCalled() }) + test('ignores a stored preview session while the service account token is set', async () => { + vi.mocked(isServiceAccountTokenSet).mockReturnValue(true) + vi.mocked(getCurrentStoredStoreAppSession).mockReturnValue({ + ...STORED_SESSION, + kind: 'preview', + preview: { + placeholderAccountUuid: 'placeholder-uuid', + shopId: '123', + name: 'Lavender Candles', + country: 'US', + createdAt: '2026-06-08T12:00:00.000Z', + accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token', + }, + }) + + const result = await getStoreInfo({store: SHOP}) + + expect(getCurrentStoredStoreAppSession).not.toHaveBeenCalled() + expect(getPreviewStore).not.toHaveBeenCalled() + expect(fetchDestinationsContext).toHaveBeenCalledWith({store: SHOP, noPrompt: false}) + expect(result.displayName).toBe('My Shop (Org)') + }) + + test('throws the BP error instead of falling back to stored store auth while the service account token is set', async () => { + vi.mocked(isServiceAccountTokenSet).mockReturnValue(true) + mockStoreAuthFallback() + + await expect(getStoreInfo({store: SHOP})).rejects.toMatchObject({ + message: `Couldn't find a store with domain ${SHOP} for the current account.`, + }) + expect(getCurrentStoredStoreAppSession).not.toHaveBeenCalled() + expect(loadStoredStoreSession).not.toHaveBeenCalled() + expect(graphqlRequest).not.toHaveBeenCalled() + }) + test('uses the refreshed session store returned by the stored-auth loader', async () => { mockStoreAuthFallback() vi.mocked(loadStoredStoreSession).mockResolvedValue({ diff --git a/packages/store/src/cli/services/store/info/index.ts b/packages/store/src/cli/services/store/info/index.ts index d31cf2e2b2c..5a0f8a4a91a 100644 --- a/packages/store/src/cli/services/store/info/index.ts +++ b/packages/store/src/cli/services/store/info/index.ts @@ -13,7 +13,7 @@ import {adminUrl} from '@shopify/cli-kit/node/api/admin' import {graphqlRequest} from '@shopify/cli-kit/node/api/graphql' import {compact} from '@shopify/cli-kit/common/object' import {extractMyshopifyHandle} from '@shopify/cli-kit/common/url' -import {setLastSeenUserId} from '@shopify/cli-kit/node/session' +import {setLastSeenUserId, isServiceAccountTokenSet} from '@shopify/cli-kit/node/session' import {outputDebug} from '@shopify/cli-kit/node/output' import type {DestinationsContext, OrganizationShopFields} from '../../../utilities/store-lookup/types.js' import type {StoreInfoResult, StoreInfoStoreOwner} from './types.js' @@ -62,7 +62,9 @@ export async function getStoreInfo(options: GetStoreInfoOptions): Promise