fix(providers): complete OAuth logins that finish in the browser

OpenCode's authorize response reports how the client must finish: `code`
expects a pasted code, while `auto` requires the client to call
oauth/callback immediately and hold it open — upstream blocks in there
polling for the device code or waiting on its loopback redirect, and only
that call persists the credential. Every auth plugin OpenCode ships uses
`auto`; none use `code`.

The page implemented only `code`. It opened the browser, showed a paste
field no provider can fill, and never called back, so a successful sign-in
stored nothing and the app sat unchanged. Authorization now drives the UI:
`auto` chains straight into the callback behind a waiting state with a
cancel, and the paste field appears only when a provider actually asks
for a code.

Two smaller failures shared that surface. Prompts were never collected,
which put GitHub Copilot Enterprise out of reach entirely, so a method
that declares them now asks first and passes the answers to authorize.
Device codes are also recovered from the instructions text, where they
actually live — the old code read fields the API does not return, so the
copy button never appeared.

The callback is exempt from the ordinary proxy deadline and gets a
15-minute budget, bounded by the shortest upstream expiry we know of.
A human sign-in with 2FA does not fit in four minutes, and expiring it
turned a completed login into a 504.
This commit is contained in:
Bohdan Triapitsyn
2026-08-04 19:14:58 +03:00
parent 8c37061886
commit 687681c83b
19 changed files with 1239 additions and 290 deletions
@@ -1386,6 +1386,17 @@ export const settingsDict = {
'settings.providers.page.auth.apiKeyPlaceholder': 'sk-...',
'settings.providers.page.auth.oauthMethodFallback': 'OAuth method {index}',
'settings.providers.page.auth.pasteAuthorizationCodePlaceholder': 'Paste authorization code',
'settings.providers.page.auth.oauth.starting': 'Starting authorization…',
'settings.providers.page.auth.oauth.waiting': 'Waiting for authorization…',
'settings.providers.page.auth.oauth.waitingHint': 'Finish signing in in your browser. Keep this page open — the connection completes on its own.',
'settings.providers.page.auth.oauth.codeHint': 'Copy the authorization code from your browser and paste it here.',
'settings.providers.page.auth.oauth.deviceCodeLabel': 'Device code',
'settings.providers.page.auth.oauth.linkLabel': 'Authorization link',
'settings.providers.page.auth.oauth.promptRequired': 'Fill in “{field}” to continue',
'settings.providers.page.auth.oauth.error.sessionExpired': 'The authorization request expired. Connect again to restart it.',
'settings.providers.page.auth.oauth.error.codeRequired': 'This provider needs the authorization code from your browser.',
'settings.providers.page.auth.oauth.error.declined': 'Authorization was declined or did not complete.',
'settings.providers.page.auth.oauth.error.invalidInput': 'The details you entered were rejected.',
'settings.providers.page.auth.connected': 'Connected',
'settings.providers.page.auth.incomplete': 'Credentials missing',
'settings.providers.page.auth.incompleteHint': '· Add an API key or {env:VAR} before using this provider in chat',
@@ -1416,6 +1427,9 @@ export const settingsDict = {
'settings.providers.page.actions.open': 'Open',
'settings.providers.page.actions.copy': 'Copy',
'settings.providers.page.actions.complete': 'Complete',
'settings.providers.page.actions.continue': 'Continue',
'settings.providers.page.actions.cancel': 'Cancel',
'settings.providers.page.actions.tryAgain': 'Try again',
'settings.providers.page.actions.hide': 'Hide',
'settings.providers.page.actions.reconnect': 'Reconnect',
'settings.providers.page.actions.edit': 'Edit',
@@ -1430,7 +1444,6 @@ export const settingsDict = {
'settings.providers.page.toast.apiKeySaved': 'API key saved',
'settings.providers.page.toast.oauthStartFailed': 'Failed to start OAuth flow',
'settings.providers.page.toast.oauthDetailsMissing': 'No OAuth details returned',
'settings.providers.page.toast.completeOAuthInBrowser': 'Complete the OAuth flow in your browser',
'settings.providers.page.toast.oauthCompleteFailed': 'Failed to complete OAuth flow',
'settings.providers.page.toast.oauthCompleted': 'OAuth connection completed',
'settings.providers.page.toast.oauthLinkCopied': 'OAuth link copied',