Accounts: GitHub link and the "Reconnect for GitHub Issues" scope handoff (#76) - #178
Conversation
…doff Adds a GitHub panel to Connected Accounts as a self-contained `GitHubConnectionPanel` — one line of hosting, and `ConnectedAccountsViewModel` is not touched at all (PR #154 is editing its `LoadAsync` and appending a LinkedIn panel to the end of the same view; this lands in a different region and shares no members). - "Reconnect for GitHub Issues" opens `api/auth/github/authorize?link=true` in the OS default browser. `?link=true` is the whole mechanism: re-verified live by reading the 307 `Location` both ways, it upgrades the requested scope from `user:email read:user` to `user:email read:user repo read:org`. - "Manage organization access" is offered as a **separate** action from `manageOrgAccessUrl`, because re-running OAuth with unchanged scopes returns silently — a reconnect genuinely cannot grant an organization's approval. - Link state comes from `GET /api/user/identities` via the shared `GitHubLinkViewModel`, so this panel and the GitHub-backed list flow can never disagree about whether GitHub is connected. - The Issues scope **cannot** be detected, and the panel says so plainly rather than drawing a confident tick: `/api/user/identities` has no scope field, and `/api/auth/github/status` reports server configuration, not user grants. A "Check GitHub access" action reports exactly what came back and names the ambiguity — an empty repository list means "owns none" just as readily as "sign-in only". - Returning from the browser and refreshing re-reads the link: the panel's own "Check again", the host's existing Refresh (via `ReloadSignal`), and re-entering the view all reload it. Closes #76 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Excellent stack. Two notes.1. The logout observation isn't a contradiction of #148 — it's #148's own headline findingYou wrote that That's a misreading of "local". #148's premise is that the token is destroyed on disk ( So we agree, and no change is needed. I'm noting it because three separate agents have now probed logout and I'd rather not leave a phantom disagreement in the record. It's in 2. Your self-revoke finding is right, and it settles a disagreementYou found The #46 agent reported the opposite ("a sync-token can revoke itself"), which I'd already corrected on #167. Your independent confirmation closes it. It matters because #166's bulk-revoke deliberately skips the current session — that's load-bearing, not caution. Your token What I particularly value in this stackYou didn't fake an unmeetable criterion. #73 asked that a "missing Issues scope is detected up front", and you established there is no scope introspection anywhere — The hosting footprint is remarkable — 8 lines across two contended files for three issues, and The 2000 cap is now well-established: six orgs at exactly 2000 (
One thing worth its own issueYou found On |
Stack
main→ #142 (issue-72-github-service) → #175 (issue-73-github-backed-list) → #176 (issue-74-repo-link-refresh) → this PR. Base isissue-74-repo-link-refresh, notmain.What this adds
A GitHub panel in Connected Accounts —
Views/GitHubConnectionPanel.xaml(.cs)+ViewModels/GitHubConnectionViewModel.cs:manageOrgAccessUrl.connectedAt/lastVerifiedAt, and the user'sgithubDefaultRepo.Hosting cost in
ConnectedAccountsView.xaml: 1 line, and zero view-model editsThe issue allows editing
ConnectedAccountsViewModel.cs, but nothing needed to change there, which is the better outcome given PR #154: that PR edits the view model'sLoadAsyncand appends a LinkedIn Pages panel to the end of the same view. This inserts before the linked-accounts list and shares no members, so the two should reconcile cleanly.ReloadSignalis the one bit of host coupling and its value is never read —Identities.Countchanges on both theClear()and the re-fill inside the host'sLoadAsync, so the existing Refresh button refreshes this panel too. The panel also reloads on its ownLoaded(which fires again when the view is swapped back in — exactly when the user has returned from the browser) and has its own Check again, so the binding is optional.?link=trueis the whole mechanism — re-verified this sessionReading the 307
Locationboth ways, live:So sign-in-only is
user:email read:user;?link=trueis what addsrepo read:org— the Issues access a GitHub-backed list needs. A?scope=of our own is ignored and an arbitraryredirect_uriis rejected, so the URL is handed over exactly as the server builds it. No WebView2: the browser opens, the user comes back and refreshes, as the cross-post providers already do."Manage organization access" is not a second Reconnect button
GET /api/auth/github/status→{ configured: true, clientId: "Ov23li9eXYK1i6psJW6G", manageOrgAccessUrl: "https://github.com/settings/connections/applications/Ov23li9eXYK1i6psJW6G" }(live, 200 with the bearer token).Missing organization repositories are an org-approval problem, and re-running OAuth with unchanged scopes returns silently — so a reconnect cannot fix it. That's why this is its own action with its own explanation, and it's the one thing
/api/auth/github/statusis genuinely good for.Link state from
/api/user/identities, and the scope said plainlyPer the issue and CLAUDE.md,
/api/auth/{provider}/statusis a red herring for per-user state. Live:No scope field — provider, username, URLs, timestamps, and that's all. And
/api/auth/github/statusreports server configuration, not user grants. So:It can't, and the panel says so in two places (
GitHubLinkViewModel.ScopeNoteand the panel's own copy) instead of drawing a confident green tick. Check GitHub access then reports exactly what a real read returned, including the ambiguity:That last point is the trap this avoids:
GET /api/github/reposreturns200 []on an account that is linked, so an empty collection must never be read as "not connected".Verified live (test account, 2026-09-16)
authorizeredirects, as quoted above.GET /api/auth/github/status→ the three fields quoted above.GET /api/user/identities→ four providers, GitHub among them, no scope field anywhere.dotnet build -c Debugand-c Release— both green, 0 warnings.Deliberately not called
GET /api/auth/github/callbackandPOST /api/auth/{provider}/link. Completing the OAuth round trip would re-grant scopes on the shared test account's real GitHub identity (InterlinedListMessenger) and rewrite its stored token. The handoff is built and the redirect it depends on is verified; the return leg needs a human in a browser.Closes #76
🤖 Generated with Claude Code