UNPKG

trigger.dev

Version:

A Command-Line Interface for Trigger.dev projects

313 lines (311 loc) • 14.6 kB
import { intro, log, outro, select } from "@clack/prompts"; import { recordSpanException } from "@trigger.dev/core/v3/workers"; import open from "open"; import pRetry, { AbortError } from "p-retry"; import { z } from "zod"; import { CliApiClient } from "../apiClient.js"; import { CommonCommandOptions, SkipLoggingError, commonOptions, handleTelemetry, tracer, wrapCommandAction, } from "../cli/common.js"; import { chalkLink, prettyError } from "../utilities/cliOutput.js"; import { readAuthConfigProfile, writeAuthConfigProfile, writeAuthConfigCurrentProfileName, } from "../utilities/configFiles.js"; import { printInitialBanner } from "../utilities/initialBanner.js"; import { whoAmI } from "./whoami.js"; import { logger } from "../utilities/logger.js"; import { spinner } from "../utilities/windows.js"; import { isLinuxServer } from "../utilities/linux.js"; import { VERSION } from "../version.js"; import { env, isCI } from "std-env"; import { CLOUD_API_URL } from "../consts.js"; import { validateAccessToken, NotPersonalAccessTokenError, NotAccessTokenError, } from "../utilities/accessTokens.js"; import { links } from "@trigger.dev/core/v3"; export const LoginCommandOptions = CommonCommandOptions.extend({ apiUrl: z.string(), }); export function configureLoginCommand(program) { return commonOptions(program .command("login") .description("Login with Trigger.dev so you can perform authenticated actions")) .version(VERSION, "-v, --version", "Display the version number") .action(async (options) => { await handleTelemetry(async () => { await printInitialBanner(false, options.profile); await loginCommand(options); }); }); } export async function loginCommand(options) { return await wrapCommandAction("loginCommand", LoginCommandOptions, options, async (opts) => { return await _loginCommand(opts); }); } async function _loginCommand(options) { return login({ defaultApiUrl: options.apiUrl, embedded: false, profile: options.profile }); } export async function login(options) { return await tracer.startActiveSpan("login", async (span) => { try { const opts = { defaultApiUrl: CLOUD_API_URL, embedded: false, silent: false, ...options, }; span.setAttributes({ "cli.config.apiUrl": opts.defaultApiUrl, "cli.options.profile": opts.profile, }); if (!opts.embedded) { intro("Logging in to Trigger.dev"); } const accessTokenFromEnv = env.TRIGGER_ACCESS_TOKEN; if (accessTokenFromEnv) { const validationResult = validateAccessToken(accessTokenFromEnv); if (!validationResult.success) { // We deliberately don't surface the existence of organization access tokens to the user for now, as they're only used internally. // Once we expose them in the application, we should also communicate that option here. throw new NotAccessTokenError("Your TRIGGER_ACCESS_TOKEN is not a Personal Access Token, they start with 'tr_pat_'. You can generate one here: https://cloud.trigger.dev/account/tokens"); } const auth = { accessToken: accessTokenFromEnv, apiUrl: env.TRIGGER_API_URL ?? opts.defaultApiUrl ?? CLOUD_API_URL, }; const apiClient = new CliApiClient(auth.apiUrl, auth.accessToken); const userData = await apiClient.whoAmI(); if (!userData.success) { throw new Error(userData.error); } return { ok: true, profile: options?.profile ?? "default", userId: userData.data.userId, email: userData.data.email, dashboardUrl: userData.data.dashboardUrl, auth: { accessToken: auth.accessToken, tokenType: validationResult.type, apiUrl: auth.apiUrl, }, }; } const authConfig = readAuthConfigProfile(options?.profile); if (authConfig && authConfig.accessToken) { const whoAmIResult = await whoAmI({ profile: options?.profile ?? "default", skipTelemetry: !span.isRecording(), logLevel: logger.loggerLevel, }, true, opts.silent); if (!whoAmIResult.success) { prettyError("Unable to validate existing personal access token", whoAmIResult.error); if (!opts.embedded) { outro(`Login failed using stored token. To fix, first logout using \`trigger.dev logout${options?.profile ? ` --profile ${options.profile}` : ""}\` and then try again.`); throw new SkipLoggingError(whoAmIResult.error); } else { throw new Error(whoAmIResult.error); } } else { if (!opts.embedded) { const continueOption = await select({ message: "You are already logged in.", options: [ { value: false, label: "Exit", }, { value: true, label: "Login with a different account", }, ], initialValue: false, }); if (continueOption !== true) { outro("Already logged in"); span.setAttributes({ "cli.userId": whoAmIResult.data.userId, "cli.email": whoAmIResult.data.email, "cli.config.apiUrl": authConfig.apiUrl ?? opts.defaultApiUrl, }); span.end(); return { ok: true, profile: options?.profile ?? "default", userId: whoAmIResult.data.userId, email: whoAmIResult.data.email, dashboardUrl: whoAmIResult.data.dashboardUrl, auth: { accessToken: authConfig.accessToken, apiUrl: authConfig.apiUrl ?? opts.defaultApiUrl, tokenType: "personal", }, }; } } else { span.setAttributes({ "cli.userId": whoAmIResult.data.userId, "cli.email": whoAmIResult.data.email, "cli.config.apiUrl": authConfig.apiUrl ?? opts.defaultApiUrl, }); span.end(); return { ok: true, profile: options?.profile ?? "default", userId: whoAmIResult.data.userId, email: whoAmIResult.data.email, dashboardUrl: whoAmIResult.data.dashboardUrl, auth: { accessToken: authConfig.accessToken, apiUrl: authConfig.apiUrl ?? opts.defaultApiUrl, tokenType: "personal", }, }; } } } if (isCI) { const apiUrl = env.TRIGGER_API_URL ?? authConfig?.apiUrl ?? opts.defaultApiUrl ?? CLOUD_API_URL; const isSelfHosted = apiUrl !== CLOUD_API_URL; // This is fine, as the api URL will generally be the same as the dashboard URL for self-hosted instances const dashboardUrl = isSelfHosted ? apiUrl : "https://cloud.trigger.dev"; throw new Error(`Authentication required in CI environment. Please set the TRIGGER_ACCESS_TOKEN environment variable with a Personal Access Token. - You can generate one here: ${dashboardUrl}/account/tokens - For more information, see: ${links.docs.gitHubActions.personalAccessToken}`); } if (opts.embedded) { log.step("You must login to continue."); } const apiClient = new CliApiClient(authConfig?.apiUrl ?? opts.defaultApiUrl); //generate authorization code const authorizationCodeResult = await createAuthorizationCode(apiClient); //Link the user to the authorization code log.step(`Please visit the following URL to login:\n${chalkLink(authorizationCodeResult.url)}`); if (await isLinuxServer()) { log.message("Please install `xdg-utils` to automatically open the login URL."); } else { await open(authorizationCodeResult.url); } //poll for personal access token (we need to poll for it) const getPersonalAccessTokenSpinner = spinner(); getPersonalAccessTokenSpinner.start("Waiting for you to login"); try { const indexResult = await pRetry(() => getPersonalAccessToken(apiClient, authorizationCodeResult.authorizationCode), { //this means we're polling, same distance between each attempt factor: 1, retries: 60, minTimeout: 1000, }); getPersonalAccessTokenSpinner.stop(`Logged in with token ${indexResult.obfuscatedToken}`); writeAuthConfigProfile({ accessToken: indexResult.token, apiUrl: opts.defaultApiUrl, }, options?.profile); const whoAmIResult = await whoAmI({ profile: options?.profile ?? "default", skipTelemetry: !span.isRecording(), logLevel: logger.loggerLevel, }, opts.embedded); if (!whoAmIResult.success) { throw new Error(whoAmIResult.error); } const profileName = options?.profile ?? "default"; // Set this profile as the current default writeAuthConfigCurrentProfileName(profileName); if (opts.embedded) { log.step("Logged in successfully"); } else { outro("Logged in successfully"); } span.end(); return { ok: true, profile: profileName, userId: whoAmIResult.data.userId, email: whoAmIResult.data.email, dashboardUrl: whoAmIResult.data.dashboardUrl, auth: { accessToken: indexResult.token, apiUrl: authConfig?.apiUrl ?? opts.defaultApiUrl, tokenType: "personal", }, }; } catch (e) { getPersonalAccessTokenSpinner.stop(`Failed to get access token`); if (e instanceof AbortError) { log.error(e.message); } recordSpanException(span, e); span.end(); return { ok: false, error: e instanceof Error ? e.message : String(e), }; } } catch (e) { recordSpanException(span, e); span.end(); if (options?.embedded) { if (e instanceof NotPersonalAccessTokenError) { throw e; } return { ok: false, error: e instanceof Error ? e.message : String(e), }; } throw e; } }); } export async function getPersonalAccessToken(apiClient, authorizationCode) { return await tracer.startActiveSpan("getPersonalAccessToken", async (span) => { try { const token = await apiClient.getPersonalAccessToken(authorizationCode); if (!token.success) { throw new AbortError(token.error); } if (!token.data.token) { throw new Error("No token found yet"); } span.end(); return { token: token.data.token.token, obfuscatedToken: token.data.token.obfuscatedToken, }; } catch (e) { if (e instanceof AbortError) { recordSpanException(span, e); } span.end(); throw e; } }); } async function createAuthorizationCode(apiClient) { return await tracer.startActiveSpan("createAuthorizationCode", async (span) => { try { //generate authorization code const createAuthCodeSpinner = spinner(); createAuthCodeSpinner.start("Creating authorization code"); const authorizationCodeResult = await apiClient.createAuthorizationCode(); if (!authorizationCodeResult.success) { createAuthCodeSpinner.stop(`Failed to create authorization code\n${authorizationCodeResult.error}`); throw new SkipLoggingError(`Failed to create authorization code\n${authorizationCodeResult.error}`); } createAuthCodeSpinner.stop("Created authorization code"); span.end(); return authorizationCodeResult.data; } catch (e) { recordSpanException(span, e); span.end(); throw e; } }); } //# sourceMappingURL=login.js.map