Add resumable store auth for non-TTY flows#7649
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. |
|
/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-20260528113027Caution After installing, validate the version by running |
19e0182 to
38d4033
Compare
38d4033 to
12ce355
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,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.
*
|

WHY are these changes introduced?
Agents running
shopify store authin 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?
shopify store authreuse an existing valid store session when it already has the requested scopes.shopify store auth --resume --callback-url <url>to validate the pending callback, exchange the auth code, and store the app session.How to test your changes?
CI=1 shopify store authChecklist
patchfor bug fixes ·minorfor new features ·majorfor breaking changes) and added a changeset withpnpm changeset add