Recover on store info after a preview store has been claimed#7986
Conversation
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
There was a problem hiding this comment.
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
PreviewStoreRequestErrorfrom the preview-stores client to expose HTTP status to callers. - Updates the
store infopreview-session path to treat preview-stores 401/404 as a stale session, clearing cached auth and prompting re-authentication. - Simplifies
store authrecovery error rendering by folding “Run … to …” into thenextStepsbullet 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.
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
|
/snapit |
|
🫰✨ 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-20260702200210Caution After installing, validate the version by running |
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
|
/snapit |
|
🫰✨ 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-20260702200721Caution After installing, validate the version by running |
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
isaacroldan
left a comment
There was a problem hiding this comment.
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
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 atshopify 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 infounconditionally 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, butgetPreviewStore'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 bystore executeandstore info's Admin fallback) does the same for the same stale token.Fix
Detecting the claim (
store info)getPreviewStorenow throws a typedPreviewStoreRequestErrorextendingAbortError(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, andstore execute(both the query/mutation path and the API-version-discovery path):"No stored app authentication found for {store}."— Next steps:Run ... to authenticate."Stored app authentication for {store} is no longer valid."— Next steps show the real, previously-granted scopes and sayto re-authenticate."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 sayto 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 executeandstore 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 intothrowStoredAuthInvalidError(session)/reauthScopesFor(session)inrecovery.tsso every call site is automatically consistent.Deliberately does not clear a lingering preview session on 401/404 (in
throwIfStoredStoreAuthIsInvalidandstore 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 bystore execute— showed that's the wrong call for preview sessions:store authoverwrites the session bucket'scurrentUserIdwhen it stores a fresh session, so nothing depends on the old preview entry being removed first.store inforun, or astore executerun followed bystore info, would silently fall through to a full interactive Business Platform login instead of repeating the actionablestore authguidance. 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
tryMessagelike "To re-authenticate, run:" duplicated with the "Next steps" heading); folded into a single bullet:Run \shopify store auth ...` to re-authenticate`.storeAuthCommandNextSteps's unusedscopesparam, an unusedUNKNOWN_SCOPES_PLACEHOLDERexport flagged byknip).Testing
PreviewStoreRequestErrorcoverage inclient.test.ts(401 and 404, including that it's anAbortError).recovery.test.tsdirectly coveringrecovery.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.getStoreInfocoverage for the claimed-preview-store scenario, both via the dedicated preview-stores lookup and via a lingering preview session hitting the Admin fallback.store executecoverage (admin-transport.test.ts, bothrunAdminStoreGraphQLOperationandfetchPublicApiVersions) 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).shopify store create preview, claimed via the real browser claim flow and via a corrupted-token shortcut for faster iteration):store infoandstore executenow produce identical, consistent messaging for the same claimed store.packages/storeunit test suite (358 tests), type-check, lint, andknipall pass clean.Manual QA / tophatting
No stored auth — run against a store domain you've never used with this CLI install:
Expect:
No stored app authentication found for ...and Next steps sayingto authenticate(not "re-authenticate").Invalid standard auth — run
shopify store auth --store <store> --scopes read_products,write_ordersagainst a real store, then corrupt the storedaccessTokenin~/Library/Preferences/shopify-cli-store-nodejs/config.json(or just wait for real expiry), then runstore execute/store infoagain.Expect:
Stored app authentication for ... is no longer valid., Next steps show the real scopes (--scopes read_products,write_orders), session is cleared afterward.Claimed preview store:
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 listshould still show the store until you actually re-authenticate.Changeset
Added — this is a user-facing bug fix (
store infoandstore executeerror behavior).