trigger.dev
Version:
A Command-Line Interface for Trigger.dev projects
313 lines (311 loc) • 14.6 kB
JavaScript
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