Add resumable auth login for non-TTY flows#7641
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. |
1c2612a to
15ec209
Compare
|
/snapit |
This comment was marked as outdated.
This comment was marked as outdated.
15ec209 to
a14814d
Compare
|
/snapit |
|
🫰✨ 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-20260527092501Caution After installing, validate the version by running |
a14814d to
22b775f
Compare
Allow agents to start Shopify auth without blocking on device-code polling, then resume the stashed device code exchange after the user authorizes.
22b775f to
f07f957
Compare
|
/snapit |
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,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-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.
*
|
|
🫰✨ 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-20260527105546Caution After installing, validate the version by running |

WHY are these changes introduced?
Agents running
shopify auth loginin 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?
shopify auth loginstart device authorization, print the verification URL and code, stash the device code, and exit with--resumeguidance.shopify auth login --resumeto exchange the stashed device code and store the completed session.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.tspnpm vitest run packages/cli-kit/src/private/node/session.test.ts packages/cli-kit/src/public/node/session.test.tspnpm nx run-many --target=type-check --projects=cli-kit,cli --skip-nx-cachegit diff --checkChecklist
patchfor bug fixes ·minorfor new features ·majorfor breaking changes) and added a changeset withpnpm changeset add