Repository navigation
Explain a token's missing dev store access instead of dumping the 401 - #8999
Merged
Merged
Conversation
An app-only service account token running `store list`, `store create dev` or `store delete` crashed the CLI with the raw graphql-request error for Business Platform's 401: the full query text and variables, plus a stack trace, presented as a CLI bug. Two things were wrong. cli-kit's GraphQL client hands a 401 back raw so the caller's token refresh can run, but when the refreshed request was rejected too (or there was nothing to refresh) that raw error escaped, skipping the error handler every other failed response goes through. The client now maps a final 401 through that handler, unless the caller asked for raw errors with `handleErrors: false`, and the handler no longer tries to quote a GraphQL body a gateway's 401 doesn't have. On top of that, the store commands now say what the 401 means. The Organizations API refuses a token that holds no organization scope, which for a service account means it has no dev store access. A new `withOrganizationStoreAccess` helper in @Shopify/organizations turns that refusal into an error naming the credential and where to fix it: the service account's Dev store management access under Settings > Users > Security, a service account token in place of a legacy automation token, or the user's own permission. `store list`, `store create dev` and `store delete` use it. Co-authored-by: Eddie Gao <eddie.gao@shopify.com>
Knip failed on the two names the store package re-exported from @Shopify/organizations but never imports: isOrganizationAccessError and OrganizationStoreAccessError. The store commands only use withOrganizationStoreAccess. cli-kit now reports a 401 that survives the token refresh as an error with a statusCode instead of a raw graphql-request ClientError. app dev's upload step caught that shape with a bare `Error('Unauthorized')`, which would have replaced its "Auth session expired. Please run `shopify app dev` again." message. It now raises that same AbortError, and a test covers it. Co-authored-by: Eddie Gao <eddie.gao@shopify.com>
isaacroldan
reviewed
Oct 23, 2026
isaacroldan
left a comment
Contributor
There was a problem hiding this comment.
One issue with recovery guidance in JSON output.
🤖 Generated by the review-pr skill
| const where = ['in the Shopify admin under', {subdued: 'Settings > Users > Security'}] | ||
|
|
||
| if (isServiceAccountTokenSet()) { | ||
| super(`This service account can't manage dev stores in ${organization.businessName}.`, [ |
Contributor
There was a problem hiding this comment.
Can we put this guidance in nextSteps, or include tryMessage in the JSON output? store create dev --json and store delete --json only serialize message and nextSteps, so both return nextSteps: [] and lose the instructions about which permission to grant and where.
Let's add command tests for both JSON paths too.
🤖 Generated by the review-pr skill
isaacroldan
reviewed
Oct 23, 2026
| * @param request - The request to run. | ||
| * @returns The request's result. | ||
| */ | ||
| export async function withOrganizationStoreAccess<T>( |
Contributor
There was a problem hiding this comment.
will this scale? are we going add this to every request eventually? which ones?
isaacroldan
approved these changes
Oct 23, 2026
Review follow-up. The access guidance moves from tryMessage to nextSteps. `store create dev --json` and `store delete --json` only serialize message and nextSteps, so the guidance on which access to grant and where was lost from their JSON output. Command tests now cover both JSON paths. withOrganizationStoreAccess, which each call site had to remember to wrap around its request, is replaced by organizationStoresRequestDoc in @Shopify/organizations. It sends a Business Platform Organizations API request for an organization and turns a refused one into OrganizationStoreAccessError. Every Organizations API request in the store and organizations packages now goes through it: store list, store create dev and its creation poll, store delete and its confirmation poll, and the shop lookup behind store info and store delete. A new request gets the same handling by using the helper. Co-authored-by: Eddie Gao <eddie.gao@shopify.com>
egaodd
enabled auto-merge
October 23, 2026 15:05
isaacroldan
disabled auto-merge
October 23, 2026 15:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WHY are these changes introduced?
Found in the service accounts bug bash (P1): an app-only service account token running
shopify store list,store create devorstore deletecrashed the CLI with the raw GraphQL 401 from Business Platform, including the full query text and variables and a stack trace, presented as a CLI bug.WHAT is this pull request doing?
Two layers:
cli-kit's GraphQL client.
retryAwareRequesthands a 401 back raw so the caller's token refresh can run. When the refreshed request was rejected too (or there was nothing to refresh), that raw graphql-requestClientErrorescaped, skipping the error handler every other failed response goes through. The client now maps a final 401 through that handler, unless the caller asked for raw errors withhandleErrors: false(every caller that inspectsClientErroritself already does). The handler also stops trying to quote a GraphQL body a gateway's 401 doesn't have.The store commands say what the 401 means. Business Platform's Organizations API refuses a token that holds no organization scope, which for a service account means it has no dev store access. A new
withOrganizationStoreAccesshelper in@shopify/organizationsturns that refusal into anOrganizationStoreAccessErrornaming the credential and where to fix it:SHOPIFY_SERVICE_ACCOUNT_TOKEN.store list,store create devandstore deleteuse it.How to test your changes?
Verified against production with a service account that has App development access only (org 299069):
shopify store listandshopify store create dev --name x --plan basic --organization-id 299069now print the "This service account can't manage dev stores in A Better Looking Shop" banner, exit 1, no stack trace.--jsonreturns the error document.Unit tests: cli-kit
graphql.test.ts(final 401 with no handler / refresh fails / refresh then 401 again;handleErrors: falsekeeps the raw error), newbusiness-platform.test.tsin@shopify/organizations, plus a refusal case increate-dev-store,bp-sourceanddelete/devtests.tscand eslint pass on cli-kit, organizations and store.Measuring impact
Checklist
@shopify/cli-kit,@shopify/cli: patch)