feat(auth): one admin-set GitHub token, and no sign-in (#459)
One admin-set GitHub token, held server-side, and no sign-in anywhere.
Iris had three GitHub credential concepts — per-user device-flow tokens,
`github.issue_token`, and `github.anonymous_token` — and the complexity lived in
the bridge between them: `req.anonymous`, a session-list 403, address-keyed
limits, `usingServiceToken`. This deletes the bridge and both ends of it.
`github.token` is one PAT the operator sets, required at startup, never sent to
a client. No caller presents a GitHub credential. Gone with the login: the
device flow and `src/routes/auth.ts`, the redirect flow, the per-user token
cache, OAuth scope validation, `client_id` / `client_secret` / `oauth_scope` /
`oauth_base_url`, `github.anonymous_token`, `github.issue_token`, and
`auth_per_minute`. Net −2,003 lines across 39 files.
Two questions stay separate, because conflating them is the failure this design
would otherwise be one refactor away from. "May this caller use the API at all?"
is `server.api_token` — a plain shared secret, optional, following the
`server.quality_token` pattern, and deliberately NOT the GitHub token. It is
checked before GitHub is consulted, so a refused stranger cannot spend the
deployment's rate limit. "Who is this deployment?" is `github.token`, resolved
once per process against `GET /user`. A caller who presents the gate token is
not thereby anybody. Publishing the GitHub token to browsers so they could
authenticate with it would have made the deployment's identity readable out of a
page.
Blank gate means open, which is what lets the demo page work with no authorizing
step at all.
What that costs is documented rather than guarded. With one identity there is
nothing to isolate: `GET /v1/sessions` lists the deployment's sessions, and a
session id reaches its document. A guard hiding one consequence of a shared
identity while the others stand is theatre, so what replaces it is a boot warning
and the same sentence in README.md, docs/API.md and docs/github-auth.md.
Contributors get no attribution — issues say what a session found, not who found
it — and that was the sustainability argument for requiring a login, so the
README now states the trade instead of describing a mechanism it no longer has.
The deployment manifests were a correctness problem, not a docs problem:
`docker-compose.yml` never passed `IRIS_GITHUB_TOKEN`, which this makes required,
so `docker compose up` would have refused to boot.
Seven review rounds, each finding a defect the previous fix created. The two in
new code: the identity memo cached the resolved id rather than the lookup, so a
cold-start burst of 8 parallel requests spent 8 GitHub lookups; and `upsertUser`
sat inside the GitHub try, reporting a failed write as `401 could not
authenticate to GitHub: SQLITE_READONLY … (/private/tmp/…)` — the exact symptom
`rejectLegacyUsersTable` names as misleading, with a filesystem path published to
an unauthenticated caller. Both pinned, both pins verified by reverting the fix.
The rest were claims left describing deleted mechanisms, in five files, including
two paragraphs of docs/API.md that answered the attribution question in opposite
ways twenty lines apart. An account repoint turned out to strand every
per-session route, not just the listing, which is now documented and pinned at
the route.
test/e2e.sh grew a second deployment with the gate deleted: the open path the
demo page depends on had been covered by unit tests and never driven through the
real stack.
Closes #456
Co-Authored-By: bbertucc <46652+bbertucc@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>