Add a GitHub reference summary route for PR and issue pills #429
- MA
## Why
This is the first of three PRs that turn GitHub pull request and issue links in a document into status pills (PR 2) with hover cards (PR 3). It adds only the server data path and its supporting pieces. There are no editor or UI changes yet.
## What
- **`@chopin/protocol/github-reference`**: `parseGitHubReference(url)` accepts only github.com `/pull/N` and `/issues/N` URLs. It tolerates `www.`, `#fragments`, query strings, a trailing slash and `/files`/`/commits`/`/checks`. The module also adds the compact `owner/repo/pull/N` request key and the shared summary and result types. A pull request's state is `open | closed | merged` plus `draft`. An issue's state is `open | closed` plus `stateReason: completed | not_planned` (a `duplicate` reason maps to `not_planned`). - **`GitHubClient`**: - `installedRepository()`, which `repositoryAccess()` now delegates to, also returns the installation that grants access. - `pullRequest()` and `issue()` validate GitHub's responses and support ETag `If-None-Match`. - The installation permissions now include `issues`. - **`GitHubReferences`** resolver: a bounded cache (500 entries, 60 s) keyed by repository **node ID**, kind and number. It revalidates with ETags, coalesces identical in-flight fetches and allows at most 4 concurrent GitHub calls. - **Route** `GET /api/channels/:channelId/github-references?ref=owner/repo/pull/12&ref=…` accepts 1–20 refs and returns `{ references: { [ref]: { status: "ok", summary } | { status: "unavailable" } | { status: "rate-limited" } } }`. - **E2E fake GitHub**: `issues: "read"` on the fake installations, plus `octo-org/score` fixtures. Pulls 1–4 are open, merged, closed and draft. Issues 5–7 are open, completed and not planned. Anything else returns 404. - **Docs**: the permission lists, the HTTP catalogs and the route's authorization model.
## New GitHub App permission: owners must approve
The App now needs **Issues: Read-only** for issue summaries. Existing installations keep their old permissions until an owner approves the update. Until then, issue links resolve as `unavailable` and stay plain links. Pull request summaries use the existing Pull requests read permission.
## Authorization model
Admission is not authorization, and the route is not a general GitHub proxy:
1. It requires a session cookie. A missing one returns 401. 2. The caller must have current `pull` access to the document's own repository, checked through the App installation, and the repository node ID must still match the stored channel. Otherwise the route returns 404. 3. Each ref is strictly validated (owner and repo name rules, a positive integer, `pull` or `issues` only). The route deduplicates refs and caps them at 20; anything else returns 400. 4. **Per target repository**, the caller's own token must reach the repository through an active, non-suspended App installation. The caller needs `pull` on it, and the installation needs `pull_requests` or `issues` read permission. If any of these fails, the result is `unavailable`, which looks the same as a reference that doesn't exist. 5. The shared cache and in-flight coalescing are consulted **only after** step 4 passes for the current caller. Coalesced flights share only a status. If another user's token was rejected, the current caller fetches with its own token rather than taking that failure. 6. A rejection of the caller's own token raises a 401 inside `sessions.use`, so the normal refresh-and-retry path applies.
## Tests
- `bun test`: 4241 pass, 0 fail. New coverage: - Parser: accepted and rejected URLs, key round trip. - Client with injected fetch: merged, draft, open and closed mapping; issue state reasons; a PR served through the issues endpoint returns 404; ETag 304; 404, 403, 429 and secondary rate limits; the installation's issue permission. - Resolver: missing installation, missing permission, suspended installation; cache served only after each user's own access check; node-ID keying; coalescing and the concurrency cap; rate limits; eviction on 404; propagation of the caller's own 401; the cache bound. - Route: 401; no repository read access, unaffiliated repository and unknown channel return 404; a repository outside the installation returns `unavailable`; empty, oversized (21 refs) and malformed batches return 400, while 20 refs are accepted; a cached summary reaches a second user only after that user's own access check. - `bun run types` and `bun run ci` pass. - E2E for this route is deferred to PR 2, which renders the pills. The fake fixtures are in place.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
MaggieAppletonmerged 21f0255d076elast month on GitHub
MaggieAppletonhead-ref-deletedlast month on GitHub
MaggieAppletonreferenced this in Draw GitHub pull request and issue links as state pills #434last month on GitHub
MaggieAppletonreferenced this in Open a GitHub card from a pill on hover, focus or caret #435last month on GitHub