diff --git a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts index 2e73bc2c4..f924336c8 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts @@ -1462,6 +1462,91 @@ export class _StackClientAppImplIncomplete { + if (!options.appUrl) { + throw new Error("appUrl is required and must be set to the URL of the app you're authenticating with"); + } + + // Step 1: Initiate the CLI auth process + const response = await this._interface.sendClientRequest( + "/auth/cli", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + expires_in_millis: options.expiresInMillis, + }), + }, + null + ); + + if (!response.ok) { + throw new Error(`Failed to initiate CLI auth: ${response.status} ${await response.text()}`); + } + + const initResult = await response.json(); + const pollingCode = initResult.polling_code; + const loginCode = initResult.login_code; + + // Step 2: Open the browser for the user to authenticate + const url = `${options.appUrl}/handler/cli-auth-confirm?login_code=${encodeURIComponent(loginCode)}`; + console.log(`Please visit the following URL to authenticate:\n${url}`); + + // Try to open the browser if we're in a NodeJS or browser environment + + // Step 3: Poll for the token + let attempts = 0; + const maxAttempts = 300; // 10 minutes with 2-second intervals + + while (attempts < maxAttempts) { + attempts++; + const pollResponse = await this._interface.sendClientRequest("/auth/cli/poll", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + polling_code: pollingCode, + }), + }, null); + + const pollResult = await pollResponse.json(); + + if (pollResponse.status === 201 && pollResult.status === "success") { + return pollResult.refresh_token; + } else if (pollResult.status === "waiting") { + // Wait for 2 seconds before polling again + await wait(2000); + } else if (pollResult.status === "expired") { + throw new Error("CLI authentication request expired. Please try again."); + } else if (pollResult.status === "used") { + throw new Error("This authentication token has already been used."); + } else { + throw new Error(`Unexpected status from CLI auth polling: ${pollResult.status}`); + } + } + + throw new Error("Timed out waiting for CLI authentication."); + } + async signInWithPasskey(): Promise> { this._ensurePersistentTokenStore(); const session = await this._getSession(); diff --git a/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts b/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts index c9d1052db..f5ac2673f 100644 --- a/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts +++ b/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts @@ -42,6 +42,7 @@ export type StackClientApp>, signInWithPasskey(): Promise>, callOAuthCallback(): Promise, + promptCliLogin(options: { appUrl: string, expiresInMillis?: number }): Promise, sendForgotPasswordEmail(email: string, options?: { callbackUrl?: string }): Promise>, sendMagicLinkEmail(email: string, options?: { callbackUrl?: string }): Promise>, resetPassword(options: { code: string, password: string }): Promise>,