fix: stop 500s when an account has no usable PDS credentials - #18
Closed
CountableNewt wants to merge 4 commits into
Closed
CountableNewt wants to merge 4 commits into
CountableNewt wants to merge 4 commits into
Conversation
The environment banner is an internal aid for telling local and dev apart; it should never be shown to end users on skej.at. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Teams, calendar, approvals, and posting on behalf of other accounts are Skej Pro features still under development. One flag now gates them all: on by default in local/test/dev, off in prod. Gated endpoints do not exist when the flag is off: Pro-only routes are never registered, non-self account DIDs throw the router's own unknown-route error, and a draft status gets the same treatment as any unrecognized status value, so nothing about the Pro surface is fingerprintable. The web app learns the flag at runtime via /v1/me and hides the calendar, account switcher, and team management cards. Also fixes unknown routes returning 500 internal_error: ErrorMiddleware now renders Hummingbird HTTPErrors with their real status, so unmatched paths properly return 404 not_found. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generate app.bsky.richtext.facet#tag facets next to link facets so hashtags render as tag links on Bluesky. Detection mirrors @atproto/api: a hashtag starts the text or follows whitespace, needs a character that is not a digit, punctuation, or an invisible separator, trims trailing punctuation, and caps the tag at 64 characters. Stale link and tag facets sent by a client are now regenerated from the text rather than preserved, so byte offsets cannot drift after an edit. Already-scheduled posts pick up tag facets at publish time through the shadow record path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Listing schedules resolves brand permissions first, which walks every managed account and calls listRecords for each. authenticatedSession sat outside the do/catch that falls back to cached records, so any account without an OAuth session row threw notConfigured and the request became an opaque 500 -- for every user, not just the account at fault. Dev hits this because dev/seed inserts six accounts with no OAuth sessions. Move the session lookup inside the fallback in listRecords and getRecord so a missing or rejected credential reads through to the local cache, the same way an unreachable PDS already did. Because reads now degrade quietly, add the detection that makes that safe: PDSClientError.isAuthFailure is the single definition of "credentials are gone" (shared with ScheduleWorker.classify so they cannot drift), and a read that falls back for auth reasons marks the account needs_reauth. Transient failures deliberately do not. Writes still cannot succeed without credentials, so map auth failures to 409 account_needs_reauth instead of a 500. Not 401 -- the Skej session is valid, only the PDS credential is dead, and a 401 reads as "signed out". ErrorMiddleware also logs unhandled errors now; it swallowed them silently, which is why this surfaced as a bare 500 with nothing to go on. Surface all of this in the UI, since needs_reauth was previously set only by the worker and read by nothing. requestJSON throws SkejApiError carrying the error code, a banner names the stale account and offers a Reconnect that starts OAuth with the handle prefilled, both account pickers mark stale accounts, and the settings page gains a status badge and its own reconnect card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Author
|
Closing in favor of a topic branch off |
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.
Fixes the 500 on
GET /v1/accounts/:did/schedulesreported on the dev instance, and adds the reconnect handling that the fix makes necessary.Root cause
Listing schedules resolves brand permissions first, which walks every managed account and calls
listRecordsfor each.authenticatedSessionsat outside thedo/catchthat falls back to cached records, so any account without an OAuth session row threwnotConfiguredand the request became an opaque 500 — for every user, not just the account at fault.Dev hit this constantly because
dev/seedinserts six accounts with no OAuth sessions. Prod is exposed too, just less often: any real account whose token is revoked or expires produces the same 500 for everyone.The bug was invisible to tests because
InMemoryPDSClientreturns[:]for unknown DIDs instead of throwing.Changes
Read path —
listRecordsandgetRecordnow wrap the session lookup, so a missing or rejected credential reads through to the local cache the same way an unreachable PDS already did.Detection — because reads now degrade quietly, a dead token would otherwise look healthy until the next publish.
PDSClientError.isAuthFailureis now the single definition of "credentials are gone" (notConfigured, 401/403,invalid_grant), shared withScheduleWorker.classifyso the two can't drift. A read that falls back for auth reasons marks the accountneeds_reauth; transient failures (503, 429) deliberately don't.Write path — writes still can't succeed without credentials, so auth failures map to 409
account_needs_reauthinstead of a 500. Deliberately not 401: the Skej session is valid, only the PDS credential is dead, and a 401 reads as "signed out" and would bounce the user to sign-in.Diagnosability —
ErrorMiddlewareswallowed unhandled errors silently, which is why this surfaced as a bare 500 with nothing to go on. It now logs method, path, and the reflected error.UI —
needs_reauthwas previously set only by the worker and read by nothing.requestJSONnow throwsSkejApiErrorcarrying the error code; a banner names the stale account and offers a Reconnect that starts OAuth with the handle prefilled; both account pickers mark stale accounts; the settings page gains a status badge and its own reconnect card.Verification
Caught error: .notConfiguredbefore the fix.needs_reauth, confirmed the banner and picker warning appear, clicked Reconnect, confirmed the account returned toactiveand the banner cleared.Prod notes
Prod runs
SKEJ_PRO_ENABLED=false. TheConnected Accountcard sits outside the Pro gate so both reauth surfaces work, and/v1/accountsfilters to self — so prod won't see the seeded-demo-account noise that dev does.🤖 Generated with Claude Code