can1357/oh-my-pi · error · AIError.ValidationError
Credential ${id} is not OAuth (provider=${provider}, type=${
Error message
Credential ${id} is not OAuth (provider=${provider}, type=${target.credential.type}) What it means
#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.
Source
Thrown at packages/ai/src/auth-storage.ts:6702
/**
* Force-refresh the OAuth credential with the given id, bypassing the
* not-yet-expired guard. Used by the auth-broker server to honour
* `POST /v1/credential/:id/refresh`.
*
* Returns the redacted snapshot entry for the refreshed row.
* Throws when no OAuth credential with that id is loaded.
*/
async forceRefreshCredentialById(id: number, signal?: AbortSignal): Promise<AuthCredentialSnapshotEntry> {
return this.refreshCredentialById(id, signal);
}
async #forceRefreshCredentialByIdUnshared(id: number, signal?: AbortSignal): Promise<AuthCredentialSnapshotEntry> {
for (const [provider, entries] of this.#data) {
const index = entries.findIndex(entry => entry.id === id);
if (index === -1) continue;
const target = entries[index];
if (target.credential.type !== "oauth") {
throw new AIError.ValidationError(
`Credential ${id} is not OAuth (provider=${provider}, type=${target.credential.type})`,
);
}
// The exact credential we are about to refresh — captured before the
// await so a definitive failure can CAS-disable the row against the
// value we actually attempted (NOT the expires:0 clone below).
const attempted = target.credential;
// Pass a clone with expires=0 so the cached not-yet-expired short-circuit
// in #refreshOAuthCredential doesn't suppress the requested refresh.
const stale: OAuthCredential = { ...attempted, expires: 0 };
let refreshed: OAuthCredentials;
try {
refreshed = await this.#refreshOAuthCredential(provider as Provider, stale, id, signal);
} catch (error) {
// A definitively-dead grant tears the row down here, where the
// attempted credential is known. CAS on the persisted credential so a
// peer/login rotation in flight leaves the freshly-rotated row intact.
if (AIError.isDefinitiveOAuthFailure(String(error))) {View on GitHub (pinned to 9690622007)
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
Example fix
// before
await storage.forceRefreshCredentialById(id); // may hit an api-key row
// after
const entry = storage.listCredentials().find(c => c.id === id);
if (entry?.credential.type !== "oauth") throw new Error(`credential ${id} is not refreshable`);
await storage.forceRefreshCredentialById(id); Defensive patterns
Strategy: validation
Validate before calling
const entry = storage.listCredentials().find(c => c.id === id);
if (!entry || entry.credential.type !== "oauth") {
throw new Error(`credential ${id} is not OAuth; skip refresh`);
} Type guard
function isOAuthEntry(e: { credential: { type: string } }): boolean {
return e.credential.type === "oauth";
} Try / catch
try {
await storage.forceRefreshCredentialById(id);
} catch (error) {
if (error instanceof AIError.ValidationError && /is not OAuth/.test(error.message)) {
return; // non-OAuth credentials don't refresh
}
throw error;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ${providerLabel} login requires onPrompt callback
- GitLab OAuth token response missing required fields
- Token response missing required fields
- Device authorization response missing required fields
- Qwen token/API key is required
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/bd87bb74f4d50a1d.
Report an issue: GitHub.