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 34142a98ebf..7620d79df41 100644 --- a/packages/store/src/cli/services/store/info/index.test.ts +++ b/packages/store/src/cli/services/store/info/index.test.ts @@ -188,11 +188,42 @@ describe('getStoreInfo', () => { subdomain: SHOP, accessUrl: 'https://app.shopify.com/auth/preview-store?token=fresh-access-token', saveUrl: 'https://admin.shopify.com/store-transfer/accept/claim-token', + authScopes: [], }) // The admin URL doesn't resolve for an unclaimed preview store, so it's deliberately omitted. expect(result.adminUrl).toBeUndefined() }) + test('surfaces cached Admin API scopes for locally stored preview stores', async () => { + vi.mocked(getCurrentStoredStoreAppSession).mockReturnValueOnce({ + store: SHOP, + clientId: STORE_AUTH_APP_CLIENT_ID, + userId: 'preview:placeholder-uuid', + accessToken: 'shpat_preview_token', + scopes: ['read_themes', 'write_themes'], + acquiredAt: '2026-06-08T12:00:00.000Z', + kind: 'preview', + preview: { + placeholderAccountUuid: 'placeholder-uuid', + shopId: '123', + name: 'Lavender Candles', + createdAt: '2026-06-08T12:00:00.000Z', + accessUrl: 'https://app.shopify.com/auth/preview-store?token=stale-access-token', + }, + }) + vi.mocked(claimPreviewStore).mockResolvedValueOnce({ + claimUrl: 'https://admin.shopify.com/store-transfer/accept/claim-token', + }) + vi.mocked(getPreviewStore).mockResolvedValueOnce({ + shop: {id: '123', name: 'Lavender Candles', domain: SHOP}, + accessUrl: 'https://app.shopify.com/auth/preview-store?token=fresh-access-token', + }) + + const result = await getStoreInfo({store: SHOP}) + + expect(result.authScopes).toEqual(['read_themes', 'write_themes']) + }) + test('prefers BP when store auth exists and BP can resolve the store', async () => { mockStoredStoreAuth() diff --git a/packages/store/src/cli/services/store/info/index.ts b/packages/store/src/cli/services/store/info/index.ts index c0f78e2bc8f..f9f6c14caf7 100644 --- a/packages/store/src/cli/services/store/info/index.ts +++ b/packages/store/src/cli/services/store/info/index.ts @@ -251,7 +251,9 @@ function buildPreviewStoreResult(args: { saveUrl: previewStoreUrls.saveUrl, } - return {...compact(fields), subdomain: store} as StoreInfoResult + // `authScopes` is always present for preview stores (even when empty) so consumers can rely on the + // key to learn which Admin API scopes are preapproved. There's no way to grant more scopes later. + return {...compact(fields), subdomain: store, authScopes: previewSession.scopes} as StoreInfoResult } // The BP `ShopifyShopID` scalar is the bare numeric id; the admin GID is derived locally. diff --git a/packages/store/src/cli/services/store/info/result.test.ts b/packages/store/src/cli/services/store/info/result.test.ts index d5679c50a30..4ec155baa38 100644 --- a/packages/store/src/cli/services/store/info/result.test.ts +++ b/packages/store/src/cli/services/store/info/result.test.ts @@ -73,6 +73,13 @@ describe('renderStoreInfoResult', () => { expect(renderInfo).not.toHaveBeenCalled() }) + test('includes authScopes in the JSON output when present', () => { + renderStoreInfoResult(baseResult({authScopes: ['read_themes', 'write_themes']}), 'json') + + const payload = vi.mocked(outputResult).mock.calls[0]?.[0] as string + expect(JSON.parse(payload).authScopes).toEqual(['read_themes', 'write_themes']) + }) + test('renders a Store details section in text format', () => { renderStoreInfoResult(baseResult(), 'text') diff --git a/packages/store/src/cli/services/store/info/types.ts b/packages/store/src/cli/services/store/info/types.ts index 4ce92149a46..cb142ad1c8a 100644 --- a/packages/store/src/cli/services/store/info/types.ts +++ b/packages/store/src/cli/services/store/info/types.ts @@ -28,6 +28,10 @@ export interface StoreInfoResult { adminUrl?: string accessUrl?: string saveUrl?: string + // Preapproved Admin API access scopes for the store (currently only preview stores, which + // cache the scopes granted at creation time). Preview stores aren't a logged-in experience, so + // there's no way to grant additional scopes later. + authScopes?: string[] } /**