Skip to content

Explain a token's missing dev store access instead of dumping the 401 - #8999

Merged
isaacroldan merged 3 commits into
mainfrom
river/service-account-store-access-errors
Oct 23, 2026
Merged

isaacroldan merged 3 commits into
mainfrom
river/service-account-store-access-errors

Conversation

@egaodd

@egaodd egaodd commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

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 dev or store delete crashed 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. retryAwareRequest hands 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-request ClientError 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 (every caller that inspects ClientError itself 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 withOrganizationStoreAccess helper in @shopify/organizations turns that refusal into an OrganizationStoreAccessError naming the credential and where to fix it:

  • service account token: "This service account can't manage dev stores in . Give it Dev store management access in the Shopify admin under Settings > Users > Security, or use a token from a service account that has it."
  • legacy automation token: points at a service account token in SHOPIFY_SERVICE_ACCOUNT_TOKEN.
  • logged-in user: "You don't have permission to manage dev stores in ."

store list, store create dev and store delete use it.

How to test your changes?

Verified against production with a service account that has App development access only (org 299069):

  • shopify store list and shopify store create dev --name x --plan basic --organization-id 299069 now print the "This service account can't manage dev stores in A Better Looking Shop" banner, exit 1, no stack trace. --json returns the error document.
  • A dev-store token still lists, creates and deletes stores.

Unit tests: cli-kit graphql.test.ts (final 401 with no handler / refresh fails / refresh then 401 again; handleErrors: false keeps the raw error), new business-platform.test.ts in @shopify/organizations, plus a refusal case in create-dev-store, bp-source and delete/dev tests. tsc and eslint pass on cli-kit, organizations and store.

Measuring impact

  • n/a (bug fix)

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • Changeset added (@shopify/cli-kit, @shopify/cli: patch)

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>
@egaodd
egaodd requested a review from a team as a code owner October 23, 2026 13:56
@egaodd egaodd self-assigned this Oct 23, 2026
@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label Oct 23, 2026
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 isaacroldan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}.`, [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

* @param request - The request to run.
* @returns The request's result.
*/
export async function withOrganizationStoreAccess<T>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this scale? are we going add this to every request eventually? which ones?

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
egaodd enabled auto-merge October 23, 2026 15:05
@isaacroldan
isaacroldan disabled auto-merge October 23, 2026 15:36
@isaacroldan
isaacroldan merged commit fd6bc50 into main Oct 23, 2026
52 of 53 checks passed
@isaacroldan
isaacroldan deleted the river/service-account-store-access-errors branch October 23, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants