modelos-secure 0.1.1 → 0.2.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/assets/login-CipN1qFD.js +1 -0
- package/assets/{login-WwE-Gunj.js → login-DZgP66jr.js} +47 -11
- package/cli.js +1754 -244
- package/hook/clients.mjs +11 -3
- package/hook/edge.mjs +405 -0
- package/hook/hook.mjs +35 -7
- package/hook/hosts.d.mts +2 -0
- package/hook/hosts.mjs +26 -0
- package/package.json +1 -1
- package/python/modelos_secure/clients.py +15 -2
- package/python/modelos_secure/edge.py +348 -0
- package/python/modelos_secure/hook.py +33 -4
- package/assets/login-BcHFyX6z.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as loginCommand } from "./login-DZgP66jr.js";
|
|
@@ -5,7 +5,7 @@ import { execFile, spawn } from "node:child_process";
|
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { createServer } from "node:http";
|
|
7
7
|
import { emitKeypressEvents } from "node:readline";
|
|
8
|
-
import { request } from "node:https";
|
|
8
|
+
import { request as request$1 } from "node:https";
|
|
9
9
|
import { checkServerIdentity } from "node:tls";
|
|
10
10
|
// src/ui.ts
|
|
11
11
|
var color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
@@ -22,7 +22,22 @@ function say(line = "") {
|
|
|
22
22
|
console.log(line);
|
|
23
23
|
}
|
|
24
24
|
var STEP_WIDTH = 13;
|
|
25
|
+
var lastStep = "";
|
|
26
|
+
/** The last step that finished, so a stop can say where it happened. */
|
|
27
|
+
var lastFinishedStep = () => lastStep;
|
|
28
|
+
/** Work to finish before the process ends on Ctrl-C, such as stopping a review it started. */
|
|
29
|
+
var onStop = [];
|
|
30
|
+
function beforeStopping(work) {
|
|
31
|
+
onStop.push(work);
|
|
32
|
+
return () => {
|
|
33
|
+
onStop.splice(onStop.indexOf(work), 1);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function finishBeforeStopping() {
|
|
37
|
+
await Promise.race([Promise.allSettled(onStop.map((work) => work())), new Promise((resolve) => setTimeout(resolve, 5e3))]);
|
|
38
|
+
}
|
|
25
39
|
function step(name, detail, state = "done") {
|
|
40
|
+
if (state === "done") lastStep = name;
|
|
26
41
|
const right = state === "done" ? "done" : "";
|
|
27
42
|
say(` ${bold(name.padEnd(STEP_WIDTH))}${detail}${right ? ` ${grey(right)}` : ""}`);
|
|
28
43
|
}
|
|
@@ -83,15 +98,19 @@ async function ask(question, why, choices) {
|
|
|
83
98
|
async function askText(question, secret = false) {
|
|
84
99
|
process.stdout.write(` ${question} `);
|
|
85
100
|
if (!interactive()) return "";
|
|
86
|
-
return new Promise((resolve) => {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
87
102
|
let value = "";
|
|
88
103
|
emitKeypressEvents(process.stdin);
|
|
89
104
|
process.stdin.setRawMode(true);
|
|
90
105
|
process.stdin.resume();
|
|
91
106
|
const onKey = (text, key) => {
|
|
92
107
|
if (key.ctrl && key.name === "c") {
|
|
108
|
+
process.stdin.off("keypress", onKey);
|
|
93
109
|
process.stdin.setRawMode(false);
|
|
94
|
-
process.
|
|
110
|
+
process.stdin.pause();
|
|
111
|
+
process.stdout.write("\n");
|
|
112
|
+
reject(new Interrupted());
|
|
113
|
+
return;
|
|
95
114
|
}
|
|
96
115
|
if (key.name === "return") {
|
|
97
116
|
process.stdin.off("keypress", onKey);
|
|
@@ -282,7 +301,7 @@ function gcloudIdentityToken(audience) {
|
|
|
282
301
|
/** A fetch that connects to the URL's host and checks its certificate against `name`. */
|
|
283
302
|
function fetchCheckingName(name) {
|
|
284
303
|
return (url, init = {}) => new Promise((resolve, reject) => {
|
|
285
|
-
const outgoing = request(new URL(url), {
|
|
304
|
+
const outgoing = request$1(new URL(url), {
|
|
286
305
|
method: init.method ?? "GET",
|
|
287
306
|
headers: init.headers,
|
|
288
307
|
servername: name,
|
|
@@ -426,13 +445,27 @@ function refusalKind(answer) {
|
|
|
426
445
|
if (!json && status < 500 && (status < 300 || page)) return "not-the-app";
|
|
427
446
|
return null;
|
|
428
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* IAP's own words for a refusal, when it gave them as plain text, such as "Invalid bearer token.
|
|
450
|
+
* Audience doesn't match the allowlisted oauth clients for this application." A page gives none.
|
|
451
|
+
*/
|
|
452
|
+
function iapReason(headers, text) {
|
|
453
|
+
if ((headers.get("content-type") ?? "").includes("html")) return void 0;
|
|
454
|
+
const line = text.trim().split("\n")[0]?.trim() ?? "";
|
|
455
|
+
if (!(headers.get("x-goog-iap-generated-response") === "true" || line.startsWith("Invalid IAP credentials")) || !line || line.startsWith("<")) return void 0;
|
|
456
|
+
const words = line.replace(/^Invalid IAP credentials:\s*/i, "").slice(0, 240).trim();
|
|
457
|
+
if (!words) return void 0;
|
|
458
|
+
return /[.!?]$/.test(words) ? words : `${words}.`;
|
|
459
|
+
}
|
|
429
460
|
/** What happened, why, and what to do, in the voice of the CLI's other failures. */
|
|
430
|
-
function refusalMessage(kind, credential, url, ci = Boolean(process.env.CI)) {
|
|
461
|
+
function refusalMessage(kind, credential, url, ci = Boolean(process.env.CI), detail = {}) {
|
|
431
462
|
if (kind === "not-the-app") return `${url} answered, but it is not ModelOS Secure. Check the address against the login command on the tokens page.`;
|
|
432
463
|
if (kind === "no-sign-in") return ci ? "This address needs a Google sign-in, and none was found. In CI, set MODELOS_SECURE_CLIENT_ID with gcloud signed in as a service account, or set MODELOS_SECURE_IAP_TOKEN." : "This address needs a Google sign-in, and this machine has none yet. Run modelos-secure login with the command from the tokens page.";
|
|
433
464
|
if (kind === "not-allowed") return credential.email ? `You are signed in to Google as ${credential.email}, but that account is not allowed through to ModelOS Secure. Ask whoever runs ModelOS Secure to give it access.` : "This Google account is not allowed through to ModelOS Secure. Ask whoever runs ModelOS Secure to give it access.";
|
|
434
|
-
|
|
435
|
-
if (credential.source === "
|
|
465
|
+
const said = detail.reason ? `It said: ${detail.reason}` : detail.redirected ? "It sent the request to its sign-in page instead." : "";
|
|
466
|
+
if (credential.source === "env") return `Google refused the token in MODELOS_SECURE_IAP_TOKEN: it has expired, or it was made for another client ID. Make a new one for the client ID on the tokens page.${said ? ` ${said}` : ""}`;
|
|
467
|
+
if (credential.source === "gcloud") return `Google refused the identity token from gcloud: it was made for another client ID, or the account is not a service account. Check MODELOS_SECURE_CLIENT_ID against the tokens page.${said ? ` ${said}` : ""}`;
|
|
468
|
+
if (said) return `Google did not let your sign-in through to ModelOS Secure. ${said} Run modelos-secure login again. If it says the same, send this message to whoever runs ModelOS Secure.`;
|
|
436
469
|
return "Google refused your sign-in: it has expired or was revoked. Run modelos-secure login again.";
|
|
437
470
|
}
|
|
438
471
|
// src/api.ts
|
|
@@ -528,8 +561,8 @@ function apiClient(connection, options = {}) {
|
|
|
528
561
|
const sent = await credential();
|
|
529
562
|
const url = `${connection.url}/api/v1${path}`;
|
|
530
563
|
const headers = {
|
|
531
|
-
authorization: `Bearer ${connection.token}`,
|
|
532
|
-
|
|
564
|
+
authorization: `Bearer ${sent.token ?? connection.token}`,
|
|
565
|
+
"x-modelos-secure-token": connection.token,
|
|
533
566
|
...body ? { "content-type": body.type } : {}
|
|
534
567
|
};
|
|
535
568
|
const response = await (connection.tlsName ? fetchCheckingName(connection.tlsName)(url, {
|
|
@@ -560,7 +593,10 @@ function apiClient(connection, options = {}) {
|
|
|
560
593
|
body: parsed,
|
|
561
594
|
sent: Boolean(sent.token)
|
|
562
595
|
});
|
|
563
|
-
if (kind) throw new IapRefusal(kind, response.status, refusalMessage(kind, sent, connection.url
|
|
596
|
+
if (kind) throw new IapRefusal(kind, response.status, refusalMessage(kind, sent, connection.url, void 0, {
|
|
597
|
+
reason: iapReason(response.headers, text),
|
|
598
|
+
redirected: response.status >= 300 && response.status < 400
|
|
599
|
+
}));
|
|
564
600
|
if (!response.ok) {
|
|
565
601
|
const error = typeof parsed === "object" && parsed !== null ? parsed.error : void 0;
|
|
566
602
|
throw new ApiError(typeof error === "string" ? error : `The server answered ${response.status.toString()}.`, response.status);
|
|
@@ -622,4 +658,4 @@ async function loginCommand(given = {}) {
|
|
|
622
658
|
const who = connection.google?.email;
|
|
623
659
|
say(`Saved. This machine now works in ${me.workspace}${who ? `, signed in to Google as ${who}` : ""}.`);
|
|
624
660
|
}
|
|
625
|
-
export {
|
|
661
|
+
export { interactive as _, apiClient as a, say as b, Interrupted as c, beforeStopping as d, blue as f, grey as g, finishBeforeStopping as h, IapRefusal as i, ask as l, dash as m, parseLoginLine as n, atAddress as o, bold as p, ApiError as r, readConnection as s, loginCommand as t, askText as u, lastFinishedStep as v, step as x, red as y };
|