Skip to content

Add resumable auth login for non-TTY flows#7641

Draft
gonzaloriestra wants to merge 1 commit into
agent-auth/add-statusfrom
agent-auth/resumable-login
Draft

Add resumable auth login for non-TTY flows#7641
gonzaloriestra wants to merge 1 commit into
agent-auth/add-statusfrom
agent-auth/resumable-login

Conversation

@gonzaloriestra
Copy link
Copy Markdown
Contributor

@gonzaloriestra gonzaloriestra commented May 26, 2026

WHY are these changes introduced?

Agents running shopify auth login in non-TTY contexts should not block on device-code polling. This builds on #7629 by letting agents start login, hand the URL and code to a user, and resume after authorization.

WHAT is this pull request doing?

  • Makes non-TTY shopify auth login start device authorization, print the verification URL and code, stash the device code, and exit with --resume guidance.
  • Adds shopify auth login --resume to exchange the stashed device code and store the completed session.
  • Refreshes command docs/manifests and adds focused cli-kit and command tests.

How to test your changes?

  • pnpm vitest run packages/cli/src/cli/commands/auth/login.test.ts packages/cli-kit/src/private/node/session/device-authorization.test.ts packages/cli-kit/src/private/node/conf-store.test.ts packages/cli-kit/src/public/node/session-device-auth.test.ts packages/cli-kit/src/public/node/session-auth-status.test.ts
  • pnpm vitest run packages/cli-kit/src/private/node/session.test.ts packages/cli-kit/src/public/node/session.test.ts
  • pnpm nx run-many --target=type-check --projects=cli-kit,cli --skip-nx-cache
  • git diff --check

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • The change is user-facing — I've identified the correct bump type (patch for bug fixes · minor for new features · major for breaking changes) and added a changeset with pnpm changeset add

Allow agents to start Shopify auth without blocking on device-code polling, then resume the stashed device code exchange after the user authorizes.
Copy link
Copy Markdown
Contributor Author

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.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@gonzaloriestra gonzaloriestra mentioned this pull request May 26, 2026
4 tasks
@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label May 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Differences in type declarations

We 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:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/private/node/conf-store.d.ts
@@ -18,6 +18,13 @@ interface Cache {
     [mostRecentOccurrenceKey: MostRecentOccurrenceKey]: CacheValue<boolean>;
     [rateLimitKey: RateLimitKey]: CacheValue<number[]>;
 }
+export interface PendingDeviceAuth {
+    deviceCode: string;
+    userCode: string;
+    verificationUriComplete: string;
+    interval: number;
+    expiresAt: number;
+}
 export interface ConfSchema {
     sessionStore: string;
     currentSessionId?: string;
@@ -25,6 +32,7 @@ export interface ConfSchema {
     currentDevSessionId?: string;
     cache?: Cache;
     autoUpgradeEnabled?: boolean;
+    pendingDeviceAuth?: PendingDeviceAuth;
 }
 /**
  * Get session.
@@ -58,6 +66,22 @@ 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 for a resumable non-interactive login flow.
+ *
+ * @returns Pending device auth state, if present.
+ */
+export declare function getPendingDeviceAuth(config?: LocalStorage<ConfSchema>): PendingDeviceAuth | undefined;
+/**
+ * Stash pending device auth state for a later .
+ *
+ * @param auth - Pending device auth state.
+ */
+export declare function setPendingDeviceAuth(auth: PendingDeviceAuth, config?: LocalStorage<ConfSchema>): void;
+/**
+ * Clear pending device auth state after completion or expiry.
+ */
+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/constants.d.ts
@@ -7,6 +7,7 @@ export declare const environmentVariables: {
     enableCliRedirect: string;
     env: string;
     firstPartyDev: string;
+    hostedApps: string;
     noAnalytics: string;
     optOutInstrumentation: string;
     appAutomationToken: string;
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,14 @@ 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 resumable non-interactive flow.
+ *
+ * @param identityToken - Identity token returned by the OAuth device code flow.
+ * @param applications - Applications to exchange access tokens for.
+ * @param existingAlias - Optional alias from a previous session to preserve if the email fetch fails.
+ * @returns A complete session with identity and application tokens.
+ */
+export declare function completeAuthFlow(identityToken: IdentityToken, applications: OAuthApplications, existingAlias?: string): Promise<Session>;
 export {};
\ No newline at end of file
packages/cli-kit/dist/public/node/session.d.ts
@@ -33,6 +33,21 @@ interface ServiceAccountInfo {
 interface UnknownAccountInfo {
     type: 'UnknownAccount';
 }
+export type AuthStatusName = 'authenticated' | 'needs_refresh' | 'not_authenticated' | 'invalid';
+export interface AuthStatus {
+    status: AuthStatusName;
+    authenticated: boolean;
+    account?: {
+        userId: string;
+        alias?: string;
+    };
+    identityFqdn?: string;
+    expiresAt?: string;
+    agentGuidance: {
+        instruction: string;
+        nextCommand?: string;
+    };
+}
 /**
  * Type guard to check if an account is a UserAccount.
  *
@@ -47,6 +62,12 @@ export declare function isUserAccount(account: AccountInfo): account is UserAcco
  * @returns True if the account is a ServiceAccount.
  */
 export declare function isServiceAccount(account: AccountInfo): account is ServiceAccountInfo;
+/**
+ * Returns the current Shopify CLI authentication status without starting a login flow.
+ *
+ * @returns The current authentication status.
+ */
+export declare function getAuthStatus(): Promise<AuthStatus>;
 /**
  * Ensure that we have a valid session with no particular scopes.
  *
@@ -128,6 +149,40 @@ export declare function ensureAuthenticatedBusinessPlatform(scopes?: BusinessPla
  * @returns A promise that resolves when the logout is complete.
  */
 export declare function logout(): Promise<void>;
+export interface StartDeviceAuthLoginResult {
+    verificationUriComplete: string;
+    userCode: string;
+    expiresAt: string;
+}
+/**
+ * Start a resumable device authorization flow for non-interactive .
+ *
+ * @returns Instructions needed to authorize the device code and resume login.
+ */
+export declare function startDeviceAuthLogin(): Promise<StartDeviceAuthLoginResult>;
+export type ResumeDeviceAuthLoginResult = {
+    status: 'success';
+    alias: string;
+} | {
+    status: 'pending';
+    verificationUriComplete: string;
+    userCode: string;
+} | {
+    status: 'expired';
+    message: string;
+} | {
+    status: 'denied';
+    message: string;
+} | {
+    status: 'no_pending';
+    message: string;
+};
+/**
+ * Resume a previously started non-interactive device authorization flow.
+ *
+ * @returns The result of exchanging the stashed device code.
+ */
+export declare function resumeDeviceAuthLogin(): Promise<ResumeDeviceAuthLoginResult>;
 /**
  * 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 for presenting the device authorization instructions.
  * @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/public/node/context/local.d.ts
@@ -42,6 +42,13 @@ export declare function isShopify(env?: NodeJS.ProcessEnv): Promise<boolean>;
  * @returns True if the SHOPIFY_UNIT_TEST environment variable is truthy.
  */
 export declare function isUnitTest(env?: NodeJS.ProcessEnv): boolean;
+/**
+ * Returns true if the CLI is running in hosted apps mode.
+ *
+ * @param env - The environment variables from the environment of the current process.
+ * @returns True if the HOSTED_APPS environment variable is truthy.
+ */
+export declare function isHostedAppsMode(env?: NodeJS.ProcessEnv): boolean;
 /**
  * Returns true if reporting analytics is enabled.
  *

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.

1 participant