@nicodoggie/node-kiwi-tcms-api
Version:
Vibe-coded Node.js wrapper for Kiwi TCMS XML-RPC API. Use at your own risk.
288 lines ⢠10.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.KiwiClient = void 0;
const axios_1 = __importDefault(require("axios"));
const axios_xmlrpc_client_1 = require("./axios-xmlrpc-client");
/**
* Base XML-RPC client for Kiwi TCMS
*/
class KiwiClient {
client;
config;
sessionId;
cfAuthCookie;
constructor(config) {
this.config = {
timeout: 30000,
...config,
};
// We'll initialize the client after getting CF auth if needed
}
/**
* Get Cloudflare Access authorization cookie using axios
*/
async getCloudflareAuthCookie() {
if (!this.config.cloudflareClientId || !this.config.cloudflareClientSecret) {
return undefined;
}
try {
console.log('š Getting Cloudflare Access authorization cookie...');
const url = new URL(this.config.baseUrl);
const testUrl = `${url.protocol}//${url.hostname}${url.pathname || '/'}`;
const response = await axios_1.default.get(testUrl, {
headers: {
'CF-Access-Client-Id': this.config.cloudflareClientId,
'CF-Access-Client-Secret': this.config.cloudflareClientSecret,
'User-Agent': 'Node.js Kiwi TCMS Client',
},
timeout: this.config.timeout,
maxRedirects: 10, // axios handles redirects automatically
validateStatus: () => true, // Accept any status code
});
console.log(`š Cloudflare Auth Response: ${response.status} ${response.statusText}`);
// Extract CF_Authorization cookie
const cookies = response.headers['set-cookie'];
if (cookies) {
for (const cookie of cookies) {
if (cookie.startsWith('CF_Authorization=')) {
const cfAuth = cookie.split(';')[0]; // Get just the CF_Authorization=value part
console.log('ā
Got Cloudflare Authorization cookie');
return cfAuth;
}
}
}
// Also check if we're already authenticated (no cookie needed)
if (response.status === 200) {
console.log('ā
Cloudflare Access already authenticated (no cookie needed)');
return 'authenticated';
}
console.log('ā ļø No CF_Authorization cookie found in response');
console.log('Response headers:', response.headers);
return undefined;
}
catch (error) {
console.error('ā Failed to get Cloudflare auth cookie:', error.message);
return undefined;
}
}
/**
* Initialize the XML-RPC client with proper authentication
*/
async initializeClient() {
if (this.client) {
return; // Already initialized
}
// Get Cloudflare auth cookie if needed
if (this.config.cloudflareClientId && this.config.cloudflareClientSecret) {
this.cfAuthCookie = await this.getCloudflareAuthCookie();
}
// Parse URL to get host, port, and path
const url = new URL(this.config.baseUrl);
// Debug logging
console.log(`š§ XML-RPC Client Configuration:`);
console.log(` Base URL: ${this.config.baseUrl}`);
console.log(` Host: ${url.hostname}`);
console.log(` Protocol: ${url.protocol}`);
console.log(` Path: ${url.pathname}`);
// Prepare headers
const headers = {
'User-Agent': 'Node.js Kiwi TCMS Client',
...this.config.headers,
};
// Add Cloudflare Access credentials if provided and no cookie obtained
if (this.config.cloudflareClientId && this.config.cloudflareClientSecret && !this.cfAuthCookie) {
headers['CF-Access-Client-Id'] = this.config.cloudflareClientId;
headers['CF-Access-Client-Secret'] = this.config.cloudflareClientSecret;
console.log(` Cloudflare Access: Using headers`);
}
else if (this.cfAuthCookie && this.cfAuthCookie !== 'authenticated') {
headers['Cookie'] = this.cfAuthCookie;
console.log(` Cloudflare Access: Using cookie`);
}
else {
console.log(` Cloudflare Access: Disabled`);
}
console.log(` Headers:`, Object.keys(headers));
// Create our custom AxiosXmlRpcClient
this.client = new axios_xmlrpc_client_1.AxiosXmlRpcClient({
url: this.config.baseUrl,
timeout: this.config.timeout,
headers: headers,
});
console.log(`ā
AxiosXmlRpcClient initialized successfully`);
}
/**
* Make an XML-RPC method call
*/
async call(method, params = []) {
// Ensure client is initialized before making calls
await this.initializeClient();
if (!this.client) {
throw new Error('Failed to initialize XML-RPC client');
}
try {
const result = await this.client.methodCall(method, params);
return result;
}
catch (error) {
// Enhanced error reporting
let errorMessage = `XML-RPC Error: ${error.message}`;
console.error('\nš Debugging XML-RPC Call:');
console.error(` Method: ${method}`);
console.error(` Params: ${JSON.stringify(params)}`);
console.error(` Error: ${error.message}`);
if (error.response) {
console.error(` HTTP Status: ${error.response.status} ${error.response.statusText}`);
if (error.response.headers) {
console.error(' Response Headers:', error.response.headers);
}
}
console.error('\n' + '='.repeat(60));
throw new Error(errorMessage);
}
}
/**
* Authenticate with the Kiwi TCMS server
*/
async login() {
if (!this.config.username || !this.config.password) {
throw new Error('Username and password are required for authentication');
}
try {
const sessionId = await this.call('Auth.login', [
this.config.username,
this.config.password,
]);
this.sessionId = sessionId;
return sessionId;
}
catch (error) {
throw new Error(`Authentication failed: ${error.message}`);
}
}
/**
* Logout from the Kiwi TCMS server
*/
async logout() {
if (this.sessionId) {
try {
await this.call('Auth.logout');
this.sessionId = undefined;
}
catch (error) {
// Ignore logout errors, but clear session
this.sessionId = undefined;
}
}
}
/**
* Make an authenticated XML-RPC call
* Note: Kiwi TCMS uses session cookies for authentication, not session ID parameters
*/
async authenticatedCall(method, params = []) {
if (!this.sessionId) {
await this.login();
}
// Don't pass session ID as parameter - Kiwi TCMS uses session cookies
try {
return await this.call(method, params);
}
catch (error) {
// If authentication error, try to re-login once
if (error.message.includes('Authentication') ||
error.message.includes('Session')) {
this.sessionId = undefined;
await this.login();
// Retry with original params (no session ID)
return await this.call(method, params);
}
throw error;
}
}
/**
* Get the current session ID
*/
getSessionId() {
return this.sessionId;
}
/**
* Check if client is authenticated
*/
isAuthenticated() {
return !!this.sessionId;
}
/**
* Set session ID manually (useful for token-based auth)
*/
setSessionId(sessionId) {
this.sessionId = sessionId;
}
/**
* Test connectivity to the server
*/
async testConnection() {
try {
const methods = await this.call('system.listMethods');
return methods;
}
catch (error) {
throw new Error(`Failed to connect to Kiwi TCMS server: ${error.message}`);
}
}
async getVersion() {
try {
return await this.call('Kiwi.version');
}
catch (error) {
throw new Error(`Failed to get Kiwi TCMS version: ${error.message}`);
}
}
/**
* List available XML-RPC methods (useful for debugging)
*/
async listMethods() {
try {
return await this.call('system.listMethods');
}
catch (error) {
throw new Error(`Failed to list methods: ${error.message}`);
}
}
/**
* Set a custom header for all requests
*/
setHeader(name, value) {
if (!this.config.headers) {
this.config.headers = {};
}
this.config.headers[name] = value;
// Note: xmlrpc client doesn't support dynamic header updates
// Headers are set during client creation only
console.warn('Header changes require recreating the client. Consider creating a new KiwiClient instance.');
}
/**
* Set Cloudflare Access credentials
*/
setCloudflareAccess(clientId, clientSecret) {
this.config.cloudflareClientId = clientId;
this.config.cloudflareClientSecret = clientSecret;
console.warn('Cloudflare Access credential changes require recreating the client. Consider creating a new KiwiClient instance.');
}
/**
* Get current headers configuration
*/
getHeaders() {
return this.config.headers;
}
/**
* Get the current configuration
*/
getConfig() {
return { ...this.config };
}
}
exports.KiwiClient = KiwiClient;
//# sourceMappingURL=client.js.map