UNPKG

@bsv/p2p

Version:

A client for P2P messaging and payments

393 lines 18.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MessageBoxClient = void 0; const sdk_1 = require("@bsv/sdk"); const authsocket_client_1 = require("@bsv/authsocket-client"); const logger_js_1 = require("./Utils/logger.js"); /** * Extendable class for interacting with a MessageBoxServer */ class MessageBoxClient { constructor({ host = 'https://messagebox.babbage.systems', walletClient, enableLogging = false }) { /** * Tracks rooms the client has already joined */ this.joinedRooms = new Set(); this.host = host; this.walletClient = walletClient; this.authFetch = new sdk_1.AuthFetch(this.walletClient); // Enable or disable logging based on user preference if (enableLogging === true) { logger_js_1.Logger.enable(); } } /** * Getter for joinedRooms to use in tests */ getJoinedRooms() { return this.joinedRooms; } getIdentityKey() { if (this.myIdentityKey == null) { throw new Error('[MB CLIENT ERROR] Identity key is not set'); } return this.myIdentityKey; } // Add a getter for testing purposes get testSocket() { return this.socket; } /** * Establish an initial WebSocket connection (optional) */ async initializeConnection() { logger_js_1.Logger.log('[MB CLIENT] initializeConnection() STARTED'); // 🔹 Confirm function is called if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') { logger_js_1.Logger.log('[MB CLIENT] Fetching identity key...'); try { const keyResult = await this.walletClient.getPublicKey({ identityKey: true }); this.myIdentityKey = keyResult.publicKey; logger_js_1.Logger.log(`[MB CLIENT] Identity key fetched successfully: ${this.myIdentityKey}`); } catch (error) { logger_js_1.Logger.error('[MB CLIENT ERROR] Failed to fetch identity key:', error); throw new Error('Identity key retrieval failed'); } } if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') { logger_js_1.Logger.error('[MB CLIENT ERROR] Identity key is still missing after retrieval!'); throw new Error('Identity key is missing'); } logger_js_1.Logger.log('[MB CLIENT] Setting up WebSocket connection...'); if (this.socket == null) { this.socket = (0, authsocket_client_1.AuthSocketClient)(this.host, { wallet: this.walletClient }); let identitySent = false; let authenticated = false; this.socket.on('connect', () => { var _a; logger_js_1.Logger.log('[MB CLIENT] Connected to WebSocket.'); if (!identitySent) { logger_js_1.Logger.log('[MB CLIENT] Sending authentication data:', this.myIdentityKey); if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') { logger_js_1.Logger.error('[MB CLIENT ERROR] Cannot send authentication: Identity key is missing!'); } else { (_a = this.socket) === null || _a === void 0 ? void 0 : _a.emit('authenticated', { identityKey: this.myIdentityKey }); identitySent = true; } } }); // Listen for authentication success from the server this.socket.on('authenticationSuccess', (data) => { logger_js_1.Logger.log(`[MB CLIENT] WebSocket authentication successful: ${JSON.stringify(data)}`); authenticated = true; }); // Handle authentication failures this.socket.on('authenticationFailed', (data) => { logger_js_1.Logger.error(`[MB CLIENT ERROR] WebSocket authentication failed: ${JSON.stringify(data)}`); authenticated = false; }); this.socket.on('disconnect', () => { logger_js_1.Logger.log('[MB CLIENT] Disconnected from MessageBox server'); this.socket = undefined; identitySent = false; authenticated = false; }); this.socket.on('error', (error) => { logger_js_1.Logger.error('[MB CLIENT ERROR] WebSocket error:', error); }); // Wait for authentication confirmation before proceeding await new Promise((resolve, reject) => { setTimeout(() => { if (authenticated) { logger_js_1.Logger.log('[MB CLIENT] WebSocket fully authenticated and ready!'); resolve(); } else { reject(new Error('[MB CLIENT ERROR] WebSocket authentication timed out!')); } }, 5000); // Timeout after 5 seconds }); } } /** * Join a WebSocket room before sending messages */ async joinRoom(messageBox) { var _a, _b; logger_js_1.Logger.log(`[MB CLIENT] Attempting to join WebSocket room: ${messageBox}`); // Ensure WebSocket connection is established first if (this.socket == null) { logger_js_1.Logger.log('[MB CLIENT] No WebSocket connection. Initializing...'); await this.initializeConnection(); } if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') { throw new Error('[MB CLIENT ERROR] Identity key is not defined'); } const roomId = `${(_a = this.myIdentityKey) !== null && _a !== void 0 ? _a : ''}-${messageBox}`; if (this.joinedRooms.has(roomId)) { logger_js_1.Logger.log(`[MB CLIENT] Already joined WebSocket room: ${roomId}`); return; } try { logger_js_1.Logger.log(`[MB CLIENT] Joining WebSocket room: ${roomId}`); await ((_b = this.socket) === null || _b === void 0 ? void 0 : _b.emit('joinRoom', roomId)); this.joinedRooms.add(roomId); logger_js_1.Logger.log(`[MB CLIENT] Successfully joined room: ${roomId}`); } catch (error) { logger_js_1.Logger.error(`[MB CLIENT ERROR] Failed to join WebSocket room: ${roomId}`, error); } } async listenForLiveMessages({ onMessage, messageBox }) { var _a; logger_js_1.Logger.log(`[MB CLIENT] Setting up listener for WebSocket room: ${messageBox}`); // Ensure WebSocket connection and room join await this.joinRoom(messageBox); // Ensure identity key is available before creating roomId if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') { throw new Error('[MB CLIENT ERROR] Identity key is missing. Cannot construct room ID.'); } const roomId = `${this.myIdentityKey}-${messageBox}`; logger_js_1.Logger.log(`[MB CLIENT] Listening for messages in room: ${roomId}`); (_a = this.socket) === null || _a === void 0 ? void 0 : _a.on(`sendMessage-${roomId}`, (message) => { logger_js_1.Logger.log(`[MB CLIENT] Received message in room ${roomId}:`, message); onMessage(message); }); } /** * Sends a message over WebSocket if connected; falls back to HTTP otherwise. */ async sendLiveMessage({ recipient, messageBox, body }) { if (recipient == null || recipient.trim() === '') { throw new Error('[MB CLIENT ERROR] Recipient identity key is required'); } if (messageBox == null || messageBox.trim() === '') { throw new Error('[MB CLIENT ERROR] MessageBox is required'); } if (body == null || (typeof body === 'string' && body.trim() === '')) { throw new Error('[MB CLIENT ERROR] Message body cannot be empty'); } // Ensure WebSocket connection and room join before sending await this.joinRoom(messageBox); if (this.socket == null || !this.socket.connected) { logger_js_1.Logger.warn('[MB CLIENT WARNING] WebSocket not connected, falling back to HTTP'); return await this.sendMessage({ recipient, messageBox, body }); } // Generate message ID let messageId; try { const hmac = await this.walletClient.createHmac({ data: Array.from(new TextEncoder().encode(JSON.stringify(body))), protocolID: [0, 'messagebox'], keyID: '1', counterparty: recipient }); messageId = Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join(''); } catch (error) { logger_js_1.Logger.error('[MB CLIENT ERROR] Failed to generate HMAC:', error); throw new Error('Failed to generate message identifier.'); } const roomId = `${recipient}-${messageBox}`; logger_js_1.Logger.log(`[MB CLIENT] Sending WebSocket message to room: ${roomId}`); return await new Promise((resolve, reject) => { var _a, _b; const ackEvent = `sendMessageAck-${roomId}`; let handled = false; const ackHandler = (response) => { if (handled) return; handled = true; const socketAny = this.socket; if (typeof (socketAny === null || socketAny === void 0 ? void 0 : socketAny.off) === 'function') { socketAny.off(ackEvent, ackHandler); } logger_js_1.Logger.log('[MB CLIENT] Received WebSocket acknowledgment:', response); if (response == null || response.status !== 'success') { logger_js_1.Logger.warn('[MB CLIENT] WebSocket message failed, falling back to HTTP'); this.sendMessage({ recipient, messageBox, body }).then(resolve).catch(reject); } else { logger_js_1.Logger.log('[MB CLIENT] Message sent successfully via WebSocket:', response); resolve(response); } }; // Register listener before emitting (_a = this.socket) === null || _a === void 0 ? void 0 : _a.on(ackEvent, ackHandler); // Send the message (_b = this.socket) === null || _b === void 0 ? void 0 : _b.emit('sendMessage', { roomId, message: { messageId, recipient, body: typeof body === 'string' ? body : JSON.stringify(body) } }); // Timeout fallback after 10 seconds setTimeout(() => { if (!handled) { handled = true; const socketAny = this.socket; if (typeof (socketAny === null || socketAny === void 0 ? void 0 : socketAny.off) === 'function') { socketAny.off(ackEvent, ackHandler); // 🧹 Clean up listener } logger_js_1.Logger.warn('[CLIENT] WebSocket acknowledgment timed out, falling back to HTTP'); this.sendMessage({ recipient, messageBox, body }).then(resolve).catch(reject); } }, 10000); }); } /** * Leaves a WebSocket room. */ async leaveRoom(messageBox) { if (this.socket == null) { logger_js_1.Logger.warn('[MB CLIENT] Attempted to leave a room but WebSocket is not connected.'); return; } if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') { throw new Error('[MB CLIENT ERROR] Identity key is not defined'); } const roomId = `${this.myIdentityKey}-${messageBox}`; logger_js_1.Logger.log(`[MB CLIENT] Leaving WebSocket room: ${roomId}`); this.socket.emit('leaveRoom', roomId); // Ensure the room is removed from tracking this.joinedRooms.delete(roomId); } /** * Closes WebSocket connection. */ async disconnectWebSocket() { if (this.socket != null) { logger_js_1.Logger.log('[MB CLIENT] Closing WebSocket connection...'); this.socket.disconnect(); this.socket = undefined; } else { logger_js_1.Logger.log('[MB CLIENT] No active WebSocket connection to close.'); } } /** * Sends a message via HTTP */ async sendMessage(message) { var _a, _b; if (message.recipient == null || message.recipient.trim() === '') { throw new Error('You must provide a message recipient!'); } if (message.messageBox == null || message.messageBox.trim() === '') { throw new Error('You must provide a messageBox to send this message into!'); } if (message.body == null || (typeof message.body === 'string' && message.body.trim().length === 0)) { throw new Error('Every message must have a body!'); } // Generate HMAC let messageId; try { const hmac = await this.walletClient.createHmac({ data: Array.from(new TextEncoder().encode(JSON.stringify(message.body))), protocolID: [0, 'messagebox'], keyID: '1', counterparty: message.recipient }); messageId = (_a = message.messageId) !== null && _a !== void 0 ? _a : Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join(''); } catch (error) { logger_js_1.Logger.error('[MB CLIENT ERROR] Failed to generate HMAC:', error); throw new Error('Failed to generate message identifier.'); } const requestBody = { message: { ...message, messageId, body: JSON.stringify(message.body) } }; try { logger_js_1.Logger.log('[MB CLIENT] Sending HTTP request to:', `${this.host}/sendMessage`); logger_js_1.Logger.log('[MB CLIENT] Request Body:', JSON.stringify(requestBody, null, 2)); // Ensure the identity key is fetched before sending if (this.myIdentityKey == null || this.myIdentityKey === '') { try { const keyResult = await this.walletClient.getPublicKey({ identityKey: true }); this.myIdentityKey = keyResult.publicKey; logger_js_1.Logger.log(`[MB CLIENT] Fetched identity key before sending request: ${this.myIdentityKey}`); } catch (error) { logger_js_1.Logger.error('[MB CLIENT ERROR] Failed to fetch identity key:', error); throw new Error('Identity key retrieval failed'); } } // Now create the headers AFTER ensuring identityKey is set const authHeaders = { 'Content-Type': 'application/json' }; logger_js_1.Logger.log('[MB CLIENT] Sending Headers:', JSON.stringify(authHeaders, null, 2)); const response = await this.authFetch.fetch(`${this.host}/sendMessage`, { method: 'POST', headers: authHeaders, body: JSON.stringify(requestBody) }); // Debug: Check if bodyUsed before reading logger_js_1.Logger.log('[MB CLIENT] Raw Response:', response); logger_js_1.Logger.log('[MB CLIENT] Response Body Used?', response.bodyUsed); // Read body only if it's not already consumed if (response.bodyUsed) { throw new Error('[MB CLIENT ERROR] Response body has already been used!'); } const parsedResponse = await response.json(); logger_js_1.Logger.log('[MB CLIENT] Raw Response Body:', parsedResponse); if (!response.ok) { logger_js_1.Logger.error(`[MB CLIENT ERROR] Failed to send message. HTTP ${response.status}: ${response.statusText}`); throw new Error(`Message sending failed: HTTP ${response.status} - ${response.statusText}`); } if (parsedResponse.status !== 'success') { logger_js_1.Logger.error(`[MB CLIENT ERROR] Server returned an error: ${String(parsedResponse.description)}`); throw new Error((_b = parsedResponse.description) !== null && _b !== void 0 ? _b : 'Unknown error from server.'); } logger_js_1.Logger.log('[MB CLIENT] Message successfully sent.'); return { ...parsedResponse, messageId }; } catch (error) { logger_js_1.Logger.error('[MB CLIENT ERROR] Network or timeout error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to send message: ${errorMessage}`); } } /** * Lists messages from MessageBoxServer */ async listMessages({ messageBox }) { if (messageBox.trim() === '') { throw new Error('MessageBox cannot be empty'); } const response = await this.authFetch.fetch(`${this.host}/listMessages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messageBox }) }); const parsedResponse = await response.json(); if (parsedResponse.status === 'error') { throw new Error(parsedResponse.description); } return parsedResponse.messages; } /** * Acknowledges one or more messages as having been received */ async acknowledgeMessage({ messageIds }) { if (!Array.isArray(messageIds) || messageIds.length === 0) { throw new Error('Message IDs array cannot be empty'); } logger_js_1.Logger.log(`[MB CLIENT] Acknowledging messages: ${JSON.stringify(messageIds)}`); const acknowledged = await this.authFetch.fetch(`${this.host}/acknowledgeMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messageIds }) }); const parsedAcknowledged = await acknowledged.json(); if (parsedAcknowledged.status === 'error') { throw new Error(parsedAcknowledged.description); } return parsedAcknowledged.status; } } exports.MessageBoxClient = MessageBoxClient; //# sourceMappingURL=MessageBoxClient.js.map