Skip to content

Add resumable store auth for non-TTY flows#7649

Closed
gonzaloriestra wants to merge 1 commit into
agent-auth/resumable-loginfrom
agent-auth/resumable-store-auth
Closed

Add resumable store auth for non-TTY flows#7649
gonzaloriestra wants to merge 1 commit into
agent-auth/resumable-loginfrom
agent-auth/resumable-store-auth

Conversation

@gonzaloriestra
Copy link
Copy Markdown
Contributor

@gonzaloriestra gonzaloriestra commented May 27, 2026

WHY are these changes introduced?

Agents running shopify store auth in non-TTY contexts should be able to start app/store authorization without waiting on the local callback server. This stacks on the resumable account auth work and gives store auth an explicit start/resume path.

WHAT is this pull request doing?

  • Makes non-TTY shopify store auth reuse an existing valid store session when it already has the requested scopes.
  • Starts OAuth without waiting when no usable session exists, stores pending PKCE state, prints the authorization URL, and tells the user to resume with the browser callback URL.
  • Adds shopify store auth --resume --callback-url <url> to validate the pending callback, exchange the auth code, and store the app session.
  • Refreshes generated command docs/manifests and adds focused service, command, presenter, and storage tests.

How to test your changes?

CI=1 shopify store auth

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

Copy link
Copy Markdown
Contributor Author

gonzaloriestra commented May 27, 2026

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.

@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label May 27, 2026
Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions
Copy link
Copy Markdown
Contributor

🫰✨ Thanks @gonzaloriestra! 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-20260528113027

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.

@gonzaloriestra gonzaloriestra force-pushed the agent-auth/resumable-store-auth branch from 19e0182 to 38d4033 Compare May 28, 2026 12:14
@gonzaloriestra gonzaloriestra force-pushed the agent-auth/resumable-store-auth branch from 38d4033 to 12ce355 Compare May 28, 2026 14:13
@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/common/version.d.ts
@@ -1 +1 @@
-export declare const CLI_KIT_VERSION = "4.1.0";
\ No newline at end of file
+export declare const CLI_KIT_VERSION = "4.0.0";
\ No newline at end of file
packages/cli-kit/dist/public/node/session-prompt.d.ts
@@ -1,3 +1,6 @@
+interface PromptSessionSelectOptions {
+    forceNewSession?: boolean;
+}
 /**
  * Prompts the user to select from existing sessions or log in with a different account.
  *
@@ -5,6 +8,8 @@
  * - Otherwise, shows a prompt with all available sessions and the option to log in with a different account.
  *
  * @param alias - Optional alias of the account to switch to.
+ * @param options - Optional prompt behavior.
  * @returns Promise with the alias of the chosen session.
  */
-export declare function promptSessionSelect(alias?: string): Promise<string>;
\ No newline at end of file
+export declare function promptSessionSelect(alias?: string, options?: PromptSessionSelectOptions): Promise<string>;
+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