From 56095b8f44f629cf44ea9699a08b95ec6bcd9679 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 28 Jun 2024 08:25:08 -0400 Subject: [PATCH] Add "whoami" command --- packages/replayio/src/bin.ts | 3 +- packages/replayio/src/commands/login.ts | 6 +-- packages/replayio/src/commands/logout.ts | 4 +- .../src/commands/upload-source-maps.ts | 3 +- packages/replayio/src/commands/whoami.ts | 54 +++++++++++++++++++ .../src/utils/browser/reportBrowserCrash.ts | 7 ++- .../initialization/checkAuthentication.ts | 5 -- .../src/utils/initialization/initialize.ts | 4 +- .../src/authentication/getAccessToken.ts | 36 +++++++++---- .../shared/src/protocol/ProtocolClient.ts | 2 +- 10 files changed, 95 insertions(+), 29 deletions(-) create mode 100644 packages/replayio/src/commands/whoami.ts delete mode 100644 packages/replayio/src/utils/initialization/checkAuthentication.ts diff --git a/packages/replayio/src/bin.ts b/packages/replayio/src/bin.ts index 60400422..95220055 100644 --- a/packages/replayio/src/bin.ts +++ b/packages/replayio/src/bin.ts @@ -1,3 +1,4 @@ +import { initLogger } from "@replay-cli/shared/logger"; import { exitProcess } from "@replay-cli/shared/process/exitProcess"; import { setUserAgent } from "@replay-cli/shared/userAgent"; import { name, version } from "../package.json"; @@ -14,7 +15,7 @@ import "./commands/remove"; import "./commands/update"; import "./commands/upload"; import "./commands/upload-source-maps"; -import { initLogger } from "@replay-cli/shared/logger"; +import "./commands/whoami"; initLogger(name, version); diff --git a/packages/replayio/src/commands/login.ts b/packages/replayio/src/commands/login.ts index 95c75b63..f277a95c 100644 --- a/packages/replayio/src/commands/login.ts +++ b/packages/replayio/src/commands/login.ts @@ -1,13 +1,13 @@ +import { getAccessToken } from "@replay-cli/shared/authentication/getAccessToken"; import { exitProcess } from "@replay-cli/shared/process/exitProcess"; import { registerCommand } from "../utils/commander/registerCommand"; -import { checkAuthentication } from "../utils/initialization/checkAuthentication"; import { promptForAuthentication } from "../utils/initialization/promptForAuthentication"; registerCommand("login").description("Log into your Replay account (or register)").action(login); async function login() { - const authenticated = await checkAuthentication(); - if (authenticated) { + const { accessToken } = await getAccessToken(); + if (accessToken) { console.log("You are already signed in!"); } else { await promptForAuthentication(); diff --git a/packages/replayio/src/commands/logout.ts b/packages/replayio/src/commands/logout.ts index f39df17f..d7d4c204 100644 --- a/packages/replayio/src/commands/logout.ts +++ b/packages/replayio/src/commands/logout.ts @@ -9,8 +9,8 @@ registerCommand("logout").description("Log out of your Replay account").action(l async function logout() { await logoutIfAuthenticated(); - const token = await getAccessToken(); - if (token) { + const { accessToken } = await getAccessToken(); + if (accessToken) { const name = process.env.REPLAY_API_KEY ? "REPLAY_API_KEY" : "RECORD_REPLAY_API_KEY"; console.log( diff --git a/packages/replayio/src/commands/upload-source-maps.ts b/packages/replayio/src/commands/upload-source-maps.ts index 9085b677..85001ead 100644 --- a/packages/replayio/src/commands/upload-source-maps.ts +++ b/packages/replayio/src/commands/upload-source-maps.ts @@ -41,12 +41,13 @@ async function uploadSourceMaps( root?: string; } ) { + const { accessToken } = await getAccessToken(); const uploadPromise = uploadSourceMapsExternal({ extensions, filepaths: filePaths, group, ignore, - key: await getAccessToken(), + key: accessToken, root, server: replayApiServer, }); diff --git a/packages/replayio/src/commands/whoami.ts b/packages/replayio/src/commands/whoami.ts new file mode 100644 index 00000000..b85564bb --- /dev/null +++ b/packages/replayio/src/commands/whoami.ts @@ -0,0 +1,54 @@ +import { getAccessToken } from "@replay-cli/shared/authentication/getAccessToken"; +import { getAuthInfo } from "@replay-cli/shared/graphql/getAuthInfo"; +import { exitProcess } from "@replay-cli/shared/process/exitProcess"; +import { dim, emphasize, highlight, link } from "@replay-cli/shared/theme"; +import { registerCommand } from "../utils/commander/registerCommand"; +import { name as packageName } from "../../package.json"; + +registerCommand("whoami", { + checkForNpmUpdate: false, + checkForRuntimeUpdate: false, + requireAuthentication: false, +}) + .description("Display info about the current user") + .action(info); + +const DOCS_URL = "https://docs.replay.io/reference/api-keys"; + +async function info() { + const { accessToken, apiKeySource } = await getAccessToken(); + if (accessToken) { + const authInfo = await getAuthInfo(accessToken); + + if (apiKeySource) { + console.log(`You are authenticated by API key ${dim(`(process.env.${apiKeySource})`)}`); + console.log(""); + if (authInfo.type === "user") { + console.log(`This is a ${emphasize("personal")} API key`); + console.log(`Recordings you upload are ${emphasize("private")} by default`); + } else { + console.log(`This is a ${emphasize("team")} API key`); + console.log(`Recordings you upload are ${emphasize("shared with other team members")}`); + } + console.log(""); + console.log(`Learn more about API keys at ${link(DOCS_URL)}`); + } else { + console.log( + `You signed into your Replay account using an ${emphasize("email and password")}` + ); + console.log(""); + console.log(`Recordings you upload are ${emphasize("private")} by default`); + console.log(""); + console.log(`Learn about other ways to sign in at ${link(DOCS_URL)}`); + } + } else { + console.log("You are not authenticated"); + console.log(""); + console.log(`Sign in by running ${highlight(`${packageName} login`)}`); + console.log(""); + console.log("You can also authenticate with an API key"); + console.log(`Learn more at ${link(DOCS_URL)}`); + } + + await exitProcess(0); +} diff --git a/packages/replayio/src/utils/browser/reportBrowserCrash.ts b/packages/replayio/src/utils/browser/reportBrowserCrash.ts index fbc9b0d4..3488af70 100644 --- a/packages/replayio/src/utils/browser/reportBrowserCrash.ts +++ b/packages/replayio/src/utils/browser/reportBrowserCrash.ts @@ -1,10 +1,10 @@ +import { getAccessToken } from "@replay-cli/shared/authentication/getAccessToken"; import { getReplayPath } from "@replay-cli/shared/getReplayPath"; import { logger } from "@replay-cli/shared/logger"; +import { getUserAgent } from "@replay-cli/shared/userAgent"; import { readFile, writeFileSync } from "fs-extra"; import { File, FormData, fetch } from "undici"; import { replayApiServer } from "../../config"; -import { getUserAgent } from "@replay-cli/shared/userAgent"; -import { checkAuthentication } from "../../utils/initialization/checkAuthentication"; import { getCurrentRuntimeMetadata } from "../../utils/initialization/getCurrentRuntimeMetadata"; import { runtimeMetadata } from "../../utils/installation/config"; import { findMostRecentFile } from "../findMostRecentFile"; @@ -13,8 +13,7 @@ export async function reportBrowserCrash(stderr: string) { const errorLogPath = getReplayPath("recorder-crash.log"); writeFileSync(errorLogPath, stderr, "utf8"); - const accessToken = await checkAuthentication(); - + const { accessToken } = await getAccessToken(); if (!accessToken) { return { errorLogPath, diff --git a/packages/replayio/src/utils/initialization/checkAuthentication.ts b/packages/replayio/src/utils/initialization/checkAuthentication.ts deleted file mode 100644 index 76ec3ffc..00000000 --- a/packages/replayio/src/utils/initialization/checkAuthentication.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { getAccessToken } from "@replay-cli/shared/authentication/getAccessToken"; - -export async function checkAuthentication() { - return await getAccessToken(); -} diff --git a/packages/replayio/src/utils/initialization/initialize.ts b/packages/replayio/src/utils/initialization/initialize.ts index 30730021..7f6a566b 100644 --- a/packages/replayio/src/utils/initialization/initialize.ts +++ b/packages/replayio/src/utils/initialization/initialize.ts @@ -1,9 +1,9 @@ import { raceWithTimeout } from "@replay-cli/shared/async/raceWithTimeout"; +import { getAccessToken } from "@replay-cli/shared/authentication/getAccessToken"; import { initLaunchDarklyFromAccessToken } from "@replay-cli/shared/launch-darkly/initLaunchDarklyFromAccessToken"; import { initMixpanelForUserSession } from "@replay-cli/shared/mixpanel/initMixpanelForUserSession"; import { name as packageName, version as packageVersion } from "../../../package.json"; import { logPromise } from "../async/logPromise"; -import { checkAuthentication } from "./checkAuthentication"; import { checkForNpmUpdate } from "./checkForNpmUpdate"; import { checkForRuntimeUpdate } from "./checkForRuntimeUpdate"; import { promptForAuthentication } from "./promptForAuthentication"; @@ -22,7 +22,7 @@ export async function initialize({ // These initialization steps can run in parallel to improve startup time // None of them should log anything though; that would interfere with the initialization-in-progress message const promises = Promise.all([ - checkAuthentication(), + getAccessToken().then(({ accessToken }) => accessToken), shouldCheckForRuntimeUpdate ? raceWithTimeout(checkForRuntimeUpdate(), 5_000) : Promise.resolve(), diff --git a/packages/shared/src/authentication/getAccessToken.ts b/packages/shared/src/authentication/getAccessToken.ts index ce8a67ca..21b47f32 100644 --- a/packages/shared/src/authentication/getAccessToken.ts +++ b/packages/shared/src/authentication/getAccessToken.ts @@ -6,29 +6,45 @@ import { cachedAuthPath } from "./config"; import { refreshAccessTokenOrThrow } from "./refreshAccessTokenOrThrow"; import { CachedAuthDetails } from "./types"; -export async function getAccessToken(): Promise { +export type AccessTokenInfo = { + accessToken: string | undefined; + apiKeySource: "REPLAY_API_KEY" | "RECORD_REPLAY_API_KEY" | undefined; +}; + +const NO_ACCESS_TOKEN: AccessTokenInfo = { + accessToken: undefined, + apiKeySource: undefined, +}; + +export async function getAccessToken(): Promise { if (process.env.REPLAY_API_KEY) { logger.debug("Using token from env (REPLAY_API_KEY)"); - return process.env.REPLAY_API_KEY; + return { + accessToken: process.env.REPLAY_API_KEY, + apiKeySource: "REPLAY_API_KEY", + }; } else if (process.env.RECORD_REPLAY_API_KEY) { logger.debug("Using token from env (RECORD_REPLAY_API_KEY)"); - return process.env.RECORD_REPLAY_API_KEY; + return { + accessToken: process.env.RECORD_REPLAY_API_KEY, + apiKeySource: "RECORD_REPLAY_API_KEY", + }; } let { accessToken, refreshToken } = readFromCache(cachedAuthPath) ?? {}; if (typeof accessToken !== "string") { logger.debug("Unexpected accessToken value", { accessToken }); - return; + return NO_ACCESS_TOKEN; } if (typeof refreshToken !== "string") { logger.debug("Unexpected refreshToken", { refreshToken }); - return; + return NO_ACCESS_TOKEN; } const [_, encodedToken, __] = accessToken.split(".", 3); if (typeof encodedToken !== "string") { logger.debug("Token did not contain a valid payload", { accessToken: maskString(accessToken) }); - return; + return NO_ACCESS_TOKEN; } let payload: any; @@ -36,12 +52,12 @@ export async function getAccessToken(): Promise { payload = JSON.parse(Buffer.from(encodedToken, "base64").toString()); } catch (error) { logger.debug("Failed to decode token", { accessToken: maskString(accessToken), error }); - return; + return NO_ACCESS_TOKEN; } if (typeof payload !== "object") { logger.debug("Token payload was not an object"); - return; + return NO_ACCESS_TOKEN; } const expiration = (payload?.exp ?? 0) * 1000; @@ -57,11 +73,11 @@ export async function getAccessToken(): Promise { } catch (error) { writeToCache(cachedAuthPath, undefined); updateCachedAuthInfo(accessToken, undefined); - return; + return NO_ACCESS_TOKEN; } } else { logger.debug(`Access token valid until ${expirationDate.toLocaleDateString()}`); } - return accessToken; + return { accessToken, apiKeySource: undefined }; } diff --git a/packages/shared/src/protocol/ProtocolClient.ts b/packages/shared/src/protocol/ProtocolClient.ts index 39960634..f4f829a3 100644 --- a/packages/shared/src/protocol/ProtocolClient.ts +++ b/packages/shared/src/protocol/ProtocolClient.ts @@ -153,7 +153,7 @@ export default class ProtocolClient { private onSocketOpen = async () => { try { - const accessToken = await getAccessToken(); + const { accessToken } = await getAccessToken(); assert(accessToken, "No access token found"); await setAccessToken(this, { accessToken });