@99xio/xians-sdk-typescript
Version:
A lightweight, framework-agnostic SDK for Agent WebSocket/SignalR communication
1,286 lines (1,278 loc) • 110 kB
JavaScript
'use strict';
var signalR = require('@microsoft/signalr');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var signalR__namespace = /*#__PURE__*/_interopNamespaceDefault(signalR);
/**
* Shared types for XiansAi SDK
* Used by both SocketSDK and RestSDK to avoid duplication
*/
/**
* Message type enum - shared across all SDKs
*/
exports.MessageType = void 0;
(function (MessageType) {
MessageType["Chat"] = "Chat";
MessageType["Data"] = "Data";
MessageType["Handoff"] = "Handoff";
})(exports.MessageType || (exports.MessageType = {}));
/**
* Unified connection state enum used across all real-time SDKs
*/
exports.ConnectionState = void 0;
(function (ConnectionState) {
ConnectionState["Disconnected"] = "Disconnected";
ConnectionState["Connecting"] = "Connecting";
ConnectionState["Connected"] = "Connected";
ConnectionState["Disconnecting"] = "Disconnecting";
ConnectionState["Reconnecting"] = "Reconnecting";
ConnectionState["Failed"] = "Failed";
})(exports.ConnectionState || (exports.ConnectionState = {}));
/**
* Standard configuration defaults shared across SDKs
*/
const SDK_DEFAULTS = {
connectionTimeout: 30000, // 30 seconds
reconnectDelay: 5000, // 5 seconds
maxReconnectAttempts: 5, // 5 attempts
autoReconnect: true, // Enable by default
requestTimeout: 30000 // For HTTP requests
};
/**
* Chat Socket SDK for real-time chat communication using SignalR
*/
/**
* Chat Socket SDK class for real-time chat communication using SignalR
*
* Supports consistent authentication methods across all SDKs:
* - API key authentication: Uses 'apikey' query parameter (recommended for consistent auth)
* - JWT authentication: Uses 'access_token' query parameter + Authorization header via SignalR accessTokenFactory
* - Combined authentication: Both API key and JWT can be provided simultaneously (JWT takes precedence)
*
* The SDK automatically handles reconnection and provides event-driven communication.
*/
class SocketSDK {
constructor(options) {
this.connection = null;
this.connectionState = exports.ConnectionState.Disconnected;
this.reconnectAttempts = 0;
this.isDisposed = false;
this.eventHandlers = {};
// Validate required fields
if (!options.tenantId) {
throw new Error('tenantId is required');
}
if (!options.apiKey && !options.getJwtToken && !options.jwtToken) {
throw new Error('Either apiKey, jwtToken, or getJwtToken callback is required');
}
if (!options.serverUrl) {
throw new Error('serverUrl is required');
}
this.options = {
logger: (level, message, data) => console.log(`[${level.toUpperCase()}] ${message}`, data || ''),
autoReconnect: SDK_DEFAULTS.autoReconnect,
reconnectDelay: SDK_DEFAULTS.reconnectDelay,
maxReconnectAttempts: SDK_DEFAULTS.maxReconnectAttempts,
connectionTimeout: SDK_DEFAULTS.connectionTimeout,
...options
};
this.eventHandlers = options.eventHandlers || {};
// Note: setupConnection is now called in connect() method since it's async
}
/**
* Sets up the SignalR connection with authentication
*/
async setupConnection() {
// Build the complete URL with query parameters
const hubUrl = await this.buildConnectionUrl();
const connectionOptions = {
transport: signalR__namespace.HttpTransportType.WebSockets,
};
// Only use accessTokenFactory for JWT authentication
if (this.options.getJwtToken || this.options.jwtToken) {
connectionOptions.accessTokenFactory = async () => {
return await this.getJwtToken();
};
}
const connectionBuilder = new signalR__namespace.HubConnectionBuilder()
.withUrl(hubUrl, connectionOptions)
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: () => this.options.reconnectDelay
})
.configureLogging(signalR__namespace.LogLevel.Information);
this.connection = connectionBuilder.build();
// Debug: Log all SignalR events received (for debugging purposes)
if (this.options.logger) {
const originalOn = this.connection.on.bind(this.connection);
this.connection.on = (methodName, callback) => {
return originalOn(methodName, (...args) => {
this.options.logger('debug', `🔍 SignalR event: ${methodName}`, {
argsCount: args.length,
firstArg: args.length > 0 ? args[0] : null
});
return callback(...args);
});
};
}
this.setupEventHandlers();
}
/**
* Builds the complete connection URL with query parameters
* Supports consistent authentication methods: apikey for API keys, access_token for JWT
*/
async buildConnectionUrl() {
const url = new URL(`${this.options.serverUrl}/ws/chat`);
// Always include tenantId
url.searchParams.set('tenantId', this.options.tenantId);
// Add authentication parameters based on the method used
// Use consistent parameter names with other SDKs and server expectations
if (this.options.apiKey) {
// For API key authentication: use apikey parameter (consistent with other SDKs)
url.searchParams.set('apikey', this.options.apiKey);
}
if (this.options.getJwtToken || this.options.jwtToken) {
// JWT tokens are handled via accessTokenFactory in SignalR connection
// Do not add JWT tokens to query parameters for security reasons
if (this.options.logger) {
this.options.logger('debug', 'JWT authentication will be handled via accessTokenFactory');
}
}
const finalUrl = url.toString();
// Debug logging to verify URL construction
if (this.options.logger) {
this.options.logger('debug', 'Built connection URL', {
url: finalUrl.split('?')[0], // Log URL without sensitive tokens
tenantId: this.options.tenantId,
primaryAuthMethod: this.getAuthType(),
hasApiKey: !!this.options.apiKey,
hasJwtToken: !!(this.options.jwtToken || this.options.getJwtToken),
usingBothMethods: !!this.options.apiKey && !!(this.options.jwtToken || this.options.getJwtToken)
});
}
return finalUrl;
}
/**
* Get JWT token for SignalR accessTokenFactory
* @returns JWT token
*/
async getJwtToken() {
if (this.options.getJwtToken) {
try {
return await this.options.getJwtToken();
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Failed to get JWT token', error);
}
throw new Error(`Failed to get JWT token: ${error}`);
}
}
else if (this.options.jwtToken) {
return this.options.jwtToken;
}
else {
throw new Error('No JWT token available');
}
}
/**
* Gets the authentication token based on the configured method
* When both API key and JWT methods are provided, JWT takes precedence
*/
async getAuthToken() {
// Prioritize JWT methods when available
if (this.options.getJwtToken) {
try {
return await this.options.getJwtToken();
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Failed to get JWT token', error);
}
throw new Error(`Failed to get JWT token: ${error}`);
}
}
else if (this.options.jwtToken) {
return this.options.jwtToken;
}
else if (this.options.apiKey) {
return this.options.apiKey;
}
else {
throw new Error('No authentication method available');
}
}
/**
* Sets up event handlers for the SignalR connection
*/
setupEventHandlers() {
if (!this.connection)
return;
// Connection state events
this.connection.onclose((error) => {
this.updateConnectionState(exports.ConnectionState.Disconnected);
if (this.options.logger) {
this.options.logger('info', 'Connection closed', error);
}
if (this.options.autoReconnect && !this.isDisposed && error) {
this.handleReconnection();
}
});
this.connection.onreconnecting((error) => {
this.updateConnectionState(exports.ConnectionState.Reconnecting);
if (this.options.logger) {
this.options.logger('info', 'Attempting to reconnect', error);
}
this.eventHandlers.onReconnecting?.(error?.message);
});
this.connection.onreconnected((connectionId) => {
this.updateConnectionState(exports.ConnectionState.Connected);
this.reconnectAttempts = 0;
if (this.options.logger) {
this.options.logger('info', 'Reconnected successfully', connectionId);
}
this.eventHandlers.onReconnected?.(connectionId);
});
// Chat communication events matching ChatHub SignalR methods
// SignalR method names are case-sensitive and usually lowercase
this.connection.on('ThreadHistory', (history) => {
const safeHistory = history || [];
if (this.options.logger) {
this.options.logger('debug', 'Received thread history (Pascal)', { count: safeHistory.length });
}
this.eventHandlers.onThreadHistory?.(safeHistory);
});
this.connection.on('threadhistory', (history) => {
const safeHistory = history || [];
if (this.options.logger) {
this.options.logger('debug', 'Received thread history (lowercase)', { count: safeHistory.length });
}
this.eventHandlers.onThreadHistory?.(safeHistory);
});
this.connection.on('InboundProcessed', (threadId) => {
if (this.options.logger) {
this.options.logger('debug', 'Inbound message processed (Pascal)', { threadId });
}
this.eventHandlers.onInboundProcessed?.(threadId);
});
this.connection.on('inboundprocessed', (threadId) => {
if (this.options.logger) {
this.options.logger('debug', 'Inbound message processed (lowercase)', { threadId });
}
this.eventHandlers.onInboundProcessed?.(threadId);
});
this.connection.on('ReceiveChat', (message) => {
if (this.options.logger) {
this.options.logger('info', '🔔 [NEW-Pascal] Received agent chat message via ReceiveChat', {
messageId: message.id,
direction: message.direction,
text: message.text ? message.text.substring(0, 100) + '...' : 'No text',
messageType: message.messageType
});
}
// Check if this is actually a handoff message sent through chat channel
const isHandoffMessage = message.messageType === 'Handoff';
if (isHandoffMessage) {
if (this.options.logger) {
this.options.logger('info', '🔄 [ROUTING] Detected handoff message in chat channel, routing to onReceiveHandoff', {
messageId: message.id,
messageType: message.messageType,
textPrefix: message.text ? message.text.substring(0, 20) : 'No text'
});
}
// Route to handoff handler instead
this.eventHandlers.onReceiveHandoff?.(message);
}
else {
// Handle as regular chat message
this.eventHandlers.onReceiveChat?.(message);
}
});
this.connection.on('ReceiveData', (message) => {
if (this.options.logger) {
this.options.logger('info', '🔔 [NEW-Pascal] Received agent data message via ReceiveData', {
messageId: message.id,
direction: message.direction,
hasData: !!message.data,
messageType: message.messageType
});
}
// Check if this is actually a handoff message sent through data channel
const isHandoffMessage = message.messageType === 'Handoff';
if (isHandoffMessage) {
if (this.options.logger) {
this.options.logger('info', '🔄 [ROUTING] Detected handoff message in data channel, routing to onReceiveHandoff', {
messageId: message.id,
messageType: message.messageType,
textPrefix: message.text ? message.text.substring(0, 20) : 'No text'
});
}
// Route to handoff handler instead
this.eventHandlers.onReceiveHandoff?.(message);
}
else {
// Handle as regular data message
this.eventHandlers.onReceiveData?.(message);
}
});
this.connection.on('ReceiveHandoff', (message) => {
if (this.options.logger) {
this.options.logger('info', '🔔 [NEW-Pascal] Received agent handoff message via ReceiveHandoff', {
messageId: message.id,
direction: message.direction,
hasData: !!message.data
});
}
this.eventHandlers.onReceiveHandoff?.(message);
});
this.connection.on('Error', (error) => {
if (this.options.logger) {
this.options.logger('error', 'Received error', error);
}
this.eventHandlers.onError?.(error);
});
this.connection.on('ConnectionError', (error) => {
if (this.options.logger) {
this.options.logger('error', 'Connection error', error);
}
this.eventHandlers.onConnectionError?.(error);
});
// Legacy method support - register old SignalR methods that server might still call
// Route them to the appropriate modern handlers
this.connection.on('ReceiveMessage', (message) => {
// ignore
});
this.connection.on('receivemessage', (message) => {
// ignore
});
this.connection.on('ReceiveMetadata', (message) => {
// ignore
});
this.connection.on('receivemetadata', (message) => {
// ignore
});
}
/**
* Updates the connection state and notifies handlers
*/
updateConnectionState(newState) {
const oldState = this.connectionState;
this.connectionState = newState;
this.eventHandlers.onConnectionStateChanged?.(oldState, newState);
}
/**
* Handles reconnection logic with exponential backoff
*/
async handleReconnection() {
if (this.reconnectAttempts >= this.options.maxReconnectAttempts || this.isDisposed) {
if (this.options.logger) {
this.options.logger('error', 'Max reconnection attempts reached');
}
return;
}
this.reconnectAttempts++;
const delay = this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
if (this.options.logger) {
this.options.logger('info', `Reconnection attempt ${this.reconnectAttempts} in ${delay}ms`);
}
setTimeout(async () => {
try {
await this.connect();
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Reconnection attempt failed', error);
}
this.handleReconnection();
}
}, delay);
}
/**
* Connects to the SignalR hub
*/
async connect() {
if (this.isDisposed) {
throw new Error('Connection has been disposed');
}
// Setup connection if not already done
if (!this.connection) {
if (this.options.logger) {
this.options.logger('debug', 'Setting up new connection', { tenantId: this.options.tenantId, authType: this.getAuthType() });
}
await this.setupConnection();
}
if (!this.connection) {
throw new Error('Failed to initialize connection');
}
if (this.connectionState === exports.ConnectionState.Connected ||
this.connectionState === exports.ConnectionState.Connecting) {
return;
}
this.updateConnectionState(exports.ConnectionState.Connecting);
if (this.options.logger) {
this.options.logger('debug', 'Starting SignalR connection', { connectionState: this.connectionState });
}
try {
await this.connection.start();
this.updateConnectionState(exports.ConnectionState.Connected);
this.reconnectAttempts = 0;
if (this.options.logger) {
this.options.logger('info', 'Connected to bot hub successfully');
}
}
catch (error) {
this.updateConnectionState(exports.ConnectionState.Disconnected);
if (this.options.logger) {
this.options.logger('error', 'Failed to connect to bot hub', error);
}
throw error;
}
}
/**
* Disconnects from the SignalR hub
*/
async disconnect() {
if (!this.connection || this.connectionState === exports.ConnectionState.Disconnected) {
return;
}
this.updateConnectionState(exports.ConnectionState.Disconnecting);
try {
await this.connection.stop();
this.updateConnectionState(exports.ConnectionState.Disconnected);
if (this.options.logger) {
this.options.logger('info', 'Disconnected from bot hub');
}
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Error during disconnection', error);
}
throw error;
}
}
/**
* Sends an inbound message to the chat system
*/
async sendInboundMessage(request, messageType) {
if (!this.connection || this.connectionState !== exports.ConnectionState.Connected) {
throw new Error('Connection is not established');
}
try {
// Ensure participantId is set
if (!request.participantId) {
throw new Error('participantId is required');
}
// Add JWT token to authorization field if available
const messageRequest = { ...request };
if (this.options.jwtToken || this.options.getJwtToken) {
try {
const jwtToken = await this.getJwtToken();
messageRequest.authorization = jwtToken;
if (this.options.logger) {
this.options.logger('debug', 'Added JWT token to message authorization field');
}
}
catch (error) {
if (this.options.logger) {
this.options.logger('warn', 'Failed to get JWT token for message authorization field', error);
}
// Continue without JWT in message (connection-level auth still applies)
}
}
if (this.options.logger) {
this.options.logger('debug', 'Sending inbound message', {
messageType,
hasAuthorization: !!messageRequest.authorization,
participantId: request.participantId,
requestId: request.requestId
});
}
await this.connection.invoke('SendInboundMessage', messageRequest, messageType);
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Failed to send inbound message', error);
}
throw error;
}
}
/**
* Gets thread history for a workflow and participant
*/
async getThreadHistory(workflow, participantId, page = 0, pageSize = 50, scope) {
if (!this.connection || this.connectionState !== exports.ConnectionState.Connected) {
throw new Error('Connection is not established');
}
try {
if (this.options.logger) {
this.options.logger('debug', 'Requesting thread history', { workflow, participantId, page, pageSize, scope });
}
if (scope) {
await this.connection.invoke('GetScopedThreadHistory', workflow, participantId, page, pageSize, scope);
}
else {
await this.connection.invoke('GetThreadHistory', workflow, participantId, page, pageSize);
}
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Failed to get thread history', error);
}
throw error;
}
}
/**
* Delete thread for a workflow and participant
*/
async deleteThread(workflow, participantId) {
if (!this.connection || this.connectionState !== exports.ConnectionState.Connected) {
throw new Error('Connection is not established');
}
try {
if (this.options.logger) {
this.options.logger('debug', 'Deleting thread', { workflow, participantId, tenantId: this.options.tenantId });
}
await this.connection.invoke('DeleteThread', workflow, participantId);
if (this.options.logger) {
this.options.logger('info', 'Thread deleted successfully', { workflow, participantId });
}
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Failed to delete thread', error);
}
throw error;
}
}
/**
* Subscribes to agent notifications for a workflow
*/
async subscribeToAgent(workflow, participantId) {
if (!this.connection || this.connectionState !== exports.ConnectionState.Connected) {
throw new Error('Connection is not established');
}
try {
// Log expected group name for debugging
const expectedWorkflowId = workflow.startsWith(this.options.tenantId + ':') ? workflow : `${this.options.tenantId}:${workflow}`;
const expectedGroupName = expectedWorkflowId + participantId + this.options.tenantId;
if (this.options.logger) {
this.options.logger('info', '🔗 Subscribing to agent group', {
workflow,
participantId,
tenantId: this.options.tenantId,
expectedWorkflowId,
expectedGroupName
});
}
await this.connection.invoke('SubscribeToAgent', workflow, participantId, this.options.tenantId);
if (this.options.logger) {
this.options.logger('info', '✅ Successfully subscribed to agent group');
}
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', '❌ Failed to subscribe to agent', error);
}
throw error;
}
}
/**
* Unsubscribes from agent notifications for a workflow
*/
async unsubscribeFromAgent(workflow, participantId) {
if (!this.connection || this.connectionState !== exports.ConnectionState.Connected) {
throw new Error('Connection is not established');
}
try {
if (this.options.logger) {
this.options.logger('debug', 'Unsubscribing from agent', { workflow, participantId, tenantId: this.options.tenantId });
}
await this.connection.invoke('UnsubscribeFromAgent', workflow, participantId, this.options.tenantId);
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Failed to unsubscribe from agent', error);
}
throw error;
}
}
/**
* Updates event handlers
*/
updateEventHandlers(handlers) {
this.eventHandlers = { ...this.eventHandlers, ...handlers };
}
/**
* Gets the current connection state
*/
getConnectionState() {
return this.connectionState;
}
/**
* Checks if the connection is established
*/
isConnected() {
return this.connectionState === exports.ConnectionState.Connected;
}
/**
* Gets the tenant ID
*/
getTenantId() {
return this.options.tenantId;
}
/**
* Gets the authentication type being used
* When both API key and JWT methods are provided, JWT takes precedence
*/
getAuthType() {
if (this.options.getJwtToken)
return 'jwtCallback';
if (this.options.jwtToken)
return 'jwtToken';
return 'apiKey';
}
/**
* Updates the API key (switches to API key authentication)
*/
updateApiKey(apiKey) {
if (!apiKey) {
throw new Error('apiKey cannot be empty');
}
this.options.apiKey = apiKey;
this.options.jwtToken = undefined;
this.options.getJwtToken = undefined;
// Recreate connection with new auth
this.recreateConnection();
}
/**
* Updates the JWT token (switches to JWT token authentication)
*/
updateJwtToken(jwtToken) {
if (!jwtToken) {
throw new Error('jwtToken cannot be empty');
}
this.options.jwtToken = jwtToken;
this.options.apiKey = undefined;
this.options.getJwtToken = undefined;
// Recreate connection with new auth
this.recreateConnection();
}
/**
* Updates the JWT token callback (switches to JWT callback authentication)
*/
updateJwtTokenCallback(getJwtToken) {
if (!getJwtToken) {
throw new Error('getJwtToken callback cannot be null');
}
this.options.getJwtToken = getJwtToken;
this.options.apiKey = undefined;
this.options.jwtToken = undefined;
// Recreate connection with new auth
this.recreateConnection();
}
/**
* Recreates the connection with new authentication
*/
async recreateConnection() {
// Don't recreate if SDK has been disposed
if (this.isDisposed) {
return;
}
const wasConnected = this.isConnected();
if (wasConnected) {
await this.disconnect();
}
// Clear the current connection
this.connection = null;
if (wasConnected && !this.isDisposed) {
await this.connect();
}
}
/**
* Disposes the SDK and cleans up resources
*/
async dispose() {
this.isDisposed = true;
if (this.connection) {
await this.disconnect();
this.connection = null;
}
this.eventHandlers = {};
if (this.options.logger) {
this.options.logger('info', 'BotSocketSDK disposed');
}
}
}
/**
* Rest SDK for HTTP-based chat communication with UserApi endpoints
*/
/**
* Rest SDK class for HTTP-based chat communication
*/
class RestSDK {
constructor(options) {
this.isDisposed = false;
// Validate required fields
if (!options.tenantId) {
throw new Error('tenantId is required');
}
if (!options.apiKey && !options.getJwtToken && !options.jwtToken) {
throw new Error('Either apiKey, jwtToken, or getJwtToken callback is required');
}
if (!options.serverUrl) {
throw new Error('serverUrl is required');
}
this.options = {
logger: (level, message, data) => console.log(`[${level.toUpperCase()}] ${message}`, data || ''),
requestTimeout: SDK_DEFAULTS.requestTimeout,
defaultConverseTimeout: 60,
maxConverseTimeout: 300,
...options
};
}
/**
* Get JWT token for Authorization header
* @returns JWT token
*/
async getJwtToken() {
if (this.options.getJwtToken) {
try {
return await this.options.getJwtToken();
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Failed to get JWT token', error);
}
throw new Error(`Failed to get JWT token: ${error}`);
}
}
else if (this.options.jwtToken) {
return this.options.jwtToken;
}
else {
throw new Error('No JWT token available');
}
}
/**
* Builds query parameters for requests
*/
buildQueryParams(params) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.set(key, value.toString());
}
});
return searchParams.toString();
}
/**
* Makes an HTTP request with authentication and error handling
* Supports multiple authentication methods: apikey query param, Authorization header, or access_token query param fallback
* Can use both API key and JWT simultaneously when both are provided
*/
async makeRequest(endpoint, method = 'GET', queryParams, body) {
if (this.isDisposed) {
throw new Error('SDK has been disposed');
}
try {
const url = new URL(`${this.options.serverUrl}${endpoint}`);
// Always add tenantId to query params as required by the server
const finalQueryParams = {
...queryParams,
tenantId: this.options.tenantId
};
// Add API key authentication if available
if (this.options.apiKey) {
// For API key authentication: add apikey to query params
finalQueryParams.apikey = this.options.apiKey;
}
if (finalQueryParams) {
const params = this.buildQueryParams(finalQueryParams);
if (params) {
url.search = params;
}
}
const headers = {
'Content-Type': 'application/json',
};
// Add JWT authentication if available
if (this.options.jwtToken || this.options.getJwtToken) {
const jwtToken = await this.getJwtToken();
headers['Authorization'] = `Bearer ${jwtToken}`;
if (this.options.logger) {
this.options.logger('debug', 'Using Authorization header for JWT authentication');
}
}
const requestOptions = {
method,
headers,
signal: AbortSignal.timeout(this.options.requestTimeout),
};
if (body && method === 'POST') {
requestOptions.body = JSON.stringify(body);
}
if (this.options.logger) {
this.options.logger('debug', `Making ${method} request to ${url.toString()}`, {
hasBody: !!body,
hasApiKey: !!this.options.apiKey,
hasJwtToken: !!(this.options.jwtToken || this.options.getJwtToken),
usingBothMethods: !!this.options.apiKey && !!(this.options.jwtToken || this.options.getJwtToken),
primaryAuthMethod: this.getAuthType(),
tenantId: this.options.tenantId
});
}
const response = await fetch(url.toString(), requestOptions);
let data;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
data = await response.json();
}
else {
const text = await response.text();
if (text) {
data = text;
}
}
if (!response.ok) {
const error = typeof data === 'object' && data && 'message' in data
? data.message
: `HTTP ${response.status}: ${response.statusText}`;
if (this.options.logger) {
this.options.logger('error', `Request failed: ${error}`, {
status: response.status,
statusText: response.statusText,
data
});
}
return {
success: false,
error,
statusCode: response.status,
data
};
}
if (this.options.logger) {
this.options.logger('debug', `Request successful`, { status: response.status });
}
return {
success: true,
data,
statusCode: response.status
};
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Request failed with exception', error);
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
success: false,
error: errorMessage
};
}
}
/**
* Sends a message to a workflow without waiting for response
*/
async send(request) {
if (!request.workflow) {
throw new Error('workflow is required');
}
if (!request.type) {
throw new Error('type is required');
}
if (!request.participantId) {
throw new Error('participantId is required');
}
// Add JWT token to authorization field if available
const messageRequest = { ...request };
if (this.options.jwtToken || this.options.getJwtToken) {
try {
const jwtToken = await this.getJwtToken();
messageRequest.authorization = jwtToken;
if (this.options.logger) {
this.options.logger('debug', 'Added JWT token to message authorization field');
}
}
catch (error) {
if (this.options.logger) {
this.options.logger('warn', 'Failed to get JWT token for message authorization field', error);
}
// Continue without JWT in message (header auth still applies)
}
}
const queryParams = {
workflow: messageRequest.workflow,
type: messageRequest.type,
participantId: messageRequest.participantId,
requestId: messageRequest.requestId,
text: messageRequest.text
};
if (this.options.logger) {
this.options.logger('info', 'Sending message to workflow', {
workflow: messageRequest.workflow,
type: messageRequest.type,
participantId: messageRequest.participantId,
hasText: !!messageRequest.text,
hasData: !!messageRequest.data,
hasAuthorization: !!messageRequest.authorization
});
}
return await this.makeRequest('/api/user/rest/send', 'POST', queryParams, messageRequest);
}
/**
* Sends a message to a workflow and waits synchronously for response
*/
async converse(request) {
if (!request.workflow) {
throw new Error('workflow is required');
}
if (!request.type) {
throw new Error('type is required');
}
if (!request.participantId) {
throw new Error('participantId is required');
}
// Add JWT token to authorization field if available
const messageRequest = { ...request };
if (this.options.jwtToken || this.options.getJwtToken) {
try {
const jwtToken = await this.getJwtToken();
messageRequest.authorization = jwtToken;
if (this.options.logger) {
this.options.logger('debug', 'Added JWT token to message authorization field');
}
}
catch (error) {
if (this.options.logger) {
this.options.logger('warn', 'Failed to get JWT token for message authorization field', error);
}
// Continue without JWT in message (header auth still applies)
}
}
const timeoutSeconds = messageRequest.timeoutSeconds;
const queryParams = {
workflow: messageRequest.workflow,
type: messageRequest.type,
participantId: messageRequest.participantId,
timeoutSeconds,
requestId: messageRequest.requestId,
text: messageRequest.text
};
if (this.options.logger) {
this.options.logger('info', 'Starting conversation with workflow', {
workflow: messageRequest.workflow,
type: messageRequest.type,
participantId: messageRequest.participantId,
timeoutSeconds,
hasText: !!messageRequest.text,
hasData: !!messageRequest.data,
hasAuthorization: !!messageRequest.authorization
});
}
try {
const result = await this.makeRequest('/api/user/rest/converse', 'POST', queryParams, messageRequest);
if (this.options.logger) {
this.options.logger('info', 'Conversation completed', {
success: result.success,
messageCount: result.data?.length || 0
});
}
return result;
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Conversation failed', error);
}
throw error;
}
}
/**
* Gets conversation history for a workflow and participant
*/
async getHistory(request) {
if (!request.workflow) {
throw new Error('workflow is required');
}
if (!request.participantId) {
throw new Error('participantId is required');
}
const queryParams = {
workflow: request.workflow,
participantId: request.participantId,
page: request.page || 1,
pageSize: request.pageSize || 50,
scope: request.scope
};
if (this.options.logger) {
this.options.logger('debug', 'Requesting conversation history', {
workflow: request.workflow,
participantId: request.participantId,
page: queryParams.page,
pageSize: queryParams.pageSize,
scope: request.scope
});
}
return await this.makeRequest('/api/user/rest/history', 'GET', queryParams);
}
/**
* Gets the tenant ID
*/
getTenantId() {
return this.options.tenantId;
}
/**
* Gets the authentication type being used
* When both API key and JWT methods are provided, JWT takes precedence
*/
getAuthType() {
if (this.options.getJwtToken)
return 'jwtCallback';
if (this.options.jwtToken)
return 'jwtToken';
return 'apiKey';
}
/**
* Updates the API key (switches to API key authentication)
*/
updateApiKey(apiKey) {
if (!apiKey) {
throw new Error('apiKey cannot be empty');
}
this.options.apiKey = apiKey;
this.options.jwtToken = undefined;
this.options.getJwtToken = undefined;
}
/**
* Updates the JWT token (switches to JWT token authentication)
*/
updateJwtToken(jwtToken) {
if (!jwtToken) {
throw new Error('jwtToken cannot be empty');
}
this.options.jwtToken = jwtToken;
this.options.apiKey = undefined;
this.options.getJwtToken = undefined;
}
/**
* Updates the JWT token callback (switches to JWT callback authentication)
*/
updateJwtTokenCallback(getJwtToken) {
if (!getJwtToken) {
throw new Error('getJwtToken callback cannot be null');
}
this.options.getJwtToken = getJwtToken;
this.options.apiKey = undefined;
this.options.jwtToken = undefined;
}
/**
* Disposes the SDK and cleans up resources
*/
dispose() {
this.isDisposed = true;
if (this.options.logger) {
this.options.logger('info', 'RestSDK disposed');
}
}
}
/**
* SSE SDK for Server-Sent Events based real-time communication with UserApi endpoints
*/
// Import EventSource polyfill for Node.js environments
let EventSourceImpl;
if (typeof globalThis !== 'undefined' && globalThis.EventSource) {
// Use native EventSource (browser environment)
EventSourceImpl = globalThis.EventSource;
}
else if (typeof window !== 'undefined' && window.EventSource) {
// Use native EventSource (browser environment with window)
EventSourceImpl = window.EventSource;
}
else {
// Fallback to polyfill for Node.js environment
try {
const EventSourcePolyfill = require('eventsource');
EventSourceImpl = EventSourcePolyfill.EventSource;
}
catch (e) {
throw new Error('EventSource is not available and polyfill could not be loaded. Install "eventsource" package for Node.js support.');
}
}
/**
* SSE SDK class for real-time Server-Sent Events communication
*
* Supports multiple authentication methods:
* - API key authentication: Recommended for SSE due to reliable browser support
* - JWT authentication: Supported but may have limitations due to EventSource header restrictions in some browsers
* - Combined authentication: Both API key and JWT can be provided simultaneously (JWT takes precedence)
*
* For JWT authentication, the SDK attempts to use Authorization headers where supported,
* and falls back to query parameters when custom headers aren't available.
*/
class SseSDK {
constructor(options) {
this.eventSource = null;
this.connectionState = exports.ConnectionState.Disconnected;
this.eventHandlers = new Map();
this.sseEventHandlers = {};
this.reconnectAttempts = 0;
this.connectionParams = null;
this.isDisposed = false;
this.reconnectTimer = null;
// Validate required fields
if (!options.tenantId) {
throw new Error('tenantId is required');
}
if (!options.apiKey && !options.getJwtToken && !options.jwtToken) {
throw new Error('Either apiKey, jwtToken, or getJwtToken callback is required');
}
if (!options.serverUrl) {
throw new Error('serverUrl is required');
}
this.options = {
logger: (level, message, data) => console.log(`[${level.toUpperCase()}] ${message}`, data || ''),
maxReconnectAttempts: SDK_DEFAULTS.maxReconnectAttempts,
reconnectDelay: SDK_DEFAULTS.reconnectDelay,
connectionTimeout: SDK_DEFAULTS.connectionTimeout,
autoReconnect: SDK_DEFAULTS.autoReconnect,
...options
};
// Store event handlers (consistent with SocketSDK)
this.sseEventHandlers = options.eventHandlers || {};
// Initialize legacy event handler sets for backward compatibility
this.eventHandlers.set('message', new Set());
this.eventHandlers.set('error', new Set());
this.eventHandlers.set('connected', new Set());
this.eventHandlers.set('disconnected', new Set());
this.eventHandlers.set('reconnecting', new Set());
this.eventHandlers.set('heartbeat', new Set());
}
/**
* Gets the authentication token based on the configured method
* When both API key and JWT methods are provided, JWT takes precedence
*/
async getAuthToken() {
// Prioritize JWT methods when available
if (this.options.getJwtToken) {
try {
return await this.options.getJwtToken();
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Failed to get JWT token', error);
}
throw new Error(`Failed to get JWT token: ${error}`);
}
}
else if (this.options.jwtToken) {
return this.options.jwtToken;
}
else if (this.options.apiKey) {
return this.options.apiKey;
}
else {
throw new Error('No authentication method available');
}
}
/**
* Get JWT token for EventSource Authorization header
* @returns JWT token
*/
async getJwtToken() {
if (this.options.getJwtToken) {
try {
return await this.options.getJwtToken();
}
catch (error) {
if (this.options.logger) {
this.options.logger('error', 'Failed to get JWT token', error);
}
throw new Error(`Failed to get JWT token: ${error}`);
}
}
else if (this.options.jwtToken) {
return this.options.jwtToken;
}
else {
throw new Error('No JWT token available');
}
}
/**
* Builds the SSE endpoint URL with authentication
* Can use both API key and JWT simultaneously when both are provided
*/
async buildSseUrl(params) {
const url = new URL(`${this.options.serverUrl}/api/user/sse/events`);
// Always add required parameters
url.searchParams.set('workflow', params.workflow);
url.searchParams.set('participantId', params.participantId);
url.searchParams.set('tenantId', this.options.tenantId);
if (params.scope) {
url.searchParams.set('scope', params.scope);
}
if (params.heartbeatSeconds) {
url.searchParams.set('heartbeatSeconds', params.heartbeatSeconds.toString());
}
// Add API key authentication if available
if (this.options.apiKey) {
// For API key authentication: add apikey to query params
url.searchParams.set('apikey', this.options.apiKey);
}
// JWT authentication will be handled in attemptConnection method via EventSource headers
if (this.options.getJwtToken || this.options.jwtToken) {
if (this.options.logger) {
this.options.logger('debug', 'JWT authentication will be handled via EventSource headers where supported');
}
}
return url.toString();
}
/**
* Connects to the SSE stream
*/
async connect(params) {
if (this.isDisposed) {
throw new Error('SDK has been disposed');
}
if (this.connectionState === exports.ConnectionState.Connected ||
this.connectionState === exports.ConnectionState.Connecting) {
if (this.options.logger) {
this.options.logger('warn', 'Already connected or connecting to SSE stream');
}
return;
}
this.connectionParams = params;
await this.attemptConnection();
}
/**
* Attempts to establish SSE connection
*/
async attemptConnection() {
if (!this.connectionParams) {
throw new Error('No connection parameters set');
}
try {
this.setConnectionState(exports.ConnectionState.Connecting);
const url = await this.buildSseUrl(this.connectionParams);
if (this.options.logger) {
this.options.logger('debug', `Connecting to SSE stream: ${url.split('?')[0]}`, {
workflow: this.connectionParams.workflow,
participantId: this.connectionParams.participantId,
scope: this.connectionParams.scope,
authMethod: this.getAuthType()
});
}
// Create EventSource with custom headers for JWT authentication
let eventSource;