Recover on theme commands after a preview store has been claimed - #8190
Draft
amcaplan wants to merge 3 commits into
Draft
Recover on theme commands after a preview store has been claimed#8190amcaplan wants to merge 3 commits into
amcaplan wants to merge 3 commits into
Conversation
`fetchApiVersions` wrapped every failed version-discovery request in a plain `AbortError`, destroying the HTTP status, and `fetchTheme` mapped any GraphQL failure to `undefined` - which callers read as "this theme is gone". A 401 says nothing about whether a theme exists, so `ThemeManager.fetch` would delete the cached development theme id and report "Development theme #X could not be found" for what was really an authentication failure. Add `AdminApiRequestError`, which carries the status alongside the existing message, and narrow `fetchTheme` so an unauthorized request propagates instead of being flattened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The recovery helpers lived in `packages/store`, which `packages/theme` cannot import. Move them into a new public cli-kit module so both packages share one source of truth for the message a rejected stored session produces. Clearing behaviour is inverted on the way: an invalid preview session is now cleared rather than retained. The retained record was what made the recovery unreachable - `throwIfPreviewStore` refuses to start `store auth` for any store that still has a stored preview session, so the `store auth` next step these errors print could never run, and there is no reset flag to escape it. `store info` reaches the same conclusion through its own preview branch, which returns before any Admin call and never touched the shared helper, so it gets the same rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Theme commands flattened a stored `store auth` session to
`{token, storeFqdn}` before running, so a rejected request had no way to
tell that the token came from a preview store whose claim the CLI has no
local signal for. Thread the stored session through as
`ThemeSessionContext` and classify at both the single- and
multi-environment boundaries.
Only preview sessions are classified, and only on a 401. Theme commands
use the stored access token as-is, with no expiry check and no refresh,
so an ordinary expired-but-refreshable standard session would 401 here
for a reason re-authenticating wouldn't fix. A 404 is left alone because
theme commands accept `--theme <id>`, where a genuine not-found must not
be reported as a claimed store.
Multi-environment failures previously prefixed the environment onto
`error.message`, which silently dropped a `FatalError`'s `tryMessage`,
`nextSteps` and custom sections - the parts that make the new error
actionable. Render the environment as a headline and forward the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Differences in type declarationsWe detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:
New type declarationspackages/cli-kit/dist/public/node/store-auth-recovery.d.tsimport type { LocalStorage } from './local-storage.js';
import type { StoreAuthSessionSchema, StoredStoreAppSession } from './store-auth-session.js';
/**
* Throw an actionable error reporting that no store-app authentication is stored for a store.
*
* @param store - The store FQDN the caller tried to use.
* @throws AbortError pointing at `shopify store auth`.
*/
export declare function throwMissingStoredStoreAuthError(store: string): never;
/**
* Throw a caller-supplied message alongside the next step for re-authenticating a stored session.
*
* @param message - The message describing why the stored session can no longer be used.
* @param session - The stored store auth session that needs re-authenticating.
* @throws AbortError pointing at `shopify store auth`.
*/
export declare function throwReauthenticateStoreAuthError(message: string, session: StoredStoreAppSession): never;
/**
* Throw an actionable error reporting that a stored store-app session is no longer valid.
*
* A preview store's local session has no way to know it was claimed through the browser claim
* flow; a rejected request the first time the stale session is used again is the only signal.
* Surfacing that possibility is more useful than the generic "no longer valid" message a standard
* session gets, so every call site that detects an invalid stored session (regardless of which API
* it hit) should go through here instead of writing its own message.
*
* @param session - The stored store auth session that is no longer valid.
* @throws AbortError pointing at `shopify store auth`.
*/
export declare function throwStoredStoreAuthInvalidError(session: StoredStoreAppSession): never;
interface InvalidStoredStoreAuthOptions {
/** HTTP statuses that mean the stored session is invalid. Defaults to 401 and 404. */
invalidStatuses?: ReadonlyArray<number>;
/** Storage override for tests. */
storage?: LocalStorage<StoreAuthSessionSchema>;
}
/**
* Convert a failed request into an actionable re-authentication error when its HTTP status means
* the stored store-app session behind it stopped being accepted.
*
* The status is read from either shape a rejected Admin API request arrives in: carried directly on
* the error (cli-kit's typed transport errors) or nested under `response` (graphql-request's
* `ClientError`). When it matches, the stored session is cleared from local storage and an
* `AbortError` pointing at `shopify store auth` is thrown. An error carrying any other status - or
* no status at all - is left untouched for the caller to classify.
*
* @param error - The error thrown by the request made with the stored session.
* @param session - The stored store auth session the request was made with.
* @param options - Statuses to classify as invalid, and a storage override for tests.
* @throws AbortError pointing at `shopify store auth` when the stored session is invalid.
*/
export declare function throwIfStoredStoreAuthIsInvalid(error: unknown, session: StoredStoreAppSession, options?: InvalidStoredStoreAuthOptions): void;
export {};
Existing type declarationspackages/cli-kit/dist/public/node/store-auth-session.d.ts@@ -55,7 +55,11 @@ interface StoredStoreAppSessionBucket {
[userId: string]: StoredStoreAppSession;
};
}
-interface StoreAuthSessionSchema {
+/**
+ * Local-storage layout for store-auth sessions, keyed by {@link storeAuthSessionKey}. Exported so
+ * that sibling modules taking a storage override can type it the same way this module does.
+ */
+export interface StoreAuthSessionSchema {
[key: string]: StoredStoreAppSessionBucket;
}
/**
packages/cli-kit/dist/public/node/api/admin.d.ts@@ -1,8 +1,22 @@
import { GraphQLResponseOptions, GraphQLVariables } from './graphql.js';
import { AdminSession } from '../session.js';
+import { AbortError } from '../error.js';
import { RequestModeInput } from '../http.js';
import { Variables } from 'graphql-request';
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
+/**
+ * Thrown when the Admin API rejects a request with a status a caller may need to act on. It carries
+ * the HTTP status so callers can classify the failure - for instance a stored `shopify store auth`
+ * session that stopped being accepted once its preview store was claimed - rather than parsing the
+ * rendered message. It renders exactly like any other abort error.
+ */
+export declare class AdminApiRequestError extends AbortError {
+ /**
+ * HTTP status the Admin API responded with.
+ */
+ readonly status: number;
+ constructor(status: number, message: string);
+}
/**
* Executes a GraphQL query against the Admin API.
*
|
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?
PR #7986 made
shopify store infoandstore executefail actionably when run against a preview store that has since been claimed through the browser claim flow — the stored session stayskind: 'preview'forever, the CLI has no local signal of the claim, and the stale Admin token starts returning 401. Theme commands never got that treatment: they reported an opaque failure, or worse, "Development theme #X could not be found" for what was actually an authentication problem.WHAT is this pull request doing?
Ports the same recovery to
themecommands, and fixes two things that stopped the recovery from working at all.store infoafter a preview store has been claimed #7986 prints could never be followed.throwIfPreviewStoreabortsstore authfor any store that still has a stored preview session, and the recovery deliberately retained that session — so the command every one of these errors points at refused to run, with no reset flag to escape.store infowas the last path still broken, because its preview branch returns before any Admin call and never reached the shared helper. Invalid preview sessions are now cleared, which is what lets the suggested command start. This reverses a deliberate choice from Recover onstore infoafter a preview store has been claimed #7986; the trade-off is that preview credentials are write-once, so clearing is irreversible — but a preview token never expires, so a 401 on one means it stopped being accepted outright and nothing usable is discarded.storereaches a 401 only afterloadStoredStoreSessionhas already refreshed an expired token, so there the status is definitive. Theme uses the stored token as-is, so an ordinary expired-but-refreshable session 401s for a reason re-authenticating wouldn't fix — and clearing it would destroy a usable refresh token. Theme therefore classifies preview sessions only and never clears.Also included: the Admin transport stops destroying the HTTP status (
AdminApiRequestError),fetchThemestops reporting a 401 as a missing theme, and multi-environment failures stop dropping thenextStepsthat make these errors actionable.Known follow-up (not in this PR): theme commands still read stored tokens with no expiry check or refresh, unlike
store'sloadStoredStoreSession. That gap is why theme classifies preview-only. Exposing a refresh-aware lifecycle at the cli-kit layer would let theme handle standard sessions properly too.How to test your changes?
packages/store/src/cli/services/store/auth/preview-claim-recovery.test.tsis the key one — it composes both halves against realLocalStoragein a temp directory:store authrefuses to start, the 401 is classified and the session cleared, thenstore authgets past the guard.Full suite green: 266 files / 2764 passed / 2 skipped, plus type-check, lint and knip.
Post-release steps
None.
Checklist
patchfor bug fixes ·minorfor new features ·majorfor breaking changes) and added a changeset withpnpm changeset add🤖 Generated with Claude Code