@smartsamurai/krapi-sdk
Version:
KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)
1,560 lines (1,549 loc) • 92.1 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/client.ts
var client_exports = {};
__export(client_exports, {
KrapiClient: () => KrapiClient,
default: () => client_default
});
module.exports = __toCommonJS(client_exports);
// src/client/backup-api-manager.ts
var BackupApiManager = class {
constructor(axiosInstance) {
this.axiosInstance = axiosInstance;
}
/**
* Create a project backup
*/
async createProject(projectId, options) {
const response = await this.axiosInstance.post(`/krapi/k1/projects/${projectId}/backup`, options || {});
return response.data;
}
/**
* Restore a project from backup
*/
async restoreProject(projectId, backupId, password, options) {
const response = await this.axiosInstance.post(`/krapi/k1/projects/${projectId}/restore`, {
backup_id: backupId,
password,
...options
});
return response.data;
}
/**
* List backups
*/
async list(projectId, type) {
const url = projectId ? `/krapi/k1/projects/${projectId}/backups${type ? `?type=${type}` : ""}` : `/krapi/k1/backups${type ? `?type=${type}` : ""}`;
const response = await this.axiosInstance.get(
url
);
return response.data;
}
/**
* Delete a backup
*/
async delete(backupId) {
const response = await this.axiosInstance.delete(`/krapi/k1/backups/${backupId}`);
return response.data;
}
/**
* Create a system backup
*/
async createSystem(options) {
const response = await this.axiosInstance.post("/krapi/k1/backup/system", options || {});
return response.data;
}
};
// src/utils/response-normalizer.ts
var ResponseNormalizer = class {
/**
* Normalize document list responses from different API formats
*/
static normalizeDocumentListResponse(response) {
if (!response) return [];
if (Array.isArray(response)) {
return response;
}
if (typeof response === "object" && response !== null) {
if ("data" in response) {
const data = response.data;
if (Array.isArray(data)) {
return data;
}
}
if ("documents" in response) {
const documents = response.documents;
if (Array.isArray(documents)) {
return documents;
}
}
}
return [];
}
/**
* Normalize single document responses
*/
static normalizeDocumentResponse(response) {
if (!response) return {};
if (typeof response === "object" && response !== null) {
if ("data" in response) {
const data = response.data;
if (data && typeof data === "object") {
return data;
}
}
return response;
}
return {};
}
/**
* Normalize collection responses
*/
static normalizeCollectionResponse(response) {
if (!response) return {};
if (typeof response === "object" && response !== null) {
if ("data" in response) {
const data = response.data;
if (data && typeof data === "object") {
return data;
}
}
return response;
}
return {};
}
};
// src/client/collections-api-manager.ts
var CollectionsApiManager = class {
constructor(axiosInstance, collectionsClient) {
/**
* Documents API methods
*/
this.documents = {
/**
* List documents in a collection
*/
list: async (projectId, collectionName, options) => {
const url = `/projects/${projectId}/collections/${collectionName}/documents`;
const params = new URLSearchParams();
if (options?.limit) params.append("limit", options.limit.toString());
if (options?.offset) params.append("offset", options.offset.toString());
if (options?.filter) params.append("filter", JSON.stringify(options.filter));
if (options?.sort) params.append("sort", JSON.stringify(options.sort));
const response = await this.axiosInstance.get(
`/krapi/k1${url}${params.toString() ? `?${params.toString()}` : ""}`
);
const normalizedData = ResponseNormalizer.normalizeDocumentListResponse(response.data);
return {
success: true,
data: normalizedData
};
},
/**
* Get a single document
*/
get: async (projectId, collectionName, documentId) => {
const response = await this.collectionsClient.getDocument(projectId, collectionName, documentId);
const result = {
success: response.success,
data: ResponseNormalizer.normalizeDocumentResponse(response.data)
};
if (response.error) {
result.error = response.error;
}
return result;
},
/**
* Create a new document
*/
create: async (projectId, collectionName, data) => {
const response = await this.axiosInstance.post(
`/krapi/k1/projects/${projectId}/collections/${collectionName}/documents`,
{ data, created_by: "client" }
);
const normalized = ResponseNormalizer.normalizeDocumentResponse(response.data);
return {
success: true,
data: normalized
};
},
/**
* Update a document
*/
update: async (projectId, collectionName, documentId, data) => {
return this.collectionsClient.updateDocument(
projectId,
collectionName,
documentId,
{ data, updated_by: "client" }
);
},
/**
* Delete a document
*/
delete: async (projectId, collectionName, documentId) => {
return this.collectionsClient.deleteDocument(projectId, collectionName, documentId);
},
/**
* Bulk create documents
*/
bulkCreate: async (projectId, collectionName, documents) => {
const createRequests = documents.map((doc) => ({
data: doc,
created_by: "client"
}));
return this.collectionsClient.bulkCreateDocuments(projectId, collectionName, createRequests);
},
/**
* Bulk update documents with filter
*/
bulkUpdate: async (projectId, collectionName, filter, updates) => {
const response = await this.axiosInstance.put(
`/krapi/k1/projects/${projectId}/collections/${collectionName}/documents/bulk`,
{ filter, updates }
);
return response.data;
},
/**
* Bulk delete documents with filter
*/
bulkDelete: async (projectId, collectionName, filter) => {
const response = await this.axiosInstance.post(
`/krapi/k1/projects/${projectId}/collections/${collectionName}/documents/bulk-delete`,
{ filter }
);
return response.data;
},
/**
* Search documents
*/
search: async (projectId, collectionName, query, options) => {
const searchOptions = {
text: query
};
if (options?.fields !== void 0) searchOptions.fields = options.fields;
if (options?.limit !== void 0) searchOptions.limit = options.limit;
return this.collectionsClient.searchDocuments(projectId, collectionName, searchOptions);
},
/**
* Aggregate documents
*/
aggregate: async (projectId, collectionName, options) => {
const aggregations = {};
if (options.operations) {
for (const op of options.operations) {
const opType = op.operation;
if (["count", "sum", "avg", "min", "max"].includes(op.operation)) {
aggregations[op.field] = { type: opType, field: op.field };
}
}
}
const aggregateOptions = {
aggregations
};
if (options.groupBy) {
aggregateOptions.group_by = [options.groupBy];
}
return this.collectionsClient.aggregateDocuments(projectId, collectionName, aggregateOptions);
}
};
/**
* Collection management methods
*/
this.collections = {
/**
* List collections in a project
*/
list: async (projectId) => {
const result = await this.collectionsClient.getProjectCollections(projectId);
if (result && typeof result === "object") {
if ("collections" in result && Array.isArray(result.collections)) {
return {
success: true,
data: result.collections
};
}
if ("data" in result && Array.isArray(result.data)) {
return {
success: true,
data: result.data
};
}
}
return {
success: true,
data: []
};
},
/**
* Get a single collection
*/
get: async (projectId, collectionName) => {
return this.collectionsClient.getCollection(projectId, collectionName);
},
/**
* Create a new collection
*/
create: async (projectId, collectionData) => {
const response = await this.collectionsClient.createCollection(
projectId,
collectionData
);
const normalizedData = ResponseNormalizer.normalizeCollectionResponse(response.data);
const result = {
success: response.success,
data: normalizedData
};
if (response.error) {
result.error = response.error;
}
return result;
},
/**
* Update a collection
*/
update: async (projectId, collectionName, updates) => {
return this.collectionsClient.updateCollection(projectId, collectionName, updates);
},
/**
* Delete a collection
*/
delete: async (projectId, collectionName) => {
return this.collectionsClient.deleteCollection(projectId, collectionName);
}
};
this.axiosInstance = axiosInstance;
this.collectionsClient = collectionsClient;
}
};
// src/core/krapi-error.ts
var KrapiError = class _KrapiError extends Error {
/**
* Create a new KrapiError instance
*
* @param {string} message - Error message
* @param {ErrorCode} [code] - Error code
* @param {number} [status] - HTTP status code
* @param {Record<string, unknown>} [details] - Additional error details
* @param {string} [requestId] - Request ID for tracking
* @param {unknown} [cause] - Original error that caused this error
*/
constructor(message, code = "INTERNAL_ERROR", status, details, requestId, cause) {
super(message);
this.name = "KrapiError";
this.code = code;
if (status !== void 0) this.status = status;
if (details !== void 0) this.details = details;
if (requestId !== void 0) this.requestId = requestId;
if (cause !== void 0) this.cause = cause;
this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
if (Error.captureStackTrace) {
Error.captureStackTrace(this, _KrapiError);
}
}
// Static factory methods
/**
* Create KrapiError from HttpError
*
* @param httpError - HttpError instance to convert
* @param context - Additional context to include
* @returns New KrapiError instance
*/
static fromHttpError(httpError, context) {
let errorCode = "INTERNAL_ERROR";
if (httpError.status) {
switch (httpError.status) {
case 400:
errorCode = "BAD_REQUEST";
break;
case 401:
errorCode = "UNAUTHORIZED";
break;
case 403:
errorCode = "FORBIDDEN";
break;
case 404:
errorCode = "NOT_FOUND";
break;
case 409:
errorCode = "CONFLICT";
break;
case 422:
errorCode = "UNPROCESSABLE_ENTITY";
break;
case 429:
errorCode = "RATE_LIMIT_EXCEEDED";
break;
case 500:
errorCode = "SERVER_ERROR";
break;
case 502:
case 503:
errorCode = "SERVICE_UNAVAILABLE";
break;
case 408:
errorCode = "TIMEOUT";
break;
}
}
return new _KrapiError(
httpError.message,
errorCode,
httpError.status,
{
httpError: {
code: httpError.code,
responseData: httpError.responseData
},
...context
}
);
}
/**
* Create KrapiError from generic Error
*
* @param error - Generic error to convert
* @param defaultCode - Default error code if cannot be inferred
* @param context - Additional context to include
* @returns New KrapiError instance
*/
static fromError(error, defaultCode = "INTERNAL_ERROR", context) {
const code = getErrorCodeFromMessage(error.message) || defaultCode;
return new _KrapiError(
error.message,
code,
void 0,
{
originalStack: error.stack,
...context
},
void 0,
error
);
}
/**
* Create validation error
*
* @param message - Error message
* @param field - Field that failed validation
* @param value - Invalid value (will be masked for security)
* @param context - Additional context
* @returns New validation error
*/
static validationError(message, field, value, context) {
return new _KrapiError(
message,
"VALIDATION_ERROR",
400,
{
field,
value: maskSensitiveValue(value),
...context
}
);
}
/**
* Create not found error
*
* @param message - Error message or resource type
* @param context - Additional context (optional)
* @returns New not found error
*/
static notFound(message, context) {
return new _KrapiError(
message,
"NOT_FOUND",
404,
context
);
}
/**
* Create authentication error
*
* @param message - Error message
* @param context - Additional context
* @returns New authentication error
*/
static authError(message = "Authentication required", context) {
return new _KrapiError(
message,
"UNAUTHORIZED",
401,
context
);
}
/**
* Create authorization error
*
* @param message - Error message
* @param context - Additional context
* @returns New authorization error
*/
static forbidden(message = "Access forbidden", context) {
return new _KrapiError(
message,
"FORBIDDEN",
403,
context
);
}
/**
* Create conflict error
*
* @param message - Error message
* @param context - Additional context
* @returns New conflict error
*/
static conflict(message, context) {
return new _KrapiError(
message,
"CONFLICT",
409,
context
);
}
/**
* Create internal server error
*
* @param message - Error message
* @param context - Additional context
* @returns New internal server error
*/
static internalError(message = "Internal server error", context) {
return new _KrapiError(
message,
"INTERNAL_ERROR",
500,
context
);
}
/**
* Create bad request error
*
* @param message - Error message
* @param context - Additional context
* @returns New bad request error
*/
static badRequest(message, context) {
return new _KrapiError(
message,
"BAD_REQUEST",
400,
context
);
}
/**
* Create service unavailable error
*
* @param message - Error message
* @param context - Additional context
* @returns New service unavailable error
*/
static serviceUnavailable(message = "Service unavailable", context) {
return new _KrapiError(
message,
"SERVICE_UNAVAILABLE",
503,
context
);
}
// Instance methods
/**
* Create a new error with additional context
*
* @param context - Additional context to merge
* @returns New KrapiError with merged context
*/
withContext(context) {
return new _KrapiError(
this.message,
this.code,
this.status,
{
...this.details || {},
...context
},
this.requestId,
this.cause
);
}
/**
* Check if this is a validation error
*
* @returns True if this is a validation error
*/
isValidationError() {
return this.code === "VALIDATION_ERROR";
}
/**
* Check if this is a not found error
*
* @returns True if this is a not found error
*/
isNotFound() {
return this.code === "NOT_FOUND";
}
/**
* Check if this is an authentication error
*
* @returns True if this is an authentication error
*/
isAuthError() {
return this.code === "UNAUTHORIZED" || this.code === "FORBIDDEN";
}
/**
* Check if this is a client error (4xx)
*
* @returns True if this is a client error
*/
isClientError() {
return this.status !== void 0 && this.status >= 400 && this.status < 500;
}
/**
* Check if this is a server error (5xx)
*
* @returns True if this is a server error
*/
isServerError() {
return this.status !== void 0 && this.status >= 500;
}
/**
* Convert error to JSON format
*
* @returns {Object} Error as JSON object
*/
toJSON() {
const result = {
code: this.code,
message: this.message,
timestamp: this.timestamp
};
if (this.status !== void 0) result.status = this.status;
if (this.details !== void 0) result.details = this.details;
if (this.requestId !== void 0) result.request_id = this.requestId;
if (this.cause !== void 0) {
result.cause = this.cause instanceof Error ? this.cause.message : String(this.cause);
}
return result;
}
/**
* Get detailed error message
*
* @returns {string} Detailed error message
*/
getDetailedMessage() {
let message = this.message;
if (this.code) {
message = `[${this.code}] ${message}`;
}
if (this.status) {
message = `${message} (HTTP ${this.status})`;
}
return message;
}
};
function maskSensitiveValue(value) {
if (typeof value === "string") {
if (value.length > 10 && (value.toLowerCase().includes("password") || value.toLowerCase().includes("token") || value.toLowerCase().includes("secret") || value.toLowerCase().includes("key"))) {
return `${value.substring(0, 4)}****`;
}
if (value.length > 50) {
return `${value.substring(0, 20)}...[${value.length - 40} chars]...${value.substring(value.length - 20)}`;
}
}
return value;
}
function getErrorCodeFromMessage(message) {
const lowerMessage = message.toLowerCase();
if (lowerMessage.includes("unauthorized") || lowerMessage.includes("not authorized")) {
return "UNAUTHORIZED";
}
if (lowerMessage.includes("forbidden") || lowerMessage.includes("permission denied")) {
return "FORBIDDEN";
}
if (lowerMessage.includes("invalid credentials") || lowerMessage.includes("authentication failed")) {
return "UNAUTHORIZED";
}
if (lowerMessage.includes("not found") || lowerMessage.includes("does not exist")) {
return "NOT_FOUND";
}
if (lowerMessage.includes("already exists") || lowerMessage.includes("duplicate")) {
return "CONFLICT";
}
if (lowerMessage.includes("validation") || lowerMessage.includes("invalid") || lowerMessage.includes("required") || lowerMessage.includes("missing")) {
return "VALIDATION_ERROR";
}
if (lowerMessage.includes("network") || lowerMessage.includes("connection") || lowerMessage.includes("timeout")) {
return "NETWORK_ERROR";
}
if (lowerMessage.includes("rate limit")) {
return "RATE_LIMIT_EXCEEDED";
}
if (lowerMessage.includes("internal server") || lowerMessage.includes("database error") || lowerMessage.includes("query failed")) {
return "INTERNAL_ERROR";
}
if (lowerMessage.includes("service unavailable") || lowerMessage.includes("temporarily unavailable")) {
return "SERVICE_UNAVAILABLE";
}
if (lowerMessage.includes("bad request")) {
return "BAD_REQUEST";
}
return void 0;
}
// src/http-clients/http-error.ts
var HttpError = class _HttpError extends Error {
/**
* Create a new HttpError instance
*
* @param {string} message - Error message
* @param {Object} options - Error options
* @param {number} [options.status] - HTTP status code
* @param {string} [options.method] - HTTP method
* @param {string} [options.url] - Request URL
* @param {Record<string, string>} [options.requestHeaders] - Request headers
* @param {unknown} [options.requestBody] - Request body/data sent
* @param {Record<string, unknown>} [options.requestQuery] - Request query parameters
* @param {unknown} [options.responseData] - Response data
* @param {Record<string, string>} [options.responseHeaders] - Response headers
* @param {string} [options.code] - Error code
* @param {unknown} [options.originalError] - Original error
*/
constructor(message, options = {}) {
super(message);
this.name = "HttpError";
if (options.status !== void 0) {
this.status = options.status;
}
if (options.method !== void 0) {
this.method = options.method;
}
if (options.url !== void 0) {
this.url = options.url;
}
if (options.requestHeaders !== void 0) {
this.requestHeaders = options.requestHeaders;
}
if (options.requestBody !== void 0) {
this.requestBody = options.requestBody;
}
if (options.requestQuery !== void 0) {
this.requestQuery = options.requestQuery;
}
if (options.responseData !== void 0) {
this.responseData = options.responseData;
}
if (options.responseHeaders !== void 0) {
this.responseHeaders = options.responseHeaders;
}
if (options.code !== void 0) {
this.code = options.code;
}
if (options.originalError !== void 0) {
this.originalError = options.originalError;
}
this.isApiError = options.status !== void 0 && options.status >= 400;
this.isNetworkError = options.status === void 0;
this.isAuthError = options.status === 401 || options.status === 403;
this.isClientError = options.status !== void 0 && options.status >= 400 && options.status < 500;
this.isServerError = options.status !== void 0 && options.status >= 500;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, _HttpError);
}
}
/**
* Get a detailed error message with all available information
*
* @returns {string} Detailed error message
*/
getDetailedMessage() {
const parts = [this.message];
if (this.status) {
parts.push(`(HTTP ${this.status})`);
}
if (this.method && this.url) {
parts.push(`[${this.method} ${this.url}]`);
}
if (this.code) {
parts.push(`[Code: ${this.code}]`);
}
if (this.responseData && typeof this.responseData === "object") {
const response = this.responseData;
if (response.error && typeof response.error === "string") {
parts.push(`Backend error: ${response.error}`);
} else if (response.message && typeof response.message === "string") {
parts.push(`Backend message: ${response.message}`);
}
}
return parts.join(" ");
}
/**
* Convert error to JSON for logging
*
* @returns {Record<string, unknown>} JSON representation
*/
toJSON() {
return {
name: this.name,
message: this.message,
status: this.status,
method: this.method,
url: this.url,
code: this.code,
isApiError: this.isApiError,
isNetworkError: this.isNetworkError,
isAuthError: this.isAuthError,
isClientError: this.isClientError,
isServerError: this.isServerError,
requestHeaders: this.requestHeaders,
requestBody: this.requestBody,
requestQuery: this.requestQuery,
responseData: this.responseData,
responseHeaders: this.responseHeaders,
originalError: this.originalError,
stack: this.stack
};
}
};
// src/utils/error-handler.ts
function normalizeError(error, defaultCode = "INTERNAL_ERROR", context) {
if (error instanceof KrapiError) {
return context ? enrichError(error, context) : error;
}
if (error instanceof HttpError) {
return transformHttpError(error, context);
}
if (error instanceof Error) {
const code = getErrorCodeFromMessage2(error.message);
return new KrapiError(
error.message,
code,
void 0,
{
...context,
originalError: error,
stack: error.stack
}
);
}
if (typeof error === "string") {
const code = getErrorCodeFromMessage2(error);
return new KrapiError(
error,
code,
void 0,
context
);
}
return new KrapiError(
"An unexpected error occurred",
defaultCode,
void 0,
{
...context,
originalError: error
}
);
}
function transformHttpError(httpError, context) {
const errorCode = getErrorCodeFromStatus(httpError.status);
let message = httpError.message;
if (httpError.status && !message.includes(`HTTP ${httpError.status}`)) {
message = `${message} (HTTP ${httpError.status})`;
}
const krapiError = new KrapiError(
message,
errorCode,
httpError.status,
{
...context,
httpError: {
method: httpError.method,
url: httpError.url,
status: httpError.status,
code: httpError.code,
isApiError: httpError.isApiError,
isNetworkError: httpError.isNetworkError,
isAuthError: httpError.isAuthError,
isClientError: httpError.isClientError,
isServerError: httpError.isServerError,
// Request context - what was sent
requestHeaders: httpError.requestHeaders,
requestBody: httpError.requestBody,
requestQuery: httpError.requestQuery,
// Response context - what was received
responseData: httpError.responseData,
responseHeaders: httpError.responseHeaders
},
originalError: httpError
}
);
return krapiError;
}
function enrichError(error, context) {
const mergedDetails = {
...error.details || {},
...context
};
if (error.details?.originalError) {
mergedDetails.originalError = error.details.originalError;
}
return new KrapiError(
error.message,
error.code,
error.status,
mergedDetails,
error.requestId,
error.cause
);
}
function getErrorCodeFromStatus(status) {
if (!status) {
return "NETWORK_ERROR";
}
switch (status) {
case 400:
return "BAD_REQUEST";
case 401:
return "UNAUTHORIZED";
case 403:
return "FORBIDDEN";
case 404:
return "NOT_FOUND";
case 409:
return "CONFLICT";
case 422:
return "UNPROCESSABLE_ENTITY";
case 429:
return "RATE_LIMIT_EXCEEDED";
case 500:
return "SERVER_ERROR";
case 502:
case 503:
case 504:
return "SERVICE_UNAVAILABLE";
case 408:
return "TIMEOUT";
default:
if (status >= 400 && status < 500) {
return "BAD_REQUEST";
}
if (status >= 500) {
return "SERVER_ERROR";
}
return "INTERNAL_ERROR";
}
}
function getErrorCodeFromMessage2(message) {
const lowerMessage = message.toLowerCase();
if (lowerMessage.includes("unauthorized") || lowerMessage.includes("not authorized")) {
return "UNAUTHORIZED";
}
if (lowerMessage.includes("forbidden") || lowerMessage.includes("permission denied")) {
return "FORBIDDEN";
}
if (lowerMessage.includes("invalid credentials") || lowerMessage.includes("authentication failed")) {
return "UNAUTHORIZED";
}
if (lowerMessage.includes("not found") || lowerMessage.includes("does not exist")) {
return "NOT_FOUND";
}
if (lowerMessage.includes("already exists") || lowerMessage.includes("duplicate")) {
return "CONFLICT";
}
if (lowerMessage.includes("validation") || lowerMessage.includes("invalid") || lowerMessage.includes("required") || lowerMessage.includes("missing")) {
return "VALIDATION_ERROR";
}
if (lowerMessage.includes("network") || lowerMessage.includes("connection") || lowerMessage.includes("timeout")) {
return "NETWORK_ERROR";
}
if (lowerMessage.includes("rate limit")) {
return "RATE_LIMIT_EXCEEDED";
}
if (lowerMessage.includes("internal server") || lowerMessage.includes("database error") || lowerMessage.includes("query failed")) {
return "INTERNAL_ERROR";
}
if (lowerMessage.includes("service unavailable") || lowerMessage.includes("temporarily unavailable")) {
return "SERVICE_UNAVAILABLE";
}
if (lowerMessage.includes("bad request")) {
return "BAD_REQUEST";
}
return "INTERNAL_ERROR";
}
// src/client/project-api-manager.ts
var ProjectApiManager = class {
constructor(axiosInstance) {
this.axiosInstance = axiosInstance;
}
/**
* Get all projects
* @returns {Promise<Project[]>} Array of projects
*/
async getAll() {
try {
const response = await this.axiosInstance.get(
"/krapi/k1/projects"
);
return Array.isArray(response.data) ? response.data : [];
} catch (error) {
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getProjects"
});
}
}
/**
* Get project by ID
* @param {string} id - Project ID
* @returns {Promise<Project>} Project object
*/
async get(id) {
if (!id || typeof id !== "string") {
throw KrapiError.validationError("Project ID is required", "id");
}
try {
const response = await this.axiosInstance.get(
`/krapi/k1/projects/${id}`
);
return response.data;
} catch (error) {
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getProject",
projectId: id
});
}
}
/**
* Create a new project
* @param {CreateProjectRequest} data - Project data
* @returns {Promise<Project>} Created project
*/
async create(data) {
if (!data || !data.name || typeof data.name !== "string" || data.name.trim() === "") {
throw KrapiError.validationError("Project name is required", "name");
}
try {
const response = await this.axiosInstance.post(
"/krapi/k1/projects",
data
);
return response.data;
} catch (error) {
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "createProject"
});
}
}
/**
* Update a project
* @param {string} id - Project ID
* @param {UpdateProjectRequest} data - Updated project data
* @returns {Promise<Project>} Updated project
*/
async update(id, data) {
if (!id || typeof id !== "string") {
throw KrapiError.validationError("Project ID is required", "id");
}
if (!data || typeof data !== "object") {
throw KrapiError.validationError("Update data is required", "data");
}
try {
const response = await this.axiosInstance.put(
`/krapi/k1/projects/${id}`,
data
);
return response.data;
} catch (error) {
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "updateProject",
projectId: id
});
}
}
/**
* Delete a project
* @param {string} id - Project ID
* @returns {Promise<void>}
*/
async delete(id) {
if (!id || typeof id !== "string") {
throw KrapiError.validationError("Project ID is required", "id");
}
try {
await this.axiosInstance.delete(`/krapi/k1/projects/${id}`);
} catch (error) {
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "deleteProject",
projectId: id
});
}
}
};
// src/http-clients/base-http-client.ts
var import_axios = __toESM(require("axios"));
// src/utils/endpoint-utils.ts
function extractPort(endpoint) {
try {
const url = new URL(endpoint);
if (url.port) {
return parseInt(url.port, 10);
}
return url.protocol === "https:" ? 443 : 80;
} catch {
return null;
}
}
function isBackendUrl(endpoint) {
const port = extractPort(endpoint);
return port === 3470;
}
// src/http-clients/base-http-client.ts
var BaseHttpClient = class {
/**
* Create a new BaseHttpClient instance
*
* @param {HttpClientConfig} config - HTTP client configuration
*/
constructor(config) {
this.baseUrl = config.baseUrl.replace(/\/$/, "");
if (config.apiKey) this.apiKey = config.apiKey;
if (config.sessionToken) this.sessionToken = config.sessionToken;
if (config.projectId) this.projectId = config.projectId;
if (config.retry) this.retryConfig = config.retry;
}
/**
* Initialize the HTTP client with interceptors
*
* Sets up axios instance with authentication interceptors and error handling.
*
* @returns {Promise<void>}
*
* @example
* await client.initializeClient();
*/
async initializeClient() {
if (this.httpClient !== void 0) return;
let baseURL = this.baseUrl;
const isBackend = isBackendUrl(baseURL);
const hasApiPath = /\/api\/krapi\/k1(\/|$)/.test(baseURL);
const hasKrapiPath = /\/krapi\/k1(\/|$)/.test(baseURL) && !hasApiPath;
if (hasApiPath) {
} else if (hasKrapiPath) {
if (isBackend) {
} else {
baseURL = baseURL.replace("/krapi/k1", "/api/krapi/k1");
}
} else {
if (isBackend) {
baseURL = `${baseURL}/krapi/k1`;
} else {
baseURL = `${baseURL}/api/krapi/k1`;
}
}
this.httpClient = import_axios.default.create({
baseURL,
timeout: 3e4,
headers: {
"Content-Type": "application/json"
}
});
this.httpClient.interceptors.request.use(
(config) => {
if (config.data instanceof FormData) {
delete config.headers["Content-Type"];
delete config.headers["content-type"];
}
const token = this.getCurrentToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
delete config.headers["X-API-Key"];
}
if (config.url) {
let normalizedPath = config.url;
normalizedPath = normalizedPath.replace(/^(\/api)?\/krapi\/k1/, "");
const isProjectListOrCreate = /^\/projects\/?(\?|$)/.test(
normalizedPath
);
const isProjectScoped = /^\/projects\/[^/]+/.test(normalizedPath);
if (isProjectListOrCreate) {
delete config.headers["X-Project-ID"];
delete config.headers["x-project-id"];
} else if (isProjectScoped && this.projectId) {
config.headers["X-Project-ID"] = this.projectId;
}
}
if (process.env.NODE_ENV === "development" || process.env.DEBUG_SDK_HEADERS) {
const normalizedPath = config.url?.replace(/^(\/api)?\/krapi\/k1/, "") || "";
const isProjectListOrCreate = /^\/projects(\/|\?|$)/.test(
normalizedPath
);
const isProjectScoped = /^\/projects\/[^/]+/.test(normalizedPath);
const hasProjectIdHeader = !!(config.headers["X-Project-ID"] || config.headers["x-project-id"]);
const relevantHeaders = Object.keys(config.headers).filter(
(k) => k.toLowerCase().includes("project") || k.toLowerCase() === "authorization"
).reduce((acc, k) => {
acc[k] = config.headers[k] ? "present" : "missing";
return acc;
}, {});
console.log("[SDK Interceptor]", {
method: config.method?.toUpperCase(),
url: config.url,
normalizedPath,
hasProjectId: !!this.projectId,
isProjectListOrCreate,
isProjectScoped,
willAddHeader: isProjectScoped && !!this.projectId,
willRemoveHeader: isProjectListOrCreate,
actualHeaderPresent: hasProjectIdHeader,
relevantHeaders
});
}
return config;
}
);
this.httpClient.interceptors.response.use(
(response) => response.data,
// Return just the data
(error) => {
if ((0, import_axios.isAxiosError)(error)) {
const axiosError = error;
const status = axiosError.response?.status;
const responseData = axiosError.response?.data;
const method = axiosError.config?.method?.toUpperCase();
const relativeUrl = axiosError.config?.url;
const baseUrl = axiosError.config?.baseURL || "";
const fullUrl = relativeUrl ? `${baseUrl}${relativeUrl}` : baseUrl;
const requestBody = axiosError.config?.data;
const requestQuery = axiosError.config?.params || {};
const responseHeaders = {};
if (axiosError.response?.headers) {
Object.keys(axiosError.response.headers).forEach((key) => {
const value = axiosError.response?.headers[key];
if (typeof value === "string") {
responseHeaders[key] = value;
} else if (Array.isArray(value) && value.length > 0) {
responseHeaders[key] = value.join(", ");
}
});
}
const requestHeaders = {};
if (axiosError.config && axiosError.config.headers) {
const headers = axiosError.config.headers;
Object.keys(headers).forEach((key) => {
const value = headers[key];
if (typeof value === "string") {
if (key.toLowerCase() === "authorization") {
requestHeaders[key] = `${value.substring(0, 20)}...`;
} else {
requestHeaders[key] = value;
}
}
});
}
let errorMessage = "HTTP request failed";
let errorCode;
if (status) {
if (responseData) {
if (typeof responseData === "object" && responseData !== null) {
const data = responseData;
errorMessage = data.error || data.message || `HTTP ${status} ${axiosError.response?.statusText || "Error"}`;
if (data.code && typeof data.code === "string") {
errorCode = data.code;
}
} else if (typeof responseData === "string") {
errorMessage = responseData;
}
} else {
errorMessage = `HTTP ${status} ${axiosError.response?.statusText || "Error"}`;
}
if (status === 404) {
const isBackend2 = isBackendUrl(baseUrl);
if (isBackend2) {
errorMessage += `
- Backend URL detected (port 3470) - SDK should use /krapi/k1 path
- Verify the endpoint path is correct
- Check that backend routes are accessible at /krapi/k1/...`;
} else {
errorMessage += `
- Frontend URL detected (port 3498) - SDK should use /api/krapi/k1 path
- The SDK should automatically append /api/krapi/k1/ to your endpoint
- Verify the endpoint path is correct`;
}
} else if (status === 401) {
const currentToken = this.getCurrentToken();
const isSessionToken = !!this.sessionToken;
if (isSessionToken) {
errorMessage += `
- Invalid or expired session token
- Verify the session token is correct
- Check if the session has expired
- Ensure you're logged in and the session is active
- Try logging in again to get a new session token`;
} else if (currentToken) {
errorMessage += `
- Invalid or expired API key
- Check that your API key is correct
- Verify the API key has the required scopes
- Ensure the API key hasn't been revoked`;
} else {
errorMessage += `
- Authentication required
- No session token or API key provided
- Set a session token using setSessionToken() or provide an API key`;
}
} else if (status === 403) {
const isSessionToken = !!this.sessionToken;
if (isSessionToken) {
errorMessage += `
- Your session token may not have permission for this operation
- Check the user's role and permissions
- Verify the session token belongs to a user with sufficient access
- Ensure you're using the correct authentication method`;
} else {
errorMessage += `
- Your API key may not have permission for this operation
- Check the API key scopes and permissions
- Verify you're using the correct authentication method`;
}
}
const httpErrorOptions = {};
if (status !== void 0) httpErrorOptions.status = status;
if (method !== void 0) httpErrorOptions.method = method;
if (fullUrl !== void 0) httpErrorOptions.url = fullUrl;
if (Object.keys(requestHeaders).length > 0)
httpErrorOptions.requestHeaders = requestHeaders;
if (requestBody !== void 0) httpErrorOptions.requestBody = requestBody;
if (Object.keys(requestQuery).length > 0)
httpErrorOptions.requestQuery = requestQuery;
if (responseData !== void 0)
httpErrorOptions.responseData = responseData;
if (Object.keys(responseHeaders).length > 0)
httpErrorOptions.responseHeaders = responseHeaders;
if (errorCode !== void 0) httpErrorOptions.code = errorCode;
if (error !== void 0) httpErrorOptions.originalError = error;
const httpError2 = new HttpError(errorMessage, httpErrorOptions);
return Promise.reject(httpError2);
} else {
const networkDisplayUrl = relativeUrl ? `${baseUrl.replace(/^https?:\/\/[^/]+/, "")}${relativeUrl}` : fullUrl || "endpoint";
if (axiosError.code === "ECONNABORTED" || axiosError.message.includes("timeout")) {
errorMessage = `Request timeout: ${method || "Request"} to ${networkDisplayUrl} exceeded ${axiosError.config?.timeout || 3e4}ms`;
errorCode = "TIMEOUT";
} else if (axiosError.code === "ENOTFOUND" || axiosError.code === "ECONNREFUSED") {
const isBackend2 = isBackendUrl(baseUrl);
const connectionType = isBackend2 ? "backend URL (port 3470)" : "frontend URL (port 3498)";
errorMessage = `Cannot connect to Krapi Server at ${networkDisplayUrl}.
- Is the server running?
- Are you using the ${connectionType}?
- Check firewall settings if accessing remotely.`;
errorCode = "NETWORK_ERROR";
} else if (axiosError.code === "ECONNRESET" || axiosError.message.includes("socket hang up")) {
errorMessage = `Connection reset by server at ${networkDisplayUrl}.
- The server may have closed the connection unexpectedly
- This can happen with long-running queries or large result sets
- Try reducing query scope or increasing timeout if applicable
- Check server logs for errors`;
errorCode = "CONNECTION_RESET";
} else {
errorMessage = `Network error: ${axiosError.message || "Failed to connect to server"}
- Verify the endpoint URL is correct
- Check network connectivity
- Ensure the server is accessible`;
errorCode = "NETWORK_ERROR";
}
const requestBody2 = axiosError.config?.data;
const requestQuery2 = axiosError.config?.params || {};
const httpErrorOptions = {};
if (method !== void 0) httpErrorOptions.method = method;
if (fullUrl !== void 0) httpErrorOptions.url = fullUrl;
if (Object.keys(requestHeaders).length > 0)
httpErrorOptions.requestHeaders = requestHeaders;
if (requestBody2 !== void 0) httpErrorOptions.requestBody = requestBody2;
if (Object.keys(requestQuery2).length > 0)
httpErrorOptions.requestQuery = requestQuery2;
if (errorCode !== void 0) httpErrorOptions.code = errorCode;
if (error !== void 0) httpErrorOptions.originalError = error;
const httpError2 = new HttpError(errorMessage, httpErrorOptions);
return Promise.reject(httpError2);
}
}
if (error instanceof Error) {
const httpError2 = new HttpError(error.message, {
originalError: error,
code: "UNKNOWN_ERROR"
});
return Promise.reject(httpError2);
}
const httpError = new HttpError("Unknown error occurred", {
originalError: error,
code: "UNKNOWN_ERROR"
});
return Promise.reject(httpError);
}
);
}
/**
* Get current authentication token (session token or API key)
*
* This method is used by the request interceptor to get the current token
* at request time, ensuring we always use the latest token value.
*
* @returns {string | undefined} Current token or undefined if none set
* @private
*/
getCurrentToken() {
return this.sessionToken || this.apiKey;
}
/**
* Set project ID for project-scoped operations
*
* The project ID will be automatically added as X-Project-ID header
* only for project-scoped routes (e.g., /projects/{id}/...).
* It will NOT be added for list/create operations (e.g., /projects).
*
* @param {string} projectId - Project ID
* @returns {void}
*
* @example
* client.setProjectId('project-id-here');
*/
setProjectId(projectId) {
this.projectId = projectId;
}
/**
* Set session token for authentication
*
* @param {string} token - Session token
* @returns {void}
* @throws {Error} If HTTP client is not initialized
*
* @example
* client.setSessionToken('session-token-here');
*/
// Authentication methods
setSessionToken(token) {
if (!this.httpClient) {
throw KrapiError.serviceUnavailable(
"HTTP client not initialized. Call initializeClient() first or ensure connect() was called."
);
}
this.sessionToken = token;
delete this.apiKey;
this.httpClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
delete this.httpClient.defaults.headers.common["X-API-Key"];
}
/**
* Set API key for authentication
*
* @param {string} key - API key
* @returns {void}
*
* @example
* client.setApiKey('api-key-here');
*/
setApiKey(key) {
if (!this.httpClient) {
throw KrapiError.serviceUnavailable(
"HTTP client not initialized. Call initializeClient() first or ensure connect() was called."
);
}
this.apiKey = key;
delete this.sessionToken;
this.httpClient.defaults.headers.common["Authorization"] = `Bearer ${key}`;
delete this.httpClient.defaults.headers.common["X-API-Key"];
}
/**
* Clear authentication credentials
*
* Removes both session token and API key.
*
* @returns {void}
*
* @example
* client.clearAuth();
*/
clearAuth() {
delete this.sessionToken;
delete this.apiKey;
}
/**
* Send GET request
*
* @template T
* @param {string} endpoint - API endpoint
* @param {QueryOptions} [params] - Query parameters
* @returns {Promise<ApiResponse<T>>} API response
*
* @example
* const response = await client.get('/projects', { limit: 10 });
*/
/**
* Execute request with retry logic if configured
*
* @template T
* @param {() => Promise<T>} requestFn - Request function to execute
* @returns {Promise<T>} Request result
*/
async executeWithRetry(requestFn) {
if (!this.retryConfig?.enabled) {
return requestFn();
}
const maxRetries = this.retryConfig.maxRetries ?? 3;
const retryDelay = this.retryConfig.retryDelay ?? 1e3;
const retryableStatusCodes = this.retryConfig.retryableStatusCodes ?? [
408,
429,
500,
502,
503,
504
];
const retryableErrorCodes = this.retryConfig.retryableErrorCodes ?? [
"TIMEOUT",
"NETWORK_ERROR"
];
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await requestFn();
} catch (error) {
lastError = error;
if (attempt >= maxRetries) {
break;
}
let shouldRetry = false;
if (error instanceof HttpError) {
if (error.status && retryableStatusCodes.includes(error.status)) {
shouldRetry = true;
}
if (error.code && retryableErrorCodes.includes(error.code)) {
shouldRetry = true;
}
} else if (error instanceof Error) {
if (error.message.includes("timeout") || error.message.includes("ECONNREFUSED") || error.message.includes("ENOTFOUND")) {
shouldRetry = true;