{"record":{"id":"68d9f252f1feb06b","repo":"can1357/oh-my-pi","slug":"oauth-refresh-did-not-produce-a-usable-credential","errorCode":null,"errorMessage":"OAuth refresh did not produce a usable credential for provider: ${provider}","messagePattern":"OAuth refresh did not produce a usable credential for provider: (.+?)","errorType":"exception","errorClass":"AIError.OAuthError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/auth-storage.ts","lineNumber":5255,"sourceCode":"\t\t\t\t\t),\n\t\t\t\tisDefinitiveFailure: error => AIError.isDefinitiveOAuthFailure(String(error)),\n\t\t\t\tdisabledCause: error => `oauth refresh failed: ${String(error)}`,\n\t\t\t});\n\t\t\tif (result.credential) {\n\t\t\t\tif (result.refreshed) {\n\t\t\t\t\t// We performed this refresh ourselves — trust the provider's new token\n\t\t\t\t\t// even when its lifetime is shorter than the refresh skew (some grants\n\t\t\t\t\t// are legitimately short-lived); the next resolve simply treats it as\n\t\t\t\t\t// due for refresh again instead of rejecting a token we just minted.\n\t\t\t\t\tif (Date.now() < result.credential.expires) return result.credential;\n\t\t\t\t} else if (Date.now() + OAUTH_REFRESH_SKEW_MS < result.credential.expires) {\n\t\t\t\t\t// Reloaded (not refreshed by us) credential — match #refreshOAuthCredential's\n\t\t\t\t\t// freshness contract: a reload within the refresh skew still counts as\n\t\t\t\t\t// needing refresh, so returning it here would make the final candidate pass\n\t\t\t\t\t// refresh the same row again and replay the token we just failed on.\n\t\t\t\t\treturn result.credential;\n\t\t\t\t}\n\t\t\t\tthrow new AIError.OAuthError(\n\t\t\t\t\t`OAuth refresh did not produce a usable credential for provider: ${provider}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tkind: \"token-refresh\",\n\t\t\t\t\t\tprovider,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow new AIError.OAuthError(`OAuth credential no longer exists for provider: ${provider}`, {\n\t\t\t\tkind: \"token-refresh\",\n\t\t\t\tprovider,\n\t\t\t});\n\t\t}\n\t\treturn this.#requestOAuthCredentialRefresh(provider, credential, credentialId, signal);\n\t}\n\n\tasync #requestOAuthCredentialRefresh(\n\t\tprovider: Provider,\n\t\tcredential: OAuthCredential,","sourceCodeStart":5237,"sourceCodeEnd":5273,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/auth-storage.ts#L5237-L5273","documentation":"Thrown as AIError.OAuthError with kind 'token-refresh' when an OAuth token refresh completes but does not yield a usable credential (and it is not a reload of an already-refreshed credential). The library treats this as a definitive refresh failure for the provider so the retry layer can stop replaying the failed token.","triggerScenarios":"A refresh attempt (or reload path) returns a result whose credential is undefined/unusable — e.g. the refresh succeeded server-side but produced no storable credential, or the row was removed mid-refresh so no candidate remains.","commonSituations":"Provider revoked the refresh token or the account (refresh returns empty); refresh endpoint returns 200 with a malformed/absent token payload; concurrent logout removing the credential between refresh start and finish.","solutions":["Re-run the full OAuth login flow for the provider to obtain a new refresh token","Check the provider account status — revoked apps/sessions produce empty refresh results","Inspect provider API responses/logs for why the refresh returned no token","Retry the request; withOAuthAccess treats kind 'token-refresh' as a replay-then-stop signal, so re-authentication is the real fix"],"exampleFix":"// before\nconst access = await withOAuthAccess(storage, \"codex\", call); // OAuthError token-refresh\n// after\ntry {\n\treturn await withOAuthAccess(storage, \"codex\", call);\n} catch (error) {\n\tif (error instanceof AIError.OAuthError && error.kind === \"token-refresh\") {\n\t\tawait reLogin(provider); // refresh token is dead; get a new one\n\t\treturn withOAuthAccess(storage, \"codex\", call);\n\t}\n\tthrow error;\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isTokenRefreshError(e: unknown): e is AIError.OAuthError {\n\treturn e instanceof AIError.OAuthError && e.kind === \"token-refresh\";\n}","tryCatchPattern":"try {\n\treturn await withOAuthAccess(storage, provider, attempt);\n} catch (error) {\n\tif (isTokenRefreshError(error)) {\n\t\tawait reLogin(provider); // refresh produced nothing usable\n\t\treturn withOAuthAccess(storage, provider, attempt);\n\t}\n\tthrow error;\n}","preventionTips":["Re-authenticate proactively when refresh tokens approach provider expiry windows","Handle kind \"token-refresh\" OAuthErrors as 're-login required', never as retryable","Monitor provider account/consent revocation events"],"tags":["oauth","token-refresh","reauth-required"],"backgroundTag":"oauth-refresh-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}