diff --git a/packages/app/src/cli/api/graphql/business-platform-organizations/generated/types.d.ts b/packages/app/src/cli/api/graphql/business-platform-organizations/generated/types.d.ts index 200b94d9837..e6d67fd75a1 100644 --- a/packages/app/src/cli/api/graphql/business-platform-organizations/generated/types.d.ts +++ b/packages/app/src/cli/api/graphql/business-platform-organizations/generated/types.d.ts @@ -80,13 +80,16 @@ export type Scalars = { URL: { input: string; output: string; } }; -/** Operators for user filter queries. */ +/** Operators for filter queries. */ export type Operator = /** Between operator. */ | 'BETWEEN' /** Equals operator. */ | 'EQUALS' - /** In operator. */ + /** + * In operator. Accepts a comma-separated string of values (e.g. + * "value1,value2,value3"). Not supported for all filter fields. + */ | 'IN'; export type OrganizationUserProvisionShopAccessInput = { @@ -113,17 +116,23 @@ export type ShopFilterField = * `inactive`, `cancelled`, `client_transfer`, `plus_client_transfer`, * `development_legacy`, `custom`, `fraudulent`, `staff`, `trial`, * `plus_development`, `retail`, `shop_pay_commerce_components`, `non_profit`. + * With the `In` operator, use raw plan names (e.g. "professional,shopify_plus"). */ | 'SHOP_PLAN' /** The active/inactive status of the shop. Values: `active`, `inactive`. */ | 'STORE_STATUS' /** - * The type of the shop. Values: `development`, `production`, `app_development`, - * `development_superset`, `client_transfer`, `collaborator`. + * The type of the shop. Does not support the `In` operator. Values: + * `development`, `production`, `app_development`, `development_superset`, + * `client_transfer`, `collaborator`. */ | 'STORE_TYPE'; -/** Represents a single filter option for shop queries. */ +/** + * Represents a single filter option for shop queries. When using the `In` + * operator, pass a comma-separated string of values (e.g. "value1,value2"). + * Maximum 20 values. + */ export type ShopFilterInput = { field: ShopFilterField; operator: Operator; diff --git a/packages/cli/src/cli/services/store/auth/index.test.ts b/packages/cli/src/cli/services/store/auth/index.test.ts index 89df34ba6db..e3dc4be55c5 100644 --- a/packages/cli/src/cli/services/store/auth/index.test.ts +++ b/packages/cli/src/cli/services/store/auth/index.test.ts @@ -306,6 +306,42 @@ describe('store auth service', () => { expect(setStoredStoreAppSession).not.toHaveBeenCalled() }) + test('authenticateStoreWithApp succeeds when scopes input is space-separated', async () => { + const waitForStoreAuthCodeMock = vi.fn().mockImplementation(async (options) => { + await options.onListening?.() + return 'abc123' + }) + + const result = await authenticateStoreWithApp( + { + store: 'shop.myshopify.com', + scopes: 'read_products read_inventory', + }, + { + openURL: vi.fn().mockResolvedValue(true), + waitForStoreAuthCode: waitForStoreAuthCodeMock, + exchangeStoreAuthCodeForToken: vi.fn().mockResolvedValue({ + access_token: 'token', + scope: 'read_products,read_inventory', + expires_in: 86400, + associated_user: {id: 42, email: 'test@example.com'}, + }), + presenter: { + openingBrowser: vi.fn(), + manualAuthUrl: vi.fn(), + success: vi.fn(), + }, + }, + ) + + expect(result.scopes).toEqual(['read_products', 'read_inventory']) + expect(setStoredStoreAppSession).toHaveBeenCalledWith( + expect.objectContaining({ + scopes: ['read_products', 'read_inventory'], + }), + ) + }) + test('authenticateStoreWithApp accepts compressed write scopes that imply requested read scopes', async () => { const waitForStoreAuthCodeMock = vi.fn().mockImplementation(async (options) => { await options.onListening?.() diff --git a/packages/cli/src/cli/services/store/auth/scopes.test.ts b/packages/cli/src/cli/services/store/auth/scopes.test.ts index 5dbb0dba04f..c157d5f00a6 100644 --- a/packages/cli/src/cli/services/store/auth/scopes.test.ts +++ b/packages/cli/src/cli/services/store/auth/scopes.test.ts @@ -17,6 +17,30 @@ describe('store auth scope helpers', () => { expect(mergeRequestedAndStoredScopes(['read_products'], ['read_orders'])).toEqual(['read_orders', 'read_products']) }) + test('parseStoreAuthScopes splits space-separated scopes', () => { + expect(parseStoreAuthScopes('read_products read_inventory')).toEqual(['read_products', 'read_inventory']) + }) + + test('parseStoreAuthScopes splits mixed comma-and-space delimiters', () => { + expect(parseStoreAuthScopes('read_products, read_inventory,write_orders')).toEqual([ + 'read_products', + 'read_inventory', + 'write_orders', + ]) + }) + + test('resolveGrantedScopes succeeds when granted scopes are space-separated', () => { + expect( + resolveGrantedScopes( + { + access_token: 'token', + scope: 'read_products read_inventory', + }, + ['read_products', 'read_inventory'], + ), + ).toEqual(['read_products', 'read_inventory']) + }) + test('resolveGrantedScopes accepts compressed write scopes that imply requested reads', () => { expect( resolveGrantedScopes( diff --git a/packages/cli/src/cli/services/store/auth/scopes.ts b/packages/cli/src/cli/services/store/auth/scopes.ts index efe8a2b5913..ea3225934a7 100644 --- a/packages/cli/src/cli/services/store/auth/scopes.ts +++ b/packages/cli/src/cli/services/store/auth/scopes.ts @@ -3,10 +3,7 @@ import {outputContent, outputDebug} from '@shopify/cli-kit/node/output' import type {StoreTokenResponse} from './token-client.js' export function parseStoreAuthScopes(input: string): string[] { - const scopes = input - .split(',') - .map((scope) => scope.trim()) - .filter(Boolean) + const scopes = input.split(/[ ,]+/).filter(Boolean) if (scopes.length === 0) { throw new AbortError('At least one scope is required.', 'Pass --scopes as a comma-separated list.')