From 9772ca8c2f4b81c6f8347b174664727cd830d99e Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Wed, 27 May 2026 16:02:57 +0200 Subject: [PATCH] Improve agent guidance for store auth --- .../generated/generated_docs_data_v2.json | 2 + packages/cli/README.md | 19 ++- packages/cli/oclif.manifest.json | 4 +- .../store/src/cli/commands/store/auth.test.ts | 4 + packages/store/src/cli/commands/store/auth.ts | 9 +- .../src/cli/services/store/auth/index.test.ts | 102 ++++++++++++++- .../src/cli/services/store/auth/index.ts | 118 ++++++++++++------ .../cli/services/store/auth/result.test.ts | 71 +++-------- .../src/cli/services/store/auth/result.ts | 2 + .../services/store/auth/session-store.test.ts | 1 + 10 files changed, 229 insertions(+), 103 deletions(-) diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index 811b2c94321..e011d0cebd7 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -4190,6 +4190,7 @@ "name": "--scopes ", "value": "string", "description": "Comma-separated Admin API scopes to request for the app.", + "isOptional": false, "environmentValue": "SHOPIFY_FLAG_SCOPES" }, { @@ -4216,6 +4217,7 @@ "name": "-s, --store ", "value": "string", "description": "The myshopify.com domain of the store to authenticate against.", + "isOptional": false, "environmentValue": "SHOPIFY_FLAG_STORE" } ], diff --git a/packages/cli/README.md b/packages/cli/README.md index 4f7060bb8a3..ebc53ac7a08 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -2160,12 +2160,13 @@ USAGE $ shopify store auth --scopes -s [-j] [--no-color] [--verbose] FLAGS - -j, --json [env: SHOPIFY_FLAG_JSON] Output the result as JSON. Automatically disables color output. - -s, --store= (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store to authenticate - against. - --no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output. - --scopes= (required) [env: SHOPIFY_FLAG_SCOPES] Comma-separated Admin API scopes to request for the app. - --verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output. + -j, --json [env: SHOPIFY_FLAG_JSON] Output the result as JSON. Automatically disables color output. + -s, --store= (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store to authenticate + against. + --no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output. + --scopes= (required) [env: SHOPIFY_FLAG_SCOPES] Comma-separated Admin API scopes to request for the + app. + --verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output. DESCRIPTION Authenticate an app against a store for store commands. @@ -2175,6 +2176,12 @@ DESCRIPTION Re-run this command if the stored token is missing, expires, or no longer has the scopes you need. + In an interactive terminal, Shopify CLI opens or prints the authorization URL and waits for authentication to complete. + Agents should keep the command running until the browser authorization finishes. + + In a non-TTY environment, Shopify CLI returns the current session if it already has the requested scopes. If no usable + session exists, it starts the same OAuth flow and waits for authentication to complete. + EXAMPLES $ shopify store auth --store shop.myshopify.com --scopes read_products,write_products diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index a09a174ed0c..990aba1360f 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -5730,8 +5730,8 @@ "args": { }, "customPluginName": "@shopify/store", - "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.", - "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.", + "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\nIn an interactive terminal, Shopify CLI opens or prints the authorization URL and waits for authentication to complete. Agents should keep the command running until the browser authorization finishes.\n\nIn a non-TTY environment, Shopify CLI returns the current session if it already has the requested scopes. If no usable session exists, it starts the same OAuth flow and waits for authentication to complete.", + "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\nIn an interactive terminal, Shopify CLI opens or prints the authorization URL and waits for authentication to complete. Agents should keep the command running until the browser authorization finishes.\n\nIn a non-TTY environment, Shopify CLI returns the current session if it already has the requested scopes. If no usable session exists, it starts the same OAuth flow and waits for authentication to complete.", "examples": [ "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products", "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products --json" diff --git a/packages/store/src/cli/commands/store/auth.test.ts b/packages/store/src/cli/commands/store/auth.test.ts index 2b3e0efa856..f013b32246b 100644 --- a/packages/store/src/cli/commands/store/auth.test.ts +++ b/packages/store/src/cli/commands/store/auth.test.ts @@ -40,6 +40,10 @@ describe('store auth command', () => { expect(StoreAuth.flags.store).toBeDefined() expect(StoreAuth.flags.scopes).toBeDefined() expect(StoreAuth.flags.json).toBeDefined() + expect(StoreAuth.flags.store.required).toBe(true) + expect(StoreAuth.flags.scopes.required).toBe(true) + expect('resume' in StoreAuth.flags).toBe(false) + expect('callback-url' in StoreAuth.flags).toBe(false) expect('port' in StoreAuth.flags).toBe(false) expect('client-secret-file' in StoreAuth.flags).toBe(false) }) diff --git a/packages/store/src/cli/commands/store/auth.ts b/packages/store/src/cli/commands/store/auth.ts index 9a7c0eb2fdf..a13b847f7eb 100644 --- a/packages/store/src/cli/commands/store/auth.ts +++ b/packages/store/src/cli/commands/store/auth.ts @@ -10,7 +10,11 @@ export default class StoreAuth extends StoreCommand { static descriptionWithMarkdown = `Authenticates the app against the specified store for store commands and stores an online access token for later reuse. -Re-run this command if the stored token is missing, expires, or no longer has the scopes you need.` +Re-run this command if the stored token is missing, expires, or no longer has the scopes you need. + +In an interactive terminal, Shopify CLI opens or prints the authorization URL and waits for authentication to complete. Agents should keep the command running until the browser authorization finishes. + +In a non-TTY environment, Shopify CLI returns the current session if it already has the requested scopes. If no usable session exists, it starts the same OAuth flow and waits for authentication to complete.` static description = this.descriptionWithoutMarkdown() @@ -38,6 +42,7 @@ Re-run this command if the stored token is missing, expires, or no longer has th public async run(): Promise { const {flags} = await this.parse(StoreAuth) + const presenter = createStoreAuthPresenter(flags.json ? 'json' : 'text') await authenticateStoreWithApp( { @@ -45,7 +50,7 @@ Re-run this command if the stored token is missing, expires, or no longer has th scopes: flags.scopes, }, { - presenter: createStoreAuthPresenter(flags.json ? 'json' : 'text'), + presenter, }, ) } diff --git a/packages/store/src/cli/services/store/auth/index.test.ts b/packages/store/src/cli/services/store/auth/index.test.ts index a02c10ce5ac..8a0cf653d2f 100644 --- a/packages/store/src/cli/services/store/auth/index.test.ts +++ b/packages/store/src/cli/services/store/auth/index.test.ts @@ -1,17 +1,27 @@ import {authenticateStoreWithApp} from './index.js' -import {setStoredStoreAppSession} from './session-store.js' +import {getCurrentStoredStoreAppSession, setStoredStoreAppSession} from './session-store.js' import {STORE_AUTH_APP_CLIENT_ID} from './config.js' import {recordStoreFqdnMetadata} from '../attribution.js' import {setLastSeenUserId} from '@shopify/cli-kit/node/session' -import {describe, expect, test, vi} from 'vitest' +import {randomUUID} from '@shopify/cli-kit/node/crypto' +import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system' +import {beforeEach, describe, expect, test, vi} from 'vitest' vi.mock('./session-store.js') vi.mock('../attribution.js') vi.mock('@shopify/cli-kit/node/session') -vi.mock('@shopify/cli-kit/node/system', () => ({openURL: vi.fn().mockResolvedValue(true)})) +vi.mock('@shopify/cli-kit/node/system', () => ({ + openURL: vi.fn().mockResolvedValue(true), + terminalSupportsPrompting: vi.fn().mockReturnValue(true), +})) vi.mock('@shopify/cli-kit/node/crypto', () => ({randomUUID: vi.fn().mockReturnValue('state-123')})) describe('store auth service', () => { + beforeEach(() => { + vi.mocked(randomUUID).mockReturnValue('state-123') + vi.mocked(terminalSupportsPrompting).mockReturnValue(true) + }) + test('authenticateStoreWithApp opens the browser, stores the session, and returns auth result', async () => { const openURL = vi.fn().mockResolvedValue(true) const presenter = { @@ -77,6 +87,92 @@ describe('store auth service', () => { }) }) + test('authenticateStoreWithApp keeps waiting for auth when the terminal cannot prompt', async () => { + const openURL = vi.fn().mockResolvedValue(false) + const presenter = { + openingBrowser: vi.fn(), + manualAuthUrl: vi.fn(), + success: vi.fn(), + } + const waitForStoreAuthCodeMock = vi.fn().mockImplementation(async (options) => { + await options.onListening?.() + return 'abc123' + }) + + const result = await authenticateStoreWithApp( + { + store: 'shop.myshopify.com', + scopes: 'read_products', + }, + { + openURL, + presenter, + terminalSupportsPrompting: vi.fn().mockReturnValue(false), + waitForStoreAuthCode: waitForStoreAuthCodeMock, + exchangeStoreAuthCodeForToken: vi.fn().mockResolvedValue({ + access_token: 'token', + scope: 'read_products', + expires_in: 86400, + associated_user: {id: 42, email: 'test@example.com'}, + }), + }, + ) + + expect(result).toEqual( + expect.objectContaining({ + store: 'shop.myshopify.com', + userId: '42', + scopes: ['read_products'], + }), + ) + expect(presenter.openingBrowser).toHaveBeenCalledOnce() + expect(presenter.manualAuthUrl).toHaveBeenCalledWith( + expect.stringContaining('https://shop.myshopify.com/admin/oauth/authorize?'), + ) + expect(presenter.success).toHaveBeenCalledWith(result) + }) + + test('authenticateStoreWithApp returns existing session without auth when non-TTY scopes are already granted', async () => { + const presenter = { + openingBrowser: vi.fn(), + manualAuthUrl: vi.fn(), + success: vi.fn(), + } + vi.mocked(getCurrentStoredStoreAppSession).mockReturnValue({ + store: 'shop.myshopify.com', + clientId: STORE_AUTH_APP_CLIENT_ID, + userId: '42', + accessToken: 'token', + scopes: ['read_products'], + acquiredAt: '2026-03-27T00:00:00.000Z', + associatedUser: {id: 42, email: 'test@example.com'}, + }) + + const result = await authenticateStoreWithApp( + { + store: 'shop.myshopify.com', + scopes: 'read_products', + }, + { + presenter, + resolveExistingScopes: vi.fn().mockResolvedValue({scopes: ['read_products'], authoritative: true}), + terminalSupportsPrompting: vi.fn().mockReturnValue(false), + waitForStoreAuthCode: vi.fn(), + exchangeStoreAuthCodeForToken: vi.fn(), + }, + ) + + expect(result).toEqual( + expect.objectContaining({ + store: 'shop.myshopify.com', + userId: '42', + scopes: ['read_products'], + associatedUser: expect.objectContaining({email: 'test@example.com'}), + }), + ) + expect(presenter.success).toHaveBeenCalledWith(result) + }) + test('authenticateStoreWithApp uses remote scopes by default when available', async () => { const openURL = vi.fn().mockResolvedValue(true) const presenter = { diff --git a/packages/store/src/cli/services/store/auth/index.ts b/packages/store/src/cli/services/store/auth/index.ts index 8856623f96c..01546bb6372 100644 --- a/packages/store/src/cli/services/store/auth/index.ts +++ b/packages/store/src/cli/services/store/auth/index.ts @@ -1,14 +1,15 @@ import {STORE_AUTH_APP_CLIENT_ID} from './config.js' -import {setStoredStoreAppSession} from './session-store.js' +import {setStoredStoreAppSession, type StoredStoreAppSession} from './session-store.js' import {exchangeStoreAuthCodeForToken} from './token-client.js' import {waitForStoreAuthCode} from './callback.js' import {createPkceBootstrap} from './pkce.js' import {mergeRequestedAndStoredScopes, parseStoreAuthScopes, resolveGrantedScopes} from './scopes.js' import {resolveExistingStoreAuthScopes, type ResolvedStoreAuthScopes} from './existing-scopes.js' +import {loadStoredStoreSession} from './session-lifecycle.js' import {createStoreAuthPresenter, type StoreAuthPresenter, type StoreAuthResult} from './result.js' import {recordStoreFqdnMetadata} from '../attribution.js' import {setLastSeenUserId} from '@shopify/cli-kit/node/session' -import {openURL} from '@shopify/cli-kit/node/system' +import {openURL, terminalSupportsPrompting} from '@shopify/cli-kit/node/system' import {outputContent, outputDebug, outputToken} from '@shopify/cli-kit/node/output' import {AbortError} from '@shopify/cli-kit/node/error' import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' @@ -24,6 +25,7 @@ interface StoreAuthDependencies { exchangeStoreAuthCodeForToken: typeof exchangeStoreAuthCodeForToken resolveExistingScopes: (store: string) => Promise presenter: StoreAuthPresenter + terminalSupportsPrompting: typeof terminalSupportsPrompting } const defaultStoreAuthDependencies: StoreAuthDependencies = { @@ -32,45 +34,31 @@ const defaultStoreAuthDependencies: StoreAuthDependencies = { exchangeStoreAuthCodeForToken, resolveExistingScopes: resolveExistingStoreAuthScopes, presenter: createStoreAuthPresenter('text'), + terminalSupportsPrompting, } -export async function authenticateStoreWithApp( - input: StoreAuthInput, - dependencies: Partial = {}, -): Promise { - const resolvedDependencies: StoreAuthDependencies = {...defaultStoreAuthDependencies, ...dependencies} - const store = normalizeStoreFqdn(input.store) - await recordStoreFqdnMetadata(store, false) - const requestedScopes = parseStoreAuthScopes(input.scopes) - const existingScopeResolution = await resolvedDependencies.resolveExistingScopes(store) - const scopes = mergeRequestedAndStoredScopes(requestedScopes, existingScopeResolution.scopes) - const validationScopes = existingScopeResolution.authoritative ? scopes : requestedScopes - - if (existingScopeResolution.scopes.length > 0) { - outputDebug( - outputContent`Merged requested scopes ${outputToken.raw(requestedScopes.join(','))} with existing scopes ${outputToken.raw(existingScopeResolution.scopes.join(','))} for ${outputToken.raw(store)}`, - ) - } - - const bootstrap = createPkceBootstrap({ - store, +function storedSessionToStoreAuthResult( + session: StoredStoreAppSession, + scopes: string[], + acquiredAt = session.acquiredAt, +): StoreAuthResult { + return { + store: session.store, + userId: session.userId, scopes, - exchangeCodeForToken: resolvedDependencies.exchangeStoreAuthCodeForToken, - }) - const { - authorization: {authorizationUrl}, - } = bootstrap - - resolvedDependencies.presenter.openingBrowser() + acquiredAt, + expiresAt: session.expiresAt, + refreshTokenExpiresAt: session.refreshTokenExpiresAt, + hasRefreshToken: Boolean(session.refreshToken), + associatedUser: session.associatedUser, + } +} - const code = await resolvedDependencies.waitForStoreAuthCode({ - ...bootstrap.waitForAuthCodeOptions, - onListening: async () => { - const opened = await resolvedDependencies.openURL(authorizationUrl) - if (!opened) resolvedDependencies.presenter.manualAuthUrl(authorizationUrl) - }, - }) - const tokenResponse = await bootstrap.exchangeCodeForToken(code) +async function persistStoreAuthToken( + tokenResponse: Awaited>, + store: string, + validationScopes: string[], +): Promise { await recordStoreFqdnMetadata(store, true) const userId = tokenResponse.associated_user?.id?.toString() @@ -81,7 +69,6 @@ export async function authenticateStoreWithApp( const now = Date.now() const expiresAt = tokenResponse.expires_in ? new Date(now + tokenResponse.expires_in * 1000).toISOString() : undefined - const result: StoreAuthResult = { store, userId, @@ -120,6 +107,61 @@ export async function authenticateStoreWithApp( outputContent`Session persisted for ${outputToken.raw(store)} (user ${outputToken.raw(userId)}, expires ${outputToken.raw(expiresAt ?? 'unknown')})`, ) + return result +} + +export async function authenticateStoreWithApp( + input: StoreAuthInput, + dependencies: Partial = {}, +): Promise { + const resolvedDependencies: StoreAuthDependencies = {...defaultStoreAuthDependencies, ...dependencies} + const store = normalizeStoreFqdn(input.store) + await recordStoreFqdnMetadata(store, false) + const requestedScopes = parseStoreAuthScopes(input.scopes) + const existingScopeResolution = await resolvedDependencies.resolveExistingScopes(store) + const scopes = mergeRequestedAndStoredScopes(requestedScopes, existingScopeResolution.scopes) + const validationScopes = existingScopeResolution.authoritative ? scopes : requestedScopes + + if (existingScopeResolution.scopes.length > 0) { + outputDebug( + outputContent`Merged requested scopes ${outputToken.raw(requestedScopes.join(','))} with existing scopes ${outputToken.raw(existingScopeResolution.scopes.join(','))} for ${outputToken.raw(store)}`, + ) + } + + const bootstrap = createPkceBootstrap({ + store, + scopes, + exchangeCodeForToken: resolvedDependencies.exchangeStoreAuthCodeForToken, + }) + const { + authorization: {authorizationUrl}, + } = bootstrap + + if (!resolvedDependencies.terminalSupportsPrompting()) { + const existingMergedScopes = mergeRequestedAndStoredScopes(requestedScopes, existingScopeResolution.scopes) + if ( + existingScopeResolution.authoritative && + existingMergedScopes.length === existingScopeResolution.scopes.length && + existingMergedScopes.every((scope) => existingScopeResolution.scopes.includes(scope)) + ) { + const session = await loadStoredStoreSession(store) + const result = storedSessionToStoreAuthResult(session, existingScopeResolution.scopes) + resolvedDependencies.presenter.success(result) + return result + } + } + + resolvedDependencies.presenter.openingBrowser() + + const code = await resolvedDependencies.waitForStoreAuthCode({ + ...bootstrap.waitForAuthCodeOptions, + onListening: async () => { + const opened = await resolvedDependencies.openURL(authorizationUrl) + if (!opened) resolvedDependencies.presenter.manualAuthUrl(authorizationUrl) + }, + }) + const result = await persistStoreAuthToken(await bootstrap.exchangeCodeForToken(code), store, validationScopes) + resolvedDependencies.presenter.success(result) return result } diff --git a/packages/store/src/cli/services/store/auth/result.test.ts b/packages/store/src/cli/services/store/auth/result.test.ts index 29e11516d71..4f21a052373 100644 --- a/packages/store/src/cli/services/store/auth/result.test.ts +++ b/packages/store/src/cli/services/store/auth/result.test.ts @@ -1,41 +1,12 @@ import {createStoreAuthPresenter} from './result.js' -import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' +import {beforeEach, describe, expect, test} from 'vitest' import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' -function captureStandardStreams() { - const stdout: string[] = [] - const stderr: string[] = [] - - const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(((chunk: string | Uint8Array) => { - stdout.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')) - return true - }) as typeof process.stdout.write) - const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(((chunk: string | Uint8Array) => { - stderr.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')) - return true - }) as typeof process.stderr.write) - - return { - stdout: () => stdout.join(''), - stderr: () => stderr.join(''), - restore: () => { - stdoutSpy.mockRestore() - stderrSpy.mockRestore() - }, - } -} - describe('store auth presenter', () => { - const originalUnitTestEnv = process.env.SHOPIFY_UNIT_TEST - beforeEach(() => { mockAndCaptureOutput().clear() }) - afterEach(() => { - process.env.SHOPIFY_UNIT_TEST = originalUnitTestEnv - }) - test('renders human success output in text mode', () => { const output = mockAndCaptureOutput() const presenter = createStoreAuthPresenter('text') @@ -75,30 +46,26 @@ describe('store auth presenter', () => { expect(output.info()).not.toContain('shopify store execute') }) - test('writes browser guidance to stderr and json success to stdout', () => { - process.env.SHOPIFY_UNIT_TEST = 'false' - const streams = captureStandardStreams() + test('writes browser guidance and json success output', () => { + const output = mockAndCaptureOutput() const presenter = createStoreAuthPresenter('json') - try { - presenter.openingBrowser() - presenter.manualAuthUrl('https://shop.myshopify.com/admin/oauth/authorize?client_id=test') - presenter.success({ - store: 'shop.myshopify.com', - userId: '42', - scopes: ['read_products'], - acquiredAt: '2026-04-02T00:00:00.000Z', - hasRefreshToken: true, - associatedUser: {id: 42, email: 'merchant@example.com'}, - }) - } finally { - streams.restore() - } + presenter.openingBrowser() + presenter.manualAuthUrl('https://shop.myshopify.com/admin/oauth/authorize?client_id=test') + presenter.success({ + store: 'shop.myshopify.com', + userId: '42', + scopes: ['read_products'], + acquiredAt: '2026-04-02T00:00:00.000Z', + hasRefreshToken: true, + associatedUser: {id: 42, email: 'merchant@example.com'}, + }) - expect(streams.stderr()).toContain('Shopify CLI will open the app authorization page in your browser.') - expect(streams.stderr()).toContain('Browser did not open automatically. Open this URL manually:') - expect(streams.stderr()).toContain('https://shop.myshopify.com/admin/oauth/authorize?client_id=test') - expect(streams.stdout()).toContain('"store": "shop.myshopify.com"') - expect(streams.stdout()).not.toContain('Authenticated') + expect(output.info()).toContain('Shopify CLI will open the app authorization page in your browser.') + expect(output.info()).toContain('Keep this command running until authentication completes in the browser.') + expect(output.info()).toContain('Browser did not open automatically. Open this URL manually:') + expect(output.info()).toContain('https://shop.myshopify.com/admin/oauth/authorize?client_id=test') + expect(output.output()).toContain('"store": "shop.myshopify.com"') + expect(output.output()).not.toContain('Authenticated') }) }) diff --git a/packages/store/src/cli/services/store/auth/result.ts b/packages/store/src/cli/services/store/auth/result.ts index 58098a7c4f4..50831e3e093 100644 --- a/packages/store/src/cli/services/store/auth/result.ts +++ b/packages/store/src/cli/services/store/auth/result.ts @@ -44,12 +44,14 @@ function buildStoreAuthSuccessText(result: StoreAuthResult): {completed: string[ function displayStoreAuthOpeningBrowser(): void { outputInfo('Shopify CLI will open the app authorization page in your browser.') + outputInfo('Keep this command running until authentication completes in the browser.') outputInfo('') } function displayStoreAuthManualAuthUrl(authorizationUrl: string): void { outputInfo('Browser did not open automatically. Open this URL manually:') outputInfo(outputContent`${outputToken.link(authorizationUrl)}`) + outputInfo('Keep this command running until authentication completes in the browser.') outputInfo('') } diff --git a/packages/store/src/cli/services/store/auth/session-store.test.ts b/packages/store/src/cli/services/store/auth/session-store.test.ts index 523060f5975..a7e29f46021 100644 --- a/packages/store/src/cli/services/store/auth/session-store.test.ts +++ b/packages/store/src/cli/services/store/auth/session-store.test.ts @@ -166,4 +166,5 @@ describe('store session storage', () => { expect(() => clearStoredStoreAppSession('shop.myshopify.com', '42', storage as any)).not.toThrow() expect(storage.get(storeAuthSessionKey('shop.myshopify.com'))).toBeUndefined() }) + })