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 };
}