@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 +218 -79
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
|
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
|
-
|
|
969
|
-
} else {
|
|
970
|
-
token = login.token;
|
|
1117
|
+
return completeTwoFactor(spinner, baseUrl, login);
|
|
971
1118
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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.
|
|
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);
|