feat(auth): check all five identity providers for connection health - #67
Merged
Merged
Conversation
LinkedIdentitiesView read only the LinkedIn and Twitter status routes, so a Bluesky, Mastodon or GitHub row always rendered as healthy and the user's first signal of trouble was a cross-post failing. - Add Services/APIClient+Identities.swift with the three missing status calls (github, bluesky, mastodon). Mastodon is per-instance — the route 400s without ?instance= — so each linked instance gets its own call, percent-encoded. - Add Models/IdentityHealth.swift: a pure resolver mapping the five outcomes onto connected / needs reconnect / unknown. Kept out of APIClient (HTTP-only) so the rules are testable without a network, and because MockURLSession serves stubs FIFO with no path matching — five concurrent async let calls would race it. - Read the five routes in parallel from .task via async let, with a task group for the Mastodon instances. - A failed status call degrades to unknown and never downgrades a healthy row; the default snapshot is unknown, so a view that never got an answer cannot render a connection as disconnected. - Offer Reconnect on a stale row via OAuthCoordinator link flow, prefilling the Mastodon prompt with the row's existing instance. Because the ?link=true callback ends on a web redirect rather than the custom-scheme token handoff, a server-side success arrives here as a cancellation: reload before believing it failed, and point at the web only if the row is still stale afterwards. - Route a status-route 401 through authState.handleUnauthorized() rather than a logout, per CLAUDE.md; same for the identities and unlink calls, which previously fell through to a generic error. Honest limits of what these routes can prove, documented in both new files: LinkedIn/Twitter/GitHub status read no per-user data at all (server OAuth config only), so their configured:false means the provider is unusable for everyone and maps to unknown, not needs-reconnect — reconnecting cannot fix it. Bluesky and Mastodon report only whether an identity row exists, not token freshness, so a token revoked upstream still reads as connected. The real signal (LinkedIdentity.needsReconnect) exists server-side and is already returned by getLinkedIdentitiesForUser, but GET /api/user/identities does not select it. Register the four new files in project.pbxproj (no synced groups). Refs #63 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFt41K2Z9ghobFHy8Jiqgx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
LinkedIdentitiesViewchecked only the LinkedIn and Twitter status routes, so a Bluesky, Mastodon or GitHub row always rendered as a healthy "connected" account — the user's first signal of trouble was a cross-post failing later. All five routes are now read in parallel when the view appears, and each identity shows its own health.Closes #63
What's included
Commit 44c303b:
Services/APIClient+Identities.swift(new) — the three missing status calls (github,bluesky,mastodon). Mastodon is per-instance (the route 400s without?instance=), so each linked instance gets its own percent-encoded call. GitHub's route is public; the other two are Bearer.Models/IdentityHealth.swift(new) — a pure resolver mapping the five outcomes onto connected · needs reconnect · unknown. Kept out ofAPIClient(HTTP-only per CLAUDE.md) so the rules are testable without a network, and becauseMockURLSessionserves stubs FIFO with no path matching — five concurrentasync letcalls would race it.Views/LinkedIdentitiesView.swift— reads the five routes from.taskviaasync let(plus a task group for the Mastodon instances), renders a per-row health badge with a reason, and offers Reconnect on a stale row through the existingOAuthCoordinator.authenticate(provider:instance:link:true)flow. The Mastodon instance prompt still works and now prefills the row's existing instance when reconnecting.unknownand never downgrades a healthy identity. The default snapshot isunknown, so a view that never got an answer cannot render a connection as disconnected.authState.handleUnauthorized()rather than a logout or a generic error, per CLAUDE.md.project.pbxprojvia thexcodeprojgem.What these routes can and cannot prove
Worth a review eye, because it shapes the mapping (documented in both new files):
lib/integrations/connected-accounts-status.ts: "nothing here reads per-user data"). Soconfigured: falsemeans the provider is unusable for everyone, not that this user's connection went stale. That maps to unknown, not needs-reconnect: reconnecting cannot fix missing server credentials, so offering the button there would be a dead end.LinkedIdentityrow exists — the same tableGET /api/user/identitiesreads. They catch a row that vanished server-side, not a revoked token.Follow-up (backend ask): a token revoked upstream still reads as connected, so issue #63's "revoking a provider elsewhere shows needs reconnect" isn't fully reachable from the app yet. The real signal already exists —
LinkedIdentity.needsReconnect, flagged bylib/twitter/token-refresh.tson permanent auth failure and already returned bygetLinkedIdentitiesForUser(alongsideexpiresSoon) — butGET /api/user/identitiesdoesn'tselectit. Wiring that route togetLinkedIdentitiesForUserwould make this view report true per-identity health with no further iOS work.Known caveat: the
?link=truecallback authenticates via the web session cookie and finishes on a web redirect rather than the custom-scheme token handoff (the same constraint that keepslinkingEnabled = false). A reconnect the server actually completed therefore surfaces as anOAuthError.cancelled, so the view reloads before believing it failed and only points at the web if the row is still stale afterwards.Testing
xcodebuild -scheme InterlinedList -destination 'platform=iOS Simulator,id=6BBED4E1…' -parallel-testing-enabled NO -skip-testing:InterlinedListTests/E2EReadOnlyTests build— BUILD SUCCEEDED, no new warnings.APIClientIdentityStatusTests(11 — path, decode and failure per route, incl. a Bearer 401 and instance percent-encoding) andIdentityHealthTests(13 — all five providers connected, all five failing tounknown, the default snapshot, per-instance Mastodon resolution, and the two different meanings ofconfigured: false).🤖 Generated with Claude Code