Skip to content

Commit

Permalink
Add "whoami" command
Browse files Browse the repository at this point in the history
  • Loading branch information
bvaughn committed Jun 28, 2024
1 parent 33656ea commit 56095b8
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 29 deletions.
3 changes: 2 additions & 1 deletion packages/replayio/src/bin.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);

Expand Down
6 changes: 3 additions & 3 deletions packages/replayio/src/commands/login.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
4 changes: 2 additions & 2 deletions packages/replayio/src/commands/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion packages/replayio/src/commands/upload-source-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
54 changes: 54 additions & 0 deletions packages/replayio/src/commands/whoami.ts
Original file line number Diff line number Diff line change
@@ -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);
}
7 changes: 3 additions & 4 deletions packages/replayio/src/utils/browser/reportBrowserCrash.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand Down

This file was deleted.

4 changes: 2 additions & 2 deletions packages/replayio/src/utils/initialization/initialize.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(),
Expand Down
36 changes: 26 additions & 10 deletions packages/shared/src/authentication/getAccessToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,58 @@ import { cachedAuthPath } from "./config";
import { refreshAccessTokenOrThrow } from "./refreshAccessTokenOrThrow";
import { CachedAuthDetails } from "./types";

export async function getAccessToken(): Promise<string | undefined> {
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<AccessTokenInfo> {
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<CachedAuthDetails>(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;
try {
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;
Expand All @@ -57,11 +73,11 @@ export async function getAccessToken(): Promise<string | undefined> {
} 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 };
}
2 changes: 1 addition & 1 deletion packages/shared/src/protocol/ProtocolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down

0 comments on commit 56095b8

Please sign in to comment.