// {{ display_name }} MCP server — generated by mcpify. Do not hand-edit.

import { createHash, randomBytes } from 'node:crypto';

import axios from 'axios';

import type { AuthConfig, AuthStrategy, Credentials } from '../auth-strategy.js';
import { MissingCredentialsError, TokenExpiredError } from '../errors.js';

function base64url(buffer: Buffer): string {
  return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

/** Generates a PKCE `code_verifier`/`code_challenge` pair (RFC 7636), for
 * the setup wizard's authorization-code flow. */
export function createPkcePair(): { verifier: string; challenge: string } {
  const verifier = base64url(randomBytes(32));
  const challenge = base64url(createHash('sha256').update(verifier).digest());
  return { verifier, challenge };
}

interface TokenResponse {
  access_token: string;
  refresh_token?: string;
  expires_in?: number;
}

export class OAuth2Strategy implements AuthStrategy {
  async authenticate(config: AuthConfig): Promise<Credentials> {
    const { clientId, clientSecret, authorizationCode, redirectUri, tokenUrl, codeVerifier } = config;
    if (!clientId || !tokenUrl) {
      throw new MissingCredentialsError(['clientId', 'tokenUrl']);
    }

    const response = await axios.post<TokenResponse>(
      tokenUrl,
      new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: clientId,
        ...(clientSecret ? { client_secret: clientSecret } : {}),
        ...(authorizationCode ? { code: authorizationCode } : {}),
        ...(redirectUri ? { redirect_uri: redirectUri } : {}),
        ...(codeVerifier ? { code_verifier: codeVerifier } : {}),
      }),
      { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
    );

    return this.fromTokenResponse(response.data, { clientId, clientSecret, tokenUrl });
  }

  async refreshToken(credentials: Credentials): Promise<Credentials> {
    const { refreshToken, clientId, clientSecret, tokenUrl } = credentials;
    if (!refreshToken || !tokenUrl) {
      throw new TokenExpiredError();
    }

    const response = await axios.post<TokenResponse>(
      tokenUrl,
      new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: clientId ?? '',
        ...(clientSecret ? { client_secret: clientSecret } : {}),
      }),
      { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
    );

    return this.fromTokenResponse(response.data, { clientId, clientSecret, tokenUrl });
  }

  validateCredentials(credentials: Credentials): boolean {
    if (typeof credentials.accessToken !== 'string') return false;
    if (!credentials.expiresAt) return true;
    return Date.now() < Number(credentials.expiresAt);
  }

  private fromTokenResponse(
    data: TokenResponse,
    context: { clientId?: string; clientSecret?: string; tokenUrl?: string },
  ): Credentials {
    const expiresAt = data.expires_in ? String(Date.now() + data.expires_in * 1000) : undefined;
    return {
      accessToken: data.access_token,
      ...(data.refresh_token ? { refreshToken: data.refresh_token } : {}),
      ...(expiresAt ? { expiresAt } : {}),
      ...(context.clientId ? { clientId: context.clientId } : {}),
      ...(context.clientSecret ? { clientSecret: context.clientSecret } : {}),
      ...(context.tokenUrl ? { tokenUrl: context.tokenUrl } : {}),
      authorizationHeader: `Bearer ${data.access_token}`,
    };
  }
}
