๐Ÿ“ฆ EqualifyEverything / equalify-iris-bench

๐Ÿ“„ login.mjs ยท 78 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78// Get a token for the deployment under test, via GitHub's device flow.
//
// Iris has no API keys and no anonymous mode: a GitHub token is required on every
// call, because the token is the identity that feedback from a session is filed
// under. The flow is three requests โ€” begin, approve in a browser, poll โ€” and doing
// it by hand with curl and jq is the fiddliest step in getting started, so it lives
// here instead.
//
// The token is printed and never written anywhere. Where it goes next is the
// operator's decision, and a credential this script silently dropped into a file
// would be one nobody remembered was there.

import { args, log, sleep } from "./util.mjs";

async function post(url, body) {
  const res = await fetch(url, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body ?? {}),
    signal: AbortSignal.timeout(30_000),
  });
  const text = await res.text();
  let json;
  try {
    json = JSON.parse(text);
  } catch {
    json = null;
  }
  return { status: res.status, json, text };
}

async function main() {
  const a = args();
  const base = (a.base ?? process.env.IRIS_BASE_URL ?? "https://iris.equalify.uic.edu/v1").replace(/\/$/, "");

  const begin = await post(`${base}/auth/github/device`);
  if (begin.status !== 200 || !begin.json?.device_code) {
    console.error(`POST ${base}/auth/github/device -> ${begin.status}: ${begin.text.slice(0, 300)}`);
    process.exit(1);
  }
  const { device_code, user_code, verification_uri, expires_in } = begin.json;

  // stderr, so `IRIS_TOKEN=$(node src/login.mjs)` works โ€” the token is the only
  // thing on stdout.
  process.stderr.write(
    `\n  Open ${verification_uri}\n  and enter the code:  ${user_code}\n\n` +
      `  Waiting for approval (expires in ${Math.round((expires_in ?? 900) / 60)} min)...\n`,
  );

  // GitHub's own interval, honoured rather than guessed, and widened on `slow_down`
  // โ€” polling too fast is what causes that error in the first place.
  let interval = (begin.json.interval ?? 5) * 1000;
  const deadline = Date.now() + (expires_in ?? 900) * 1000;
  for (;;) {
    await sleep(interval);
    if (Date.now() > deadline) {
      console.error("the device code expired before it was approved; run this again");
      process.exit(1);
    }
    const poll = await post(`${base}/auth/github/device/poll`, { device_code });
    if (poll.status === 200 && poll.json?.access_token) {
      process.stderr.write("\n  Approved.\n\n");
      // Say what to do with it, once, on stderr โ€” then the token alone on stdout.
      process.stderr.write(`  Put this in .env as IRIS_TOKEN, then run with --env-file=.env\n\n`);
      process.stdout.write(`${poll.json.access_token}\n`);
      return;
    }
    if (poll.status === 202) {
      if (poll.json?.error === "slow_down") interval += 5000;
      continue;
    }
    console.error(`poll -> ${poll.status}: ${poll.text.slice(0, 300)}`);
    process.exit(1);
  }
}

await main();