feat(auth): refresh OAuth tokens in the credential resolver #6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/oauth-refresh-resolver"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Cline was reachable two ways and only one of them refreshed.
ClineAuthran the browser flow and itsgetAccessToken()renewed a token at or near expiry, but theCredentialResolverthatcreateServiceshanded to the provider catalog read the auth store directly and returnedcredential.accessTokenverbatim. Chat requests therefore started failing with a bare 401 once the access token aged out, with a usable refresh token sitting next to it on disk and nothing surfacing the reason.Changes
provider/credentials.ts(new) —createStoreCredentialResolver(store, options?)delegates to the provider's OAuth flow instead of reading the raw token. The flow object is cached per provider id for the life of the resolver:ClineAuthcollapses concurrent refreshes internally viainFlightRefresh, and that only holds while the instance is shared, so constructing one per request would stampede the refresh endpoint.provider/oauth.ts(new) — one shared source of truth for which catalog ids have a browser flow, replacing the ad-hocOAUTH_PROVIDERSset incommands/auth.ts. TheOAuthProviderinterface covers the full surface the CLI needs (getAccessToken,login,loginWithApiKey,verify), so the auth command no longer namesClineAuthat all. Backed by aMaprather than an object literal, since{}["constructor"]is both truthy and callable.createServices— swaps the inlinestore.get→credential.accessTokenblock forcreateStoreCredentialResolver(auth), built once so every request in a session shares its flows.Typed auth failure
Split by case rather than throwing on all of them:
undefined. This must stay:catalog.createProviderwraps the resolver as(await resolve(id)) ?? (await envCredentialResolver(id)), so throwing here would break theCLINE_API_KEYenv fallback.AuthErrorpropagates. A rejected refresh now reports "Cline session expired... Runthorny auth login cline" instead of a bare 401.AuthError extends ThornyError, whichcli/src/index.tsalready prints plainly without a stack trace, so no CLI change was needed.listModels()still swallows it and falls back to the static list, sothorny modelsdoes not explode for a provider you have not logged into.Documented on the
CredentialResolvertype.Tests
packages/core/test/credential-resolver.test.ts, 9 tests. The core case is covered twice: the resolver returns the refreshed token and writes the new credential back to the store, and end-to-end throughregisterBuiltinProvidersasserting the header the chat provider actually sends isBearer refreshed-token. Also: a token not near expiry is left alone with no refresh call, three concurrent resolves collapse to one round-trip, a rejected refresh raisesAuthError, an empty store resolves toundefinedwith the env var still winning, ansk_api-key stored under an OAuth provider passes straight through, and anonecredential stays unauthenticated.Full suite: 147 pass, 0 fail.
typecheckandbiome checkclean.Note on scope
This branch is stacked on
fix/ollama-config-baseurl, which is pushed but has no open PR and is not yet inmain. Both reshape theregisterBuiltinProviders/createServiceswiring, so they were kept in order to avoid a conflict. This PR therefore contains two commits — the baseurl fix and this one. If the baseurl fix should land separately, open its PR first and rebase this one onto the result.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.