diff --git a/package.json b/package.json
index c2ba23d..14a58b9 100644
--- a/package.json
+++ b/package.json
@@ -49,4 +49,4 @@
"typescript": "^5.6.2"
},
"version": "0.1.53"
-}
\ No newline at end of file
+}
diff --git a/src/lib/clusters/clusters.tsx b/src/lib/clusters/clusters.tsx
index bdba2c3..9621daf 100644
--- a/src/lib/clusters/clusters.tsx
+++ b/src/lib/clusters/clusters.tsx
@@ -336,7 +336,9 @@ function UserAddedDisplay(props: {
- Waiting for user to be ready...
+
+ Waiting for user to be ready, this should take about 30 seconds...
+
diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts
index ba0957a..f7d6f0d 100644
--- a/src/lib/tokens.ts
+++ b/src/lib/tokens.ts
@@ -9,334 +9,338 @@ import ora from "ora";
import { getCommandBase } from "../helpers/command.ts";
import { getAuthToken, isLoggedIn } from "../helpers/config.ts";
import {
- logLoginMessageAndQuit,
- logSessionTokenExpiredAndQuit,
+ logLoginMessageAndQuit,
+ logSessionTokenExpiredAndQuit,
} from "../helpers/errors.ts";
import { getApiUrl } from "../helpers/urls.ts";
export const TOKEN_EXPIRATION_SECONDS = {
- IN_7_DAYS: 7 * 24 * 60 * 60,
- IN_14_DAYS: 14 * 24 * 60 * 60,
- IN_30_DAYS: 30 * 24 * 60 * 60,
- IN_60_DAYS: 60 * 24 * 60 * 60,
- IN_90_DAYS: 90 * 24 * 60 * 60,
- IN_100_YEARS: 100 * 365 * 24 * 60 * 60,
+ IN_7_DAYS: 7 * 24 * 60 * 60,
+ IN_14_DAYS: 14 * 24 * 60 * 60,
+ IN_30_DAYS: 30 * 24 * 60 * 60,
+ IN_60_DAYS: 60 * 24 * 60 * 60,
+ IN_90_DAYS: 90 * 24 * 60 * 60,
+ IN_100_YEARS: 100 * 365 * 24 * 60 * 60,
};
export function registerTokens(program: Command) {
- const tokens = program
- .command("tokens")
- .description("Manage account access tokens.");
-
- tokens
- .command("create")
- .description("Create a new access token")
- .action(createTokenAction);
-
- tokens
- .command("list")
- .alias("ls")
- .description("List all tokens")
- .action(listTokensAction);
-
- tokens
- .command("delete")
- .alias("rm")
- .description("Delete a token")
- .requiredOption("--id ", "Specify the token ID")
- .option("--force", "Force delete the token, skipping confirmation")
- .action(deleteTokenAction);
+ const tokens = program
+ .command("tokens")
+ .description("Manage account access tokens.");
+
+ tokens
+ .command("create")
+ .description("Create a new access token")
+ .action(createTokenAction);
+
+ tokens
+ .command("list")
+ .alias("ls")
+ .description("List all tokens")
+ .action(listTokensAction);
+
+ tokens
+ .command("delete")
+ .alias("rm")
+ .description("Delete a token")
+ .requiredOption("--id ", "Specify the token ID")
+ .option("--force", "Force delete the token, skipping confirmation")
+ .action(deleteTokenAction);
}
// --
interface TokenObject {
- id: string;
- token?: string;
- name?: string;
- description?: string;
- is_sandbox: boolean;
- created_at: string;
- last_active_at: string;
- expires_at: string;
- origin_client: string;
- is_system: boolean;
+ id: string;
+ token?: string;
+ name?: string;
+ description?: string;
+ is_sandbox: boolean;
+ created_at: string;
+ last_active_at: string;
+ expires_at: string;
+ origin_client: string;
+ is_system: boolean;
}
interface PostTokenRequestBody {
- expires_in_seconds: number;
- name?: string;
- description?: string;
- origin_client: string;
+ expires_in_seconds: number;
+ name?: string;
+ description?: string;
+ origin_client: string;
}
async function createTokenAction() {
- const loggedIn = await isLoggedIn();
- if (!loggedIn) {
- logLoginMessageAndQuit();
- }
-
- // collect duration
- const expiresInSeconds = await select({
- message: "Select token expiration:",
- default: TOKEN_EXPIRATION_SECONDS.IN_100_YEARS,
- choices: [
- { name: "1 week", value: TOKEN_EXPIRATION_SECONDS.IN_7_DAYS },
- { name: "2 weeks", value: TOKEN_EXPIRATION_SECONDS.IN_14_DAYS },
- { name: "1 month", value: TOKEN_EXPIRATION_SECONDS.IN_30_DAYS },
- { name: "2 months", value: TOKEN_EXPIRATION_SECONDS.IN_60_DAYS },
- { name: "3 months", value: TOKEN_EXPIRATION_SECONDS.IN_90_DAYS },
- {
- name: "Never Expire",
- value: TOKEN_EXPIRATION_SECONDS.IN_100_YEARS,
- },
- ],
- });
-
- // collect name & description
- const name = await input({
- message: `Name your token ${chalk.gray("(optional, ↵ to skip)")}:`,
- default: "",
- });
- const description = await input({
- message: `Description for your token ${chalk.gray(
- "(optional, ↵ to skip)",
- )}:`,
- default: "",
- });
-
- // generate token
- console.log("\n");
- const loadingSpinner = ora("Generating token").start();
-
- const response = await fetch(await getApiUrl("tokens_create"), {
- method: "POST",
- body: JSON.stringify({
- expires_in_seconds: expiresInSeconds,
- name,
- description,
- origin_client: "cli",
- } as PostTokenRequestBody),
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${await getAuthToken()}`,
- },
- });
- if (!response.ok) {
- if (response.status === 401) {
- await logSessionTokenExpiredAndQuit();
- }
-
- // TODO: handle specific errors
-
- loadingSpinner.fail("Failed to create token");
- process.exit(1);
- }
-
- // display token to user
- const data = await response.json();
- loadingSpinner.succeed(chalk.gray("Access token created 🎉"));
- // @ts-ignore: Deno has narrower types for fetch responses, but we know this code works atm.
- console.log(chalk.green(data.token) + "\n");
-
- // tell them they will set this in the Authorization header
- console.log(
- `${chalk.gray(`Pass this in the 'Authorization' header of API requests:`)}`,
- );
- console.log(
- [
- chalk.gray("{ "),
- chalk.white("Authorization"),
- chalk.gray(": "),
- chalk.green('"Bearer '),
- chalk.magenta(""),
- chalk.green('"'),
- chalk.gray(" }"),
- ].join(""),
- );
- console.log("\n");
-
- // give them a sample curl
- const pingUrl = await getApiUrl("ping");
- console.log(`${chalk.gray("Here is a sample curl to get your started:")}`);
- console.log(
- chalk.white(
- // @ts-ignore: Deno has narrower types for fetch responses, but we know this code works atm.
- `curl --request GET --url ${pingUrl} --header 'Authorization: Bearer ${data.token}'`,
- ),
- );
- console.log("\n");
-
- // tip user on other commands
- const base = getCommandBase();
-
- const table = new Table({
- colWidths: [20, 30],
- });
- table.push(["View All Tokens", chalk.magenta(`${base} tokens list`)]);
- table.push(["Delete a Token", chalk.magenta(`${base} tokens delete`)]);
-
- console.log(`${chalk.gray("And other commands you can try:")}`);
- console.log(table.toString());
-
- process.exit(0);
+ const loggedIn = await isLoggedIn();
+ if (!loggedIn) {
+ logLoginMessageAndQuit();
+ }
+
+ // collect duration
+ const expiresInSeconds = await select({
+ message: "Select token expiration:",
+ default: TOKEN_EXPIRATION_SECONDS.IN_100_YEARS,
+ choices: [
+ { name: "1 week", value: TOKEN_EXPIRATION_SECONDS.IN_7_DAYS },
+ { name: "2 weeks", value: TOKEN_EXPIRATION_SECONDS.IN_14_DAYS },
+ { name: "1 month", value: TOKEN_EXPIRATION_SECONDS.IN_30_DAYS },
+ { name: "2 months", value: TOKEN_EXPIRATION_SECONDS.IN_60_DAYS },
+ { name: "3 months", value: TOKEN_EXPIRATION_SECONDS.IN_90_DAYS },
+ {
+ name: "Never Expire",
+ value: TOKEN_EXPIRATION_SECONDS.IN_100_YEARS,
+ },
+ ],
+ });
+
+ // collect name & description
+ const name = await input({
+ message: `Name your token ${chalk.gray("(optional, ↵ to skip)")}:`,
+ default: "",
+ });
+ const description = await input({
+ message: `Description for your token ${
+ chalk.gray(
+ "(optional, ↵ to skip)",
+ )
+ }:`,
+ default: "",
+ });
+
+ // generate token
+ console.log("\n");
+ const loadingSpinner = ora("Generating token").start();
+
+ const response = await fetch(await getApiUrl("tokens_create"), {
+ method: "POST",
+ body: JSON.stringify({
+ expires_in_seconds: expiresInSeconds,
+ name,
+ description,
+ origin_client: "cli",
+ } as PostTokenRequestBody),
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${await getAuthToken()}`,
+ },
+ });
+ if (!response.ok) {
+ if (response.status === 401) {
+ await logSessionTokenExpiredAndQuit();
+ }
+
+ // TODO: handle specific errors
+
+ loadingSpinner.fail("Failed to create token");
+ process.exit(1);
+ }
+
+ // display token to user
+ const data = await response.json();
+ loadingSpinner.succeed(chalk.gray("Access token created 🎉"));
+ // @ts-ignore: Deno has narrower types for fetch responses, but we know this code works atm.
+ console.log(chalk.green(data.token) + "\n");
+
+ // tell them they will set this in the Authorization header
+ console.log(
+ `${chalk.gray(`Pass this in the 'Authorization' header of API requests:`)}`,
+ );
+ console.log(
+ [
+ chalk.gray("{ "),
+ chalk.white("Authorization"),
+ chalk.gray(": "),
+ chalk.green('"Bearer '),
+ chalk.magenta(""),
+ chalk.green('"'),
+ chalk.gray(" }"),
+ ].join(""),
+ );
+ console.log("\n");
+
+ // give them a sample curl
+ const pingUrl = await getApiUrl("ping");
+ console.log(`${chalk.gray("Here is a sample curl to get your started:")}`);
+ console.log(
+ chalk.white(
+ // @ts-ignore: Deno has narrower types for fetch responses, but we know this code works atm.
+ `curl --request GET --url ${pingUrl} --header 'Authorization: Bearer ${data.token}'`,
+ ),
+ );
+ console.log("\n");
+
+ // tip user on other commands
+ const base = getCommandBase();
+
+ const table = new Table({
+ colWidths: [20, 30],
+ });
+ table.push(["View All Tokens", chalk.magenta(`${base} tokens list`)]);
+ table.push(["Delete a Token", chalk.magenta(`${base} tokens delete`)]);
+
+ console.log(`${chalk.gray("And other commands you can try:")}`);
+ console.log(table.toString());
+
+ process.exit(0);
}
// --
async function listTokensAction() {
- const loggedIn = await isLoggedIn();
- if (!loggedIn) {
- logLoginMessageAndQuit();
- }
-
- const loadingSpinner = ora("Fetching tokens...").start();
-
- // fetch tokens
- const tokensListUrl = await getApiUrl("tokens_list");
- const response = await fetch(tokensListUrl, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${await getAuthToken()}`,
- },
- });
- if (!response.ok) {
- if (response.status === 401) {
- await logSessionTokenExpiredAndQuit();
- }
-
- // TODO: handle specific errors
-
- loadingSpinner.fail("Failed to fetch tokens");
- process.exit(1);
- }
- loadingSpinner.stop(); // hide spinner
-
- // show account tokens
- const responseBody = await response.json();
- // @ts-ignore: Deno has narrower types for fetch responses, but we know this code works atm.
- const tokens = responseBody.data as Array;
-
- // show empty table if no tokens
- if (tokens.length === 0) {
- const table = new Table({
- head: [chalk.gray("Access Tokens")],
- colWidths: [50],
- });
- table.push([
- { colSpan: 1, content: "No access tokens found", hAlign: "center" },
- ]);
- console.log(table.toString() + "\n");
-
- // prompt user that they can generate one
- const base = getCommandBase();
- console.log(
- chalk.gray("Generate your first token with: ") +
- chalk.magenta(`${base} tokens create`),
- );
-
- process.exit(0);
- }
-
- // display table
- const tokensTable = new Table({
- head: [
- chalk.gray("Token ID"),
- chalk.gray("Name"),
- chalk.gray("Last Active At"),
- chalk.gray("Expires"),
- ],
- colWidths: [40, 15, 25, 25],
- });
- for (const token of tokens) {
- tokensTable.push([
- chalk.gray(token.id),
- token.name ? token.name : chalk.gray("(empty)"),
- chalk.green(formatDate(token.last_active_at)),
- chalk.white(formatDate(token.expires_at)),
- ]);
- }
- console.log(tokensTable.toString());
-
- process.exit(0);
+ const loggedIn = await isLoggedIn();
+ if (!loggedIn) {
+ logLoginMessageAndQuit();
+ }
+
+ const loadingSpinner = ora("Fetching tokens...").start();
+
+ // fetch tokens
+ const tokensListUrl = await getApiUrl("tokens_list");
+ const response = await fetch(tokensListUrl, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${await getAuthToken()}`,
+ },
+ });
+ if (!response.ok) {
+ if (response.status === 401) {
+ await logSessionTokenExpiredAndQuit();
+ }
+
+ // TODO: handle specific errors
+
+ loadingSpinner.fail("Failed to fetch tokens");
+ process.exit(1);
+ }
+ loadingSpinner.stop(); // hide spinner
+
+ // show account tokens
+ const responseBody = await response.json();
+ // @ts-ignore: Deno has narrower types for fetch responses, but we know this code works atm.
+ const tokens = responseBody.data as Array;
+
+ // show empty table if no tokens
+ if (tokens.length === 0) {
+ const table = new Table({
+ head: [chalk.gray("Access Tokens")],
+ colWidths: [50],
+ });
+ table.push([
+ { colSpan: 1, content: "No access tokens found", hAlign: "center" },
+ ]);
+ console.log(table.toString() + "\n");
+
+ // prompt user that they can generate one
+ const base = getCommandBase();
+ console.log(
+ chalk.gray("Generate your first token with: ") +
+ chalk.magenta(`${base} tokens create`),
+ );
+
+ process.exit(0);
+ }
+
+ // display table
+ const tokensTable = new Table({
+ head: [
+ chalk.gray("Token ID"),
+ chalk.gray("Name"),
+ chalk.gray("Last Active At"),
+ chalk.gray("Expires"),
+ ],
+ colWidths: [40, 15, 25, 25],
+ });
+ for (const token of tokens) {
+ tokensTable.push([
+ chalk.gray(token.id),
+ token.name ? token.name : chalk.gray("(empty)"),
+ chalk.green(formatDate(token.last_active_at)),
+ chalk.white(formatDate(token.expires_at)),
+ ]);
+ }
+ console.log(tokensTable.toString());
+
+ process.exit(0);
}
function formatDate(isoString: string): string {
- return dayjs(isoString).format("MMM D, YYYY [at] h:mma").toLowerCase();
+ return dayjs(isoString).format("MMM D, YYYY [at] h:mma").toLowerCase();
}
// --
async function deleteTokenAction({
- id,
- force,
+ id,
+ force,
}: {
- id: string;
- force?: boolean;
+ id: string;
+ force?: boolean;
}) {
- const loggedIn = await isLoggedIn();
- if (!loggedIn) {
- logLoginMessageAndQuit();
- }
-
- if (force) {
- await deleteTokenById(id);
- }
-
- const deleteTokenConfirmed = await confirm({
- message: `Are you sure you want to delete this token? ${chalk.gray(
- "(it will stop working immediately.)",
- )}`,
- default: false,
- });
- if (!deleteTokenConfirmed) {
- process.exit(0);
- } else {
- const verySureConfirmed = await confirm({
- message:
- chalk.red("Very sure?") + " " + chalk.gray("(just double-checking)"),
- default: false,
- });
-
- if (!verySureConfirmed) {
- process.exit(0);
- } else {
- await deleteTokenById(id);
- }
- }
+ const loggedIn = await isLoggedIn();
+ if (!loggedIn) {
+ logLoginMessageAndQuit();
+ }
+
+ if (force) {
+ await deleteTokenById(id);
+ }
+
+ const deleteTokenConfirmed = await confirm({
+ message: `Are you sure you want to delete this token? ${
+ chalk.gray(
+ "(it will stop working immediately.)",
+ )
+ }`,
+ default: false,
+ });
+ if (!deleteTokenConfirmed) {
+ process.exit(0);
+ } else {
+ const verySureConfirmed = await confirm({
+ message: chalk.red("Very sure?") + " " +
+ chalk.gray("(just double-checking)"),
+ default: false,
+ });
+
+ if (!verySureConfirmed) {
+ process.exit(0);
+ } else {
+ await deleteTokenById(id);
+ }
+ }
}
async function deleteTokenById(id: string) {
- const deleteTokenByIdUrl = await getApiUrl("tokens_delete_by_id", { id });
- const loadingSpinner = ora("Deleting token...").start();
-
- const response = await fetch(deleteTokenByIdUrl, {
- method: "DELETE",
- headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
- },
- });
- if (!response.ok) {
- if (response.status === 401) {
- await logSessionTokenExpiredAndQuit();
- }
-
- const error = await response.json();
- // @ts-ignore: Deno has narrower types for fetch responses, but we know this code works atm.
- if (error.code === "token.not_found") {
- loadingSpinner.fail("Token not found");
- process.exit(1);
- }
-
- // TODO: handle more specific errors
-
- // generic catch-all
- loadingSpinner.fail("Failed to delete token");
- process.exit(1);
- }
-
- loadingSpinner.stop();
- console.log(chalk.gray("Token deleted. 🧼"));
-
- process.exit(0);
+ const deleteTokenByIdUrl = await getApiUrl("tokens_delete_by_id", { id });
+ const loadingSpinner = ora("Deleting token...").start();
+
+ const response = await fetch(deleteTokenByIdUrl, {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ },
+ });
+ if (!response.ok) {
+ if (response.status === 401) {
+ await logSessionTokenExpiredAndQuit();
+ }
+
+ const error = await response.json();
+ // @ts-ignore: Deno has narrower types for fetch responses, but we know this code works atm.
+ if (error.code === "token.not_found") {
+ loadingSpinner.fail("Token not found");
+ process.exit(1);
+ }
+
+ // TODO: handle more specific errors
+
+ // generic catch-all
+ loadingSpinner.fail("Failed to delete token");
+ process.exit(1);
+ }
+
+ loadingSpinner.stop();
+ console.log(chalk.gray("Token deleted. 🧼"));
+
+ process.exit(0);
}