promigence 0.1.0-beta.12
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/NOTICE +14 -0
- package/README.md +88 -0
- package/dist/account.js +850 -0
- package/dist/agent.js +374 -0
- package/dist/argv.js +159 -0
- package/dist/bin.js +332 -0
- package/dist/bundle.js +133 -0
- package/dist/capacity.js +69 -0
- package/dist/commands.d.ts +110 -0
- package/dist/commands.js +2675 -0
- package/dist/connectreq.js +125 -0
- package/dist/credfile.js +188 -0
- package/dist/credits.js +197 -0
- package/dist/devcontainer.d.ts +99 -0
- package/dist/devcontainer.js +744 -0
- package/dist/device.js +129 -0
- package/dist/dirsnap.js +378 -0
- package/dist/errfix.js +262 -0
- package/dist/generated.d.ts +35 -0
- package/dist/generated.js +307 -0
- package/dist/github.js +307 -0
- package/dist/gitlab.js +210 -0
- package/dist/guide.d.ts +19 -0
- package/dist/guide.js +366 -0
- package/dist/handlers.js +511 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/init.d.ts +40 -0
- package/dist/init.js +352 -0
- package/dist/install.d.ts +23 -0
- package/dist/install.js +127 -0
- package/dist/io.d.ts +37 -0
- package/dist/io.js +318 -0
- package/dist/live.js +469 -0
- package/dist/login.js +1063 -0
- package/dist/mcp.js +889 -0
- package/dist/money.js +5 -0
- package/dist/orgs.js +475 -0
- package/dist/pricing.d.ts +52 -0
- package/dist/pricing.js +181 -0
- package/dist/report.js +132 -0
- package/dist/routes.js +135 -0
- package/dist/runners-github.js +257 -0
- package/dist/runners-gitlab.js +125 -0
- package/dist/runners-spec.js +137 -0
- package/dist/runners.js +1458 -0
- package/dist/sdk/blake3.d.ts +10 -0
- package/dist/sdk/blake3.js +111 -0
- package/dist/sdk/bundle.d.ts +96 -0
- package/dist/sdk/bundle.js +227 -0
- package/dist/sdk/ca-guard.js +166 -0
- package/dist/sdk/caller.d.ts +73 -0
- package/dist/sdk/caller.js +47 -0
- package/dist/sdk/client.d.ts +3015 -0
- package/dist/sdk/client.js +2583 -0
- package/dist/sdk/command-errors.d.ts +41 -0
- package/dist/sdk/command-errors.js +36 -0
- package/dist/sdk/context.d.ts +119 -0
- package/dist/sdk/context.js +588 -0
- package/dist/sdk/core.d.ts +285 -0
- package/dist/sdk/core.js +266 -0
- package/dist/sdk/credstore.d.ts +90 -0
- package/dist/sdk/credstore.js +206 -0
- package/dist/sdk/dispatcher.js +86 -0
- package/dist/sdk/github.d.ts +158 -0
- package/dist/sdk/github.js +78 -0
- package/dist/sdk/gitlab.d.ts +104 -0
- package/dist/sdk/gitlab.js +20 -0
- package/dist/sdk/handles.d.ts +82 -0
- package/dist/sdk/handles.js +135 -0
- package/dist/sdk/hint.js +7 -0
- package/dist/sdk/http2.js +1921 -0
- package/dist/sdk/index.d.ts +34 -0
- package/dist/sdk/index.js +15 -0
- package/dist/sdk/keyed.gen.js +7 -0
- package/dist/sdk/models.gen.d.ts +6162 -0
- package/dist/sdk/models.gen.js +1 -0
- package/dist/sdk/proxy.js +249 -0
- package/dist/sdk/pty.d.ts +171 -0
- package/dist/sdk/pty.js +496 -0
- package/dist/sdk/sandbox.d.ts +537 -0
- package/dist/sdk/sandbox.js +725 -0
- package/dist/sdk/ssh.d.ts +25 -0
- package/dist/sdk/ssh.js +114 -0
- package/dist/sdk/trace.js +80 -0
- package/dist/sdk/transport-worker.js +466 -0
- package/dist/sdk/warm.js +40 -0
- package/dist/ssh.js +267 -0
- package/dist/surface.d.ts +95 -0
- package/dist/surface.js +1452 -0
- package/dist/templates.js +53 -0
- package/dist/term.js +50 -0
- package/dist/timing.js +71 -0
- package/dist/untar.js +324 -0
- package/dist/uploadguard.js +75 -0
- package/llms.txt +209 -0
- package/package.json +52 -0
package/dist/login.js
ADDED
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { hostname } from "node:os";
|
|
5
|
+
import { win32 } from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import { AuthError, DEFAULT_API_URL, PromigenceClient, PromigenceError, UnreachableError, credentialsPath, readCredentials, readCredentialsWithStore, } from "./sdk/index.js";
|
|
8
|
+
import { forgetKeychainReads, storeLabel } from "./sdk/credstore.js";
|
|
9
|
+
import { bool, clientOpts, nexts, positional, str } from "./commands.js";
|
|
10
|
+
import { MINTED_BY, checkCredentialsPath, loginMintedKey, revokeReplacedKey, storeCredentials, storedWhere, } from "./credfile.js";
|
|
11
|
+
import { emit, okEnvelope, recordNotice, usageError, write } from "./io.js";
|
|
12
|
+
import { sh, shellArg } from "./term.js";
|
|
13
|
+
import { keysCreateInOrg, projectKeyClient } from "./orgs.js";
|
|
14
|
+
import { offerAfterLogin } from "./credits.js";
|
|
15
|
+
import { deviceFetch, deviceHeader } from "./device.js";
|
|
16
|
+
export function expiryBody(ctx) {
|
|
17
|
+
const raw = str(ctx, "--expires");
|
|
18
|
+
if (raw === undefined || raw.trim().toLowerCase() === "never")
|
|
19
|
+
return {};
|
|
20
|
+
const v = raw.trim();
|
|
21
|
+
const short = /^(\d+)\s*([hm])$/i.exec(v);
|
|
22
|
+
if (short) {
|
|
23
|
+
const n = Number(short[1]);
|
|
24
|
+
if (n < 1)
|
|
25
|
+
throw usageError(`--expires ${JSON.stringify(v)} is not in the future`, "Pass a lifetime of at least 1m, e.g. --expires 1h.");
|
|
26
|
+
const at = new Date(Date.now() + n * (short[2].toLowerCase() === "h" ? 3_600_000 : 60_000));
|
|
27
|
+
return { expires_at: at.toISOString().replace(/\.\d{3}Z$/, "Z") };
|
|
28
|
+
}
|
|
29
|
+
const unit = /^(\d+)\s*([dwy])$/i.exec(v);
|
|
30
|
+
if (unit) {
|
|
31
|
+
const n = Number(unit[1]);
|
|
32
|
+
const perUnit = { d: 1, w: 7, y: 365 };
|
|
33
|
+
return { expires_in_days: n * (perUnit[unit[2].toLowerCase()] ?? 1) };
|
|
34
|
+
}
|
|
35
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(v) || /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/.test(v))
|
|
36
|
+
return { expires_at: v };
|
|
37
|
+
throw usageError(`--expires ${JSON.stringify(v)} is not a lifetime`, "Use 1h, 90m, 30d, 12w, 1y, a date like 2027-01-31, a UTC time like 2027-01-31T12:00:00Z, or never (the default: the key does not expire).");
|
|
38
|
+
}
|
|
39
|
+
export function expiryLine(expiresAt) {
|
|
40
|
+
return expiresAt
|
|
41
|
+
? ` expires: ${expiresAt} (the owners are mailed 14 and 3 days before)`
|
|
42
|
+
: " expires: never. For a key that should stop on its own, create it with --expires 90d (or 30d, 1y, a date).";
|
|
43
|
+
}
|
|
44
|
+
export const LOGIN_WAIT_MS = 5 * 60_000;
|
|
45
|
+
export const SIGNUP_WAIT_MS = 15 * 60_000;
|
|
46
|
+
const b64url = (b) => b.toString("base64url");
|
|
47
|
+
export function pkcePair() {
|
|
48
|
+
const verifier = b64url(randomBytes(32));
|
|
49
|
+
return { verifier, challenge: b64url(createHash("sha256").update(verifier).digest()) };
|
|
50
|
+
}
|
|
51
|
+
export function noncePair() {
|
|
52
|
+
const raw = b64url(randomBytes(32));
|
|
53
|
+
return { raw, hashed: b64url(createHash("sha256").update(raw).digest()) };
|
|
54
|
+
}
|
|
55
|
+
function anonClient(ctx, apiUrl) {
|
|
56
|
+
return new PromigenceClient({ ...clientOpts(ctx), ...(apiUrl === undefined ? {} : { apiUrl }), onNotice: recordNotice });
|
|
57
|
+
}
|
|
58
|
+
export function isLoopbackHost(hostname) {
|
|
59
|
+
return hostname === "localhost" || hostname === "[::1]" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname);
|
|
60
|
+
}
|
|
61
|
+
export function isSecureUrl(raw) {
|
|
62
|
+
let u;
|
|
63
|
+
try {
|
|
64
|
+
u = new URL(raw);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return u.protocol === "https:" || (u.protocol === "http:" && isLoopbackHost(u.hostname));
|
|
70
|
+
}
|
|
71
|
+
export const OFFICIAL_SIGN_IN_HOSTS = ["auth.promigence.ai"];
|
|
72
|
+
export function isOfficialSignInHost(host) {
|
|
73
|
+
return OFFICIAL_SIGN_IN_HOSTS.includes(host.toLowerCase());
|
|
74
|
+
}
|
|
75
|
+
export const OFFICIAL_CONSOLE_HOSTS = ["console.promigence.ai"];
|
|
76
|
+
export function checkAuthConfig(cfg, apiUrl) {
|
|
77
|
+
const official = sameUrl(undefined, apiUrl);
|
|
78
|
+
for (const [label, raw] of [["authorize_url", cfg.authorize_url], ["token_url", cfg.token_url]]) {
|
|
79
|
+
let host;
|
|
80
|
+
try {
|
|
81
|
+
host = new URL(String(raw)).host;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
host = "";
|
|
85
|
+
}
|
|
86
|
+
if (!host || !isSecureUrl(String(raw))) {
|
|
87
|
+
throw new AuthError(`the API at ${apiUrl} named a sign-in page that is not https: ${label} ${JSON.stringify(raw)}`, "Not signing in through it. Check PROMIGENCE_API_URL / --api-url, or report it with promigence --version.");
|
|
88
|
+
}
|
|
89
|
+
if (official && !isOfficialSignInHost(host)) {
|
|
90
|
+
throw new AuthError(`the API at ${apiUrl} named a sign-in page on ${host}, which is not the Promigence sign-in domain (${label})`, "Not signing in through it: something between you and the API changed its answer. Check your network or proxy, then run `promigence login` again.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (cfg.console_authorize_url !== undefined && cfg.console_authorize_url !== null) {
|
|
94
|
+
const raw = String(cfg.console_authorize_url);
|
|
95
|
+
let host;
|
|
96
|
+
try {
|
|
97
|
+
host = new URL(raw).host;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
host = "";
|
|
101
|
+
}
|
|
102
|
+
if (!host || !isSecureUrl(raw)) {
|
|
103
|
+
throw new AuthError(`the API at ${apiUrl} named an authorize page that is not https: ${JSON.stringify(raw)}`, "Not signing in through it. Check PROMIGENCE_API_URL / --api-url, or report it with promigence --version.");
|
|
104
|
+
}
|
|
105
|
+
if (official && !OFFICIAL_CONSOLE_HOSTS.includes(host.toLowerCase())) {
|
|
106
|
+
throw new AuthError(`the API at ${apiUrl} named an authorize page on ${host}, which is not the Promigence console`, "Not signing in through it: something between you and the API changed its answer. Check your network or proxy, then run `promigence login` again.");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function tlsChecksOff(env = process.env) {
|
|
111
|
+
const v = env["NODE_TLS_REJECT_UNAUTHORIZED"];
|
|
112
|
+
return v !== undefined && /^\s*(0|false)\s*$/i.test(v);
|
|
113
|
+
}
|
|
114
|
+
export function refuseWithoutTlsChecks(what, env = process.env) {
|
|
115
|
+
if (!tlsChecksOff(env))
|
|
116
|
+
return;
|
|
117
|
+
throw usageError(`refusing to ${what}: TLS certificate checks are off in this shell (NODE_TLS_REJECT_UNAUTHORIZED=${env["NODE_TLS_REJECT_UNAUTHORIZED"]})`, "Unset it (unset NODE_TLS_REJECT_UNAUTHORIZED) and run the command again. Behind a TLS-inspecting proxy, trust its CA instead: NODE_EXTRA_CA_CERTS=/path/to/ca.pem.");
|
|
118
|
+
}
|
|
119
|
+
function apiUrlSource() {
|
|
120
|
+
if (process.env["PROMIGENCE_API_URL"])
|
|
121
|
+
return "PROMIGENCE_API_URL";
|
|
122
|
+
if (readCredentials().api_url)
|
|
123
|
+
return `the api_url in ${credentialsPath()}`;
|
|
124
|
+
return "the stored sign-in";
|
|
125
|
+
}
|
|
126
|
+
async function ask(prompt) {
|
|
127
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
128
|
+
try {
|
|
129
|
+
return await new Promise((resolve) => rl.question(prompt, resolve));
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
rl.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function confirmApiUrl(ctx, apiUrl, fromFlag) {
|
|
136
|
+
if (!isSecureUrl(apiUrl)) {
|
|
137
|
+
throw usageError(`refusing to sign in over ${apiUrl}: not https`, "The ID token and the new key would cross the network in the clear. Use the https URL (http is accepted only for localhost).");
|
|
138
|
+
}
|
|
139
|
+
await write(process.stderr, `Signing in to ${apiUrl}\n`);
|
|
140
|
+
if (sameUrl(undefined, apiUrl) || fromFlag || bool(ctx, "--yes"))
|
|
141
|
+
return;
|
|
142
|
+
const src = apiUrlSource();
|
|
143
|
+
if (process.stdin.isTTY && process.stderr.isTTY) {
|
|
144
|
+
const answer = await ask(`${apiUrl} (set by ${src}) is not the Promigence API (${DEFAULT_API_URL}). `
|
|
145
|
+
+ "Sign in there and store its key? [y/N] ");
|
|
146
|
+
if (/^\s*y(es)?\s*$/i.test(answer))
|
|
147
|
+
return;
|
|
148
|
+
throw usageError(`not signing in to ${apiUrl}`, `To sign in to the Promigence API instead: ${sh `promigence login --api-url ${DEFAULT_API_URL}`}. If you trust ${apiUrl}: ${sh `promigence login --api-url ${apiUrl}`}.`);
|
|
149
|
+
}
|
|
150
|
+
throw usageError(`${apiUrl} (set by ${src}) is not the Promigence API; not signing in to it without confirmation`, `If you trust it, pass it explicitly: ${sh `promigence login --api-url ${apiUrl}`} (or --yes). To sign in to the Promigence API instead: ${sh `promigence login --api-url ${DEFAULT_API_URL}`}.`);
|
|
151
|
+
}
|
|
152
|
+
async function authConfig(c) {
|
|
153
|
+
try {
|
|
154
|
+
return await c.request({ method: "GET", path: "/auth/config", anonymous: true });
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
if (e instanceof PromigenceError && e.code === "not_found") {
|
|
158
|
+
throw new AuthError(`self-serve login is not enabled on ${c.apiUrl}`, "Use a key issued by the operator: promigence auth login --from-env PROMIGENCE_API_KEY.");
|
|
159
|
+
}
|
|
160
|
+
throw e;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export function headless() {
|
|
164
|
+
if (process.env["SSH_CONNECTION"] || process.env["SSH_TTY"])
|
|
165
|
+
return true;
|
|
166
|
+
if (process.platform === "linux" && !process.env["DISPLAY"] && !process.env["WAYLAND_DISPLAY"])
|
|
167
|
+
return true;
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
function system32Exe(name, env) {
|
|
171
|
+
const root = env["SystemRoot"] ?? env["SYSTEMROOT"];
|
|
172
|
+
return win32.join(root && /^[A-Za-z]:[\\/]/.test(root) ? root : "C:\\Windows", "System32", name);
|
|
173
|
+
}
|
|
174
|
+
export function browserCommand(url, platform = process.platform, env = process.env) {
|
|
175
|
+
let href;
|
|
176
|
+
try {
|
|
177
|
+
href = new URL(url).href;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
if (!isSecureUrl(href))
|
|
183
|
+
return undefined;
|
|
184
|
+
if (platform === "darwin")
|
|
185
|
+
return ["open", [href]];
|
|
186
|
+
if (platform === "win32")
|
|
187
|
+
return [system32Exe("rundll32.exe", env), ["url.dll,FileProtocolHandler", href]];
|
|
188
|
+
return ["xdg-open", [href]];
|
|
189
|
+
}
|
|
190
|
+
const OPENER_ENV_DROP = new Set(["PROMIGENCE_API_KEY", "PROMIGENCE_ID_TOKEN", "PROMIGENCE_LOGIN_NONCE", "E2B_API_KEY"]);
|
|
191
|
+
const OPENER_ENV_DROP_PATTERN = /^PROMIGENCE_\w*(KEY|TOKEN|NONCE|SECRET)\w*$/;
|
|
192
|
+
export function openerEnv(env = process.env) {
|
|
193
|
+
return Object.fromEntries(Object.entries(env).filter(([k]) => {
|
|
194
|
+
const up = k.toUpperCase();
|
|
195
|
+
return !OPENER_ENV_DROP.has(up) && !OPENER_ENV_DROP_PATTERN.test(up);
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
export function openBrowser(url) {
|
|
199
|
+
if (process.env["PROMIGENCE_LOGIN_BROWSER"] === "print")
|
|
200
|
+
return false;
|
|
201
|
+
const command = browserCommand(url);
|
|
202
|
+
if (!command)
|
|
203
|
+
return false;
|
|
204
|
+
const [cmd, args] = command;
|
|
205
|
+
try {
|
|
206
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true, shell: false, windowsHide: true, env: openerEnv() });
|
|
207
|
+
child.on("error", () => { });
|
|
208
|
+
child.unref();
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function bindOn(server, port, host) {
|
|
216
|
+
return new Promise((resolve) => {
|
|
217
|
+
server.once("error", (e) => resolve(e.code ?? "error"));
|
|
218
|
+
server.listen(port, host, () => resolve(undefined));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
export function isUsableLoopbackUri(raw) {
|
|
222
|
+
if (typeof raw !== "string")
|
|
223
|
+
return false;
|
|
224
|
+
let u;
|
|
225
|
+
try {
|
|
226
|
+
u = new URL(raw);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
const port = Number(u.port);
|
|
232
|
+
return u.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname)
|
|
233
|
+
&& Number.isInteger(port) && port >= 1024 && port <= 65535 && u.pathname.length > 1;
|
|
234
|
+
}
|
|
235
|
+
function loopbackUris(cfg) {
|
|
236
|
+
const all = Array.isArray(cfg.loopback_redirect_uris) ? cfg.loopback_redirect_uris : [];
|
|
237
|
+
return {
|
|
238
|
+
usable: all.filter(isUsableLoopbackUri),
|
|
239
|
+
skipped: all.filter((u) => !isUsableLoopbackUri(u)).map((u) => JSON.stringify(u)),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const LOOP_CSS = "html{background:#09090b;color:#ededf0;font:16px/1.6 ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}"
|
|
243
|
+
+ "body{min-height:100vh;margin:0;display:flex;align-items:center;justify-content:center;padding:24px}"
|
|
244
|
+
+ "main{max-width:460px;border:1px solid #2a2a30;background:#111113;padding:32px 34px}"
|
|
245
|
+
+ "b{display:block;color:#39ff14;font-size:12px;letter-spacing:.14em;text-transform:uppercase;margin-bottom:14px}"
|
|
246
|
+
+ "h1{font-size:24px;margin:0 0 10px;letter-spacing:-.01em}p{margin:0;color:#a1a1aa}";
|
|
247
|
+
const LOOP_CSP = `default-src 'none'; style-src 'sha256-${createHash("sha256").update(LOOP_CSS).digest("base64")}'`;
|
|
248
|
+
export function loopbackPage(title, body) {
|
|
249
|
+
return `<!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">`
|
|
250
|
+
+ `<title>${title}</title><style>${LOOP_CSS}</style><main><b>Promigence</b><h1>${title}</h1><p>${body}</p></main></html>`;
|
|
251
|
+
}
|
|
252
|
+
export function finishUrl(consoleOrigin, outcome) {
|
|
253
|
+
return consoleOrigin ? `${consoleOrigin}/cli/${outcome}` : undefined;
|
|
254
|
+
}
|
|
255
|
+
async function listenLoopback(uris, expectState, squatted, consoleOrigin) {
|
|
256
|
+
for (const uri of uris) {
|
|
257
|
+
const u = new URL(uri);
|
|
258
|
+
const hosts = new Set([`localhost:${u.port}`, `127.0.0.1:${u.port}`, `[::1]:${u.port}`]);
|
|
259
|
+
let settle;
|
|
260
|
+
let fail;
|
|
261
|
+
const result = new Promise((res, rej) => { settle = res; fail = rej; });
|
|
262
|
+
const server = createServer((req, res) => {
|
|
263
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
264
|
+
res.setHeader("cache-control", "no-store");
|
|
265
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
266
|
+
res.setHeader("content-security-policy", LOOP_CSP);
|
|
267
|
+
res.setHeader("referrer-policy", "no-referrer");
|
|
268
|
+
if (!hosts.has((req.headers.host ?? "").toLowerCase())) {
|
|
269
|
+
res.writeHead(400).end();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (req.method !== "GET") {
|
|
273
|
+
res.writeHead(405, { allow: "GET" }).end();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
let got;
|
|
277
|
+
try {
|
|
278
|
+
got = new URL(req.url ?? "/", uri);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
res.writeHead(400).end();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (got.pathname !== u.pathname) {
|
|
285
|
+
res.writeHead(404).end();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const code = got.searchParams.get("code");
|
|
289
|
+
const state = got.searchParams.get("state") ?? "";
|
|
290
|
+
const error = got.searchParams.get("error");
|
|
291
|
+
const page = loopbackPage;
|
|
292
|
+
const finish = (outcome, status, title, body) => {
|
|
293
|
+
const to = finishUrl(consoleOrigin, outcome);
|
|
294
|
+
if (to)
|
|
295
|
+
res.writeHead(303, { location: to }).end();
|
|
296
|
+
else
|
|
297
|
+
res.writeHead(status).end(page(title, body));
|
|
298
|
+
};
|
|
299
|
+
if (state !== expectState) {
|
|
300
|
+
res.writeHead(400).end(page("Promigence login", "This login link is not the one the CLI started. Return to the terminal."));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (error || !code) {
|
|
304
|
+
finish(error === "access_denied" ? "cancelled" : "sign-in-failed", 400, "Sign-in did not finish", "Return to your terminal for the details.");
|
|
305
|
+
fail(new AuthError(`login was not completed: ${error ?? "no code"} ${got.searchParams.get("error_description") ?? ""}`.trim(), FAILED_SIGN_IN_FIX));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
finish("signed-in", 200, "You're signed in", "The Promigence CLI is signed in. You can close this tab and return to your terminal.");
|
|
309
|
+
settle({ code, state });
|
|
310
|
+
});
|
|
311
|
+
const v4err = await bindOn(server, Number(u.port), "127.0.0.1");
|
|
312
|
+
if (v4err) {
|
|
313
|
+
squatted.push(`127.0.0.1:${u.port} (${v4err})`);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const v6 = createServer((req, res) => server.emit("request", req, res));
|
|
317
|
+
const v6err = await bindOn(v6, Number(u.port), "::1");
|
|
318
|
+
if (v6err === "EADDRINUSE" || v6err === "EACCES") {
|
|
319
|
+
squatted.push(`[::1]:${u.port} (${v6err})`);
|
|
320
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const close = server.close.bind(server);
|
|
324
|
+
server.close = ((cb) => {
|
|
325
|
+
if (v6.listening)
|
|
326
|
+
v6.close();
|
|
327
|
+
return close(cb);
|
|
328
|
+
});
|
|
329
|
+
return { server, redirectUri: uri, result };
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
export const PASTE_MAX_BYTES = 64 * 1024;
|
|
334
|
+
export const CODE_MAX_CHARS = 1024;
|
|
335
|
+
const pasteTooLong = () => usageError(`the pasted text is longer than ${PASTE_MAX_BYTES / 1024} KiB; no login code is`, "Paste only the code shown on the page after signing in, then press Enter.");
|
|
336
|
+
async function readPasted(prompt) {
|
|
337
|
+
if (process.stdin.isTTY) {
|
|
338
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
339
|
+
let seen = 0;
|
|
340
|
+
let count;
|
|
341
|
+
try {
|
|
342
|
+
return await new Promise((resolve, reject) => {
|
|
343
|
+
count = (chunk) => {
|
|
344
|
+
seen += Buffer.byteLength(chunk);
|
|
345
|
+
if (seen > PASTE_MAX_BYTES)
|
|
346
|
+
reject(pasteTooLong());
|
|
347
|
+
};
|
|
348
|
+
process.stdin.on("data", count);
|
|
349
|
+
rl.on("SIGINT", () => reject(usageError("cancelled", "Run the command again when you are ready to sign in.")));
|
|
350
|
+
rl.question(prompt, resolve);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
if (count)
|
|
355
|
+
process.stdin.off("data", count);
|
|
356
|
+
rl.close();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
await write(process.stderr, prompt);
|
|
360
|
+
const chunks = [];
|
|
361
|
+
let size = 0;
|
|
362
|
+
for await (const chunk of process.stdin) {
|
|
363
|
+
const b = Buffer.from(chunk);
|
|
364
|
+
size += b.length;
|
|
365
|
+
if (size > PASTE_MAX_BYTES)
|
|
366
|
+
throw pasteTooLong();
|
|
367
|
+
chunks.push(b);
|
|
368
|
+
}
|
|
369
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
370
|
+
}
|
|
371
|
+
function checkCode(code) {
|
|
372
|
+
if (code.length > CODE_MAX_CHARS || !/^[A-Za-z0-9._~-]+$/.test(code)) {
|
|
373
|
+
throw usageError("that does not look like a login code", "Paste only the code shown on the page.");
|
|
374
|
+
}
|
|
375
|
+
return code;
|
|
376
|
+
}
|
|
377
|
+
export const FAILED_SIGN_IN_FIX = "Run `promigence login` again. Signing in with GitHub? Your GitHub account needs a "
|
|
378
|
+
+ "verified primary email address (https://github.com/settings/emails).";
|
|
379
|
+
export function parsePasted(raw, expectState) {
|
|
380
|
+
const text = raw.trim();
|
|
381
|
+
if (!text)
|
|
382
|
+
throw usageError("no code was pasted", "Paste the code shown on the page after signing in, then press Enter.");
|
|
383
|
+
if (text.includes("code=")) {
|
|
384
|
+
let u;
|
|
385
|
+
try {
|
|
386
|
+
u = new URL(text.includes("://") ? text : `http://x/?${text.replace(/^.*\?/, "")}`);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
throw usageError("the pasted URL could not be read", "Paste only the code shown on the page, or the whole address of that page.");
|
|
390
|
+
}
|
|
391
|
+
const state = u.searchParams.get("state");
|
|
392
|
+
if (state !== null && state !== expectState) {
|
|
393
|
+
throw new AuthError("the pasted URL belongs to a different login", "Run `promigence login` again and paste the code from that login.");
|
|
394
|
+
}
|
|
395
|
+
const code = u.searchParams.get("code");
|
|
396
|
+
if (code)
|
|
397
|
+
return checkCode(code);
|
|
398
|
+
}
|
|
399
|
+
return checkCode(text);
|
|
400
|
+
}
|
|
401
|
+
async function loopbackOrPaste(ctx, cfg, state) {
|
|
402
|
+
if (bool(ctx, "--no-browser") || headless())
|
|
403
|
+
return undefined;
|
|
404
|
+
const { usable, skipped } = loopbackUris(cfg);
|
|
405
|
+
const squatted = [];
|
|
406
|
+
const via = consoleAuthorizeUrl(cfg);
|
|
407
|
+
let origin;
|
|
408
|
+
try {
|
|
409
|
+
origin = via ? new URL(via).origin : undefined;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
origin = undefined;
|
|
413
|
+
}
|
|
414
|
+
const loop = await listenLoopback(usable, state, squatted, origin);
|
|
415
|
+
if (loop)
|
|
416
|
+
return loop;
|
|
417
|
+
if (squatted.length) {
|
|
418
|
+
await write(process.stderr, `promigence ${ctx.cmd.name}: the login ports are held by another program (${squatted.join(", ")}); `
|
|
419
|
+
+ "not sharing them. Using paste mode instead.\n");
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
await write(process.stderr, `promigence ${ctx.cmd.name}: the API named no loopback redirect this CLI can listen on `
|
|
423
|
+
+ `(${skipped.join(", ") || "none"}). Using paste mode instead.\n`);
|
|
424
|
+
}
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
function authorizeUrl(cfg, params) {
|
|
428
|
+
const u = new URL(cfg.authorize_url);
|
|
429
|
+
for (const [k, v] of Object.entries(params))
|
|
430
|
+
u.searchParams.set(k, v);
|
|
431
|
+
return u.href;
|
|
432
|
+
}
|
|
433
|
+
export function signInUrl(cfg, params, signup) {
|
|
434
|
+
const u = new URL(authorizeUrl(cfg, params));
|
|
435
|
+
if (signup && !params["identity_provider"] && u.pathname === "/oauth2/authorize")
|
|
436
|
+
u.pathname = "/signup";
|
|
437
|
+
return u.href;
|
|
438
|
+
}
|
|
439
|
+
export function shownUrl(url) {
|
|
440
|
+
const u = new URL(url);
|
|
441
|
+
return `${u.origin}${u.pathname}`;
|
|
442
|
+
}
|
|
443
|
+
export function waitExpiredError(command, ms) {
|
|
444
|
+
if (command === "signup") {
|
|
445
|
+
return new AuthError(`no browser sign-up within ${Math.round(ms / 60_000)} min`, "Your account may already be created (for example if the email code arrived late). Run `promigence login` next; "
|
|
446
|
+
+ "run `promigence signup` again only if login says there is no account.");
|
|
447
|
+
}
|
|
448
|
+
return new AuthError(`no browser login within ${Math.round(ms / 1000)}s`, `Run \`promigence ${command}\` again, or \`promigence ${command} --no-browser\` to paste the code.`);
|
|
449
|
+
}
|
|
450
|
+
function withTimeout(p, ms, command = "login") {
|
|
451
|
+
let t;
|
|
452
|
+
return Promise.race([
|
|
453
|
+
p.finally(() => clearTimeout(t)),
|
|
454
|
+
new Promise((_, rej) => {
|
|
455
|
+
t = setTimeout(() => rej(waitExpiredError(command, ms)), ms);
|
|
456
|
+
}),
|
|
457
|
+
]);
|
|
458
|
+
}
|
|
459
|
+
export function browserWaitMs(isSignup, inviteExpiresInS) {
|
|
460
|
+
if (inviteExpiresInS !== undefined)
|
|
461
|
+
return inviteWaitMs(inviteExpiresInS);
|
|
462
|
+
return isSignup ? SIGNUP_WAIT_MS : LOGIN_WAIT_MS;
|
|
463
|
+
}
|
|
464
|
+
export function inviteWaitMs(expiresInS) {
|
|
465
|
+
return Math.max(LOGIN_WAIT_MS, expiresInS * 1000 - INVITE_HOLD_MARGIN_MS);
|
|
466
|
+
}
|
|
467
|
+
const INVITE_HOLD_MARGIN_MS = 30_000;
|
|
468
|
+
export function inviteHold(release, onSignal = (sig) => {
|
|
469
|
+
process.exit(sig === "SIGINT" ? 130 : 143);
|
|
470
|
+
}) {
|
|
471
|
+
let done = false;
|
|
472
|
+
const giveBack = async () => {
|
|
473
|
+
if (done)
|
|
474
|
+
return;
|
|
475
|
+
done = true;
|
|
476
|
+
let t;
|
|
477
|
+
await Promise.race([
|
|
478
|
+
release().catch(() => undefined),
|
|
479
|
+
new Promise((resolve) => { t = setTimeout(resolve, 5_000); }),
|
|
480
|
+
]);
|
|
481
|
+
if (t)
|
|
482
|
+
clearTimeout(t);
|
|
483
|
+
};
|
|
484
|
+
const handler = (sig) => { void giveBack().finally(() => onSignal(sig)); };
|
|
485
|
+
process.once("SIGINT", handler);
|
|
486
|
+
process.once("SIGTERM", handler);
|
|
487
|
+
return {
|
|
488
|
+
release: giveBack,
|
|
489
|
+
dispose: () => {
|
|
490
|
+
done = true;
|
|
491
|
+
process.off("SIGINT", handler);
|
|
492
|
+
process.off("SIGTERM", handler);
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
async function exchangeCode(cfg, code, redirectUri, verifier) {
|
|
497
|
+
const body = new URLSearchParams({
|
|
498
|
+
grant_type: "authorization_code", client_id: cfg.client_id, code, redirect_uri: redirectUri, code_verifier: verifier,
|
|
499
|
+
});
|
|
500
|
+
let res;
|
|
501
|
+
try {
|
|
502
|
+
res = await fetch(cfg.token_url, {
|
|
503
|
+
method: "POST",
|
|
504
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
505
|
+
body,
|
|
506
|
+
redirect: "error",
|
|
507
|
+
signal: AbortSignal.timeout(30_000),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
catch (e) {
|
|
511
|
+
const err = e;
|
|
512
|
+
if (/redirect/i.test(`${err.cause?.message ?? ""} ${err.message ?? ""}`)) {
|
|
513
|
+
throw new AuthError(`the login service ${cfg.token_url} answered the code exchange with a redirect; not following it`, "Nothing was sent on. Check your network or proxy, then run `promigence login` again.");
|
|
514
|
+
}
|
|
515
|
+
throw new UnreachableError(`cannot reach the login service ${cfg.token_url}: ${err.message}`);
|
|
516
|
+
}
|
|
517
|
+
const text = await res.text();
|
|
518
|
+
let j = {};
|
|
519
|
+
try {
|
|
520
|
+
j = JSON.parse(text);
|
|
521
|
+
}
|
|
522
|
+
catch { }
|
|
523
|
+
if (!res.ok || typeof j["id_token"] !== "string") {
|
|
524
|
+
throw new AuthError(`the login service refused the code: ${String(j["error"] ?? `HTTP ${res.status}`)}`, "Codes are single-use and expire in minutes. Run `promigence login` again.");
|
|
525
|
+
}
|
|
526
|
+
return j["id_token"];
|
|
527
|
+
}
|
|
528
|
+
export function writeCredentials(apiKey, apiUrl, keyId) {
|
|
529
|
+
const body = {};
|
|
530
|
+
if (apiKey)
|
|
531
|
+
body["api_key"] = apiKey;
|
|
532
|
+
if (apiUrl && apiUrl.replace(/\/+$/, "") !== DEFAULT_API_URL.replace(/\/+$/, ""))
|
|
533
|
+
body["api_url"] = apiUrl;
|
|
534
|
+
if (apiKey && keyId) {
|
|
535
|
+
body["key_id"] = keyId;
|
|
536
|
+
body["minted_by"] = MINTED_BY;
|
|
537
|
+
}
|
|
538
|
+
return storeCredentials(Object.keys(body).length ? body : undefined);
|
|
539
|
+
}
|
|
540
|
+
const sameUrl = (a, b) => (a ?? DEFAULT_API_URL).replace(/\/+$/, "") === b.replace(/\/+$/, "");
|
|
541
|
+
export function consoleAuthorizeUrl(cfg, env = process.env) {
|
|
542
|
+
const flow = (env["PROMIGENCE_LOGIN_FLOW"] ?? "").trim().toLowerCase();
|
|
543
|
+
if (flow === "direct" || flow === "legacy")
|
|
544
|
+
return undefined;
|
|
545
|
+
if (typeof cfg.console_authorize_url !== "string" || !cfg.console_authorize_url)
|
|
546
|
+
return undefined;
|
|
547
|
+
return cfg.console_authorize_url;
|
|
548
|
+
}
|
|
549
|
+
export function deviceName() {
|
|
550
|
+
return hostname().replace(/[^\x20-\x7e]/g, "").slice(0, 64);
|
|
551
|
+
}
|
|
552
|
+
export function consoleUrl(base, params) {
|
|
553
|
+
const u = new URL(base);
|
|
554
|
+
for (const [k, v] of Object.entries(params))
|
|
555
|
+
u.searchParams.set(k, v);
|
|
556
|
+
return u.href;
|
|
557
|
+
}
|
|
558
|
+
async function consoleSignIn(ctx, cfg, base, apiUrl, opts) {
|
|
559
|
+
const { verifier, challenge } = pkcePair();
|
|
560
|
+
const state = b64url(randomBytes(16));
|
|
561
|
+
const loop = await loopbackOrPaste(ctx, cfg, state);
|
|
562
|
+
const port = loop ? new URL(loop.redirectUri).port : undefined;
|
|
563
|
+
let url;
|
|
564
|
+
try {
|
|
565
|
+
const created = await new PromigenceClient({ apiUrl }).request({
|
|
566
|
+
method: "POST", path: "/auth/cli/requests", anonymous: true,
|
|
567
|
+
body: {
|
|
568
|
+
code_challenge: challenge, code_challenge_method: "S256", state,
|
|
569
|
+
...(port ? { port: Number(port) } : {}),
|
|
570
|
+
device: deviceName(), key_name: opts.keyName,
|
|
571
|
+
...(opts.provider ? { provider: opts.provider } : {}),
|
|
572
|
+
...(opts.ref ? { ref: opts.ref } : {}),
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
url = created.authorize_url;
|
|
576
|
+
}
|
|
577
|
+
catch (e) {
|
|
578
|
+
loop?.server.close();
|
|
579
|
+
throw e;
|
|
580
|
+
}
|
|
581
|
+
if (typeof url !== "string" || !url.startsWith(`${base}?request=`)) {
|
|
582
|
+
loop?.server.close();
|
|
583
|
+
throw new PromigenceError({ code: "internal", message: `the API answered an authorize page that is not its console's (${String(url).slice(0, 80)})`,
|
|
584
|
+
fix: "Run `promigence login` again; if it repeats, report it with promigence --version.", exitCode: 1 });
|
|
585
|
+
}
|
|
586
|
+
const verb = "sign in (or create your account) and authorize this CLI";
|
|
587
|
+
let code;
|
|
588
|
+
try {
|
|
589
|
+
if (loop) {
|
|
590
|
+
const opened = openBrowser(url);
|
|
591
|
+
await write(process.stderr, opened
|
|
592
|
+
? `Opening your browser to ${verb} (${shownUrl(url)}).\nWaiting for the browser (${Math.floor(opts.waitMs / 60_000)} min)…\n`
|
|
593
|
+
: `Open this URL to ${verb}:\n ${url}\nWaiting for the browser (${Math.floor(opts.waitMs / 60_000)} min)…\n`);
|
|
594
|
+
code = (await withTimeout(loop.result, opts.waitMs, ctx.cmd.name)).code;
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
await write(process.stderr, `Open this URL in any browser and ${verb}:\n ${url}\n`);
|
|
598
|
+
code = parsePasted(await readPasted("Paste the code shown after you authorize: "), state);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
loop?.server.close();
|
|
603
|
+
}
|
|
604
|
+
const viaDevice = deviceFetch(deviceHeader());
|
|
605
|
+
return new PromigenceClient({ apiUrl, ...(viaDevice ? { fetch: viaDevice } : {}) }).request({
|
|
606
|
+
method: "POST", path: "/auth/cli/token", anonymous: true,
|
|
607
|
+
body: { code, code_verifier: verifier, ...(port ? { redirect_uri: `http://127.0.0.1:${port}/callback` } : {}) },
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
function refuseRetiredPaste(cfg, command) {
|
|
611
|
+
if (typeof cfg.console_authorize_url !== "string" || !cfg.console_authorize_url)
|
|
612
|
+
return;
|
|
613
|
+
throw usageError(`${command} cannot paste a sign-in code here: with no browser on this machine, sign-in codes come from the Promigence console`, command === "promigence login" || command === "promigence signup"
|
|
614
|
+
? "Run it without PROMIGENCE_LOGIN_FLOW=direct: `promigence login --no-browser` prints the console's page to open on any device."
|
|
615
|
+
: "Run it on a machine with a browser, or set PROMIGENCE_ID_TOKEN to an ID token you already hold.");
|
|
616
|
+
}
|
|
617
|
+
async function directSignIn(ctx, cfg, apiUrl, p) {
|
|
618
|
+
const { state, verifier, challenge, nonce, idp, isSignup, keyName, ref, waitMs } = p;
|
|
619
|
+
const loop = await loopbackOrPaste(ctx, cfg, state);
|
|
620
|
+
if (!loop)
|
|
621
|
+
refuseRetiredPaste(cfg, ctx.cmd.name);
|
|
622
|
+
const redirectUri = loop ? loop.redirectUri : cfg.paste_redirect_uri;
|
|
623
|
+
const url = signInUrl(cfg, {
|
|
624
|
+
response_type: "code", client_id: cfg.client_id, redirect_uri: redirectUri,
|
|
625
|
+
scope: cfg.scopes.join(" "), state, nonce: nonce.hashed, code_challenge: challenge, code_challenge_method: "S256",
|
|
626
|
+
...(idp ? { identity_provider: idp } : {}),
|
|
627
|
+
}, isSignup);
|
|
628
|
+
const verb = url.includes("/signup?") ? "create your account" : "sign in";
|
|
629
|
+
const inboxHint = isSignup ? "Check your inbox (and spam) for the verification code.\n" : "";
|
|
630
|
+
let code;
|
|
631
|
+
try {
|
|
632
|
+
if (loop) {
|
|
633
|
+
const opened = openBrowser(url);
|
|
634
|
+
await write(process.stderr, opened
|
|
635
|
+
? `Opening your browser to ${verb} (${shownUrl(url)}).\n${inboxHint}Waiting for the browser (${Math.floor(waitMs / 60_000)} min)…\n`
|
|
636
|
+
: `Open this URL to ${verb}:\n ${url}\n${inboxHint}Waiting for the browser (${Math.floor(waitMs / 60_000)} min)…\n`);
|
|
637
|
+
code = (await withTimeout(loop.result, waitMs, ctx.cmd.name)).code;
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
await write(process.stderr, `Open this URL in any browser and ${verb}:\n ${url}\n${inboxHint}`);
|
|
641
|
+
code = parsePasted(await readPasted("Paste the code shown after signing in: "), state);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
finally {
|
|
645
|
+
loop?.server.close();
|
|
646
|
+
}
|
|
647
|
+
const idToken = await exchangeCode(cfg, code, redirectUri, verifier);
|
|
648
|
+
const viaDevice = deviceFetch(deviceHeader());
|
|
649
|
+
return new PromigenceClient({ apiKey: idToken, apiUrl, ...(viaDevice ? { fetch: viaDevice } : {}) }).request({
|
|
650
|
+
method: "POST", path: "/auth/session",
|
|
651
|
+
body: { mint_key: true, key_name: keyName, nonce: nonce.raw, ...(ref ? { ref } : {}) },
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
export async function signIn(ctx) {
|
|
655
|
+
refuseWithoutTlsChecks("sign in");
|
|
656
|
+
const anon = anonClient(ctx);
|
|
657
|
+
const preset = process.env["PROMIGENCE_ID_TOKEN"];
|
|
658
|
+
if (preset) {
|
|
659
|
+
if (!isSecureUrl(anon.apiUrl)) {
|
|
660
|
+
throw usageError(`refusing to send a sign-in token to ${anon.apiUrl}: not https`, "The token would cross the network in the clear. Use the https URL (http is accepted only for localhost).");
|
|
661
|
+
}
|
|
662
|
+
return { idToken: preset, ...(process.env["PROMIGENCE_LOGIN_NONCE"] ? { nonce: process.env["PROMIGENCE_LOGIN_NONCE"] } : {}), apiUrl: anon.apiUrl };
|
|
663
|
+
}
|
|
664
|
+
await confirmApiUrl(ctx, anon.apiUrl, false);
|
|
665
|
+
const cfg = await authConfig(anon);
|
|
666
|
+
checkAuthConfig(cfg, anon.apiUrl);
|
|
667
|
+
const { verifier, challenge } = pkcePair();
|
|
668
|
+
const nonce = noncePair();
|
|
669
|
+
const state = b64url(randomBytes(16));
|
|
670
|
+
const loop = await loopbackOrPaste(ctx, cfg, state);
|
|
671
|
+
if (!loop)
|
|
672
|
+
refuseRetiredPaste(cfg, ctx.cmd.name);
|
|
673
|
+
const redirectUri = loop ? loop.redirectUri : cfg.paste_redirect_uri;
|
|
674
|
+
const url = authorizeUrl(cfg, {
|
|
675
|
+
response_type: "code", client_id: cfg.client_id, redirect_uri: redirectUri,
|
|
676
|
+
scope: cfg.scopes.join(" "), state, nonce: nonce.hashed, code_challenge: challenge, code_challenge_method: "S256",
|
|
677
|
+
});
|
|
678
|
+
let code;
|
|
679
|
+
try {
|
|
680
|
+
if (loop) {
|
|
681
|
+
const opened = openBrowser(url);
|
|
682
|
+
await write(process.stderr, `${ctx.cmd.name} acts as you: ${opened ? "opening your browser to sign in" : "open this URL to sign in"}:\n ${url}\nWaiting for the browser (${LOGIN_WAIT_MS / 60_000} min)…\n`);
|
|
683
|
+
code = (await withTimeout(loop.result, LOGIN_WAIT_MS)).code;
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
await write(process.stderr, `${ctx.cmd.name} acts as you. Open this URL in any browser and sign in:\n ${url}\n`);
|
|
687
|
+
code = parsePasted(await readPasted("Paste the code shown after signing in: "), state);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
finally {
|
|
691
|
+
loop?.server.close();
|
|
692
|
+
}
|
|
693
|
+
return { idToken: await exchangeCode(cfg, code, redirectUri, verifier), nonce: nonce.raw, apiUrl: anon.apiUrl };
|
|
694
|
+
}
|
|
695
|
+
async function inviteCode(ctx) {
|
|
696
|
+
const flag = str(ctx, "--invite");
|
|
697
|
+
if (flag !== undefined) {
|
|
698
|
+
await write(process.stderr, "promigence signup: warning: --invite is visible to other users of this machine (ps) and "
|
|
699
|
+
+ "stays in your shell history; next time, enter it when asked or set PROMIGENCE_INVITE_CODE\n");
|
|
700
|
+
return flag;
|
|
701
|
+
}
|
|
702
|
+
const env = process.env["PROMIGENCE_INVITE_CODE"];
|
|
703
|
+
if (env)
|
|
704
|
+
return env;
|
|
705
|
+
if (!process.stdin.isTTY) {
|
|
706
|
+
throw usageError("this API needs an invite code for new accounts, and there is no terminal to ask for it", "Set PROMIGENCE_INVITE_CODE, or run `promigence signup` in a terminal and enter the code when asked.");
|
|
707
|
+
}
|
|
708
|
+
const code = await readHidden("Invite code: ");
|
|
709
|
+
if (!code.trim())
|
|
710
|
+
throw usageError("no invite code entered", "Run `promigence signup` again and enter the code you were given.");
|
|
711
|
+
return code;
|
|
712
|
+
}
|
|
713
|
+
export function readHidden(prompt) {
|
|
714
|
+
const stdin = process.stdin;
|
|
715
|
+
return new Promise((resolve, reject) => {
|
|
716
|
+
let buf = "";
|
|
717
|
+
const done = (err) => {
|
|
718
|
+
stdin.removeListener("data", onData);
|
|
719
|
+
if (stdin.isTTY)
|
|
720
|
+
stdin.setRawMode(false);
|
|
721
|
+
stdin.pause();
|
|
722
|
+
process.stderr.write("\n");
|
|
723
|
+
if (err)
|
|
724
|
+
reject(err);
|
|
725
|
+
else
|
|
726
|
+
resolve(buf);
|
|
727
|
+
};
|
|
728
|
+
const onData = (chunk) => {
|
|
729
|
+
for (const ch of chunk.toString("utf8")) {
|
|
730
|
+
if (ch === "\r" || ch === "\n" || ch === "\u0004")
|
|
731
|
+
return done();
|
|
732
|
+
if (ch === "\u0003")
|
|
733
|
+
return done(usageError("cancelled", "Run `promigence signup` again when you have the code."));
|
|
734
|
+
if (ch === "\u007f" || ch === "\b") {
|
|
735
|
+
buf = buf.slice(0, -1);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (buf.length < 256 && ch >= " ")
|
|
739
|
+
buf += ch;
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
process.stderr.write(prompt);
|
|
743
|
+
if (stdin.isTTY)
|
|
744
|
+
stdin.setRawMode(true);
|
|
745
|
+
stdin.resume();
|
|
746
|
+
stdin.on("data", onData);
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
const login = (ctx) => signInAndStore(ctx, false);
|
|
750
|
+
const signup = (ctx) => signInAndStore(ctx, true);
|
|
751
|
+
async function signInAndStore(ctx, isSignup) {
|
|
752
|
+
refuseWithoutTlsChecks("sign in");
|
|
753
|
+
const provider = str(ctx, "--provider");
|
|
754
|
+
const flagUrl = str(ctx, "--api-url");
|
|
755
|
+
if (flagUrl !== undefined && !/^https?:\/\/[^/]/i.test(flagUrl)) {
|
|
756
|
+
throw usageError(`--api-url: '${flagUrl}' is not an http(s) URL`, `Pass the API's base URL, e.g. --api-url ${DEFAULT_API_URL}.`);
|
|
757
|
+
}
|
|
758
|
+
const anon = anonClient(ctx, flagUrl);
|
|
759
|
+
await confirmApiUrl(ctx, anon.apiUrl, flagUrl !== undefined);
|
|
760
|
+
const cfg = await authConfig(anon);
|
|
761
|
+
checkAuthConfig(cfg, anon.apiUrl);
|
|
762
|
+
let idp;
|
|
763
|
+
if (provider !== undefined) {
|
|
764
|
+
idp = cfg.identity_providers[provider];
|
|
765
|
+
if (!idp) {
|
|
766
|
+
throw usageError(`--provider: '${provider}' is not a login method`, `Pass --provider ${Object.keys(cfg.identity_providers).join(" | ")}, or omit it to choose in the browser.`);
|
|
767
|
+
}
|
|
768
|
+
if (provider === "email")
|
|
769
|
+
idp = undefined;
|
|
770
|
+
}
|
|
771
|
+
const keyName = (str(ctx, "--key-name") ?? `cli@${deviceName()}`).slice(0, 64);
|
|
772
|
+
const ref = str(ctx, "--ref")?.trim();
|
|
773
|
+
if (ref !== undefined && (ref === "" || ref.length > 64)) {
|
|
774
|
+
throw usageError("--ref: a referral code is 1-64 characters", "Pass the code as you were given it, e.g. --ref K7QM2XPD.");
|
|
775
|
+
}
|
|
776
|
+
checkCredentialsPath(credentialsPath());
|
|
777
|
+
const viaConsole = consoleAuthorizeUrl(cfg);
|
|
778
|
+
if (viaConsole && isSignup && (str(ctx, "--invite") !== undefined || process.env["PROMIGENCE_INVITE_CODE"])) {
|
|
779
|
+
await write(process.stderr, "promigence signup: enter your invite code in the browser when it asks; it is not sent from here\n");
|
|
780
|
+
}
|
|
781
|
+
const { verifier, challenge } = pkcePair();
|
|
782
|
+
const nonce = noncePair();
|
|
783
|
+
const state = b64url(randomBytes(16));
|
|
784
|
+
let waitMs = browserWaitMs(isSignup);
|
|
785
|
+
let hold;
|
|
786
|
+
if (isSignup && cfg.invite_mode && !viaConsole) {
|
|
787
|
+
const code = await inviteCode(ctx);
|
|
788
|
+
const r = await anon.request({
|
|
789
|
+
method: "POST", path: "/auth/invite", anonymous: true, body: { code, nonce: nonce.raw },
|
|
790
|
+
});
|
|
791
|
+
if (r.invite_mode) {
|
|
792
|
+
waitMs = browserWaitMs(true, r.expires_in_s);
|
|
793
|
+
hold = inviteHold(() => anon.request({
|
|
794
|
+
method: "POST", path: "/auth/invite/release", anonymous: true, body: { nonce: nonce.raw },
|
|
795
|
+
}));
|
|
796
|
+
await write(process.stderr, `Invite code accepted; finish signing in within ${Math.floor(waitMs / 60_000)} minutes.\n`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
let session;
|
|
800
|
+
try {
|
|
801
|
+
session = viaConsole
|
|
802
|
+
? await consoleSignIn(ctx, cfg, viaConsole, anon.apiUrl, {
|
|
803
|
+
isSignup, ...(provider !== undefined ? { provider } : {}), keyName, waitMs, ...(ref ? { ref } : {}),
|
|
804
|
+
})
|
|
805
|
+
: await directSignIn(ctx, cfg, anon.apiUrl, {
|
|
806
|
+
state, verifier, challenge, nonce, ...(idp ? { idp } : {}), isSignup, keyName,
|
|
807
|
+
...(ref ? { ref } : {}), waitMs,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
catch (e) {
|
|
811
|
+
await hold?.release();
|
|
812
|
+
throw e;
|
|
813
|
+
}
|
|
814
|
+
finally {
|
|
815
|
+
hold?.dispose();
|
|
816
|
+
}
|
|
817
|
+
if (!session.api_key) {
|
|
818
|
+
throw new PromigenceError({ code: "internal", message: "the API created no key for this login",
|
|
819
|
+
fix: "Run `promigence login` again; if it repeats, report it with promigence --version.", exitCode: 1 });
|
|
820
|
+
}
|
|
821
|
+
const existing = readCredentials();
|
|
822
|
+
const previous = loginMintedKey();
|
|
823
|
+
const stored = writeCredentials(session.api_key, anon.apiUrl, session.key?.key_id);
|
|
824
|
+
const path = stored.path;
|
|
825
|
+
const warnings = [...stored.warnings];
|
|
826
|
+
let revoked;
|
|
827
|
+
if (previous && previous.apiKey !== session.api_key && previous.keyId !== session.key?.key_id) {
|
|
828
|
+
if (sameUrl(previous.apiUrl, anon.apiUrl)) {
|
|
829
|
+
const r = await revokeReplacedKey(previous, "this login replaced", session.api_key);
|
|
830
|
+
revoked = r.revoked;
|
|
831
|
+
if (r.warning)
|
|
832
|
+
warnings.push(r.warning);
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
const was = (previous.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
836
|
+
warnings.push(`the key the previous login minted (${previous.keyId}) belongs to ${was}, not ${anon.apiUrl}, so it was `
|
|
837
|
+
+ `not revoked and still works; revoke it with \`${sh `promigence keys revoke ${previous.keyId}`}\` from a login to ${was}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const consoleHome = viaConsole ? `${new URL(viaConsole).origin}/home` : undefined;
|
|
841
|
+
const credit = await offerAfterLogin(ctx, anon.apiUrl, session.api_key, consoleHome);
|
|
842
|
+
const location = await anon.health({ timeoutMs: 3000 }).then((h) => (typeof h.location === "string" && h.location ? h.location : undefined)).catch(() => undefined);
|
|
843
|
+
const next = nexts(["promigence me", "your org, limits and spend"], ["promigence examples", "what you can fork with no build (the built-in base)"]);
|
|
844
|
+
const fields = {
|
|
845
|
+
credentials: path, store: stored.store, mode: stored.store === "file" ? "0600" : null, created: session.created,
|
|
846
|
+
user: session.user, org: session.org, key: session.key, replaced: Boolean(existing.api_key),
|
|
847
|
+
revoked_key_id: revoked ?? null,
|
|
848
|
+
};
|
|
849
|
+
if (stored.note)
|
|
850
|
+
fields["store_note"] = stored.note;
|
|
851
|
+
if (session.referral)
|
|
852
|
+
fields["referral"] = session.referral;
|
|
853
|
+
fields["credit_offer"] = credit.field;
|
|
854
|
+
if (location)
|
|
855
|
+
fields["location"] = location;
|
|
856
|
+
if (warnings.length)
|
|
857
|
+
fields["warnings"] = warnings;
|
|
858
|
+
const rf = session.referral;
|
|
859
|
+
const referralLine = !rf ? [] : rf.status === "not_applied"
|
|
860
|
+
? [` referral: ${rf.code} not applied (${rf.detail ?? "not eligible"})`]
|
|
861
|
+
: rf.status === "under_review"
|
|
862
|
+
? [` referral: ${rf.code} recorded; it is being checked before any credit is granted`]
|
|
863
|
+
: [` referral: ${rf.code} applied: $${rf.reward_usd} of compute credit with your first paid plan invoice`];
|
|
864
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, fields, next), [
|
|
865
|
+
`promigence login: ${session.created ? "account created" : "signed in"} as ${session.user.email} (${session.user.provider})`,
|
|
866
|
+
` org: ${session.org.name} (${session.org.org_id})`,
|
|
867
|
+
...(location ? [` Your sandboxes run on our servers on the ${location}.`] : []),
|
|
868
|
+
` key: ${session.key?.prefix ?? ""}… "${session.key?.name ?? keyName}" stored in ${storedWhere(stored)}`,
|
|
869
|
+
...(session.key?.expires_at ? [` key expires: ${session.key.expires_at.slice(0, 10)} (sign in again before then; \`promigence keys list\` shows it)`] : []),
|
|
870
|
+
...(stored.note ? [` note: ${stored.note}`] : []),
|
|
871
|
+
...referralLine,
|
|
872
|
+
...(revoked ? [` replaced key ${revoked} revoked`] : []),
|
|
873
|
+
...warnings.map((w) => ` warning: ${w}`),
|
|
874
|
+
...credit.lines,
|
|
875
|
+
` next: ${next[0].cmd}`,
|
|
876
|
+
], 0, ctx.pointer);
|
|
877
|
+
}
|
|
878
|
+
;
|
|
879
|
+
const logout = async (ctx) => {
|
|
880
|
+
const { creds, store: heldIn, unavailable } = readCredentialsWithStore();
|
|
881
|
+
const path = credentialsPath();
|
|
882
|
+
if (unavailable) {
|
|
883
|
+
throw usageError(`${storeLabel(unavailable.store)} could not be read (${unavailable.reason}), so the sign-in stored in it was neither revoked nor removed`, unavailable.store === "macos-keychain"
|
|
884
|
+
? "Unlock it (security unlock-keychain, or sign in at the Mac itself) and run `promigence logout` again."
|
|
885
|
+
: "Unlock the keyring (sign in to the desktop session) and run `promigence logout` again.");
|
|
886
|
+
}
|
|
887
|
+
const minted = loginMintedKey();
|
|
888
|
+
const warnings = [];
|
|
889
|
+
let revoked;
|
|
890
|
+
if (creds.api_key && !bool(ctx, "--keep-key") && !(minted && minted.apiKey === creds.api_key)) {
|
|
891
|
+
warnings.push("the key was not minted by `promigence login` (it was stored with `promigence auth login`), so it was only "
|
|
892
|
+
+ "forgotten here and still works; revoke it with `promigence keys revoke <key_id>` if nothing else uses it");
|
|
893
|
+
}
|
|
894
|
+
else if (minted && creds.api_key && !bool(ctx, "--keep-key")) {
|
|
895
|
+
try {
|
|
896
|
+
const c = new PromigenceClient({ apiKey: minted.apiKey, apiUrl: minted.apiUrl ?? DEFAULT_API_URL, onNotice: recordNotice });
|
|
897
|
+
const list = await c.request({ method: "GET", path: "/keys" });
|
|
898
|
+
const current = list.keys.find((k) => k.current);
|
|
899
|
+
if (current) {
|
|
900
|
+
await c.request({ method: "DELETE", path: `/keys/${encodeURIComponent(current.key_id)}?reason=logout` });
|
|
901
|
+
revoked = current.key_id;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
catch (e) {
|
|
905
|
+
const err = e;
|
|
906
|
+
warnings.push(`the key ${minted.keyId} was not revoked at ${(minted.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "")} `
|
|
907
|
+
+ `(${err.code ?? "error"}: ${err.message}); revoke it with \`${sh `promigence keys revoke ${minted.keyId}`}\` from another login to that API`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const cleared = writeCredentials(undefined, creds.api_url);
|
|
911
|
+
forgetKeychainReads();
|
|
912
|
+
const after = readCredentialsWithStore();
|
|
913
|
+
if (after.creds.api_key || cleared.left.length > 0) {
|
|
914
|
+
const where = after.creds.api_key
|
|
915
|
+
? (after.store === "file" || after.store === undefined ? path : storeLabel(after.store))
|
|
916
|
+
: storeLabel(cleared.left[0]);
|
|
917
|
+
throw usageError(`the sign-in stored in ${where} could not be removed, so this machine may still be signed in`
|
|
918
|
+
+ (revoked ? ` (the key itself was revoked: ${revoked})` : ""), cleared.warnings[0] ?? `Remove it by hand, then run \`promigence logout\` again.`);
|
|
919
|
+
}
|
|
920
|
+
warnings.push(...cleared.warnings);
|
|
921
|
+
if (process.env["PROMIGENCE_API_KEY"]) {
|
|
922
|
+
warnings.push("PROMIGENCE_API_KEY is set in this environment, so commands are still signed in with it; unset it to sign out here");
|
|
923
|
+
}
|
|
924
|
+
const next = nexts(["promigence login", "sign in again"]);
|
|
925
|
+
const fields = {
|
|
926
|
+
credentials: path, store: heldIn ?? null, had_key: Boolean(creds.api_key), revoked_key_id: revoked ?? null,
|
|
927
|
+
};
|
|
928
|
+
if (warnings.length)
|
|
929
|
+
fields["warnings"] = warnings;
|
|
930
|
+
const from = heldIn === undefined || heldIn === "file" ? path : storeLabel(heldIn);
|
|
931
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, fields, next), [
|
|
932
|
+
creds.api_key
|
|
933
|
+
? `promigence logout: key removed from ${from}${revoked ? ` and revoked (${revoked})` : ""}`
|
|
934
|
+
: `promigence logout: no key in ${from}; nothing to do`,
|
|
935
|
+
...warnings.map((w) => ` warning: ${w}`),
|
|
936
|
+
], 0, ctx.pointer);
|
|
937
|
+
};
|
|
938
|
+
function keyClient(ctx) {
|
|
939
|
+
return new PromigenceClient({ ...clientOpts(ctx), onNotice: recordNotice });
|
|
940
|
+
}
|
|
941
|
+
function keyLine(k) {
|
|
942
|
+
const flags = [k.access && k.access !== "full" ? `${k.access} only` : "", k.current ? "current" : "", k.revoked_at ? `revoked ${k.revoked_at}` : "", k.minted_by ? `minted by ${k.minted_by}` : ""].filter(Boolean).join(", ");
|
|
943
|
+
return ` ${k.key_id} ${k.prefix}… ${JSON.stringify(k.name)} created ${k.created_at} last used ${k.last_used_at ?? "never"} expires ${k.expires_at ?? "never"}${flags ? ` [${flags}]` : ""}`;
|
|
944
|
+
}
|
|
945
|
+
const keysList = async (ctx) => {
|
|
946
|
+
const all = bool(ctx, "--all");
|
|
947
|
+
const path = all ? "/keys?all=1" : "/keys";
|
|
948
|
+
const project = projectFlag(ctx);
|
|
949
|
+
let r;
|
|
950
|
+
const warnings = [];
|
|
951
|
+
if (project) {
|
|
952
|
+
const pk = await projectKeyClient(ctx, project);
|
|
953
|
+
try {
|
|
954
|
+
r = await pk.c.request({ method: "GET", path });
|
|
955
|
+
}
|
|
956
|
+
finally {
|
|
957
|
+
const w = await pk.done();
|
|
958
|
+
if (w)
|
|
959
|
+
warnings.push(w);
|
|
960
|
+
}
|
|
961
|
+
r = { ...r, keys: r.keys.filter((k) => k.key_id !== pk.keyId) };
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
r = await keyClient(ctx).request({ method: "GET", path });
|
|
965
|
+
}
|
|
966
|
+
const next = project
|
|
967
|
+
? (r.keys.length ? nexts([`${sh `promigence keys revoke KEY_ID --project ${project}`}${orgFlagTail(ctx)}`, "revoke one of them, its id from this list in place of KEY_ID (then `project delete` can go ahead)"]) : [])
|
|
968
|
+
: nexts(["promigence keys create --name NAME", "mint another key (shown once)"]);
|
|
969
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { org_id: r.org_id, ...(project ? { project } : {}), keys: r.keys, ...(warnings.length ? { warnings } : {}) }, next), [
|
|
970
|
+
`promigence keys list: ${r.keys.length} key${r.keys.length === 1 ? "" : "s"} in ${r.org_id}${project ? `, project ${project}` : ""}`,
|
|
971
|
+
...r.keys.map(keyLine),
|
|
972
|
+
...warnings.map((w) => ` warning: ${w}`),
|
|
973
|
+
], 0, ctx.pointer);
|
|
974
|
+
};
|
|
975
|
+
function projectFlag(ctx) {
|
|
976
|
+
const project = str(ctx, "--project");
|
|
977
|
+
if (!project && str(ctx, "--org")) {
|
|
978
|
+
throw usageError("--org goes with --project", `Name the project too: promigence ${ctx.cmd.name} --project default --org ${shellArg(str(ctx, "--org"))}.`);
|
|
979
|
+
}
|
|
980
|
+
return project;
|
|
981
|
+
}
|
|
982
|
+
function orgFlagTail(ctx) {
|
|
983
|
+
const org = str(ctx, "--org");
|
|
984
|
+
return org ? ` --org ${shellArg(org)}` : "";
|
|
985
|
+
}
|
|
986
|
+
export function accessBody(ctx) {
|
|
987
|
+
const a = str(ctx, "--access");
|
|
988
|
+
if (a === undefined)
|
|
989
|
+
return {};
|
|
990
|
+
if (!["full", "run", "read"].includes(a)) {
|
|
991
|
+
throw usageError(`--access: '${a}' is not an access`, "Pass --access full | run | read (run: sandboxes only; read: lists and reports only).");
|
|
992
|
+
}
|
|
993
|
+
return { access: a };
|
|
994
|
+
}
|
|
995
|
+
export function accessNote(access) {
|
|
996
|
+
if (access === "run")
|
|
997
|
+
return " (run: sandboxes, runs, files; no secrets, deletes, billing or keys)";
|
|
998
|
+
if (access === "read")
|
|
999
|
+
return " (read: lists and reports only)";
|
|
1000
|
+
return "";
|
|
1001
|
+
}
|
|
1002
|
+
const keysCreate = async (ctx) => {
|
|
1003
|
+
if (str(ctx, "--org") || str(ctx, "--project") || bool(ctx, "--service"))
|
|
1004
|
+
return keysCreateInOrg(ctx);
|
|
1005
|
+
const name = str(ctx, "--name") ?? `cli@${hostname()}`;
|
|
1006
|
+
const k = await keyClient(ctx).request({ method: "POST", path: "/keys", body: { name: name.slice(0, 64), ...expiryBody(ctx), ...accessBody(ctx) } });
|
|
1007
|
+
const next = nexts(["promigence keys list", "confirm it is there"]);
|
|
1008
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { key: k }, next), [
|
|
1009
|
+
`promigence keys create: ${k.key_id} "${k.name}"${accessNote(k.access)}`,
|
|
1010
|
+
` api_key: ${k.api_key}`,
|
|
1011
|
+
" This is the only time the secret is shown. Store it now (e.g. in your CI secret store).",
|
|
1012
|
+
expiryLine(k.expires_at),
|
|
1013
|
+
], 0, ctx.pointer);
|
|
1014
|
+
};
|
|
1015
|
+
const keysRevoke = async (ctx) => {
|
|
1016
|
+
const keyId = positional(ctx, "a key id", "promigence keys revoke key_0123abcd");
|
|
1017
|
+
const path = `/keys/${encodeURIComponent(keyId)}`;
|
|
1018
|
+
const project = projectFlag(ctx);
|
|
1019
|
+
let k;
|
|
1020
|
+
const warnings = [];
|
|
1021
|
+
if (project) {
|
|
1022
|
+
const pk = await projectKeyClient(ctx, project);
|
|
1023
|
+
try {
|
|
1024
|
+
k = await pk.c.request({ method: "DELETE", path });
|
|
1025
|
+
}
|
|
1026
|
+
finally {
|
|
1027
|
+
const w = await pk.done();
|
|
1028
|
+
if (w)
|
|
1029
|
+
warnings.push(w);
|
|
1030
|
+
}
|
|
1031
|
+
k = { ...k, current: false };
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
k = await keyClient(ctx).request({ method: "DELETE", path });
|
|
1035
|
+
}
|
|
1036
|
+
const next = nexts([project ? `promigence keys list --project ${shellArg(project)}${orgFlagTail(ctx)}` : "promigence keys list", "the keys that still work"]);
|
|
1037
|
+
const lines = [`promigence keys revoke: ${k.key_id} (${k.prefix}…) revoked at ${k.revoked_at}`];
|
|
1038
|
+
if (k.also_revoked?.length) {
|
|
1039
|
+
lines.push(` Also revoked, because this key had minted them: ${k.also_revoked.join(", ")}`);
|
|
1040
|
+
}
|
|
1041
|
+
if (k.current)
|
|
1042
|
+
lines.push(" That was the key this CLI is using: run `promigence login` for a new one.");
|
|
1043
|
+
lines.push(...warnings.map((w) => ` warning: ${w}`));
|
|
1044
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { key: k, ...(warnings.length ? { warnings } : {}) }, next), lines, 0, ctx.pointer);
|
|
1045
|
+
};
|
|
1046
|
+
const billingCard = async (ctx) => {
|
|
1047
|
+
const s = await keyClient(ctx).request({ method: "POST", path: "/billing/checkout", body: {} });
|
|
1048
|
+
const next = nexts(["promigence me", "after checkout: card_on_file is true and your trial credits are still there"]);
|
|
1049
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { checkout: s }, next), [
|
|
1050
|
+
"promigence billing card: open this page to add a card (nothing is charged now):",
|
|
1051
|
+
` ${s.url}`,
|
|
1052
|
+
" Remaining trial credits are used first; usage beyond them is billed to the card.",
|
|
1053
|
+
], 0, ctx.pointer);
|
|
1054
|
+
};
|
|
1055
|
+
export const LOGIN_HANDLERS = {
|
|
1056
|
+
login,
|
|
1057
|
+
signup,
|
|
1058
|
+
logout,
|
|
1059
|
+
"keys list": keysList,
|
|
1060
|
+
"keys create": keysCreate,
|
|
1061
|
+
"keys revoke": keysRevoke,
|
|
1062
|
+
"billing card": billingCard,
|
|
1063
|
+
};
|