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
@@ -1359,6 +1359,17 @@ export const settingsDict = {
"settings.providers.page.auth.apiKeyPlaceholder": "sk-...",
"settings.providers.page.auth.oauthMethodFallback": "OAuth метод {index}",
"settings.providers.page.auth.pasteAuthorizationCodePlaceholder": "Вставити код авторизації",
"settings.providers.page.auth.oauth.starting": "Запускаємо авторизацію…",
"settings.providers.page.auth.oauth.waiting": "Очікуємо на авторизацію…",
"settings.providers.page.auth.oauth.waitingHint": "Завершіть вхід у браузері. Не закривайте цю сторінку — підключення завершиться саме.",
"settings.providers.page.auth.oauth.codeHint": "Скопіюйте код авторизації з браузера і вставте його сюди.",
"settings.providers.page.auth.oauth.deviceCodeLabel": "Код пристрою",
"settings.providers.page.auth.oauth.linkLabel": "Посилання для авторизації",
"settings.providers.page.auth.oauth.promptRequired": "Заповніть «{field}», щоб продовжити",
"settings.providers.page.auth.oauth.error.sessionExpired": "Термін дії запиту на авторизацію минув. Підключіться ще раз, щоб почати заново.",
"settings.providers.page.auth.oauth.error.codeRequired": "Цьому провайдеру потрібен код авторизації з браузера.",
"settings.providers.page.auth.oauth.error.declined": "Авторизацію відхилено або не завершено.",
"settings.providers.page.auth.oauth.error.invalidInput": "Введені дані відхилено.",
"settings.providers.page.auth.connected": "Підключено",
"settings.providers.page.auth.incomplete": "Облікові дані відсутні",
"settings.providers.page.auth.incompleteHint": "· Додайте API-ключ або {env:VAR} перед використанням цього провайдера в чаті",
@@ -1391,6 +1402,9 @@ export const settingsDict = {
"settings.providers.page.actions.open": "Відкрити",
"settings.providers.page.actions.copy": "Копіювати",
"settings.providers.page.actions.complete": "Завершити",
"settings.providers.page.actions.continue": "Продовжити",
"settings.providers.page.actions.cancel": "Скасувати",
"settings.providers.page.actions.tryAgain": "Повторити спробу",
"settings.providers.page.actions.hide": "Сховати",
"settings.providers.page.actions.reconnect": "Перепідключити",
"settings.providers.page.actions.edit": "Редагувати",
@@ -1406,7 +1420,6 @@ export const settingsDict = {
"settings.providers.page.toast.apiKeySaved": "Ключ API збережено",
"settings.providers.page.toast.oauthStartFailed": "Не вдалося запустити потік OAuth",
"settings.providers.page.toast.oauthDetailsMissing": "Деталі OAuth не повернуто",
"settings.providers.page.toast.completeOAuthInBrowser": "Завершіть процес OAuth у вашому браузері",
"settings.providers.page.toast.oauthCompleteFailed": "Не вдалося завершити потік OAuth",
"settings.providers.page.toast.oauthCompleted": "Підключення OAuth завершено",
"settings.providers.page.toast.oauthLinkCopied": "Посилання OAuth скопійовано",