From 09fbc81f490a475db5d91afd2129bf7318f61638 Mon Sep 17 00:00:00 2001 From: River Date: Wed, 14 Oct 2026 19:30:47 +0000 Subject: [PATCH 1/2] Never use saved logins in store info while the service account token is set `shopify store info` reads the login saved on this machine for the store before asking the Business Platform. A saved preview store session answers on its own, and a saved `shopify store auth` login is the fallback when the Business Platform can't find the store. With SHOPIFY_SERVICE_ACCOUNT_TOKEN set, either one makes the command answer as whoever saved that login instead of as the service account. While the token is set, store info now skips the saved login entirely: it returns what the Business Platform finds, or the Business Platform's error. `shopify store open` gets its store from store info, so it follows. Adds `usingServiceAccountToken()` to cli-kit's session module, next to `ensureNoServiceAccountToken()`, which now uses it. Co-authored-by: Eddie Gao --- .../cli-kit/src/public/node/session.test.ts | 18 +++++++++ packages/cli-kit/src/public/node/session.ts | 15 +++++++- .../src/cli/services/store/info/index.test.ts | 37 ++++++++++++++++++- .../src/cli/services/store/info/index.ts | 6 ++- 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/packages/cli-kit/src/public/node/session.test.ts b/packages/cli-kit/src/public/node/session.test.ts index 13406de7133..d2a432dc1a3 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, + usingServiceAccountToken, findSessionIdByAlias, setCurrentSessionAlias, setLastSeenUserId, @@ -72,6 +73,23 @@ describe('ensureNoServiceAccountToken', () => { ) }) +describe('usingServiceAccountToken', () => { + test('is true while SHOPIFY_SERVICE_ACCOUNT_TOKEN is the selected variable', () => { + vi.mocked(automationTokenVariable).mockReturnValue('SHOPIFY_SERVICE_ACCOUNT_TOKEN') + + expect(usingServiceAccountToken()).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(usingServiceAccountToken()).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..0ad1f67a230 100644 --- a/packages/cli-kit/src/public/node/session.ts +++ b/packages/cli-kit/src/public/node/session.ts @@ -343,6 +343,19 @@ ${outputToken.json(scopes)} return tokens.businessPlatform } +/** + * Whether the CLI authenticates with SHOPIFY_SERVICE_ACCOUNT_TOKEN. + * + * True whenever that variable is set, 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 usingServiceAccountToken(): boolean { + return automationTokenVariable() === environmentVariables.serviceAccountToken +} + /** * Fails when SHOPIFY_SERVICE_ACCOUNT_TOKEN is set, for commands that can't run with that token. * @@ -353,8 +366,8 @@ ${outputToken.json(scopes)} * @throws AbortError when SHOPIFY_SERVICE_ACCOUNT_TOKEN is set. */ export function ensureNoServiceAccountToken(): void { + if (!usingServiceAccountToken()) 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..ad73b28e48d 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, usingServiceAccountToken} 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(usingServiceAccountToken).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(usingServiceAccountToken).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..294c8d62172 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, usingServiceAccountToken} 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 Date: Wed, 14 Oct 2026 20:34:48 +0000 Subject: [PATCH 2/2] Rename usingServiceAccountToken to isServiceAccountTokenSet cli-kit's boolean helpers are named is* or has* (isCI, isTerminalInteractive, hasGit). The new helper is public API, and renaming a public export after a release counts as a breaking change, so name it to match before it ships. Co-authored-by: Eddie Gao --- packages/cli-kit/src/public/node/session.test.ts | 8 ++++---- packages/cli-kit/src/public/node/session.ts | 11 +++++------ .../store/src/cli/services/store/info/index.test.ts | 6 +++--- packages/store/src/cli/services/store/info/index.ts | 4 ++-- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/cli-kit/src/public/node/session.test.ts b/packages/cli-kit/src/public/node/session.test.ts index d2a432dc1a3..31d90f7dcf6 100644 --- a/packages/cli-kit/src/public/node/session.test.ts +++ b/packages/cli-kit/src/public/node/session.test.ts @@ -7,7 +7,7 @@ import { ensureAuthenticatedStorefront, ensureAuthenticatedThemes, ensureNoServiceAccountToken, - usingServiceAccountToken, + isServiceAccountTokenSet, findSessionIdByAlias, setCurrentSessionAlias, setLastSeenUserId, @@ -73,11 +73,11 @@ describe('ensureNoServiceAccountToken', () => { ) }) -describe('usingServiceAccountToken', () => { +describe('isServiceAccountTokenSet', () => { test('is true while SHOPIFY_SERVICE_ACCOUNT_TOKEN is the selected variable', () => { vi.mocked(automationTokenVariable).mockReturnValue('SHOPIFY_SERVICE_ACCOUNT_TOKEN') - expect(usingServiceAccountToken()).toBe(true) + expect(isServiceAccountTokenSet()).toBe(true) }) test.each([undefined, 'SHOPIFY_APP_AUTOMATION_TOKEN', 'SHOPIFY_CLI_PARTNERS_TOKEN'])( @@ -85,7 +85,7 @@ describe('usingServiceAccountToken', () => { (variable) => { vi.mocked(automationTokenVariable).mockReturnValue(variable) - expect(usingServiceAccountToken()).toBe(false) + expect(isServiceAccountTokenSet()).toBe(false) }, ) }) diff --git a/packages/cli-kit/src/public/node/session.ts b/packages/cli-kit/src/public/node/session.ts index 0ad1f67a230..e71edd0416c 100644 --- a/packages/cli-kit/src/public/node/session.ts +++ b/packages/cli-kit/src/public/node/session.ts @@ -344,15 +344,14 @@ ${outputToken.json(scopes)} } /** - * Whether the CLI authenticates with SHOPIFY_SERVICE_ACCOUNT_TOKEN. + * Whether SHOPIFY_SERVICE_ACCOUNT_TOKEN is set, which makes it the automation token the CLI authenticates with. * - * True whenever that variable is set, 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. + * 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 usingServiceAccountToken(): boolean { +export function isServiceAccountTokenSet(): boolean { return automationTokenVariable() === environmentVariables.serviceAccountToken } @@ -366,7 +365,7 @@ export function usingServiceAccountToken(): boolean { * @throws AbortError when SHOPIFY_SERVICE_ACCOUNT_TOKEN is set. */ export function ensureNoServiceAccountToken(): void { - if (!usingServiceAccountToken()) return + if (!isServiceAccountTokenSet()) return const variable = environmentVariables.serviceAccountToken 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 ad73b28e48d..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, usingServiceAccountToken} 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' @@ -418,7 +418,7 @@ The CLI is currently unable to prompt for reauthentication.`) }) test('ignores a stored preview session while the service account token is set', async () => { - vi.mocked(usingServiceAccountToken).mockReturnValue(true) + vi.mocked(isServiceAccountTokenSet).mockReturnValue(true) vi.mocked(getCurrentStoredStoreAppSession).mockReturnValue({ ...STORED_SESSION, kind: 'preview', @@ -441,7 +441,7 @@ The CLI is currently unable to prompt for reauthentication.`) }) test('throws the BP error instead of falling back to stored store auth while the service account token is set', async () => { - vi.mocked(usingServiceAccountToken).mockReturnValue(true) + vi.mocked(isServiceAccountTokenSet).mockReturnValue(true) mockStoreAuthFallback() await expect(getStoreInfo({store: SHOP})).rejects.toMatchObject({ diff --git a/packages/store/src/cli/services/store/info/index.ts b/packages/store/src/cli/services/store/info/index.ts index 294c8d62172..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, usingServiceAccountToken} 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' @@ -64,7 +64,7 @@ export async function getStoreInfo(options: GetStoreInfoOptions): Promise