From 1fe2f2ff7bf5f3107cc52e623bcfa0fad62d4b0d Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Tue, 28 Jul 2026 15:13:59 +0200 Subject: [PATCH] Log store auth session decisions in theme commands under --verbose Diagnosing why a theme command picked (or skipped) a stored store auth session previously required tracing the code: the session source was invisible in verbose output, which made support investigations slow. Emit debug lines when a stored session is adopted, skipped for missing scopes, or skipped as expired. Tokens are never logged. Assisted-By: devx/60503589-d48c-43b2-8fba-b0df1e076f15 --- .../theme/src/cli/utilities/theme-command.test.ts | 15 +++++++++++++++ packages/theme/src/cli/utilities/theme-command.ts | 11 +++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/theme/src/cli/utilities/theme-command.test.ts b/packages/theme/src/cli/utilities/theme-command.test.ts index 01a32b1cb30..31d8d27605a 100644 --- a/packages/theme/src/cli/utilities/theme-command.test.ts +++ b/packages/theme/src/cli/utilities/theme-command.test.ts @@ -13,6 +13,7 @@ import {resolvePath} from '@shopify/cli-kit/node/path' import {renderConcurrent, renderConfirmationPrompt, renderError, renderWarning} from '@shopify/cli-kit/node/ui' import {addPublicMetadata, addSensitiveMetadata} from '@shopify/cli-kit/node/metadata' import {hashString} from '@shopify/cli-kit/node/crypto' +import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' import type {Writable} from 'stream' @@ -206,6 +207,7 @@ describe('ThemeCommand', () => { vi.mocked(getCurrentStoredStoreAppSession).mockReturnValue(undefined) vi.mocked(listCurrentStoredStoreAppSessions).mockReturnValue([]) vi.mocked(fileExistsSync).mockReturnValue(true) + mockAndCaptureOutput().clear() }) describe('run', () => { @@ -279,6 +281,8 @@ describe('ThemeCommand', () => { acquiredAt: '2026-06-08T11:00:00.000Z', }) + const outputMock = mockAndCaptureOutput() + await CommandConfig.load() const command = new TestScopedThemeCommand([], CommandConfig) @@ -289,6 +293,9 @@ describe('ThemeCommand', () => { expect(command.commandCalls[0]).toMatchObject({ session: {token: 'shpat_preview_token', storeFqdn: 'test-store.myshopify.com'}, }) + expect(outputMock.debug()).toContain( + 'Using stored store auth session for test-store.myshopify.com (scopes: read_themes).', + ) }) test('uses the password flag instead of a matching store auth cache session', async () => { @@ -391,6 +398,7 @@ describe('ThemeCommand', () => { scopes: ['read_products'], acquiredAt: '2026-06-08T11:00:00.000Z', }) + const outputMock = mockAndCaptureOutput() await CommandConfig.load() const command = new TestScopedThemeCommand([], CommandConfig) @@ -400,6 +408,9 @@ describe('ThemeCommand', () => { expect(getCurrentStoredStoreAppSession).toHaveBeenCalledWith('test-store.myshopify.com') expect(ensureAuthenticatedThemes).toHaveBeenCalledWith('test-store.myshopify.com', undefined) expect(command.commandCalls[0]).toMatchObject({session: mockSession}) + expect(outputMock.debug()).toContain( + 'Ignoring stored store auth session for test-store.myshopify.com: it is missing required scopes (has: read_products; needs: read_themes).', + ) }) test('falls back to theme authentication when the stored session is expired', async () => { @@ -412,6 +423,7 @@ describe('ThemeCommand', () => { acquiredAt: '2026-06-08T11:00:00.000Z', expiresAt: new Date(Date.now() - 60 * 1000).toISOString(), }) + const outputMock = mockAndCaptureOutput() await CommandConfig.load() const command = new TestScopedThemeCommand([], CommandConfig) @@ -420,6 +432,9 @@ describe('ThemeCommand', () => { expect(ensureAuthenticatedThemes).toHaveBeenCalledWith('test-store.myshopify.com', undefined) expect(command.commandCalls[0]).toMatchObject({session: mockSession}) + expect(outputMock.debug()).toContain( + 'Ignoring stored store auth session for test-store.myshopify.com: it expired at', + ) }) test('uses a stored session whose expiry is far enough in the future', async () => { diff --git a/packages/theme/src/cli/utilities/theme-command.ts b/packages/theme/src/cli/utilities/theme-command.ts index a0ee866e572..6c42ad3e558 100644 --- a/packages/theme/src/cli/utilities/theme-command.ts +++ b/packages/theme/src/cli/utilities/theme-command.ts @@ -25,6 +25,7 @@ import {AbortController} from '@shopify/cli-kit/node/abort' import {AbortError} from '@shopify/cli-kit/node/error' import {recordEvent, compileData} from '@shopify/cli-kit/node/analytics' import {addPublicMetadata, addSensitiveMetadata} from '@shopify/cli-kit/node/metadata' +import {outputDebug} from '@shopify/cli-kit/node/output' import {cwd, joinPath, resolvePath} from '@shopify/cli-kit/node/path' import {fileExistsSync} from '@shopify/cli-kit/node/fs' import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' @@ -423,13 +424,23 @@ export default abstract class ThemeCommand extends Command { requiredScopes: string[], ): AdminSession | undefined { if (isSessionExpired(storedSession)) { + outputDebug( + `Ignoring stored store auth session for ${storeFqdn}: it expired at ${storedSession.expiresAt ?? 'unknown'}.`, + ) return undefined } if (!this.hasRequiredStoreAuthScopes(storedSession.scopes, requiredScopes)) { + outputDebug( + `Ignoring stored store auth session for ${storeFqdn}: it is missing required scopes (has: ${storedSession.scopes.join( + ', ', + )}; needs: ${requiredScopes.join(', ')}).`, + ) return undefined } + outputDebug(`Using stored store auth session for ${storeFqdn} (scopes: ${storedSession.scopes.join(', ')}).`) + setLastSeenUserId(storedSession.userId) return {