๐Ÿ“ฆ EqualifyEverything / equalify-iris

๐Ÿ“„ github.ts ยท 56 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// The one GitHub call Iris makes to identify itself.
//
// A deployment has a single GitHub identity: the token in `github.token`, set once by the
// operator and never sent to a client. This file turns that token into an account.
//
// The base URL is passed in rather than hardcoded so a deployment can target GitHub
// Enterprise, and so the suite can drive this against a mock host.
//
// What the token is used for, and it is only three things:
//
//   1. `GET /user`, here, to name the account sessions and issues belong to.
//   2. Filing agent-suggestion and agent-update issues on `upstream_repo` โ€” see
//      src/github/issue.ts.
//   3. The dedupe that runs before each of those: a title search for the issue already
//      tracking this lesson, and a comment on it when there is one.
//
// So the narrowest credential that works is a fine-grained personal access token scoped to
// `upstream_repo` alone with `Issues: read and write`. The READ half is for the dedupe in
// (3), not for a label โ€” `ensureLabel` is gone (src/github/issue.ts, and both filing paths
// pass no `labels`), because GitHub silently drops labels set by a filer without push
// access. Do not add one back on the strength of a comment here. Nothing pushes, nothing
// opens pull requests, and nothing reads code, so a classic `repo` token grants far more
// than this service uses.
//
// Two operator-visible consequences of there being one token:
//
//   - Every issue is filed under this account, and NOBODY IS CREDITED. Neither body carries
//     a human identifier โ€” see the two builders in src/github/issue.ts: what identifies a
//     contribution is the session id. On the feedback path the user's own words are quoted
//     verbatim, which is a trace of a person, not an attribution of one. Do not read this
//     as licence to put a `@name` in a body: issue.ts wraps user text in a code span
//     precisely so a name inside it cannot notify anyone.
//   - A fine-grained PAT EXPIRES, and nothing here refreshes it. The day it lapses, every
//     request 401s with "could not authenticate to GitHub" and the fix is a new token in
//     config. GitHub emails the token's owner before that happens; there is no in-process
//     warning, because a PAT's expiry is not visible in `GET /user`.

export interface GitHubUser {
  id: number;
  login: string;
}

// Identify the GitHub account behind a token.
//
// The status goes in the message because that is the only place it is read: the caller
// backs off for a fixed window on ANY failure and says so, rather than sorting a 401 from a
// 500 (see src/auth/middleware.ts for why one identity makes that distinction moot).
export async function fetchUser(token: string, apiBase: string): Promise<GitHubUser> {
  const res = await fetch(`${apiBase}/user`, {
    headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "equalify-iris" },
  });
  if (!res.ok) throw new Error(`github user lookup failed: ${res.status}`);
  const json = (await res.json()) as { id: number; login: string };
  return { id: json.id, login: json.login };
}