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

  1. Check credential.type === "oauth" before requesting a refresh by id and skip non-OAuth credentials
  2. Re-fetch the current credential list — your cached id may point at a row that changed type
  3. If you intended to refresh an OAuth credential, locate the correct id from the current storage listing
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/bd87bb74f4d50a1d. Report an issue: GitHub.