{"record":{"id":"1c7fadd5a380e108","repo":"can1357/oh-my-pi","slug":"oauth-credential-no-longer-exists-for-provider","errorCode":null,"errorMessage":"OAuth credential no longer exists for provider: ${provider}","messagePattern":"OAuth credential no longer exists for provider: (.+?)","errorType":"exception","errorClass":"AIError.OAuthError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/auth-storage.ts","lineNumber":5263,"sourceCode":"\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,\n\t\tcredentialId: number | undefined,\n\t\tsignal?: AbortSignal,\n\t): Promise<OAuthCredentials> {\n\t\tlet refreshPromise: Promise<OAuthCredentials>;\n\t\t// Caller override > store-level hook > local per-provider refresh.\n\t\t// `RemoteAuthCredentialStore` exposes the hook so a broker-backed gateway\n\t\t// routes refresh through the broker without explicit wiring.\n\t\tconst storeRefresh = this.#store.refreshOAuthCredential?.bind(this.#store);","sourceCodeStart":5245,"sourceCodeEnd":5281,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/auth-storage.ts#L5245-L5281","documentation":"Thrown as AIError.OAuthError with kind 'token-refresh' when the credential that was about to be refreshed no longer exists in storage — it was deleted between the caller's read and the refresh request. The library cannot refresh a row that is gone, so it fails definitively rather than silently recreating it.","triggerScenarios":"Calling the refresh path (e.g. getOAuthApiKey triggering a needed refresh, or #requestOAuthCredentialRefresh by credentialId) after another code path — logout, removeCredential, another process — deleted the credential row for that provider.","commonSituations":"User logged out in another window/process while a request was in flight; concurrent session cleanup removing the credential; rotating credentials in one tab while another tab refreshes the old one.","solutions":["Re-run the OAuth login flow — the credential must be re-established before any request can succeed","Coordinate logout/credential removal across processes so it doesn't race in-flight requests","Catch this (kind 'token-refresh') and re-fetch credentials from storage before retrying","If it happens repeatedly, check for another process or job deleting credentials (cleanup scripts, multi-instance setups)"],"exampleFix":"// before\nconst access = await withOAuthAccess(storage, \"codex\", call); // thrown mid-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\tif (!(await storage.getOAuthAccess(\"codex\"))) await reLogin(\"codex\");\n\t\treturn withOAuthAccess(storage, \"codex\", call);\n\t}\n\tthrow error;\n}","handlingStrategy":"type-guard","validationCode":"const access = await storage.getOAuthAccess(provider);\nif (!access) await reLogin(provider); // row gone before refresh","typeGuard":"function isCredentialGoneError(e: unknown): e is AIError.OAuthError {\n\treturn e instanceof AIError.OAuthError &&\n\t\te.kind === \"token-refresh\" &&\n\t\te.message.includes(\"no longer exists\");\n}","tryCatchPattern":"try {\n\treturn await withOAuthAccess(storage, provider, attempt);\n} catch (error) {\n\tif (isCredentialGoneError(error)) await reLogin(provider);\n\tthrow error;\n}","preventionTips":["Avoid logging out / deleting credentials while requests are in flight","Serialize logout and refresh through one owner per provider","Re-read credentials from storage after any concurrent operation instead of caching rows"],"tags":["oauth","credential-deleted","race-condition","reauth-required"],"backgroundTag":"oauth-credential-deleted","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}