Skip to content

Recover on store info after a preview store has been claimed#7986

Merged
amcaplan merged 14 commits into
mainfrom
store-info-recovery-preview-stores
Jul 3, 2026
Merged

Recover on store info after a preview store has been claimed#7986
amcaplan merged 14 commits into
mainfrom
store-info-recovery-preview-stores

Conversation

@amcaplan

@amcaplan amcaplan commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

From this Slack thread: running shopify store info (and, it turns out, shopify store execute) against a preview store after it has been claimed through the browser claim flow throws an unhelpful, non-actionable error instead of pointing the user at shopify store auth.

Root cause

The CLI has no local signal for when a preview store gets claimed — the stored session keeps reporting kind: 'preview' forever. store info unconditionally takes the preview-session branch for such stores and calls the preview-stores REST service with the (now stale) preview admin token. Once claimed, that service starts rejecting the request, but getPreviewStore's error handling only produced a generic message string with no HTTP status exposed and no recovery guidance.

Verified end-to-end against real stores: created preview stores, claimed them through the actual browser flow, and confirmed the preview-stores service really does return 401 {"error_code":"unauthorized"} afterward for the cached token — and that the real Admin API (used by store execute and store info's Admin fallback) does the same for the same stale token.

Fix

Detecting the claim (store info)

  • getPreviewStore now throws a typed PreviewStoreRequestError extending AbortError (so an uncaught instance still surfaces as a clean user-facing abort, not an unexpected-bug report) and carrying the HTTP status, so callers can classify the failure instead of only seeing a pre-rendered message string.
  • getStoreInfo's preview-session path treats a 401/404 from the preview-stores service as a signal the store has likely been claimed.

Three distinct, consistent recovery messages (recovery.ts), used everywhere a stored session is found invalid — store info's preview branch, store info's Admin fallback, and store execute (both the query/mutation path and the API-version-discovery path):

  1. No stored auth at all"No stored app authentication found for {store}." — Next steps: Run ... to authenticate.
  2. Stored auth exists but is invalid (standard session) → "Stored app authentication for {store} is no longer valid." — Next steps show the real, previously-granted scopes and say to re-authenticate.
  3. Stored auth exists but is invalid, for a preview store"The preview store {store} has likely been claimed, so its stored authentication is no longer valid." — Next steps show a <comma-separated-scopes> placeholder instead of the real scopes, and say to re-authenticate.

Case 3's placeholder matters because preview stores are preapproved for a large, fixed scope catalog (~35 scopes) that nobody deliberately chose — suggesting the user re-request all of them against what's now a live, claimed store encourages over-scoping. Case 2 still shows the real scopes since those were a deliberate choice (store auth --scopes ...) and echoing them back is a genuine convenience.

Originally case 3 was only reachable via store info's own preview-branch check; store execute and store info's Admin fallback shared the same underlying invalid-session detection (throwIfStoredStoreAuthIsInvalid) but always fell through to case 2's generic message and full scope list, even for a preview session. Consolidated the message/scope selection into throwStoredAuthInvalidError(session) / reauthScopesFor(session) in recovery.ts so every call site is automatically consistent.

Deliberately does not clear a lingering preview session on 401/404 (in throwIfStoredStoreAuthIsInvalid and store info's own check). I initially had this clearing the session (mirroring how standard-session invalidation is handled), but testing against a real claimed store — and later against a real store hit by store execute — showed that's the wrong call for preview sessions:

  • store auth overwrites the session bucket's currentUserId when it stores a fresh session, so nothing depends on the old preview entry being removed first.
  • Preview sessions have no refresh-token cycle, so the "avoid repeatedly retrying with a dead refresh token" justification for clearing standard sessions doesn't apply.
  • Clearing it caused a real regression twice: a second store info run, or a store execute run followed by store info, would silently fall through to a full interactive Business Platform login instead of repeating the actionable store auth guidance. Leaving the (already server-invalidated) session in place keeps every retry consistent. Standard sessions are still cleared as before — this only changes preview-session behavior.

Cleanup

  • Removed a redundant "Next steps" rendering bug in the shared recovery errors (a separate tryMessage like "To re-authenticate, run:" duplicated with the "Next steps" heading); folded into a single bullet: Run \shopify store auth ...` to re-authenticate`.
  • Dropped now-dead parameters/exports surfaced along the way (storeAuthCommandNextSteps's unused scopes param, an unused UNKNOWN_SCOPES_PLACEHOLDER export flagged by knip).

Testing

  • Added PreviewStoreRequestError coverage in client.test.ts (401 and 404, including that it's an AbortError).
  • Added a dedicated recovery.test.ts directly covering recovery.ts's branching logic (scope placeholder selection, message selection, "to authenticate" vs "to re-authenticate"), which had previously only been exercised indirectly through other modules' tests.
  • Added getStoreInfo coverage for the claimed-preview-store scenario, both via the dedicated preview-stores lookup and via a lingering preview session hitting the Admin fallback.
  • Added store execute coverage (admin-transport.test.ts, both runAdminStoreGraphQLOperation and fetchPublicApiVersions) for the same lingering-preview-session scenario, with paired tests showing the contrast against a standard invalid session (real scopes shown) right next to the preview case (placeholder shown).
  • Added coverage for the "no stored auth" case saying "to authenticate" rather than "to re-authenticate".
  • Manually verified live against several real preview stores (created via shopify store create preview, claimed via the real browser claim flow and via a corrupted-token shortcut for faster iteration):
    • store info and store execute now produce identical, consistent messaging for the same claimed store.
    • Running either command repeatedly (or one after the other) no longer degrades into a full interactive login prompt.
    • An unseen store domain correctly says "to authenticate"; a claimed preview store correctly says "to re-authenticate" and omits the full scope list.
  • Full packages/store unit test suite (358 tests), type-check, lint, and knip all pass clean.

Manual QA / tophatting

  1. No stored auth — run against a store domain you've never used with this CLI install:

    shopify store execute --store <unseen-store>.myshopify.com --query "query { shop { name } }"
    

    Expect: No stored app authentication found for ... and Next steps saying to authenticate (not "re-authenticate").

  2. Invalid standard auth — run shopify store auth --store <store> --scopes read_products,write_orders against a real store, then corrupt the stored accessToken in ~/Library/Preferences/shopify-cli-store-nodejs/config.json (or just wait for real expiry), then run store execute/store info again.
    Expect: Stored app authentication for ... is no longer valid., Next steps show the real scopes (--scopes read_products,write_orders), session is cleared afterward.

  3. Claimed preview store:

    shopify store create preview
    shopify store info --store <domain> --json   # copy saveUrl, open it, complete the claim in browser
    shopify store info --store <domain>
    shopify store execute --store <domain> --query "query { shop { name } }"
    

    Expect both commands: The preview store ... has likely been claimed, so its stored authentication is no longer valid., Next steps show --scopes <comma-separated-scopes> (not the full preview grant list). Run either command again — the message should stay identical, not fall back to a browser login prompt. shopify store auth list should still show the store until you actually re-authenticate.

Changeset

Added — this is a user-facing bug fix (store info and store execute error behavior).

The CLI has no local signal for when a preview store gets claimed
through the browser claim flow — the stored session keeps reporting
`kind: 'preview'` forever. `store info` would then keep calling the
preview-stores service with the stale preview token and surface a
generic, non-actionable `Preview store lookup failed with HTTP 401`
error instead of guiding the user to re-authenticate.

- getPreviewStore now throws a typed PreviewStoreRequestError that
  carries the HTTP status, so callers can classify the failure
  instead of only seeing a pre-rendered message string.
- getStoreInfo's preview-session path now treats a 401/404 from that
  service the same way the Admin API paths already treat a stale
  stored session: clear it and direct the user to `shopify store
  auth`, matching the pattern store execute already uses.
- Cleaned up the redundant 'Next steps' rendering on the shared
  store-auth recovery errors (both the 'no stored auth' and
  're-authenticate' cases), folding the instruction into the bullet
  itself instead of repeating it in a separate tryMessage line.

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
Copilot AI review requested due to automatic review settings July 2, 2026 18:54
@amcaplan amcaplan requested review from a team as code owners July 2, 2026 18:54
@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label Jul 2, 2026

Copilot AI 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.

Pull request overview

This PR improves shopify store info resilience for preview stores that have been claimed after creation by detecting stale preview-store tokens and switching to the existing “clear stored auth + prompt store auth” recovery flow.

Changes:

  • Introduces a typed PreviewStoreRequestError from the preview-stores client to expose HTTP status to callers.
  • Updates the store info preview-session path to treat preview-stores 401/404 as a stale session, clearing cached auth and prompting re-authentication.
  • Simplifies store auth recovery error rendering by folding “Run … to …” into the nextSteps bullet structure and updating affected tests.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/store/src/cli/services/store/info/index.ts Handles claimed-preview-store failures (401/404) by clearing stored session and throwing the standard re-auth recovery error.
packages/store/src/cli/services/store/info/index.test.ts Adds coverage for claimed preview stores and updates assertions for new nextSteps formatting.
packages/store/src/cli/services/store/execute/admin-transport.test.ts Updates assertions to match the new nextSteps rendering shape.
packages/store/src/cli/services/store/create/preview/client.ts Adds PreviewStoreRequestError and switches getPreviewStore to throw it for non-2xx responses.
packages/store/src/cli/services/store/create/preview/client.test.ts Adds tests ensuring PreviewStoreRequestError carries status for 401/404.
packages/store/src/cli/services/store/auth/session-lifecycle.test.ts Updates assertions to match the new nextSteps rendering shape.
packages/store/src/cli/services/store/auth/recovery.ts Refactors store-auth recovery helpers to avoid redundant “tryMessage” + “Next steps” output.
.changeset/store-info-preview-claim-reauth.md Adds a patch changeset for the user-facing store info error recovery improvement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/store/src/cli/services/store/create/preview/client.ts Outdated
@amcaplan amcaplan marked this pull request as draft July 2, 2026 19:42
amcaplan added 4 commits July 2, 2026 22:45
Verified against a real claimed store: clearing wasn't necessary
(store auth overwrites the bucket's currentUserId regardless of what
else is stored), and it caused a worse regression on retry — a
second `store info` run silently fell through to a full interactive
Business Platform login instead of repeating the actionable 'run
store auth' guidance. Preview sessions also have no refresh-token
cycle, so the wasted-retry justification for clearing standard
sessions doesn't apply. Leaving the (already server-invalidated)
session in place keeps every retry consistent until the user
actually re-authenticates.

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
Drop the unnecessary comment and the generic 'purpose' parameter;
both throwStoredStoreAuthError and throwReauthenticateStoreAuthError
just say 'to re-authenticate'.

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
Extending the plain Error meant an uncaught PreviewStoreRequestError
(any status other than 401/404) would surface to oclif as an
unexpected bug rather than a clean, user-facing abort — inconsistent
with how every other error in this module is handled. Extending
AbortError fixes that and lets the class drop its own tryMessage
field in favor of the inherited one.

Also drop an unnecessary test comment restating what the test name
already says.

Addresses #7986 (comment)

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
@amcaplan amcaplan marked this pull request as ready for review July 2, 2026 19:56
@amcaplan

amcaplan commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🫰✨ Thanks @amcaplan! Your snapshot has been published to npm.

Test the snapshot by installing your package globally:

pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260702200210

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

Preview stores are preapproved for a large, fixed scope catalog
(often 30+ scopes). Suggesting the user re-request all of them
against what's now a live, claimed store encourages over-scoping.
Use the same '<comma-separated-scopes>' placeholder as the 'no
stored auth' case instead, so they choose scopes deliberately.

Exports the placeholder from recovery.ts so both call sites share
one source of truth.

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
@amcaplan

amcaplan commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🫰✨ Thanks @amcaplan! Your snapshot has been published to npm.

Test the snapshot by installing your package globally:

pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260702200721

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

amcaplan added 7 commits July 2, 2026 23:22
throwIfStoredStoreAuthIsInvalid and runAdminStoreGraphQLOperation's
inline 401 handler both suggested re-running `store auth` with the
session's full scope list on re-auth. For a preview-store session
(kind: 'preview') that list is the entire preapproved catalog
(30+ scopes) rather than a deliberate choice, so store execute would
dump the same unwieldy list store info did before the previous fix.

- Added reauthScopesFor() in admin-errors.ts: preview-kind sessions
  get the <comma-separated-scopes> placeholder, standard sessions
  keep their real (user-chosen) scopes.
- runAdminStoreGraphQLOperation now delegates to
  throwIfStoredStoreAuthIsInvalid instead of duplicating the
  clear+reauth logic inline, which also fixes it not previously
  treating 404 as a stored-auth-invalid signal (fetchPublicApiVersions
  already did, via the shared helper).

Verified against a real preview store with an invalidated token:
store execute now shows the placeholder instead of the full scope
list.

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
Avoids duplicating the placeholder-vs-real-scopes logic (and its
explanatory comment) now that admin-errors.ts exports a shared
helper for it.

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
throwReauthenticateStoreAuthError now takes the session directly and
decides internally whether to show its real scopes or the
placeholder, instead of every call site computing that via a
separately exported reauthScopesFor() and passing the result in.
Removes a small helper and its duplicate import from admin-errors.ts
and info/index.ts.

While consolidating, found and fixed a related gap: throwIfStoredStoreAuthIsInvalid
(used by store execute and store info's Admin fallback) still
unconditionally cleared any session on 401/404, including a lingering
preview session. That reintroduced the exact 'second command falls
through to a full interactive login' problem the earlier store info
fix addressed, just via a different call path: running store execute
against a claimed-but-uncleared preview store would clear it, and a
follow-up store info would then have nothing to detect and prompt a
full BP login instead of the actionable store auth message.

Verified live: patched a real preview store's stored token to be
invalid, ran store execute (session persists, correct message), then
store info immediately after (same consistent message, no login
prompt).

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
It only had one caller left, always passing UNKNOWN_SCOPES_PLACEHOLDER.
No behavior change.

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
There are 3 cases: (1) no stored auth at all -> prompt auth, (2)
stored auth invalid -> prompt reauth, (3) stored auth invalid for a
preview store -> flag the likely claim and prompt reauth. Case 3 was
previously only distinguished in store info's own preview-branch
check; store execute and store info's Admin fallback both hit the
same underlying situation via throwIfStoredStoreAuthIsInvalid but
fell through to the generic case 2 message ('... is no longer
valid.') even for a preview session.

Added throwStoredAuthInvalidError(session) in recovery.ts to own
the case 2 vs 3 message selection (mirroring the existing scope
placeholder selection), and pointed every call site that detects an
invalid stored session at it instead of hand-writing the message.

Verified live: store execute against a preview store with an
invalidated token now says 'likely been claimed', matching store
info; a never-seen store still gets the distinct 'No stored app
authentication found' message.

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
… auth

throwStoredStoreAuthError's next-steps bullet said 'to re-authenticate'
even though case 1 (no stored auth at all) has nothing to re-do.
Only cases 2/3 (stored auth exists but is invalid/likely claimed)
should say 're-authenticate'.

Verified live: an unseen store now says '... to authenticate'.

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
It's now only referenced within recovery.ts itself.

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
Comment thread packages/store/src/cli/services/store/auth/recovery.ts

@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.

Left a comment about tests, code looks good

recovery.ts had accumulated real branching logic (reauthScopesFor's
preview-vs-standard scope selection, throwStoredAuthInvalidError's
message selection, the 'to authenticate' vs 'to re-authenticate'
split) that was only exercised indirectly through other modules'
tests. Add a recovery.test.ts covering each exported function
directly, including both the standard-session and preview-session
branches.

Assisted-By: devx/952e932f-652c-498f-8052-b2f61ce56cf7
@amcaplan amcaplan added this pull request to the merge queue Jul 3, 2026
Merged via the queue into main with commit a2c8f5d Jul 3, 2026
29 checks passed
@amcaplan amcaplan deleted the store-info-recovery-preview-stores branch July 3, 2026 12:30
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.

4 participants