@videan/cli 0.4.2 → 0.5.0
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 +212 -80
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -783,7 +783,7 @@ function visibilityOf(value) {
|
|
|
783
783
|
}
|
|
784
784
|
var VISIBILITY_LABELS = {
|
|
785
785
|
private: "Only people with access",
|
|
786
|
-
org: "Anyone in
|
|
786
|
+
org: "Anyone in your organization",
|
|
787
787
|
authenticated: "Anyone signed in to Videan",
|
|
788
788
|
public: "Anyone with the link"
|
|
789
789
|
};
|
|
@@ -872,6 +872,97 @@ 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) {
|
|
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
|
+
if (url.searchParams.get("error")) {
|
|
903
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
904
|
+
res.end(LANDING("Login cancelled", "You can close this tab."));
|
|
905
|
+
finish(() => settle.reject(new Error("Authorization was declined in the browser.")));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const token = url.searchParams.get("token");
|
|
909
|
+
if (!token) {
|
|
910
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
911
|
+
res.end(LANDING("Something went wrong", "No token arrived. Run `videan login` again."));
|
|
912
|
+
finish(() => settle.reject(new Error("The browser sent no token.")));
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
916
|
+
res.end(LANDING("You're logged in", "Return to your terminal \u2014 you can close this tab."));
|
|
917
|
+
finish(() => settle.resolve(token));
|
|
918
|
+
});
|
|
919
|
+
const timer = setTimeout(() => {
|
|
920
|
+
finish(() => settle.reject(new Error("Timed out waiting for the browser (5 minutes).")));
|
|
921
|
+
}, timeoutMs);
|
|
922
|
+
timer.unref();
|
|
923
|
+
let done = false;
|
|
924
|
+
function finish(outcome) {
|
|
925
|
+
if (done) return;
|
|
926
|
+
done = true;
|
|
927
|
+
clearTimeout(timer);
|
|
928
|
+
outcome();
|
|
929
|
+
setTimeout(() => server.close(), 100).unref();
|
|
930
|
+
}
|
|
931
|
+
server.on("error", (e) => rejectServer(e));
|
|
932
|
+
server.listen(0, "127.0.0.1", () => {
|
|
933
|
+
const address = server.address();
|
|
934
|
+
if (!address || typeof address === "string") {
|
|
935
|
+
rejectServer(new Error("Could not bind a loopback port."));
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
resolveServer({
|
|
939
|
+
port: address.port,
|
|
940
|
+
waitForToken,
|
|
941
|
+
close: () => finish(() => settle.reject(new Error("closed")))
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
function randomState() {
|
|
947
|
+
return crypto.randomBytes(16).toString("hex");
|
|
948
|
+
}
|
|
949
|
+
function consoleOriginFor(baseUrl) {
|
|
950
|
+
try {
|
|
951
|
+
const url = new URL(baseUrl);
|
|
952
|
+
if (url.protocol !== "https:" || !url.hostname.startsWith("api.")) return null;
|
|
953
|
+
return `https://${url.hostname.slice(4)}`;
|
|
954
|
+
} catch {
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function openBrowser(url) {
|
|
959
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
960
|
+
try {
|
|
961
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
962
|
+
} catch {
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
875
966
|
// src/commands/login.ts
|
|
876
967
|
async function completeTwoFactor(spinner, baseUrl, login) {
|
|
877
968
|
if (!process.stdin.isTTY) {
|
|
@@ -917,35 +1008,86 @@ async function completeTwoFactor(spinner, baseUrl, login) {
|
|
|
917
1008
|
spinner.fail("Too many wrong codes. Run `videan login` again to get a new one.");
|
|
918
1009
|
process.exit(1);
|
|
919
1010
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
)
|
|
927
|
-
);
|
|
1011
|
+
async function pickOrg(spinner, session, orgOpt, resume) {
|
|
1012
|
+
let orgs;
|
|
1013
|
+
try {
|
|
1014
|
+
orgs = await session.orgs.list();
|
|
1015
|
+
} catch (e) {
|
|
1016
|
+
spinner.fail(`Logged in, but could not list organizations: ${e?.message}`);
|
|
928
1017
|
process.exit(1);
|
|
929
1018
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1019
|
+
if (orgOpt) {
|
|
1020
|
+
const match = orgs.find((o) => o.id === orgOpt || o.name === orgOpt);
|
|
1021
|
+
if (!match) {
|
|
1022
|
+
spinner.fail(`No organization ${orgOpt} for this account.`);
|
|
1023
|
+
process.exit(1);
|
|
1024
|
+
}
|
|
1025
|
+
return match.id;
|
|
1026
|
+
}
|
|
1027
|
+
if (orgs.length > 1 && !process.stdin.isTTY) {
|
|
1028
|
+
spinner.fail(`Several organizations (${orgs.map((o) => o.id).join(", ")}). Pass --org <id>.`);
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
}
|
|
1031
|
+
if (orgs.length > 1) {
|
|
1032
|
+
spinner.stop();
|
|
1033
|
+
const pick = await inquirer2.prompt([
|
|
1034
|
+
{
|
|
1035
|
+
type: "list",
|
|
1036
|
+
name: "orgId",
|
|
1037
|
+
message: "Organization:",
|
|
1038
|
+
choices: orgs.map((o) => ({ name: `${o.name} (${o.id})`, value: o.id }))
|
|
1039
|
+
}
|
|
1040
|
+
]);
|
|
1041
|
+
spinner.start(resume);
|
|
1042
|
+
return pick.orgId;
|
|
1043
|
+
}
|
|
1044
|
+
return orgs[0]?.id;
|
|
1045
|
+
}
|
|
1046
|
+
async function finishWithToken(spinner, baseUrl, token, orgOpt) {
|
|
1047
|
+
const session = new CloudClient({ apiKey: token, baseUrl });
|
|
1048
|
+
const orgId = await pickOrg(spinner, session, orgOpt, "Creating CLI key...");
|
|
1049
|
+
if (!orgId) {
|
|
1050
|
+
setApiKey(token);
|
|
1051
|
+
spinner.warn(
|
|
1052
|
+
chalk2.yellow("Logged in, but you have no organization yet. Run `videan orgs create <name>`.")
|
|
1053
|
+
);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
const key = await session.apiKeys.create({
|
|
1058
|
+
name: `cli@${os.hostname()}`,
|
|
1059
|
+
orgId
|
|
1060
|
+
});
|
|
1061
|
+
setApiKey(key.key);
|
|
1062
|
+
setDefaultOrg(orgId);
|
|
1063
|
+
spinner.succeed(chalk2.green("Logged in."));
|
|
1064
|
+
console.log(chalk2.gray(` Org: ${orgId}`));
|
|
1065
|
+
console.log(chalk2.gray(` Key: ${key.keyPrefix}\u2026 (revoke with \`videan keys revoke ${key.id}\`)`));
|
|
1066
|
+
} catch (e) {
|
|
1067
|
+
setApiKey(token);
|
|
1068
|
+
setDefaultOrg(orgId);
|
|
1069
|
+
spinner.warn(
|
|
1070
|
+
chalk2.yellow(`Logged in with a temporary session (key creation failed: ${e?.message}).`)
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
async function browserLogin(consoleOrigin) {
|
|
1075
|
+
const state = randomState();
|
|
1076
|
+
const server = await createCallbackServer(state);
|
|
1077
|
+
const url = `${consoleOrigin}/cli-auth?port=${server.port}&state=${state}`;
|
|
1078
|
+
console.log("Opening your browser to sign in (password, Google or SSO all work):");
|
|
1079
|
+
console.log(chalk2.cyan(` ${url}`));
|
|
1080
|
+
console.log(chalk2.gray(" Waiting for the browser\u2026 (Ctrl-C to cancel)"));
|
|
1081
|
+
openBrowser(url);
|
|
1082
|
+
return server.waitForToken;
|
|
1083
|
+
}
|
|
1084
|
+
async function passwordLogin(spinner, baseUrl, email, password) {
|
|
943
1085
|
let login;
|
|
944
1086
|
try {
|
|
945
1087
|
const response = await fetch(`${baseUrl}/auth/login`, {
|
|
946
1088
|
method: "POST",
|
|
947
1089
|
headers: { "Content-Type": "application/json" },
|
|
948
|
-
body: JSON.stringify({ email
|
|
1090
|
+
body: JSON.stringify({ email, password })
|
|
949
1091
|
});
|
|
950
1092
|
if (!response.ok) {
|
|
951
1093
|
if (response.status === 401) {
|
|
@@ -965,69 +1107,59 @@ var loginCommand = new Command("login").description("Log in to Cloud").option("-
|
|
|
965
1107
|
process.exit(1);
|
|
966
1108
|
}
|
|
967
1109
|
if (login.requires_2fa) {
|
|
968
|
-
|
|
969
|
-
} else {
|
|
970
|
-
token = login.token;
|
|
1110
|
+
return completeTwoFactor(spinner, baseUrl, login);
|
|
971
1111
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1112
|
+
return login.token;
|
|
1113
|
+
}
|
|
1114
|
+
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) => {
|
|
1115
|
+
if (opts.apiUrl) setBaseUrl(opts.apiUrl.replace(/\/$/, ""));
|
|
1116
|
+
const baseUrl = (getBaseUrl() || DEFAULT_API_URL2).replace(/\/$/, "");
|
|
1117
|
+
if (opts.key) {
|
|
1118
|
+
const spinner2 = ora("Checking the key...").start();
|
|
1119
|
+
const session = new CloudClient({ apiKey: opts.key, baseUrl });
|
|
1120
|
+
const orgId = await pickOrg(spinner2, session, opts.org, "Saving...");
|
|
1121
|
+
setApiKey(opts.key);
|
|
1122
|
+
if (orgId) setDefaultOrg(orgId);
|
|
1123
|
+
spinner2.succeed(chalk2.green("Logged in with the provided key."));
|
|
1124
|
+
if (orgId) console.log(chalk2.gray(` Org: ${orgId}`));
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const password = opts.password || process.env.VIDEAN_PASSWORD || process.env.CLOUD_PASSWORD;
|
|
1128
|
+
const wantsPasswordFlow = Boolean(opts.email || password || opts.browser === false);
|
|
1129
|
+
const consoleOrigin = consoleOriginFor(baseUrl);
|
|
1130
|
+
if (!wantsPasswordFlow && process.stdin.isTTY && consoleOrigin) {
|
|
1131
|
+
let token2;
|
|
1132
|
+
try {
|
|
1133
|
+
token2 = await browserLogin(consoleOrigin);
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
console.error(chalk2.red(`Browser sign-in failed: ${e?.message || e}`));
|
|
1136
|
+
console.error(chalk2.gray(" For the prompt flow instead: videan login --no-browser"));
|
|
987
1137
|
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
1138
|
}
|
|
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
|
-
);
|
|
1139
|
+
const spinner2 = ora("Finishing up...").start();
|
|
1140
|
+
await finishWithToken(spinner2, baseUrl, token2, opts.org);
|
|
1012
1141
|
return;
|
|
1013
1142
|
}
|
|
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}).`)
|
|
1143
|
+
if ((!opts.email || !password) && !process.stdin.isTTY) {
|
|
1144
|
+
console.error(
|
|
1145
|
+
chalk2.red(
|
|
1146
|
+
"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."
|
|
1147
|
+
)
|
|
1029
1148
|
);
|
|
1149
|
+
process.exit(1);
|
|
1030
1150
|
}
|
|
1151
|
+
const prompts = [
|
|
1152
|
+
...opts.email ? [] : [{ type: "input", name: "email", message: "Email:" }],
|
|
1153
|
+
...password ? [] : [{ type: "password", name: "password", message: "Password:" }]
|
|
1154
|
+
];
|
|
1155
|
+
const answers = {
|
|
1156
|
+
email: opts.email,
|
|
1157
|
+
password,
|
|
1158
|
+
...prompts.length ? await inquirer2.prompt(prompts) : {}
|
|
1159
|
+
};
|
|
1160
|
+
const spinner = ora("Logging in...").start();
|
|
1161
|
+
const token = await passwordLogin(spinner, baseUrl, answers.email, answers.password);
|
|
1162
|
+
await finishWithToken(spinner, baseUrl, token, opts.org);
|
|
1031
1163
|
});
|
|
1032
1164
|
|
|
1033
1165
|
// src/commands/logout.ts
|
|
@@ -2388,7 +2520,7 @@ function period(start, end) {
|
|
|
2388
2520
|
// src/index.ts
|
|
2389
2521
|
installPromptCrashGuard();
|
|
2390
2522
|
var program = new Command18();
|
|
2391
|
-
program.name("videan").description("Videan CLI \u2014 deploy apps in seconds").version("0.
|
|
2523
|
+
program.name("videan").description("Videan CLI \u2014 deploy apps in seconds").version("0.5.0");
|
|
2392
2524
|
program.addCommand(loginCommand);
|
|
2393
2525
|
program.addCommand(logoutCommand);
|
|
2394
2526
|
program.addCommand(initCommand);
|