@videan/cli 0.4.3 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -872,6 +872,104 @@ function installPromptCrashGuard() {
872
872
  });
873
873
  }
874
874
 
875
+ // src/browser-login.ts
876
+ import http from "node:http";
877
+ import crypto from "node:crypto";
878
+ import { spawn } from "node:child_process";
879
+ var LANDING = (title, sub) => `<!doctype html>
880
+ <meta charset="utf-8"><title>${title}</title>
881
+ <body style="font-family:system-ui;background:#101014;color:#e8e8ea;display:grid;place-items:center;min-height:90vh">
882
+ <div style="text-align:center"><h1 style="font-weight:600">${title}</h1><p style="color:#9a9aa2">${sub}</p></div>`;
883
+ function createCallbackServer(state, timeoutMs = 5 * 60 * 1e3, redirectOrigin) {
884
+ return new Promise((resolveServer, rejectServer) => {
885
+ let settle;
886
+ const waitForToken = new Promise((resolve, reject) => {
887
+ settle = { resolve, reject };
888
+ });
889
+ waitForToken.catch(() => {
890
+ });
891
+ const server = http.createServer((req, res) => {
892
+ const url = new URL(req.url || "/", "http://127.0.0.1");
893
+ if (url.pathname !== "/callback") {
894
+ res.writeHead(404).end();
895
+ return;
896
+ }
897
+ if (url.searchParams.get("state") !== state) {
898
+ res.writeHead(403, { "Content-Type": "text/html" });
899
+ res.end(LANDING("Request refused", "This authorization was not started by this CLI."));
900
+ return;
901
+ }
902
+ const land = (done2, status, title, sub) => {
903
+ if (redirectOrigin) {
904
+ res.writeHead(302, { Location: `${redirectOrigin}/cli-auth?done=${done2}` });
905
+ res.end();
906
+ } else {
907
+ res.writeHead(status, { "Content-Type": "text/html" });
908
+ res.end(LANDING(title, sub));
909
+ }
910
+ };
911
+ if (url.searchParams.get("error")) {
912
+ land("denied", 200, "Login cancelled", "You can close this tab.");
913
+ finish(() => settle.reject(new Error("Authorization was declined in the browser.")));
914
+ return;
915
+ }
916
+ const token = url.searchParams.get("token");
917
+ if (!token) {
918
+ res.writeHead(400, { "Content-Type": "text/html" });
919
+ res.end(LANDING("Something went wrong", "No token arrived. Run `videan login` again."));
920
+ finish(() => settle.reject(new Error("The browser sent no token.")));
921
+ return;
922
+ }
923
+ land("ok", 200, "You're logged in", "Return to your terminal \u2014 you can close this tab.");
924
+ finish(() => settle.resolve(token));
925
+ });
926
+ const timer = setTimeout(() => {
927
+ finish(() => settle.reject(new Error("Timed out waiting for the browser (5 minutes).")));
928
+ }, timeoutMs);
929
+ timer.unref();
930
+ let done = false;
931
+ function finish(outcome) {
932
+ if (done) return;
933
+ done = true;
934
+ clearTimeout(timer);
935
+ outcome();
936
+ setTimeout(() => server.close(), 100).unref();
937
+ }
938
+ server.on("error", (e) => rejectServer(e));
939
+ server.listen(0, "127.0.0.1", () => {
940
+ const address = server.address();
941
+ if (!address || typeof address === "string") {
942
+ rejectServer(new Error("Could not bind a loopback port."));
943
+ return;
944
+ }
945
+ resolveServer({
946
+ port: address.port,
947
+ waitForToken,
948
+ close: () => finish(() => settle.reject(new Error("closed")))
949
+ });
950
+ });
951
+ });
952
+ }
953
+ function randomState() {
954
+ return crypto.randomBytes(16).toString("hex");
955
+ }
956
+ function consoleOriginFor(baseUrl) {
957
+ try {
958
+ const url = new URL(baseUrl);
959
+ if (url.protocol !== "https:" || !url.hostname.startsWith("api.")) return null;
960
+ return `https://${url.hostname.slice(4)}`;
961
+ } catch {
962
+ return null;
963
+ }
964
+ }
965
+ function openBrowser(url) {
966
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
967
+ try {
968
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
969
+ } catch {
970
+ }
971
+ }
972
+
875
973
  // src/commands/login.ts
876
974
  async function completeTwoFactor(spinner, baseUrl, login) {
877
975
  if (!process.stdin.isTTY) {
@@ -917,35 +1015,86 @@ async function completeTwoFactor(spinner, baseUrl, login) {
917
1015
  spinner.fail("Too many wrong codes. Run `videan login` again to get a new one.");
918
1016
  process.exit(1);
919
1017
  }
920
- var loginCommand = new Command("login").description("Log in to Cloud").option("-e, --email <email>", "Email (skips the prompt)").option("-p, --password <password>", "Password (skips the prompt; prefer VIDEAN_PASSWORD)").option("-o, --org <id>", "Organization to scope the key to (skips the picker)").option("--api-url <url>", "API base URL to use and remember (default: the public API)").action(async (opts) => {
921
- const password = opts.password || process.env.VIDEAN_PASSWORD || process.env.CLOUD_PASSWORD;
922
- if ((!opts.email || !password) && !process.stdin.isTTY) {
923
- console.error(
924
- chalk2.red(
925
- "login needs a terminal to prompt. Non-interactively, pass --email and --password (or VIDEAN_PASSWORD), or skip login entirely by setting VIDEAN_API_KEY."
926
- )
927
- );
1018
+ async function pickOrg(spinner, session, orgOpt, resume) {
1019
+ let orgs;
1020
+ try {
1021
+ orgs = await session.orgs.list();
1022
+ } catch (e) {
1023
+ spinner.fail(`Logged in, but could not list organizations: ${e?.message}`);
928
1024
  process.exit(1);
929
1025
  }
930
- const prompts = [
931
- ...opts.email ? [] : [{ type: "input", name: "email", message: "Email:" }],
932
- ...password ? [] : [{ type: "password", name: "password", message: "Password:" }]
933
- ];
934
- const answers = {
935
- email: opts.email,
936
- password,
937
- ...prompts.length ? await inquirer2.prompt(prompts) : {}
938
- };
939
- if (opts.apiUrl) setBaseUrl(opts.apiUrl.replace(/\/$/, ""));
940
- const baseUrl = (getBaseUrl() || DEFAULT_API_URL2).replace(/\/$/, "");
941
- const spinner = ora("Logging in...").start();
942
- let token;
1026
+ if (orgOpt) {
1027
+ const match = orgs.find((o) => o.id === orgOpt || o.name === orgOpt);
1028
+ if (!match) {
1029
+ spinner.fail(`No organization ${orgOpt} for this account.`);
1030
+ process.exit(1);
1031
+ }
1032
+ return match.id;
1033
+ }
1034
+ if (orgs.length > 1 && !process.stdin.isTTY) {
1035
+ spinner.fail(`Several organizations (${orgs.map((o) => o.id).join(", ")}). Pass --org <id>.`);
1036
+ process.exit(1);
1037
+ }
1038
+ if (orgs.length > 1) {
1039
+ spinner.stop();
1040
+ const pick = await inquirer2.prompt([
1041
+ {
1042
+ type: "list",
1043
+ name: "orgId",
1044
+ message: "Organization:",
1045
+ choices: orgs.map((o) => ({ name: `${o.name} (${o.id})`, value: o.id }))
1046
+ }
1047
+ ]);
1048
+ spinner.start(resume);
1049
+ return pick.orgId;
1050
+ }
1051
+ return orgs[0]?.id;
1052
+ }
1053
+ async function finishWithToken(spinner, baseUrl, token, orgOpt) {
1054
+ const session = new CloudClient({ apiKey: token, baseUrl });
1055
+ const orgId = await pickOrg(spinner, session, orgOpt, "Creating CLI key...");
1056
+ if (!orgId) {
1057
+ setApiKey(token);
1058
+ spinner.warn(
1059
+ chalk2.yellow("Logged in, but you have no organization yet. Run `videan orgs create <name>`.")
1060
+ );
1061
+ return;
1062
+ }
1063
+ try {
1064
+ const key = await session.apiKeys.create({
1065
+ name: `cli@${os.hostname()}`,
1066
+ orgId
1067
+ });
1068
+ setApiKey(key.key);
1069
+ setDefaultOrg(orgId);
1070
+ spinner.succeed(chalk2.green("Logged in."));
1071
+ console.log(chalk2.gray(` Org: ${orgId}`));
1072
+ console.log(chalk2.gray(` Key: ${key.keyPrefix}\u2026 (revoke with \`videan keys revoke ${key.id}\`)`));
1073
+ } catch (e) {
1074
+ setApiKey(token);
1075
+ setDefaultOrg(orgId);
1076
+ spinner.warn(
1077
+ chalk2.yellow(`Logged in with a temporary session (key creation failed: ${e?.message}).`)
1078
+ );
1079
+ }
1080
+ }
1081
+ async function browserLogin(consoleOrigin) {
1082
+ const state = randomState();
1083
+ const server = await createCallbackServer(state, void 0, consoleOrigin);
1084
+ const url = `${consoleOrigin}/cli-auth?port=${server.port}&state=${state}`;
1085
+ console.log("Opening your browser to sign in (password, Google or SSO all work):");
1086
+ console.log(chalk2.cyan(` ${url}`));
1087
+ console.log(chalk2.gray(" Waiting for the browser\u2026 (Ctrl-C to cancel)"));
1088
+ openBrowser(url);
1089
+ return server.waitForToken;
1090
+ }
1091
+ async function passwordLogin(spinner, baseUrl, email, password) {
943
1092
  let login;
944
1093
  try {
945
1094
  const response = await fetch(`${baseUrl}/auth/login`, {
946
1095
  method: "POST",
947
1096
  headers: { "Content-Type": "application/json" },
948
- body: JSON.stringify({ email: answers.email, password: answers.password })
1097
+ body: JSON.stringify({ email, password })
949
1098
  });
950
1099
  if (!response.ok) {
951
1100
  if (response.status === 401) {
@@ -965,69 +1114,59 @@ var loginCommand = new Command("login").description("Log in to Cloud").option("-
965
1114
  process.exit(1);
966
1115
  }
967
1116
  if (login.requires_2fa) {
968
- token = await completeTwoFactor(spinner, baseUrl, login);
969
- } else {
970
- token = login.token;
1117
+ return completeTwoFactor(spinner, baseUrl, login);
971
1118
  }
972
- const session = new CloudClient({ apiKey: token, baseUrl });
973
- let orgId;
974
- try {
975
- const orgs = await session.orgs.list();
976
- if (opts.org) {
977
- const match = orgs.find((o) => o.id === opts.org || o.name === opts.org);
978
- if (!match) {
979
- spinner.fail(`No organization ${opts.org} for this account.`);
980
- process.exit(1);
981
- }
982
- orgId = match.id;
983
- } else if (orgs.length > 1 && !process.stdin.isTTY) {
984
- spinner.fail(
985
- `Several organizations (${orgs.map((o) => o.id).join(", ")}). Pass --org <id>.`
986
- );
1119
+ return login.token;
1120
+ }
1121
+ var loginCommand = new Command("login").description("Log in to Cloud (opens your browser; --email/--password for the prompt flow)").option("-e, --email <email>", "Email \u2014 switches to the email/password flow").option("-p, --password <password>", "Password (skips the prompt; prefer VIDEAN_PASSWORD)").option("-o, --org <id>", "Organization to scope the key to (skips the picker)").option("-k, --key <api-key>", "Use an existing API key instead of signing in").option("--no-browser", "Never open a browser; prompt for email and password").option("--api-url <url>", "API base URL to use and remember (default: the public API)").action(async (opts) => {
1122
+ if (opts.apiUrl) setBaseUrl(opts.apiUrl.replace(/\/$/, ""));
1123
+ const baseUrl = (getBaseUrl() || DEFAULT_API_URL2).replace(/\/$/, "");
1124
+ if (opts.key) {
1125
+ const spinner2 = ora("Checking the key...").start();
1126
+ const session = new CloudClient({ apiKey: opts.key, baseUrl });
1127
+ const orgId = await pickOrg(spinner2, session, opts.org, "Saving...");
1128
+ setApiKey(opts.key);
1129
+ if (orgId) setDefaultOrg(orgId);
1130
+ spinner2.succeed(chalk2.green("Logged in with the provided key."));
1131
+ if (orgId) console.log(chalk2.gray(` Org: ${orgId}`));
1132
+ return;
1133
+ }
1134
+ const password = opts.password || process.env.VIDEAN_PASSWORD || process.env.CLOUD_PASSWORD;
1135
+ const wantsPasswordFlow = Boolean(opts.email || password || opts.browser === false);
1136
+ const consoleOrigin = consoleOriginFor(baseUrl);
1137
+ if (!wantsPasswordFlow && process.stdin.isTTY && consoleOrigin) {
1138
+ let token2;
1139
+ try {
1140
+ token2 = await browserLogin(consoleOrigin);
1141
+ } catch (e) {
1142
+ console.error(chalk2.red(`Browser sign-in failed: ${e?.message || e}`));
1143
+ console.error(chalk2.gray(" For the prompt flow instead: videan login --no-browser"));
987
1144
  process.exit(1);
988
- } else if (orgs.length > 1) {
989
- spinner.stop();
990
- const pick = await inquirer2.prompt([
991
- {
992
- type: "list",
993
- name: "orgId",
994
- message: "Organization:",
995
- choices: orgs.map((o) => ({ name: `${o.name} (${o.id})`, value: o.id }))
996
- }
997
- ]);
998
- orgId = pick.orgId;
999
- spinner.start("Creating CLI key...");
1000
- } else {
1001
- orgId = orgs[0]?.id;
1002
1145
  }
1003
- } catch (e) {
1004
- spinner.fail(`Logged in, but could not list organizations: ${e?.message}`);
1005
- process.exit(1);
1006
- }
1007
- if (!orgId) {
1008
- setApiKey(token);
1009
- spinner.warn(
1010
- chalk2.yellow("Logged in, but you have no organization yet. Run `videan orgs create <name>`.")
1011
- );
1146
+ const spinner2 = ora("Finishing up...").start();
1147
+ await finishWithToken(spinner2, baseUrl, token2, opts.org);
1012
1148
  return;
1013
1149
  }
1014
- try {
1015
- const key = await session.apiKeys.create({
1016
- name: `cli@${os.hostname()}`,
1017
- orgId
1018
- });
1019
- setApiKey(key.key);
1020
- setDefaultOrg(orgId);
1021
- spinner.succeed(chalk2.green("Logged in."));
1022
- console.log(chalk2.gray(` Org: ${orgId}`));
1023
- console.log(chalk2.gray(` Key: ${key.keyPrefix}\u2026 (revoke with \`videan keys revoke ${key.id}\`)`));
1024
- } catch (e) {
1025
- setApiKey(token);
1026
- setDefaultOrg(orgId);
1027
- spinner.warn(
1028
- chalk2.yellow(`Logged in with a temporary session (key creation failed: ${e?.message}).`)
1150
+ if ((!opts.email || !password) && !process.stdin.isTTY) {
1151
+ console.error(
1152
+ chalk2.red(
1153
+ "login needs a terminal to prompt. Non-interactively, pass --email and --password (or VIDEAN_PASSWORD), pass --key <api-key>, or skip login entirely by setting VIDEAN_API_KEY."
1154
+ )
1029
1155
  );
1156
+ process.exit(1);
1030
1157
  }
1158
+ const prompts = [
1159
+ ...opts.email ? [] : [{ type: "input", name: "email", message: "Email:" }],
1160
+ ...password ? [] : [{ type: "password", name: "password", message: "Password:" }]
1161
+ ];
1162
+ const answers = {
1163
+ email: opts.email,
1164
+ password,
1165
+ ...prompts.length ? await inquirer2.prompt(prompts) : {}
1166
+ };
1167
+ const spinner = ora("Logging in...").start();
1168
+ const token = await passwordLogin(spinner, baseUrl, answers.email, answers.password);
1169
+ await finishWithToken(spinner, baseUrl, token, opts.org);
1031
1170
  });
1032
1171
 
1033
1172
  // src/commands/logout.ts
@@ -2388,7 +2527,7 @@ function period(start, end) {
2388
2527
  // src/index.ts
2389
2528
  installPromptCrashGuard();
2390
2529
  var program = new Command18();
2391
- program.name("videan").description("Videan CLI \u2014 deploy apps in seconds").version("0.4.3");
2530
+ program.name("videan").description("Videan CLI \u2014 deploy apps in seconds").version("0.5.1");
2392
2531
  program.addCommand(loginCommand);
2393
2532
  program.addCommand(logoutCommand);
2394
2533
  program.addCommand(initCommand);