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.
@@ -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.exit(130);
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
- 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.";
435
- 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.";
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
- ...sent.token ? { "proxy-authorization": `Bearer ${sent.token}` } : {},
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 { step as _, apiClient as a, Interrupted as c, blue as d, bold as f, say as g, red as h, IapRefusal as i, ask as l, grey as m, parseLoginLine as n, atAddress as o, dash as p, ApiError as r, readConnection as s, loginCommand as t, askText as u };
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 };