UNPKG

@99xio/xians-sdk-typescript

Version:

A lightweight, framework-agnostic SDK for Agent WebSocket/SignalR communication

1,286 lines (1,278 loc) 110 kB
'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;