From ae0a747e941101472315b633c48ac38615af23eb Mon Sep 17 00:00:00 2001 From: Ryan DJ Lee Date: Mon, 13 Apr 2026 10:11:59 -0400 Subject: [PATCH 1/5] Fix store auth scope parsing for space-separated input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseStoreAuthScopes splits on commas only, but Windows PowerShell can transform comma-separated CLI args into space-separated strings before they reach Node.js. This causes "read_products read_inventory" to be treated as one unrecognized scope, triggering a false "fewer scopes than requested" error. Split on /[\s,]+/ instead — matching Core's own ScopeSet.parse_scopes which already handles both delimiters. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cli/services/store/auth/scopes.test.ts | 24 +++++++++++++++++++ .../cli/src/cli/services/store/auth/scopes.ts | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) 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..fc59d778707 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 requested scopes were 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..1b5b286c156 100644 --- a/packages/cli/src/cli/services/store/auth/scopes.ts +++ b/packages/cli/src/cli/services/store/auth/scopes.ts @@ -4,7 +4,7 @@ import type {StoreTokenResponse} from './token-client.js' export function parseStoreAuthScopes(input: string): string[] { const scopes = input - .split(',') + .split(/[\s,]+/) .map((scope) => scope.trim()) .filter(Boolean) From 6c956c51a5250fd1dc921ee47f4d82a579a4bf0e Mon Sep 17 00:00:00 2001 From: Ryan DJ Lee Date: Mon, 13 Apr 2026 10:20:49 -0400 Subject: [PATCH 2/5] Tighten scope delimiter regex and fix bogus test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Narrow regex from /[\s,]+/ to /[ ,]+/ to match server-side Access::ScopeSet.parse_scopes behavior (space and comma only) - Remove dead .trim() — splitting on /[ ,]+/ can't produce elements with leading/trailing whitespace - Fix test that claimed to cover space-separated parsing but actually passed comma-separated input (exercising no new code) Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/cli/services/store/auth/scopes.test.ts | 4 ++-- packages/cli/src/cli/services/store/auth/scopes.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) 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 fc59d778707..c157d5f00a6 100644 --- a/packages/cli/src/cli/services/store/auth/scopes.test.ts +++ b/packages/cli/src/cli/services/store/auth/scopes.test.ts @@ -29,12 +29,12 @@ describe('store auth scope helpers', () => { ]) }) - test('resolveGrantedScopes succeeds when requested scopes were space-separated', () => { + test('resolveGrantedScopes succeeds when granted scopes are space-separated', () => { expect( resolveGrantedScopes( { access_token: 'token', - scope: 'read_products,read_inventory', + scope: 'read_products read_inventory', }, ['read_products', 'read_inventory'], ), diff --git a/packages/cli/src/cli/services/store/auth/scopes.ts b/packages/cli/src/cli/services/store/auth/scopes.ts index 1b5b286c156..37443a0991a 100644 --- a/packages/cli/src/cli/services/store/auth/scopes.ts +++ b/packages/cli/src/cli/services/store/auth/scopes.ts @@ -4,8 +4,7 @@ import type {StoreTokenResponse} from './token-client.js' export function parseStoreAuthScopes(input: string): string[] { const scopes = input - .split(/[\s,]+/) - .map((scope) => scope.trim()) + .split(/[ ,]+/) .filter(Boolean) if (scopes.length === 0) { From 2d9ab607bf92ca10d1ce11e908dd9ea5a95955f9 Mon Sep 17 00:00:00 2001 From: Ryan DJ Lee Date: Mon, 13 Apr 2026 10:23:50 -0400 Subject: [PATCH 3/5] Fix prettier formatting for scope split chain Collapse split/filter chain to single line to satisfy prettier. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/cli/services/store/auth/scopes.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/cli/src/cli/services/store/auth/scopes.ts b/packages/cli/src/cli/services/store/auth/scopes.ts index 37443a0991a..ea3225934a7 100644 --- a/packages/cli/src/cli/services/store/auth/scopes.ts +++ b/packages/cli/src/cli/services/store/auth/scopes.ts @@ -3,9 +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(/[ ,]+/) - .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.') From 77b774afd17a700a6ca6546ca69fce0403d39242 Mon Sep 17 00:00:00 2001 From: Ryan DJ Lee Date: Mon, 13 Apr 2026 10:57:41 -0400 Subject: [PATCH 4/5] Add service-level regression test for space-separated scopes Exercises the full authenticateStoreWithApp flow with space-separated input ('read_products read_inventory') and comma-separated server response, asserting the scopes resolve correctly and persist to the session store. Addresses review feedback from dmerand. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/cli/services/store/auth/index.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) 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?.() From ad4b9fad84af5a51ff6b36cb92f669e62fa66d85 Mon Sep 17 00:00:00 2001 From: Ryan DJ Lee Date: Mon, 13 Apr 2026 13:49:32 -0400 Subject: [PATCH 5/5] Regenerate business-platform-organizations types for upstream schema update Upstream schema updated JSDoc comments for Operator, ShopFilterField, and ShopFilterInput types (added IN operator documentation). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../generated/types.d.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) 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;