UNPKG

@smartsamurai/krapi-sdk

Version:

KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)

166 lines (147 loc) 5.79 kB
import axios, { AxiosInstance } from "axios"; import { KrapiClientConfig } from "../client"; /** * HTTP Client Manager * * Handles HTTP client configuration, interceptors, and header management * for the KRAPI client SDK. */ export class HttpClientManager { private config: KrapiClientConfig; private axiosInstance: AxiosInstance; private baseUrl: string; constructor(config: KrapiClientConfig) { this.config = config; this.baseUrl = config.endpoint.replace(/\/$/, ""); // Remove trailing slash this.axiosInstance = this.createAxiosInstance(); this.setupInterceptors(); } /** * Get the configured Axios instance */ getAxiosInstance(): AxiosInstance { return this.axiosInstance; } /** * Update the API key */ setApiKey(apiKey: string): void { this.config.apiKey = apiKey; this.axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${apiKey}`; } /** * Update the session token */ setSessionToken(token: string): void { this.config.sessionToken = token; this.axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${token}`; } /** * Update the project ID */ setProjectId(projectId: string): void { this.config.projectId = projectId; // Note: We do NOT set default header here. The interceptor conditionally // adds X-Project-ID header only for project-scoped routes. } /** * Get current configuration */ getConfig(): KrapiClientConfig { return { ...this.config }; } /** * Create and configure the Axios instance */ private createAxiosInstance(): AxiosInstance { // Remove X-Project-ID from user-provided headers - it should only be set by interceptor const sanitizedHeaders = { ...this.config.headers }; if (sanitizedHeaders) { delete sanitizedHeaders["X-Project-ID"]; delete sanitizedHeaders["x-project-id"]; } // Create axios instance const instance = axios.create({ baseURL: this.baseUrl, timeout: this.config.timeout || 30000, headers: { "Content-Type": "application/json", ...sanitizedHeaders, }, }); // Set auth headers as defaults (these are always needed) if (this.config.apiKey) { instance.defaults.headers.common["Authorization"] = `Bearer ${this.config.apiKey}`; } if (this.config.sessionToken) { instance.defaults.headers.common["Authorization"] = `Bearer ${this.config.sessionToken}`; } return instance; } /** * Setup request interceptors for header management */ private setupInterceptors(): void { // Add request interceptor as the SINGLE SOURCE OF TRUTH for headers // This interceptor runs LAST and handles: // 1. Authorization header (ensures it's always set from config) // 2. X-Project-ID header (conditionally added only for project-scoped routes) // 3. Explicit removal of X-Project-ID for list/create operations // // CRITICAL: This interceptor must run AFTER all other interceptors and header merging // to ensure it has final control over headers this.axiosInstance.interceptors.request.use( (requestConfig) => { // Ensure Authorization header is always set from config (if available) // This overrides any explicit headers passed in the request const authToken = this.config.apiKey || this.config.sessionToken; if (authToken) { requestConfig.headers["Authorization"] = `Bearer ${authToken}`; } // Handle X-Project-ID header based on route type if (requestConfig.url) { // Normalize the path by removing any base path prefixes let normalizedPath = requestConfig.url; normalizedPath = normalizedPath.replace(/^(\/api)?\/krapi\/k1/, ""); // Check if this is a list/create operation (e.g., GET /projects, POST /projects) // Match /projects, /projects/, or /projects? but NOT /projects/{id} const isProjectListOrCreate = /^\/projects\/?(\?|$)/.test( normalizedPath ); // Check if this is a project-scoped route (has /projects/{id} pattern) // Project-scoped routes: /projects/{id}, /projects/{id}/settings, etc. const isProjectScoped = /^\/projects\/[^/]+/.test(normalizedPath); if (isProjectListOrCreate) { // DEFENSIVE: Explicitly remove header for list/create operations // Use undefined assignment to ensure header is completely removed // This works even if header was set in defaults or elsewhere requestConfig.headers["X-Project-ID"] = undefined as unknown as string; requestConfig.headers["x-project-id"] = undefined as unknown as string; // Also delete to be extra safe delete requestConfig.headers["X-Project-ID"]; delete requestConfig.headers["x-project-id"]; } else if (isProjectScoped && this.config.projectId) { // Only add header for project-scoped routes when projectId is set requestConfig.headers["X-Project-ID"] = this.config.projectId; } else { // For all other routes (non-project routes), ensure X-Project-ID is not present requestConfig.headers["X-Project-ID"] = undefined as unknown as string; requestConfig.headers["x-project-id"] = undefined as unknown as string; delete requestConfig.headers["X-Project-ID"]; delete requestConfig.headers["x-project-id"]; } } return requestConfig; }, undefined, { // Run this interceptor synchronously and ensure it runs last synchronous: true, } ); } }