@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
text/typescript
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,
}
);
}
}