{"record":{"id":"bd87bb74f4d50a1d","repo":"can1357/oh-my-pi","slug":"credential-id-is-not-oauth-provider-provider","errorCode":null,"errorMessage":"Credential ${id} is not OAuth (provider=${provider}, type=${target.credential.type})","messagePattern":"Credential (.+?) is not OAuth \\(provider=(.+?), type=(.+?)\\)","errorType":"validation","errorClass":"AIError.ValidationError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/auth-storage.ts","lineNumber":6702,"sourceCode":"\t/**\n\t * Force-refresh the OAuth credential with the given id, bypassing the\n\t * not-yet-expired guard. Used by the auth-broker server to honour\n\t * `POST /v1/credential/:id/refresh`.\n\t *\n\t * Returns the redacted snapshot entry for the refreshed row.\n\t * Throws when no OAuth credential with that id is loaded.\n\t */\n\tasync forceRefreshCredentialById(id: number, signal?: AbortSignal): Promise<AuthCredentialSnapshotEntry> {\n\t\treturn this.refreshCredentialById(id, signal);\n\t}\n\n\tasync #forceRefreshCredentialByIdUnshared(id: number, signal?: AbortSignal): Promise<AuthCredentialSnapshotEntry> {\n\t\tfor (const [provider, entries] of this.#data) {\n\t\t\tconst index = entries.findIndex(entry => entry.id === id);\n\t\t\tif (index === -1) continue;\n\t\t\tconst target = entries[index];\n\t\t\tif (target.credential.type !== \"oauth\") {\n\t\t\t\tthrow new AIError.ValidationError(\n\t\t\t\t\t`Credential ${id} is not OAuth (provider=${provider}, type=${target.credential.type})`,\n\t\t\t\t);\n\t\t\t}\n\t\t\t// The exact credential we are about to refresh — captured before the\n\t\t\t// await so a definitive failure can CAS-disable the row against the\n\t\t\t// value we actually attempted (NOT the expires:0 clone below).\n\t\t\tconst attempted = target.credential;\n\t\t\t// Pass a clone with expires=0 so the cached not-yet-expired short-circuit\n\t\t\t// in #refreshOAuthCredential doesn't suppress the requested refresh.\n\t\t\tconst stale: OAuthCredential = { ...attempted, expires: 0 };\n\t\t\tlet refreshed: OAuthCredentials;\n\t\t\ttry {\n\t\t\t\trefreshed = await this.#refreshOAuthCredential(provider as Provider, stale, id, signal);\n\t\t\t} catch (error) {\n\t\t\t\t// A definitively-dead grant tears the row down here, where the\n\t\t\t\t// attempted credential is known. CAS on the persisted credential so a\n\t\t\t\t// peer/login rotation in flight leaves the freshly-rotated row intact.\n\t\t\t\tif (AIError.isDefinitiveOAuthFailure(String(error))) {","sourceCodeStart":6684,"sourceCodeEnd":6720,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/auth-storage.ts#L6684-L6720","documentation":"#forceRefreshCredentialByIdUnshared looks up a stored credential by numeric id and forces a refresh of it. If the row with that id holds a non-OAuth credential (e.g. an API key), the refresh path is invalid for it and the library throws AIError.ValidationError describing the id, provider, and actual credential type.","triggerScenarios":"Calling forceRefreshCredentialById (or the unshared variant) with a credential id that resolves to an api-key (or other non-oauth) credential — typically by passing an id captured earlier after the row was replaced with a different credential type.","commonSituations":"Stale credential id from before the user replaced an OAuth login with an API key (or vice versa); iterating stored credential ids assuming all are OAuth; tooling that forces refresh on every stored credential regardless of type.","solutions":["Check credential.type === \"oauth\" before requesting a refresh by id and skip non-OAuth credentials","Re-fetch the current credential list — your cached id may point at a row that changed type","If you intended to refresh an OAuth credential, locate the correct id from the current storage listing","API-key credentials never need refresh; drop them from any forced-refresh flow"],"exampleFix":"// before\nawait storage.forceRefreshCredentialById(id); // may hit an api-key row\n// after\nconst entry = storage.listCredentials().find(c => c.id === id);\nif (entry?.credential.type !== \"oauth\") throw new Error(`credential ${id} is not refreshable`);\nawait storage.forceRefreshCredentialById(id);","handlingStrategy":"validation","validationCode":"const entry = storage.listCredentials().find(c => c.id === id);\nif (!entry || entry.credential.type !== \"oauth\") {\n\tthrow new Error(`credential ${id} is not OAuth; skip refresh`);\n}","typeGuard":"function isOAuthEntry(e: { credential: { type: string } }): boolean {\n\treturn e.credential.type === \"oauth\";\n}","tryCatchPattern":"try {\n\tawait storage.forceRefreshCredentialById(id);\n} catch (error) {\n\tif (error instanceof AIError.ValidationError && /is not OAuth/.test(error.message)) {\n\t\treturn; // non-OAuth credentials don't refresh\n\t}\n\tthrow error;\n}","preventionTips":["Filter stored credentials to type \"oauth\" before any forced-refresh flow","Re-resolve credential ids from current storage rather than caching them","Remember credential rows can change type when users switch between OAuth and API keys"],"tags":["oauth","validation","credential-type","api-key"],"backgroundTag":"credential-type-mismatch","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}