From 727f40e2768197090f6d565b0f659ba49eae8091 Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Thu, 2 Apr 2026 21:45:31 -0400 Subject: [PATCH] Add shopify store logout --- .../examples/store-auth-logout.example.sh | 1 + .../interfaces/store-auth-logout.interface.ts | 20 ++++ .../commands/store-auth-logout.doc.ts | 36 ++++++ docs-shopify.dev/commands/store-auth.doc.ts | 4 +- packages/cli/README.md | 29 +++++ packages/cli/oclif.manifest.json | 53 ++++++++- .../cli/src/cli/commands/store/auth/index.ts | 4 +- .../cli/commands/store/auth/logout.test.ts | 32 ++++++ .../cli/src/cli/commands/store/auth/logout.ts | 35 ++++++ .../cli/services/store/auth-logout.test.ts | 104 ++++++++++++++++++ .../cli/src/cli/services/store/auth-logout.ts | 36 ++++++ packages/cli/src/index.ts | 2 + 12 files changed, 352 insertions(+), 4 deletions(-) create mode 100644 docs-shopify.dev/commands/examples/store-auth-logout.example.sh create mode 100644 docs-shopify.dev/commands/interfaces/store-auth-logout.interface.ts create mode 100644 docs-shopify.dev/commands/store-auth-logout.doc.ts create mode 100644 packages/cli/src/cli/commands/store/auth/logout.test.ts create mode 100644 packages/cli/src/cli/commands/store/auth/logout.ts create mode 100644 packages/cli/src/cli/services/store/auth-logout.test.ts create mode 100644 packages/cli/src/cli/services/store/auth-logout.ts diff --git a/docs-shopify.dev/commands/examples/store-auth-logout.example.sh b/docs-shopify.dev/commands/examples/store-auth-logout.example.sh new file mode 100644 index 0000000000..e92306f3ee --- /dev/null +++ b/docs-shopify.dev/commands/examples/store-auth-logout.example.sh @@ -0,0 +1 @@ +shopify store auth logout [flags] \ No newline at end of file diff --git a/docs-shopify.dev/commands/interfaces/store-auth-logout.interface.ts b/docs-shopify.dev/commands/interfaces/store-auth-logout.interface.ts new file mode 100644 index 0000000000..48d229c4f3 --- /dev/null +++ b/docs-shopify.dev/commands/interfaces/store-auth-logout.interface.ts @@ -0,0 +1,20 @@ +// This is an autogenerated file. Don't edit this file manually. +export interface storeauthlogout { + /** + * Disable color output. + * @environment SHOPIFY_FLAG_NO_COLOR + */ + '--no-color'?: '' + + /** + * The myshopify.com domain of the store to clear local auth for. + * @environment SHOPIFY_FLAG_STORE + */ + '-s, --store ': string + + /** + * Increase the verbosity of the output. + * @environment SHOPIFY_FLAG_VERBOSE + */ + '--verbose'?: '' +} diff --git a/docs-shopify.dev/commands/store-auth-logout.doc.ts b/docs-shopify.dev/commands/store-auth-logout.doc.ts new file mode 100644 index 0000000000..7a1e4faae4 --- /dev/null +++ b/docs-shopify.dev/commands/store-auth-logout.doc.ts @@ -0,0 +1,36 @@ +// This is an autogenerated file. Don't edit this file manually. +import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs' + +const data: ReferenceEntityTemplateSchema = { + name: 'store auth logout', + description: `Clears the locally stored store auth for the specified store on this machine. + +This does not revoke the app or remove granted scopes on Shopify.`, + overviewPreviewDescription: `Clear locally stored store auth for a store.`, + type: 'command', + isVisualComponent: false, + defaultExample: { + codeblock: { + tabs: [ + { + title: 'store auth logout', + code: './examples/store-auth-logout.example.sh', + language: 'bash', + }, + ], + title: 'store auth logout', + }, + }, + definitions: [ + { + title: 'Flags', + description: 'The following flags are available for the `store auth logout` command:', + type: 'storeauthlogout', + }, + ], + category: 'store', + related: [ + ], +} + +export default data \ No newline at end of file diff --git a/docs-shopify.dev/commands/store-auth.doc.ts b/docs-shopify.dev/commands/store-auth.doc.ts index 0d74367262..43c1e82f61 100644 --- a/docs-shopify.dev/commands/store-auth.doc.ts +++ b/docs-shopify.dev/commands/store-auth.doc.ts @@ -7,7 +7,9 @@ const data: ReferenceEntityTemplateSchema = { Re-run this command if the stored token is missing, expires, or no longer has the scopes you need. -To inspect the locally stored auth state for a store, run [\`shopify store auth info\`](/docs/api/shopify-cli/store/store-auth-info).`, +To inspect the locally stored auth state for a store, run [\`shopify store auth info\`](/docs/api/shopify-cli/store/store-auth-info). + +To clear the locally stored auth state for a store, run [\`shopify store auth logout\`](/docs/api/shopify-cli/store/store-auth-logout).`, overviewPreviewDescription: `Authenticate an app against a store for store commands.`, type: 'command', isVisualComponent: false, diff --git a/packages/cli/README.md b/packages/cli/README.md index 34dfb0ac24..8cb7531aa4 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -75,6 +75,7 @@ * [`shopify search [query]`](#shopify-search-query) * [`shopify store auth`](#shopify-store-auth) * [`shopify store auth info`](#shopify-store-auth-info) +* [`shopify store auth logout`](#shopify-store-auth-logout) * [`shopify store execute`](#shopify-store-execute) * [`shopify theme check`](#shopify-theme-check) * [`shopify theme console`](#shopify-theme-console) @@ -2078,6 +2079,9 @@ DESCRIPTION To inspect the locally stored auth state for a store, run "`shopify store auth info`" (https://shopify.dev/docs/api/shopify-cli/store/store-auth-info). + To clear the locally stored auth state for a store, run "`shopify store auth logout`" + (https://shopify.dev/docs/api/shopify-cli/store/store-auth-logout). + EXAMPLES $ shopify store auth --store shop.myshopify.com --scopes read_products,write_products ``` @@ -2108,6 +2112,31 @@ EXAMPLES $ shopify store auth info --store shop.myshopify.com --json ``` +## `shopify store auth logout` + +Clear locally stored store auth for a store. + +``` +USAGE + $ shopify store auth logout -s [--no-color] [--verbose] + +FLAGS + -s, --store= (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store to clear local auth + for. + --no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output. + --verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output. + +DESCRIPTION + Clear locally stored store auth for a store. + + Clears the locally stored store auth for the specified store on this machine. + + This does not revoke the app or remove granted scopes on Shopify. + +EXAMPLES + $ shopify store auth logout --store shop.myshopify.com +``` + ## `shopify store execute` Execute GraphQL queries and mutations on a store. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 5bc51d2d43..859510c9ed 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -5739,8 +5739,8 @@ ], "args": { }, - "description": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.\n\nTo inspect the locally stored auth state for a store, run \"`shopify store auth info`\" (https://shopify.dev/docs/api/shopify-cli/store/store-auth-info).", - "descriptionWithMarkdown": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.\n\nTo inspect the locally stored auth state for a store, run [`shopify store auth info`](https://shopify.dev/docs/api/shopify-cli/store/store-auth-info).", + "description": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.\n\nTo inspect the locally stored auth state for a store, run \"`shopify store auth info`\" (https://shopify.dev/docs/api/shopify-cli/store/store-auth-info).\n\nTo clear the locally stored auth state for a store, run \"`shopify store auth logout`\" (https://shopify.dev/docs/api/shopify-cli/store/store-auth-logout).", + "descriptionWithMarkdown": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.\n\nTo inspect the locally stored auth state for a store, run [`shopify store auth info`](https://shopify.dev/docs/api/shopify-cli/store/store-auth-info).\n\nTo clear the locally stored auth state for a store, run [`shopify store auth logout`](https://shopify.dev/docs/api/shopify-cli/store/store-auth-logout).", "enableJsonFlag": false, "examples": [ "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products" @@ -5851,6 +5851,55 @@ "strict": true, "summary": "Show locally stored store auth information for a store." }, + "store:auth:logout": { + "aliases": [ + ], + "args": { + }, + "description": "Clears the locally stored store auth for the specified store on this machine.\n\nThis does not revoke the app or remove granted scopes on Shopify.", + "descriptionWithMarkdown": "Clears the locally stored store auth for the specified store on this machine.\n\nThis does not revoke the app or remove granted scopes on Shopify.", + "enableJsonFlag": false, + "examples": [ + "<%= config.bin %> <%= command.id %> --store shop.myshopify.com" + ], + "flags": { + "no-color": { + "allowNo": false, + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "type": "boolean" + }, + "store": { + "char": "s", + "description": "The myshopify.com domain of the store to clear local auth for.", + "env": "SHOPIFY_FLAG_STORE", + "hasDynamicHelp": false, + "multiple": false, + "name": "store", + "required": true, + "type": "option" + }, + "verbose": { + "allowNo": false, + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [ + ], + "id": "store:auth:logout", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Clear locally stored store auth for a store." + }, "store:execute": { "aliases": [ ], diff --git a/packages/cli/src/cli/commands/store/auth/index.ts b/packages/cli/src/cli/commands/store/auth/index.ts index ec380d70f3..90cb0a3423 100644 --- a/packages/cli/src/cli/commands/store/auth/index.ts +++ b/packages/cli/src/cli/commands/store/auth/index.ts @@ -11,7 +11,9 @@ export default class StoreAuth extends Command { Re-run this command if the stored token is missing, expires, or no longer has the scopes you need. -To inspect the locally stored auth state for a store, run [\`shopify store auth info\`](https://shopify.dev/docs/api/shopify-cli/store/store-auth-info).` +To inspect the locally stored auth state for a store, run [\`shopify store auth info\`](https://shopify.dev/docs/api/shopify-cli/store/store-auth-info). + +To clear the locally stored auth state for a store, run [\`shopify store auth logout\`](https://shopify.dev/docs/api/shopify-cli/store/store-auth-logout).` static description = this.descriptionWithoutMarkdown() diff --git a/packages/cli/src/cli/commands/store/auth/logout.test.ts b/packages/cli/src/cli/commands/store/auth/logout.test.ts new file mode 100644 index 0000000000..4a69089b9a --- /dev/null +++ b/packages/cli/src/cli/commands/store/auth/logout.test.ts @@ -0,0 +1,32 @@ +import {beforeEach, describe, expect, test, vi} from 'vitest' +import StoreAuthLogout from './logout.js' +import {displayStoreAuthLogout, logoutStoreAuth} from '../../../services/store/auth-logout.js' + +vi.mock('../../../services/store/auth-logout.js', () => ({ + logoutStoreAuth: vi.fn().mockReturnValue({store: 'shop.myshopify.com', cleared: true}), + displayStoreAuthLogout: vi.fn(), +})) + +describe('store auth logout command', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.mocked(logoutStoreAuth).mockReturnValue({store: 'shop.myshopify.com', cleared: true}) + }) + + test('passes parsed flags through to the auth logout service', async () => { + await StoreAuthLogout.run(['--store', 'shop.myshopify.com']) + + expect(logoutStoreAuth).toHaveBeenCalledWith('shop.myshopify.com') + expect(displayStoreAuthLogout).toHaveBeenCalledWith({store: 'shop.myshopify.com', cleared: true}) + }) + + test('normalizes the store flag before calling the auth logout service', async () => { + await StoreAuthLogout.run(['--store', 'https://shop.myshopify.com/admin']) + + expect(logoutStoreAuth).toHaveBeenCalledWith('shop.myshopify.com') + }) + + test('defines the expected flags', () => { + expect(StoreAuthLogout.flags.store).toBeDefined() + }) +}) diff --git a/packages/cli/src/cli/commands/store/auth/logout.ts b/packages/cli/src/cli/commands/store/auth/logout.ts new file mode 100644 index 0000000000..3cb8b862f5 --- /dev/null +++ b/packages/cli/src/cli/commands/store/auth/logout.ts @@ -0,0 +1,35 @@ +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {Flags} from '@oclif/core' +import {displayStoreAuthLogout, logoutStoreAuth} from '../../../services/store/auth-logout.js' + +export default class StoreAuthLogout extends Command { + static summary = 'Clear locally stored store auth for a store.' + + static descriptionWithMarkdown = `Clears the locally stored store auth for the specified store on this machine. + +This does not revoke the app or remove granted scopes on Shopify.` + + static description = this.descriptionWithoutMarkdown() + + static examples = ['<%= config.bin %> <%= command.id %> --store shop.myshopify.com'] + + static flags = { + ...globalFlags, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain of the store to clear local auth for.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + required: true, + }), + } + + async run(): Promise { + const {flags} = await this.parse(StoreAuthLogout) + + const result = logoutStoreAuth(flags.store) + displayStoreAuthLogout(result) + } +} diff --git a/packages/cli/src/cli/services/store/auth-logout.test.ts b/packages/cli/src/cli/services/store/auth-logout.test.ts new file mode 100644 index 0000000000..255a7072b4 --- /dev/null +++ b/packages/cli/src/cli/services/store/auth-logout.test.ts @@ -0,0 +1,104 @@ +import {beforeEach, describe, expect, test, vi} from 'vitest' +import {clearStoredStoreAppSession, getStoredStoreAppSession} from './session.js' +import {displayStoreAuthLogout, logoutStoreAuth} from './auth-logout.js' +import {outputCompleted, outputInfo} from '@shopify/cli-kit/node/output' + +vi.mock('./session.js') +vi.mock('@shopify/cli-kit/node/output') + +describe('store auth logout service', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + test('clears the locally stored auth bucket for the store', () => { + vi.mocked(getStoredStoreAppSession).mockReturnValue({ + store: 'shop.myshopify.com', + clientId: 'client-id', + userId: '42', + accessToken: 'token', + refreshToken: 'refresh-token', + scopes: ['read_products'], + acquiredAt: '2026-04-02T00:00:00.000Z', + associatedUser: {id: 42, email: 'merchant@example.com'}, + } as any) + + expect(logoutStoreAuth('shop.myshopify.com')).toEqual({ + store: 'shop.myshopify.com', + cleared: true, + }) + expect(clearStoredStoreAppSession).toHaveBeenCalledWith('shop.myshopify.com') + }) + + test('normalizes the store before looking up local auth', () => { + vi.mocked(getStoredStoreAppSession).mockReturnValue(undefined) + + expect(logoutStoreAuth('https://shop.myshopify.com/admin')).toEqual({ + store: 'shop.myshopify.com', + cleared: false, + }) + expect(getStoredStoreAppSession).toHaveBeenCalledWith('shop.myshopify.com') + expect(clearStoredStoreAppSession).not.toHaveBeenCalled() + }) + + test('normalizes the store before clearing local auth', () => { + vi.mocked(getStoredStoreAppSession).mockReturnValue({ + store: 'shop.myshopify.com', + clientId: 'client-id', + userId: '42', + accessToken: 'token', + scopes: ['read_products'], + acquiredAt: '2026-04-02T00:00:00.000Z', + } as any) + + expect(logoutStoreAuth('https://shop.myshopify.com/admin')).toEqual({ + store: 'shop.myshopify.com', + cleared: true, + }) + expect(getStoredStoreAppSession).toHaveBeenCalledWith('shop.myshopify.com') + expect(clearStoredStoreAppSession).toHaveBeenCalledWith('shop.myshopify.com') + }) + + test('returns a no-op result when no stored auth exists', () => { + vi.mocked(getStoredStoreAppSession).mockReturnValue(undefined) + + expect(logoutStoreAuth('shop.myshopify.com')).toEqual({ + store: 'shop.myshopify.com', + cleared: false, + }) + expect(clearStoredStoreAppSession).not.toHaveBeenCalled() + }) + + test('renders a completion message when stored auth is cleared', () => { + displayStoreAuthLogout({ + store: 'shop.myshopify.com', + cleared: true, + }) + + expect(outputCompleted).toHaveBeenCalledWith('Cleared locally stored store auth for shop.myshopify.com.') + }) + + test('renders an info message when there is nothing to clear', () => { + displayStoreAuthLogout({ + store: 'shop.myshopify.com', + cleared: false, + }) + + expect(outputInfo).toHaveBeenCalledWith('No locally stored store auth found for shop.myshopify.com.') + }) + + test('clears all locally stored users for the store so logout leaves no active session', () => { + vi.mocked(getStoredStoreAppSession).mockReturnValue({ + store: 'shop.myshopify.com', + clientId: 'client-id', + userId: '84', + accessToken: 'token', + scopes: ['read_products'], + acquiredAt: '2026-04-02T00:00:00.000Z', + } as any) + + logoutStoreAuth('shop.myshopify.com') + + expect(clearStoredStoreAppSession).toHaveBeenCalledWith('shop.myshopify.com') + }) +}) diff --git a/packages/cli/src/cli/services/store/auth-logout.ts b/packages/cli/src/cli/services/store/auth-logout.ts new file mode 100644 index 0000000000..43bf24c55e --- /dev/null +++ b/packages/cli/src/cli/services/store/auth-logout.ts @@ -0,0 +1,36 @@ +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {outputCompleted, outputInfo} from '@shopify/cli-kit/node/output' +import {clearStoredStoreAppSession, getStoredStoreAppSession} from './session.js' + +interface StoreAuthLogoutResult { + store: string + cleared: boolean +} + +export function logoutStoreAuth(store: string): StoreAuthLogoutResult { + const normalizedStore = normalizeStoreFqdn(store) + const session = getStoredStoreAppSession(normalizedStore) + + if (!session) { + return { + store: normalizedStore, + cleared: false, + } + } + + clearStoredStoreAppSession(normalizedStore) + + return { + store: normalizedStore, + cleared: true, + } +} + +export function displayStoreAuthLogout(result: StoreAuthLogoutResult): void { + if (!result.cleared) { + outputInfo(`No locally stored store auth found for ${result.store}.`) + return + } + + outputCompleted(`Cleared locally stored store auth for ${result.store}.`) +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index e10a97fbfa..bdc93374ed 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -17,6 +17,7 @@ import Generate from './cli/commands/notifications/generate.js' import ClearCache from './cli/commands/cache/clear.js' import StoreAuth from './cli/commands/store/auth/index.js' import StoreAuthInfo from './cli/commands/store/auth/info.js' +import StoreAuthLogout from './cli/commands/store/auth/logout.js' import StoreExecute from './cli/commands/store/execute.js' import {createGlobalProxyAgent} from 'global-agent' import ThemeCommands from '@shopify/theme' @@ -155,6 +156,7 @@ export const COMMANDS: any = { 'cache:clear': ClearCache, 'store:auth': StoreAuth, 'store:auth:info': StoreAuthInfo, + 'store:auth:logout': StoreAuthLogout, 'store:execute': StoreExecute, }