trasorio-sdk
Version:
Official Node.js SDK for Trasor.io trust infrastructure platform
392 lines (345 loc) • 12.7 kB
JavaScript
/**
* Trasor.io Node.js SDK
*
* Official Node.js SDK for Trasor.io's trust infrastructure platform.
* Provides secure, immutable audit trails for AI agent workflows.
*
* @version 1.0.1
* @author Trasor.io <support@trasor.io>
* @license MIT
*/
const { URL } = require('url');
/**
* Custom error classes for Trasor.io SDK
*/
class TrasorError extends Error {
constructor(message, statusCode = null) {
super(message);
this.name = 'TrasorError';
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor);
}
}
class AuthenticationError extends TrasorError {
constructor(message = 'Authentication failed') {
super(message, 401);
this.name = 'AuthenticationError';
}
}
class ValidationError extends TrasorError {
constructor(message = 'Validation failed') {
super(message, 400);
this.name = 'ValidationError';
}
}
class APIError extends TrasorError {
constructor(message = 'API request failed', statusCode = 500) {
super(message, statusCode);
this.name = 'APIError';
}
}
/**
* Trasor.io Client for Node.js
*
* A simple, developer-friendly client for recording audit logs with Trasor.io's
* trust infrastructure platform.
*
* @example
* const { TrasorClient } = require('trasor-sdk-node');
*
* const client = new TrasorClient('trasor_live_your_api_key');
*
* // Log a simple event
* await client.logEvent({
* agentName: 'data_processor',
* action: 'process_customer_data',
* status: 'success'
* });
*/
class TrasorClient {
/**
* Create a new Trasor.io client instance
*
* @param {string} apiKey - Your Trasor.io API key (format: trasor_live_*)
* @param {Object} options - Configuration options
* @param {string} [options.baseUrl='https://api.trasor.io'] - Base URL for the API
* @param {number} [options.timeout=30000] - Request timeout in milliseconds
* @param {boolean} [options.debug=false] - Enable debug logging
*
* @throws {ValidationError} If API key is missing or invalid format
*
* @example
* const client = new TrasorClient('trasor_live_abc123...', {
* baseUrl: 'https://api.trasor.io',
* timeout: 30000,
* debug: process.env.NODE_ENV === 'development'
* });
*/
constructor(apiKey, options = {}) {
if (!apiKey) {
throw new ValidationError('API key is required');
}
if (typeof apiKey !== 'string' || !apiKey.startsWith('trasor_')) {
throw new ValidationError('Invalid API key format. Must start with "trasor_"');
}
this.apiKey = apiKey;
this.baseUrl = options.baseUrl || 'https://api.trasor.io';
this.timeout = options.timeout || 30000;
this.debug = options.debug || process.env.VERIFIK_DEBUG === 'true';
// Ensure baseUrl doesn't end with slash
this.baseUrl = this.baseUrl.replace(/\/$/, '');
// Validate baseUrl format
try {
new URL(this.baseUrl);
} catch (error) {
throw new ValidationError('Invalid baseUrl format');
}
this._log('TrasorClient initialized', {
baseUrl: this.baseUrl,
timeout: this.timeout,
debug: this.debug
});
}
/**
* Create a new audit log entry
*
* Records an audit log event with the specified parameters. All logs are
* automatically hash-chained and verified for integrity.
*
* @param {Object} params - Log event parameters
* @param {string} params.agentName - Name/identifier of the AI agent or service
* @param {string} params.action - The action that was performed
* @param {Object} [params.inputs] - Input data/parameters for the action
* @param {Object} [params.outputs] - Output data/results from the action
* @param {Object} [params.metadata] - Additional metadata about the event
* @param {string} [params.workflowId] - Workflow or session identifier
* @param {string} [params.status] - Status of the action (e.g., "success", "error", "pending")
*
* @returns {Promise<Object>} The created audit log entry with ID, hash, and verification details
*
* @throws {ValidationError} If required parameters are missing or invalid
* @throws {AuthenticationError} If API key is invalid or unauthorized
* @throws {APIError} If the API returns an error response
* @throws {TrasorError} For other SDK-related errors
*
* @example
* const response = await client.logEvent({
* agentName: 'email_agent',
* action: 'send_notification',
* inputs: { recipient: 'user@example.com', template: 'welcome' },
* outputs: { messageId: 'msg_123', status: 'sent' },
* metadata: { campaignId: 'camp_456' },
* workflowId: 'workflow_789',
* status: 'success'
* });
*
* console.log(`Log created with ID: ${response.id}`);
*/
async logEvent(params) {
// Validate required parameters
if (!params || typeof params !== 'object') {
throw new ValidationError('Parameters must be an object');
}
const { agentName, action, inputs, outputs, metadata, workflowId, status } = params;
if (!agentName || typeof agentName !== 'string') {
throw new ValidationError('agentName must be a non-empty string');
}
if (!action || typeof action !== 'string') {
throw new ValidationError('action must be a non-empty string');
}
// Validate optional parameters
if (inputs !== undefined && (typeof inputs !== 'object' || inputs === null)) {
throw new ValidationError('inputs must be an object');
}
if (outputs !== undefined && (typeof outputs !== 'object' || outputs === null)) {
throw new ValidationError('outputs must be an object');
}
if (metadata !== undefined && (typeof metadata !== 'object' || metadata === null)) {
throw new ValidationError('metadata must be an object');
}
if (workflowId !== undefined && typeof workflowId !== 'string') {
throw new ValidationError('workflowId must be a string');
}
if (status !== undefined && typeof status !== 'string') {
throw new ValidationError('status must be a string');
}
// Prepare payload
const payload = {
agentId: agentName,
action: action
};
// Add optional parameters if provided
if (inputs !== undefined) payload.inputs = inputs;
if (outputs !== undefined) payload.outputs = outputs;
if (metadata !== undefined) payload.metadata = metadata;
if (workflowId !== undefined) payload.workflowId = workflowId;
if (status !== undefined) payload.status = status;
this._log('Logging event', payload);
// Make API request
return await this._makeRequest('POST', '/v1/logs', payload);
}
/**
* Retrieve audit logs with pagination
*
* @param {Object} [options] - Query options
* @param {number} [options.limit=50] - Number of logs to return (max 100)
* @param {number} [options.offset=0] - Number of logs to skip
* @param {string} [options.workflowId] - Filter by workflow ID
*
* @returns {Promise<Object>} Paginated list of audit logs
*
* @throws {ValidationError} If parameters are invalid
* @throws {AuthenticationError} If API key is invalid or unauthorized
* @throws {APIError} If the API returns an error response
*
* @example
* const logs = await client.getLogs({ limit: 20, offset: 0 });
* console.log(`Retrieved ${logs.logs.length} logs`);
*/
async getLogs(options = {}) {
const { limit = 50, offset = 0, workflowId } = options;
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
throw new ValidationError('limit must be an integer between 1 and 100');
}
if (!Number.isInteger(offset) || offset < 0) {
throw new ValidationError('offset must be a non-negative integer');
}
const params = new URLSearchParams({
limit: limit.toString(),
offset: offset.toString()
});
if (workflowId) {
params.append('workflowId', workflowId);
}
this._log('Getting logs', { limit, offset, workflowId });
return await this._makeRequest('GET', `/v1/logs?${params}`);
}
/**
* Verify the integrity of the audit log chain
*
* @returns {Promise<Object>} Verification results including status and any integrity issues
*
* @throws {AuthenticationError} If API key is invalid or unauthorized
* @throws {APIError} If the API returns an error response
*
* @example
* const verification = await client.verifyChain();
* console.log(`Chain integrity: ${verification.isValid}`);
*/
async verifyChain() {
this._log('Verifying chain integrity');
return await this._makeRequest('GET', '/v1/verify');
}
/**
* Get account statistics and metrics
*
* @returns {Promise<Object>} Account statistics including log count and usage metrics
*
* @throws {AuthenticationError} If API key is invalid or unauthorized
* @throws {APIError} If the API returns an error response
*
* @example
* const stats = await client.getStats();
* console.log(`Total logs: ${stats.totalLogs}`);
*/
async getStats() {
this._log('Getting account stats');
return await this._makeRequest('GET', '/v1/stats');
}
/**
* Make an authenticated request to the Trasor.io API
*
* @private
* @param {string} method - HTTP method (GET, POST, etc.)
* @param {string} endpoint - API endpoint path
* @param {Object} [data] - Request body data
* @returns {Promise<Object>} Parsed JSON response
*
* @throws {AuthenticationError} If API key is invalid or unauthorized
* @throws {APIError} If the API returns an error response
* @throws {TrasorError} For connection or other errors
*/
async _makeRequest(method, endpoint, data = null) {
const url = `${this.baseUrl}${endpoint}`;
const headers = {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'User-Agent': 'trasor-node-sdk/2.0.0'
};
const options = {
method,
headers,
timeout: this.timeout
};
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
options.body = JSON.stringify(data);
}
this._log(`Making ${method} request to ${url}`, { headers: { ...headers, Authorization: '[REDACTED]' } });
let response;
try {
// Use native fetch (Node.js 18+) or polyfill for older versions
const fetchFn = globalThis.fetch || require('node-fetch');
response = await fetchFn(url, options);
} catch (error) {
if (error.name === 'AbortError' || error.message.includes('timeout')) {
throw new TrasorError('Request timed out');
}
throw new TrasorError(`Network error: ${error.message}`);
}
this._log(`Response status: ${response.status}`);
// Handle different response status codes
if (response.status === 401) {
throw new AuthenticationError('Invalid API key or unauthorized access');
} else if (response.status === 400) {
let errorMessage = 'Bad request';
try {
const errorData = await response.json();
errorMessage = errorData.message || errorMessage;
} catch (e) {
// Ignore JSON parsing errors
}
throw new ValidationError(`Validation error: ${errorMessage}`);
} else if (response.status === 429) {
throw new APIError('Rate limit exceeded. Please try again later.', 429);
} else if (response.status >= 500) {
throw new APIError(`Server error: ${response.status}`, response.status);
} else if (!response.ok) {
throw new APIError(`API request failed: ${response.status}`, response.status);
}
// Parse JSON response
let responseData;
try {
responseData = await response.json();
} catch (error) {
throw new APIError('Invalid JSON response from API');
}
this._log('Request successful', responseData);
return responseData;
}
/**
* Internal logging method
*
* @private
* @param {string} message - Log message
* @param {Object} [data] - Additional data to log
*/
_log(message, data = null) {
if (this.debug) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [TrasorClient] ${message}`);
if (data) {
console.log(`[${timestamp}] [TrasorClient] Data:`, JSON.stringify(data, null, 2));
}
}
}
}
// Export classes and functions
module.exports = {
TrasorClient,
TrasorError,
AuthenticationError,
ValidationError,
APIError
};
// For ES6 modules compatibility
module.exports.default = TrasorClient;