From 7e206fe4d9c043f77ac439c0a12c3c833f73fe0b Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Sun, 28 Jun 2026 19:50:04 +0300 Subject: [PATCH] Cache Admin API scopes returned by preview store creation The preview store creation endpoint now returns admin_api_scopes (e.g. read_themes, write_themes) alongside the admin API token. Parse this list and persist it in the local store-auth session cache instead of an empty array, mirroring how store auth stores granted scopes. Backends that predate the field omit it, so we default to an empty list. Assisted-By: devx/f38d0794-3b14-4a2a-a849-b95ae665f83d --- .../store/create/preview/client.test.ts | 32 +++++++++++++++++++ .../services/store/create/preview/client.ts | 10 +++++- .../store/create/preview/index.test.ts | 8 +++-- .../services/store/create/preview/index.ts | 2 +- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/store/src/cli/services/store/create/preview/client.test.ts b/packages/store/src/cli/services/store/create/preview/client.test.ts index 8eb413d6e8f..c8506ed11fe 100644 --- a/packages/store/src/cli/services/store/create/preview/client.test.ts +++ b/packages/store/src/cli/services/store/create/preview/client.test.ts @@ -80,6 +80,7 @@ describe('preview store client', () => { shop: {id: 123, name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'}, placeholder_account_uuid: 'placeholder-uuid', admin_api_token: 'shpat_token', + admin_api_scopes: ['read_themes', 'write_themes'], access_url: 'https://app.shopify.com/auth/preview-store?token=access-token', }), ) @@ -106,15 +107,46 @@ describe('preview store client', () => { shop: {id: '123', name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'}, placeholderAccountUuid: 'placeholder-uuid', adminApiToken: 'shpat_token', + adminApiScopes: ['read_themes', 'write_themes'], accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token', }) }) + test('rejects the response when the backend omits admin API scopes', async () => { + vi.mocked(shopifyFetch).mockResolvedValueOnce( + response(201, { + shop: {id: 123, name: 'My Store', domain: 'x.myshopify.com'}, + admin_api_token: 'shpat_token', + access_url: 'https://app.shopify.com/auth/preview-store?token=access-token', + }), + ) + + await expect(createPreviewStore({}, {storage: inMemoryStorage('instance-1')})).rejects.toThrow( + 'Preview store creation response is missing required fields.', + ) + }) + + test('drops non-string admin API scopes', async () => { + vi.mocked(shopifyFetch).mockResolvedValueOnce( + response(201, { + shop: {id: 123, name: 'My Store', domain: 'x.myshopify.com'}, + admin_api_token: 'shpat_token', + admin_api_scopes: ['read_themes', 42, null, 'write_themes'], + access_url: 'https://app.shopify.com/auth/preview-store?token=access-token', + }), + ) + + const got = await createPreviewStore({}, {storage: inMemoryStorage('instance-1')}) + + expect(got.adminApiScopes).toEqual(['read_themes', 'write_themes']) + }) + test('omits name and country variables when absent', async () => { vi.mocked(shopifyFetch).mockResolvedValueOnce( response(201, { shop: {id: 123, name: 'My Store', domain: 'x.myshopify.com'}, admin_api_token: 'shpat_token', + admin_api_scopes: ['read_themes', 'write_themes'], access_url: 'https://app.shopify.com/auth/preview-store?token=access-token', }), ) diff --git a/packages/store/src/cli/services/store/create/preview/client.ts b/packages/store/src/cli/services/store/create/preview/client.ts index ec62c57af08..8a92e4f461e 100644 --- a/packages/store/src/cli/services/store/create/preview/client.ts +++ b/packages/store/src/cli/services/store/create/preview/client.ts @@ -40,6 +40,7 @@ export interface PreviewStoreCreateResponse { shop: PreviewStoreResponseShop placeholderAccountUuid?: string adminApiToken: string + adminApiScopes: string[] accessUrl: string } @@ -53,6 +54,7 @@ interface RawPreviewStoreCreateResponse { shop?: RawPreviewStoreResponseShop placeholder_account_uuid?: unknown admin_api_token?: unknown + admin_api_scopes?: unknown access_url?: unknown } @@ -320,8 +322,13 @@ function narrowCreateResponse(parsed: RawPreviewStoreCreateResponse): PreviewSto const accessUrl = typeof parsed.access_url === 'string' ? parsed.access_url : undefined const placeholderAccountUuid = typeof parsed.placeholder_account_uuid === 'string' ? parsed.placeholder_account_uuid : undefined + // The backend always returns `admin_api_scopes` for the granted Admin API token, so it is a + // required field. We still filter out any non-string entries defensively. + const adminApiScopes = Array.isArray(parsed.admin_api_scopes) + ? parsed.admin_api_scopes.filter((scope): scope is string => typeof scope === 'string') + : undefined - if (!id || !name || !domain || !adminApiToken || !accessUrl) { + if (!id || !name || !domain || !adminApiToken || !accessUrl || !adminApiScopes) { throw new AbortError( 'Preview store creation response is missing required fields.', `Got: ${JSON.stringify(redactPreviewStoreResponse(parsed)).slice(0, 500)}`, @@ -331,6 +338,7 @@ function narrowCreateResponse(parsed: RawPreviewStoreCreateResponse): PreviewSto return { shop: {id, name, domain}, adminApiToken, + adminApiScopes, accessUrl, ...(placeholderAccountUuid ? {placeholderAccountUuid} : {}), } diff --git a/packages/store/src/cli/services/store/create/preview/index.test.ts b/packages/store/src/cli/services/store/create/preview/index.test.ts index 63f82107e28..b5e6845c7f9 100644 --- a/packages/store/src/cli/services/store/create/preview/index.test.ts +++ b/packages/store/src/cli/services/store/create/preview/index.test.ts @@ -15,6 +15,7 @@ describe('preview store create service', () => { shop: {id: '123', name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'}, placeholderAccountUuid: 'placeholder-uuid', adminApiToken: 'shpat_token', + adminApiScopes: ['read_themes', 'write_themes'], accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token', })), setStoredStoreAppSession, @@ -29,7 +30,7 @@ describe('preview store create service', () => { clientId: STORE_AUTH_APP_CLIENT_ID, userId: `${PREVIEW_USER_ID_PREFIX}placeholder-uuid`, accessToken: 'shpat_token', - scopes: [], + scopes: ['read_themes', 'write_themes'], acquiredAt: '2026-06-08T12:00:00.000Z', kind: 'preview', preview: { @@ -68,6 +69,7 @@ describe('preview store create service', () => { createPreviewStore: vi.fn(async () => ({ shop: {id: '123', name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'}, adminApiToken: 'shpat_token', + adminApiScopes: [], accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token', })), setStoredStoreAppSession, @@ -78,7 +80,7 @@ describe('preview store create service', () => { ) expect(setStoredStoreAppSession).toHaveBeenCalledWith( - expect.objectContaining({userId: `${PREVIEW_USER_ID_PREFIX}123`}), + expect.objectContaining({userId: `${PREVIEW_USER_ID_PREFIX}123`, scopes: []}), ) expect(setLastSeenUserId).toHaveBeenCalledWith(`${PREVIEW_USER_ID_PREFIX}123`) }) @@ -88,6 +90,7 @@ describe('preview store create service', () => { const createPreviewStore = vi.fn(async () => ({ shop: {id: '123', name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'}, adminApiToken: 'shpat_token', + adminApiScopes: [], accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token', })) @@ -118,6 +121,7 @@ describe('preview store create service', () => { createPreviewStore: vi.fn(async () => ({ shop: {id: '123', name: 'Lavender Candles', domain: 'x12y45z.myshopify.com'}, adminApiToken: 'shpat_token', + adminApiScopes: [], accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token', })), setStoredStoreAppSession, diff --git a/packages/store/src/cli/services/store/create/preview/index.ts b/packages/store/src/cli/services/store/create/preview/index.ts index 8fe63078993..b311fd6d9a4 100644 --- a/packages/store/src/cli/services/store/create/preview/index.ts +++ b/packages/store/src/cli/services/store/create/preview/index.ts @@ -73,7 +73,7 @@ async function persistPreviewStoreSession( clientId: STORE_AUTH_APP_CLIENT_ID, userId, accessToken: response.adminApiToken, - scopes: [], + scopes: response.adminApiScopes, acquiredAt, kind: 'preview', preview: {