From 7bb01a96347c434eb13f59aa74dc84086378d6b7 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Tue, 28 Jul 2026 15:11:49 +0200 Subject: [PATCH] Skip expired store auth sessions in theme commands Theme commands read stored store auth sessions with the raw cache accessor and never checked expiry, so an expired token was handed to the Admin API and the resulting 401 was terminal: the theme session carries no refresh handler and the failure path never falls back to normal authentication. Reuse isSessionExpired from cli-kit (including its refresh margin) so expired sessions are skipped and the command authenticates normally instead. Assisted-By: devx/60503589-d48c-43b2-8fba-b0df1e076f15 --- .../src/cli/utilities/theme-command.test.ts | 33 +++++++++++++++++-- .../theme/src/cli/utilities/theme-command.ts | 5 +++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/theme/src/cli/utilities/theme-command.test.ts b/packages/theme/src/cli/utilities/theme-command.test.ts index 7af98289585..01a32b1cb30 100644 --- a/packages/theme/src/cli/utilities/theme-command.test.ts +++ b/packages/theme/src/cli/utilities/theme-command.test.ts @@ -17,7 +17,14 @@ import {hashString} from '@shopify/cli-kit/node/crypto' import type {Writable} from 'stream' vi.mock('@shopify/cli-kit/node/session') -vi.mock('@shopify/cli-kit/node/store-auth-session') +vi.mock('@shopify/cli-kit/node/store-auth-session', async (importOriginal) => { + const original = await importOriginal() + return { + ...original, + getCurrentStoredStoreAppSession: vi.fn(), + listCurrentStoredStoreAppSessions: vi.fn(), + } +}) vi.mock('@shopify/cli-kit/node/environments') vi.mock('@shopify/cli-kit/node/ui') vi.mock('@shopify/cli-kit/node/metadata', () => ({ @@ -395,7 +402,27 @@ describe('ThemeCommand', () => { expect(command.commandCalls[0]).toMatchObject({session: mockSession}) }) - test('does not check stored store auth cache session expiry', async () => { + test('falls back to theme authentication when the stored session is expired', async () => { + vi.mocked(getCurrentStoredStoreAppSession).mockReturnValue({ + store: 'test-store.myshopify.com', + clientId: 'store-auth-client-id', + userId: 'preview:123', + accessToken: 'shpat_preview_token', + scopes: ['read_themes'], + acquiredAt: '2026-06-08T11:00:00.000Z', + expiresAt: new Date(Date.now() - 60 * 1000).toISOString(), + }) + + await CommandConfig.load() + const command = new TestScopedThemeCommand([], CommandConfig) + + await command.run() + + expect(ensureAuthenticatedThemes).toHaveBeenCalledWith('test-store.myshopify.com', undefined) + expect(command.commandCalls[0]).toMatchObject({session: mockSession}) + }) + + test('uses a stored session whose expiry is far enough in the future', async () => { vi.mocked(getCurrentStoredStoreAppSession).mockReturnValue({ store: 'test-store.myshopify.com', clientId: 'store-auth-client-id', @@ -403,7 +430,7 @@ describe('ThemeCommand', () => { accessToken: 'shpat_preview_token', scopes: ['read_themes'], acquiredAt: '2026-06-08T11:00:00.000Z', - expiresAt: '2026-06-08T11:30:00.000Z', + expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), }) await CommandConfig.load() diff --git a/packages/theme/src/cli/utilities/theme-command.ts b/packages/theme/src/cli/utilities/theme-command.ts index aebe3c48c89..a0ee866e572 100644 --- a/packages/theme/src/cli/utilities/theme-command.ts +++ b/packages/theme/src/cli/utilities/theme-command.ts @@ -9,6 +9,7 @@ import Command, {ArgOutput, FlagOutput, noDefaultsOptions} from '@shopify/cli-ki import {AdminSession, ensureAuthenticatedThemes, setLastSeenUserId} from '@shopify/cli-kit/node/session' import { getCurrentStoredStoreAppSession, + isSessionExpired, listCurrentStoredStoreAppSessions, type StoredStoreAppSession, } from '@shopify/cli-kit/node/store-auth-session' @@ -421,6 +422,10 @@ export default abstract class ThemeCommand extends Command { storeFqdn: string, requiredScopes: string[], ): AdminSession | undefined { + if (isSessionExpired(storedSession)) { + return undefined + } + if (!this.hasRequiredStoreAuthScopes(storedSession.scopes, requiredScopes)) { return undefined }