From 240f866900d7643e5f032bef2a10fd15c446dc4e Mon Sep 17 00:00:00 2001 From: CactusBlue Date: Sat, 12 Apr 2025 16:09:51 -0700 Subject: [PATCH] CLI Login for NodeJS (#602) > [!IMPORTANT] > Adds CLI authentication for NodeJS with new error handling and updates dependencies. > > - **CLI Authentication**: > - Adds `promptCliLogin()` method in `_StackClientAppImplIncomplete` to handle CLI authentication, returning a refresh token or error. > - Handles errors `CliAuthError`, `CliAuthExpiredError`, and `CliAuthUsedError`. > - **Error Handling**: > - Adds `CliAuthError`, `CliAuthExpiredError`, and `CliAuthUsedError` to `known-errors.tsx`. > - **Dependencies**: > - Adds `open` to `dependencies` in `package.json`. > > This description was created by [Ellipsis](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral) for 98b0ccd0d252b71ed9f35f66d9f97104f03928d0. It will automatically update as commits are pushed. --------- Co-authored-by: Konsti Wohlwend --- packages/stack-shared/src/known-errors.tsx | 33 +++++++ .../apps/implementations/client-app-impl.ts | 96 +++++++++++++++++++ .../stack-app/apps/interfaces/client-app.ts | 1 + 3 files changed, 130 insertions(+) diff --git a/packages/stack-shared/src/known-errors.tsx b/packages/stack-shared/src/known-errors.tsx index 0d798b198..e9b70f115 100644 --- a/packages/stack-shared/src/known-errors.tsx +++ b/packages/stack-shared/src/known-errors.tsx @@ -1209,6 +1209,36 @@ const InvalidPollingCodeError = createKnownErrorConstructor( (json: any) => [json] as const, ); +const CliAuthError = createKnownErrorConstructor( + KnownError, + "CLI_AUTH_ERROR", + (message: string) => [ + 400, + message, + ] as const, + (json: any) => [json.message] as const, +); + +const CliAuthExpiredError = createKnownErrorConstructor( + KnownError, + "CLI_AUTH_EXPIRED_ERROR", + (message: string = "CLI authentication request expired. Please try again.") => [ + 400, + message, + ] as const, + (json: any) => [json.message] as const, +); + +const CliAuthUsedError = createKnownErrorConstructor( + KnownError, + "CLI_AUTH_USED_ERROR", + (message: string = "This authentication token has already been used.") => [ + 400, + message, + ] as const, + (json: any) => [json.message] as const, +); + const ApiKeyNotValid = createKnownErrorConstructor( KnownError, @@ -1293,6 +1323,9 @@ export const KnownErrors = { AllOverloadsFailed, ProjectAuthenticationError, PermissionIdAlreadyExists, + CliAuthError, + CliAuthExpiredError, + CliAuthUsedError, InvalidProjectAuthentication, ProjectKeyWithoutAccessType, InvalidAccessType, 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 b8d5508ef..40869b48a 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 @@ -1576,6 +1576,102 @@ export class _StackClientAppImplIncomplete void, + }): Promise> { + // 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) { + return Result.error(new KnownErrors.CliAuthError(`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)}`; + if (options.promptLink) { + options.promptLink(url); + } else { + console.log(`Please visit the following URL to authenticate:\n${url}`); + } + + + // Step 3: Poll for the token + let attempts = 0; + while (attempts < (options.maxAttempts ?? Infinity)) { + attempts++; + const pollResponse = await this._interface.sendClientRequest("/auth/cli/poll", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + polling_code: pollingCode, + }), + }, null); + + if (!pollResponse.ok) { + return Result.error(new KnownErrors.CliAuthError(`Failed to initiate CLI auth: ${pollResponse.status} ${await pollResponse.text()}`)); + } + const pollResult = await pollResponse.json(); + + if (pollResponse.status === 201 && pollResult.status === "success") { + return Result.ok(pollResult.refresh_token); + } + if (pollResult.status === "waiting") { + await wait(options.waitTimeMillis ?? 2000); + continue; + } + if (pollResult.status === "expired") { + return Result.error(new KnownErrors.CliAuthExpiredError("CLI authentication request expired. Please try again.")); + } + if (pollResult.status === "used") { + return Result.error(new KnownErrors.CliAuthUsedError("This authentication token has already been used.")); + } + return Result.error(new KnownErrors.CliAuthError(`Unexpected status from CLI auth polling: ${pollResult.status}`)); + } + + return Result.error(new KnownErrors.CliAuthError("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..544ab6065 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>,