nexu-io/open-design · error

xAI OAuth state mismatch: expected serverId=${XAI_PROVIDER_I

Error message

xAI OAuth state mismatch: expected serverId=${XAI_PROVIDER_ID}, got ${consumed.serverId}

What it means

Raised by completeXAIAuth() after a successful consume() when the stored PendingAuthState.serverId does not equal XAI_PROVIDER_ID ('xai'). The PendingAuthCache is shared across providers, so a consumed state must be validated to belong to the xAI flow before exchanging its code — otherwise one provider's code would be exchanged against another provider's token endpoint.

Source

Thrown at apps/daemon/src/integrations/xai-oauth.ts:134

  code: string;
  fetchImpl?: typeof fetch;
}

/**
 * Post-callback half of the OAuth dance. Looks up `state` in `pending`,
 * validates it (one-shot, TTL-checked by `PendingAuthCache`), and
 * exchanges `code` for tokens. Throws if `state` is unknown, expired,
 * already consumed, or was issued for a different provider.
 */
export async function completeXAIAuth(
  input: CompleteXAIAuthInput,
): Promise<OAuthTokenResponse> {
  const consumed = input.pending.consume(input.state);
  if (!consumed) {
    throw new Error('xAI OAuth state not found or expired');
  }
  if (consumed.serverId !== XAI_PROVIDER_ID) {
    throw new Error(
      `xAI OAuth state mismatch: expected serverId=${XAI_PROVIDER_ID}, got ${consumed.serverId}`,
    );
  }
  return exchangeCodeForToken(
    {
      tokenEndpoint: consumed.tokenEndpoint,
      clientId: consumed.clientId,
      redirectUri: consumed.redirectUri,
      code: input.code,
      codeVerifier: consumed.codeVerifier,
    },
    input.fetchImpl ?? fetch,
  );
}

export interface RefreshXAITokenInput {
  refreshToken: string;
  fetchImpl?: typeof fetch;

View on GitHub (pinned to 5be4028344)

Solutions

  1. Route the callback by provider: dispatch to the matching complete*Auth based on which authorize flow issued the state, not a fixed xAI path.
  2. Use provider-scoped redirect ports/paths so cross-provider callbacks cannot collide.
  3. If seeding PendingAuthState in tests, set serverId: XAI_PROVIDER_ID.

Example fix

// before
if (consumed.serverId !== XAI_PROVIDER_ID) {
  throw new Error(`xAI OAuth state mismatch: expected serverId=${XAI_PROVIDER_ID}, got ${consumed.serverId}`);
}

// after (route to the correct provider instead of throwing)
if (consumed.serverId !== XAI_PROVIDER_ID) {
  return routeToProviderCallback(consumed.serverId, input);
}
Defensive patterns

Strategy: validation

Validate before calling

import { XAI_PROVIDER_ID } from '../integrations/xai-oauth.js';

function stateBelongsToXai(consumed: { serverId: string }): boolean {
  return consumed.serverId === XAI_PROVIDER_ID;
}

const consumed = input.pending.peek(input.state); // if your cache exposes a non-consuming peek
if (consumed && !stateBelongsToXai(consumed)) {
  return { ok: false, reason: 'wrong_provider' };
}

Type guard

function isOAuthStateMismatch(err: unknown): boolean {
  return err instanceof Error && err.message.startsWith('xAI OAuth state mismatch:');
}

Try / catch

try {
  return await completeXAIAuth(input);
} catch (err) {
  if (err instanceof Error && err.message.startsWith('xAI OAuth state mismatch:')) {
    // route to the provider that actually owns this state
    return routeToProviderCallback(extractServerId(err), input);
  }
  throw err;
}

Prevention

When it happens

Trigger: A callback for a different OAuth provider (sharing the same PendingAuthCache and state space) lands on the xAI completion path, or a state value collided across providers. The consume succeeded (state was valid) but serverId mismatch proves cross-provider routing.

Common situations: Two providers use the same loopback redirect port and a callback for provider A reaches the xAI handler; state generation produced a duplicate across providers (very unlikely with generateState); test fixtures seeding states with the wrong serverId; refactor that changed a provider id but not stored states.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/61549b8179029e2b. Report an issue: GitHub.