UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

474 lines (473 loc) 14.2 kB
/** * Authentication Utilities for NeuroLink Client SDK * * Provides authentication configuration, token management, and OAuth2 support * for securing API requests to NeuroLink servers. * * @module @neurolink/client/auth */ // ============================================================================= // OAuth2 Token Manager // ============================================================================= /** * OAuth2 Token Manager for client credentials flow * * Handles token acquisition, caching, and automatic refresh for OAuth2 * client credentials authentication. * * @example Basic usage * ```typescript * const tokenManager = new OAuth2TokenManager({ * tokenUrl: 'https://auth.example.com/oauth/token', * clientId: 'your-client-id', * clientSecret: 'your-client-secret', * scope: 'api:read api:write', * }); * * // Get token (automatically refreshes if needed) * const token = await tokenManager.getToken(); * * // Use with client * const client = createClient({ * baseUrl: 'https://api.example.com', * }); * client.use(createDynamicAuthInterceptor(() => tokenManager.getToken())); * ``` */ export class OAuth2TokenManager { config; token = null; tokenExpiry = null; refreshPromise = null; refreshBufferMs; constructor(config, options) { this.config = config; this.refreshBufferMs = options?.refreshBufferMs ?? 60000; } /** * Get a valid access token * * Returns cached token if still valid, otherwise fetches a new one. * Handles concurrent requests by deduplicating token refresh calls. */ async getToken() { // Check if token is still valid if (this.token && this.tokenExpiry && Date.now() < this.tokenExpiry - this.refreshBufferMs) { return this.token; } // Deduplicate concurrent refresh requests if (this.refreshPromise) { return this.refreshPromise; } // Fetch new token this.refreshPromise = this.fetchToken(); try { return await this.refreshPromise; } finally { this.refreshPromise = null; } } /** * Invalidate the cached token * * Call this when the token is rejected by the server to force a refresh. */ invalidate() { this.token = null; this.tokenExpiry = null; } /** * Check if the cached token is valid */ isValid() { return !!(this.token && this.tokenExpiry && Date.now() < this.tokenExpiry - this.refreshBufferMs); } /** * Get the token expiry time in milliseconds */ getExpiryTime() { return this.tokenExpiry; } /** * Fetch a new token from the OAuth2 server */ async fetchToken() { const params = new URLSearchParams({ grant_type: "client_credentials", client_id: this.config.clientId, client_secret: this.config.clientSecret, }); if (this.config.scope) { params.set("scope", this.config.scope); } if (this.config.audience) { params.set("audience", this.config.audience); } const response = await fetch(this.config.tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: params.toString(), }); if (!response.ok) { const errorText = await response.text().catch(() => "Unknown error"); throw new OAuth2Error(`OAuth2 token request failed: ${response.status}`, response.status, errorText); } const data = (await response.json()); this.token = data.accessToken || data.access_token || ""; const expiresIn = data.expiresIn || data.expires_in || 3600; this.tokenExpiry = Date.now() + expiresIn * 1000; if (!this.token) { throw new OAuth2Error("OAuth2 response missing access token", 200, JSON.stringify(data)); } return this.token; } } // ============================================================================= // JWT Token Manager // ============================================================================= /** * JWT Token Manager with automatic refresh * * Manages JWT tokens with automatic refresh using a provided refresh function. * * @example * ```typescript * const tokenManager = new JWTTokenManager({ * token: 'initial-jwt-token', * expiresAt: Date.now() + 3600000, * refreshFn: async () => { * const response = await fetch('/api/auth/refresh', { * method: 'POST', * credentials: 'include', * }); * const data = await response.json(); * return { accessToken: data.token, expiresIn: data.expiresIn }; * }, * }); * ``` */ export class JWTTokenManager { config; token; tokenExpiry; refreshPromise = null; refreshBufferMs; constructor(config) { this.config = config; this.token = config.token; this.tokenExpiry = config.expiresAt; this.refreshBufferMs = config.refreshBufferMs ?? 60000; } /** * Get a valid access token */ async getToken() { if (Date.now() < this.tokenExpiry - this.refreshBufferMs) { return this.token; } if (this.refreshPromise) { return this.refreshPromise; } this.refreshPromise = this.refreshToken(); try { return await this.refreshPromise; } finally { this.refreshPromise = null; } } /** * Force token refresh */ async forceRefresh() { this.refreshPromise = this.refreshToken(); try { return await this.refreshPromise; } finally { this.refreshPromise = null; } } /** * Update token manually */ setToken(token, expiresAt) { this.token = token; this.tokenExpiry = expiresAt; } /** * Check if token is valid */ isValid() { return Date.now() < this.tokenExpiry - this.refreshBufferMs; } async refreshToken() { const result = await this.config.refreshFn(); this.token = result.accessToken; this.tokenExpiry = Date.now() + result.expiresIn * 1000; return this.token; } } // ============================================================================= // Authentication ClientMiddleware // ============================================================================= /** * Create an API key authentication middleware * * @example * ```typescript * const client = createClient({ baseUrl: 'https://api.example.com' }); * client.use(createApiKeyMiddleware('your-api-key')); * ``` */ export function createApiKeyMiddleware(apiKey, headerName = "X-API-Key") { return async (request, next) => { request.headers[headerName] = apiKey; return next(); }; } /** * Create a Bearer token authentication middleware * * @example * ```typescript * const client = createClient({ baseUrl: 'https://api.example.com' }); * client.use(createBearerTokenMiddleware('your-jwt-token')); * ``` */ export function createBearerTokenMiddleware(token) { return async (request, next) => { request.headers["Authorization"] = `Bearer ${token}`; return next(); }; } /** * Create a dynamic authentication middleware with token manager * * @example With OAuth2TokenManager * ```typescript * const tokenManager = new OAuth2TokenManager({ * tokenUrl: 'https://auth.example.com/oauth/token', * clientId: 'client-id', * clientSecret: 'client-secret', * }); * * const client = createClient({ baseUrl: 'https://api.example.com' }); * client.use(createTokenManagerMiddleware(tokenManager)); * ``` */ export function createTokenManagerMiddleware(tokenManager) { return async (request, next) => { const token = await tokenManager.getToken(); request.headers["Authorization"] = `Bearer ${token}`; return next(); }; } /** * Create an authentication middleware with retry on 401 * * Automatically refreshes token and retries request when receiving 401. * * @example * ```typescript * const tokenManager = new OAuth2TokenManager({...}); * * const client = createClient({ baseUrl: 'https://api.example.com' }); * client.use(createAuthWithRetryMiddleware(tokenManager)); * ``` */ export function createAuthWithRetryMiddleware(tokenManager, maxRetries = 1) { return async (request, next) => { let retries = 0; while (true) { const token = await tokenManager.getToken(); request.headers["Authorization"] = `Bearer ${token}`; const response = await next(); if (response.status === 401 && retries < maxRetries) { tokenManager.invalidate(); retries++; // Fetch a fresh token and update the request headers for the retry const freshToken = await tokenManager.getToken(); request.headers["Authorization"] = `Bearer ${freshToken}`; continue; } return response; } }; } /** * Create a multi-auth middleware that supports multiple authentication methods * * @example * ```typescript * const client = createClient({ baseUrl: 'https://api.example.com' }); * client.use(createMultiAuthMiddleware({ * apiKey: process.env.API_KEY, * token: sessionToken, * })); * ``` */ export function createMultiAuthMiddleware(config) { return async (request, next) => { // Add API key if provided if (config.apiKey) { const headerName = config.apiKeyHeaderName ?? "X-API-Key"; request.headers[headerName] = config.apiKey; } // Add bearer token if provided if (config.token) { const headerName = config.headerName ?? "Authorization"; request.headers[headerName] = `Bearer ${config.token}`; } // Handle dynamic token refresh if (config.refreshToken && config.tokenExpiresAt) { const bufferMs = config.refreshBufferMs ?? 60000; if (Date.now() > config.tokenExpiresAt - bufferMs) { const newToken = await config.refreshToken(); // Persist the refreshed token back to config so subsequent requests use it config.token = newToken; config.tokenExpiresAt = Date.now() + bufferMs * 2; const headerName = config.headerName ?? "Authorization"; request.headers[headerName] = `Bearer ${newToken}`; } } return next(); }; } // ============================================================================= // Error Classes // ============================================================================= /** * Error thrown during OAuth2 operations */ export class OAuth2Error extends Error { status; responseBody; constructor(message, status, responseBody) { super(message); this.name = "OAuth2Error"; this.status = status; this.responseBody = responseBody; } } /** * Error thrown when authentication fails */ export class OAuth2AuthenticationError extends Error { code; status; constructor(message, code = "AUTH_ERROR", status = 401) { super(message); this.name = "OAuth2AuthenticationError"; this.code = code; this.status = status; } } /** * Error thrown when token refresh fails */ export class TokenRefreshError extends Error { cause; constructor(message, cause) { super(message); this.name = "TokenRefreshError"; this.cause = cause; } } // ============================================================================= // Utility Functions // ============================================================================= /** * Decode a JWT token payload without verification * * @example * ```typescript * const payload = decodeJWTPayload(token); * console.log('Token expires at:', new Date(payload.exp * 1000)); * ``` */ export function decodeJWTPayload(token) { try { const parts = token.split("."); if (parts.length !== 3) { throw new Error("Invalid JWT format"); } const payload = parts[1]; const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/")); return JSON.parse(decoded); } catch { throw new Error("Failed to decode JWT token"); } } /** * Check if a JWT token is expired * * @example * ```typescript * if (isJWTExpired(token)) { * // Refresh the token * } * ``` */ export function isJWTExpired(token, bufferMs = 0) { try { const payload = decodeJWTPayload(token); const exp = payload.exp; if (!exp) { return true; } return Date.now() > exp * 1000 - bufferMs; } catch { return true; } } /** * Extract expiry time from a JWT token * * @returns Expiry time in milliseconds, or null if not available */ export function getJWTExpiry(token) { try { const payload = decodeJWTPayload(token); const exp = payload.exp; return exp ? exp * 1000 : null; } catch { return null; } } /** * Create an API key from environment variable with validation * * @example * ```typescript * const apiKey = getApiKeyFromEnv('NEUROLINK_API_KEY'); * const client = createClient({ * baseUrl: 'https://api.example.com', * apiKey, * }); * ``` */ export function getApiKeyFromEnv(envVar, options) { const value = typeof process !== "undefined" ? process.env[envVar] : undefined; if (!value && options?.required) { throw new Error(`Required environment variable ${envVar} is not set`); } return value; } // Types are already exported at their definition sites above