Add agent-compatible device auth with explicit CLI commands#7067
Add agent-compatible device auth with explicit CLI commands#7067nickwesselman wants to merge 2 commits into
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
ae3f293 to
d0f409a
Compare
3f3d1c4 to
81cc05d
Compare
441e02e to
4a566a9
Compare
de7ef6a to
3b0f0fd
Compare
When an agent runs a Shopify CLI command requiring auth, the device code polling loop blocks the process. This adds explicit commands that agents can orchestrate: - `shopify auth whoami` — check for a valid session (exit 0/1) - `shopify auth login --no-polling` — start device flow, print URL, exit - `shopify auth login --resume` — exchange stashed device code for token Also extracts `completeAuthFlow` from the private session module so the resume path can reuse the post-auth token exchange logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3b0f0fd to
4b429c2
Compare
4a566a9 to
0a48a96
Compare
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 declarationsWe found no new type declarations in this PR Existing type declarationspackages/cli-kit/dist/private/node/conf-store.d.ts@@ -18,12 +18,20 @@ interface Cache {
[mostRecentOccurrenceKey: MostRecentOccurrenceKey]: CacheValue<boolean>;
[rateLimitKey: RateLimitKey]: CacheValue<number[]>;
}
+export interface PendingDeviceAuth {
+ deviceCode: string;
+ interval: number;
+ expiresAt: number;
+ verificationUriComplete: string;
+ scopes: string[];
+}
export interface ConfSchema {
sessionStore: string;
currentSessionId?: string;
devSessionStore?: string;
currentDevSessionId?: string;
cache?: Cache;
+ pendingDeviceAuth?: PendingDeviceAuth;
}
/**
* Get session.
@@ -57,6 +65,18 @@ export declare function setCurrentSessionId(sessionId: string, config?: LocalSto
* Remove current session ID.
*/
export declare function removeCurrentSessionId(config?: LocalStorage<ConfSchema>): void;
+/**
+ * Get pending device auth state (used for non-interactive login flow).
+ */
+export declare function getPendingDeviceAuth(config?: LocalStorage<ConfSchema>): PendingDeviceAuth | undefined;
+/**
+ * Stash pending device auth state for later resumption.
+ */
+export declare function setPendingDeviceAuth(auth: PendingDeviceAuth, config?: LocalStorage<ConfSchema>): void;
+/**
+ * Clear pending device auth state.
+ */
+export declare function clearPendingDeviceAuth(config?: LocalStorage<ConfSchema>): void;
type CacheValueForKey<TKey extends keyof Cache> = NonNullable<Cache[TKey]>['value'];
/**
* Fetch from cache, or run the provided function to get the value, and cache it
packages/cli-kit/dist/private/node/session.d.ts@@ -1,3 +1,4 @@
+import { IdentityToken, Session } from './session/schema.js';
import { AdminSession } from '../../public/node/session.js';
/**
* A scope supported by the Shopify Admin API.
@@ -104,4 +105,9 @@ export interface EnsureAuthenticatedAdditionalOptions {
* @returns An instance with the access tokens organized by application.
*/
export declare function ensureAuthenticated(applications: OAuthApplications, _env?: NodeJS.ProcessEnv, { forceRefresh, noPrompt, forceNewSession }?: EnsureAuthenticatedAdditionalOptions): Promise<OAuthSession>;
+/**
+ * Given an identity token, exchange it for application tokens and build a complete session.
+ * Shared between the interactive login flow and the --resume non-interactive flow.
+ */
+export declare function completeAuthFlow(identityToken: IdentityToken, applications: OAuthApplications): Promise<Session>;
export {};
\ No newline at end of file
packages/cli-kit/dist/public/node/session.d.ts@@ -116,12 +116,53 @@ export declare function ensureAuthenticatedThemes(store: string, password: strin
* @returns The access token for the Business Platform API.
*/
export declare function ensureAuthenticatedBusinessPlatform(scopes?: BusinessPlatformScope[]): Promise<string>;
+/**
+ * Returns info about the currently logged-in user, or undefined if not logged in.
+ * Does not trigger any authentication flow.
+ *
+ * @returns The current user's alias, or undefined if not logged in.
+ */
+export declare function getCurrentUserInfo(): Promise<{
+ alias: string;
+} | undefined>;
/**
* Logout from Shopify.
*
* @returns A promise that resolves when the logout is complete.
*/
export declare function logout(): Promise<void>;
+/**
+ * Start the device authorization flow without polling.
+ * Stashes the device code for later resumption via .
+ *
+ * @returns The verification URL the user must visit to authorize.
+ */
+export declare function startDeviceAuthNoPolling(): Promise<{
+ verificationUriComplete: string;
+}>;
+export type ResumeDeviceAuthResult = {
+ status: 'success';
+ alias: string;
+} | {
+ status: 'pending';
+ verificationUriComplete: string;
+} | {
+ status: 'expired';
+ message: string;
+} | {
+ status: 'denied';
+ message: string;
+} | {
+ status: 'no_pending';
+ message: string;
+};
+/**
+ * Resume a previously started device authorization flow.
+ * Exchanges the stashed device code for tokens and stores the session.
+ *
+ * @returns The result of the resume attempt.
+ */
+export declare function resumeDeviceAuth(): Promise<ResumeDeviceAuthResult>;
/**
* Ensure that we have a valid Admin session for the given store, with access on behalf of the app.
*
packages/cli-kit/dist/private/node/session/device-authorization.d.ts@@ -15,9 +15,12 @@ export interface DeviceAuthorizationResponse {
* Also returns a used for polling the token endpoint in the next step.
*
* @param scopes - The scopes to request
+ * @param options - Optional settings. Pass to print the URL without waiting for keypress or opening a browser.
* @returns An object with the device authorization response.
*/
-export declare function requestDeviceAuthorization(scopes: string[]): Promise<DeviceAuthorizationResponse>;
+export declare function requestDeviceAuthorization(scopes: string[], { noPrompt }?: {
+ noPrompt?: boolean;
+}): Promise<DeviceAuthorizationResponse>;
/**
* Poll the Oauth token endpoint with the device code obtained from a DeviceAuthorizationResponse.
* The endpoint will return until the user completes the auth flow in the browser.
packages/cli-kit/dist/private/node/ui/components/SingleTask.d.ts@@ -4,8 +4,9 @@ interface SingleTaskProps<T> {
title: TokenizedString;
task: (updateStatus: (status: TokenizedString) => void) => Promise<T>;
onComplete?: (result: T) => void;
+ onError?: (error: Error) => void;
onAbort?: () => void;
noColor?: boolean;
}
-declare const SingleTask: <T>({ task, title, onComplete, onAbort, noColor }: SingleTaskProps<T>) => React.JSX.Element | null;
+declare const SingleTask: <T>({ task, title, onComplete, onError, onAbort, noColor }: SingleTaskProps<T>) => React.JSX.Element | null;
export { SingleTask };
\ No newline at end of file
|
Coverage report
Test suite run success3935 tests passing in 1514 suites. Report generated by 🧪jest coverage report action from 0a48a96 |
|
This PR seems inactive. If it's still relevant, please add a comment saying so. Otherwise, take no action. |

Summary
shopify auth whoamicommand — checks for a valid session (exit 0 = logged in, exit 1 = not)shopify auth login --no-polling— starts device code flow, prints URL and verification code, exits immediately without blockingshopify auth login --resume— exchanges stashed device code for token, stores session on successcompleteAuthFlowhelper from private session module so the resume path reuses the same post-auth token exchange logicPendingDeviceAuthstash in conf-store for persisting device code state between commandsgetCurrentUserInfo()public API for checking login status without triggering authshopify auth login(no flags) behavior unchangedMotivation
When an agent runs a Shopify CLI command requiring auth, the device code polling loop blocks the process until the agent's bash timeout kills it. These explicit commands let agents orchestrate the flow:
Test plan
whoamicommand tests: logged in vs not logged inlogin --no-pollingtest: stashes device code, prints URL, exitslogin --resumetests: success, pending, expired, no_pending, denied🤖 Generated with Claude Code