UNPKG

@bsv/message-box-client

Version:

A client for P2P messaging and payments

441 lines 20.7 kB
/** * @file MessageBoxClient.ts * @description * Provides the `MessageBoxClient` class — a secure client library for sending and receiving messages * via a Message Box Server over HTTP and WebSocket. Messages are authenticated, optionally encrypted, * and routed using identity-based addressing based on BRC-2/BRC-42/BRC-43 protocols. * * Core Features: * - Authenticated message transport using identity keys * - Deterministic message ID generation via HMAC (BRC-2) * - AES-256-GCM encryption using ECDH shared secrets derived via BRC-42/BRC-43 * - Support for sending messages to self (`counterparty: 'self'`) * - Live message streaming using WebSocket rooms * - Optional plaintext messaging with `skipEncryption` * - Overlay host discovery and advertisement broadcasting via SHIP * - MessageBox-based organization and acknowledgment system * * See BRC-2 for details on the encryption scheme: https://github.com/bitcoin-sv/BRCs/blob/master/wallet/0002.md * * @module MessageBoxClient * @author Project Babbage * @license Open BSV License */ import { AuthFetch, BEEF, LockingScript, HexString } from '@bsv/sdk'; import { AuthSocketClient } from '@bsv/authsocket-client'; import { AcknowledgeMessageParams, ListMessagesParams, MessageBoxClientOptions, PeerMessage, SendMessageParams, SendMessageResponse } from './types.js'; interface AdvertisementToken { host: string; txid: HexString; outputIndex: number; lockingScript: LockingScript; beef: BEEF; } /** * @class MessageBoxClient * @description * A secure client for sending and receiving authenticated, encrypted messages * through a MessageBox server over HTTP and WebSocket. * * Core Features: * - Identity-authenticated message transport (BRC-2) * - AES-256-GCM end-to-end encryption with BRC-42/BRC-43 key derivation * - HMAC-based message ID generation for deduplication * - Live WebSocket messaging with room-based subscription management * - Overlay network discovery and host advertisement broadcasting (SHIP protocol) * - Fallback to HTTP messaging when WebSocket is unavailable * * **Important:** * The MessageBoxClient automatically calls `await init()` if needed. * Manual initialization is optional but still supported. * * You may call `await init()` manually for explicit control, but you can also use methods * like `sendMessage()` or `listenForLiveMessages()` directly — the client will initialize itself * automatically if not yet ready. * * @example * const client = new MessageBoxClient({ walletClient, enableLogging: true }) * await client.init() // <- Required before using the client * await client.sendMessage({ recipient, messageBox: 'payment_inbox', body: 'Hello world' }) */ export declare class MessageBoxClient { private host; readonly authFetch: AuthFetch; private readonly walletClient; private socket?; private myIdentityKey?; private readonly joinedRooms; private readonly lookupResolver; private readonly networkPreset; private initialized; /** * @constructor * @param {Object} options - Initialization options for the MessageBoxClient. * @param {string} [options.host] - The base URL of the MessageBox server. If omitted, defaults to mainnet/testnet hosts. * @param {WalletClient} options.walletClient - Wallet instance used for authentication, signing, and encryption. * @param {boolean} [options.enableLogging=false] - Whether to enable detailed debug logging to the console. * @param {'local' | 'mainnet' | 'testnet'} [options.networkPreset='mainnet'] - Overlay network preset used for routing and advertisement lookup. * * @description * Constructs a new MessageBoxClient. * * **Note:** * Passing a `host` during construction sets the default server. * If you do not manually call `await init()`, the client will automatically initialize itself on first use. * * @example * const client = new MessageBoxClient({ * host: 'https://messagebox.example', * walletClient, * enableLogging: true, * networkPreset: 'testnet' * }) * await client.init() */ constructor(options?: MessageBoxClientOptions); /** * @method init * @async * @param {string} [targetHost] - Optional host to set or override the default host. * @returns {Promise<void>} * * @description * Initializes the MessageBoxClient by setting or anointing a MessageBox host. * * - If the client was constructed with a host, it uses that unless a different targetHost is provided. * - If no prior advertisement exists for the identity key and host, it automatically broadcasts a new advertisement. * - After calling init(), the client becomes ready to send, receive, and acknowledge messages. * * This method can be called manually for explicit control, * but will be automatically invoked if omitted. * @throws {Error} If no valid host is provided, or anointing fails. * * @example * const client = new MessageBoxClient({ host: 'https://mybox.example', walletClient }) * await client.init() * await client.sendMessage({ recipient, messageBox: 'inbox', body: 'Hello' }) */ init(targetHost?: string): Promise<void>; /** * @method assertInitialized * @private * @description * Ensures that the MessageBoxClient has completed initialization before performing sensitive operations * like sending, receiving, or acknowledging messages. * * If the client is not yet initialized, it will automatically call `await init()` to complete setup. * * Used automatically by all public methods that require initialization. */ private assertInitialized; /** * @method getJoinedRooms * @returns {Set<string>} A set of currently joined WebSocket room IDs * @description * Returns a live list of WebSocket rooms the client is subscribed to. * Useful for inspecting state or ensuring no duplicates are joined. */ getJoinedRooms(): Set<string>; /** * @method getIdentityKey * @returns {Promise<string>} The identity public key of the user * @description * Returns the client's identity key, used for signing, encryption, and addressing. * If not already loaded, it will fetch and cache it. */ getIdentityKey(): Promise<string>; /** * @property testSocket * @readonly * @returns {AuthSocketClient | undefined} The internal WebSocket client (or undefined if not connected). * @description * Exposes the underlying Authenticated WebSocket client used for live messaging. * This is primarily intended for debugging, test frameworks, or direct inspection. * * Note: Do not interact with the socket directly unless necessary. * Use the provided `sendLiveMessage`, `listenForLiveMessages`, and related methods. */ get testSocket(): ReturnType<typeof AuthSocketClient> | undefined; /** * @method initializeConnection * @async * @returns {Promise<void>} * @description * Establishes an authenticated WebSocket connection to the configured MessageBox server. * Enables live message streaming via room-based channels tied to identity keys. * * This method: * 1. Retrieves the user’s identity key if not already set * 2. Initializes a secure AuthSocketClient WebSocket connection * 3. Authenticates the connection using the identity key * 4. Waits up to 5 seconds for authentication confirmation * * If authentication fails or times out, the connection is rejected. * * @throws {Error} If the identity key is unavailable or authentication fails * * @example * const mb = new MessageBoxClient({ walletClient }) * await mb.initializeConnection() * // WebSocket is now ready for use */ initializeConnection(): Promise<void>; /** * @method resolveHostForRecipient * @async * @param {string} identityKey - The public identity key of the intended recipient. * @returns {Promise<string>} - A fully qualified host URL for the recipient's MessageBox server. * * @description * Attempts to resolve the most recently anointed MessageBox host for the given identity key * using the BSV overlay network and the `ls_messagebox` LookupResolver. * * If no advertisements are found, or if resolution fails, the client will fall back * to its own configured `host`. This allows seamless operation in both overlay and non-overlay environments. * * This method guarantees a non-null return value and should be used directly when routing messages. * * @example * const host = await resolveHostForRecipient('028d...') // → returns either overlay host or this.host */ resolveHostForRecipient(identityKey: string): Promise<string>; /** * Core lookup: ask the LookupResolver (optionally filtered by host), * decode every PushDrop output, and collect all the host URLs you find. * * @param identityKey the recipient’s public key * @param host? if passed, only look for adverts anointed at that host * @returns 0-length array if nothing valid was found */ queryAdvertisements(identityKey?: string, host?: string): Promise<AdvertisementToken[]>; /** * @method joinRoom * @async * @param {string} messageBox - The name of the WebSocket room to join (e.g., "payment_inbox"). * @returns {Promise<void>} * * @description * Joins a WebSocket room that corresponds to the user’s identity key and the specified message box. * This is required to receive real-time messages via WebSocket for a specific type of communication. * * If the WebSocket connection is not already established, this method will first initialize the connection. * It also ensures the room is only joined once, and tracks all joined rooms in an internal set. * * Room ID format: `${identityKey}-${messageBox}` * * @example * await client.joinRoom('payment_inbox') * // Now listening for real-time messages in room '028d...-payment_inbox' */ joinRoom(messageBox: string): Promise<void>; /** * @method listenForLiveMessages * @async * @param {Object} params - Configuration for the live message listener. * @param {function} params.onMessage - A callback function that will be triggered when a new message arrives. * @param {string} params.messageBox - The messageBox name (e.g., `payment_inbox`) to listen for. * @returns {Promise<void>} * * @description * Subscribes the client to live messages over WebSocket for a specific messageBox. * * This method: * - Ensures the WebSocket connection is initialized and authenticated. * - Joins the correct room formatted as `${identityKey}-${messageBox}`. * - Listens for messages broadcast to the room. * - Automatically attempts to parse and decrypt message bodies. * - Emits the final message (as a `PeerMessage`) to the supplied `onMessage` handler. * * If the incoming message is encrypted, the client decrypts it using AES-256-GCM via * ECDH shared secrets derived from identity keys as defined in [BRC-2](https://github.com/bitcoin-sv/BRCs/blob/master/wallet/0002.md). * Messages sent by the client to itself are decrypted using `counterparty = 'self'`. * * @example * await client.listenForLiveMessages({ * messageBox: 'payment_inbox', * onMessage: (msg) => console.log('Received live message:', msg) * }) */ listenForLiveMessages({ onMessage, messageBox }: { onMessage: (message: PeerMessage) => void; messageBox: string; }): Promise<void>; /** * @method sendLiveMessage * @async * @param {SendMessageParams} param0 - The message parameters including recipient, box name, body, and options. * @returns {Promise<SendMessageResponse>} A success response with the generated messageId. * * @description * Sends a message in real time using WebSocket with authenticated delivery and overlay fallback. * * This method: * - Ensures the WebSocket connection is open and joins the correct room. * - Derives a unique message ID using an HMAC of the message body and counterparty identity key. * - Encrypts the message body using AES-256-GCM based on the ECDH shared secret between derived keys, per [BRC-2](https://github.com/bitcoin-sv/BRCs/blob/master/wallet/0002.md), * unless `skipEncryption` is explicitly set to `true`. * - Sends the message to a WebSocket room in the format `${recipient}-${messageBox}`. * - Waits for acknowledgment (`sendMessageAck-${roomId}`). * - If no acknowledgment is received within 10 seconds, falls back to `sendMessage()` over HTTP. * * This hybrid delivery strategy ensures reliability in both real-time and offline-capable environments. * * @throws {Error} If message validation fails, HMAC generation fails, or both WebSocket and HTTP fail to deliver. * * @example * await client.sendLiveMessage({ * recipient: '028d...', * messageBox: 'payment_inbox', * body: { amount: 1000 } * }) */ sendLiveMessage({ recipient, messageBox, body, messageId, skipEncryption }: SendMessageParams): Promise<SendMessageResponse>; /** * @method leaveRoom * @async * @param {string} messageBox - The name of the WebSocket room to leave (e.g., `payment_inbox`). * @returns {Promise<void>} * * @description * Leaves a previously joined WebSocket room associated with the authenticated identity key. * This helps reduce unnecessary message traffic and memory usage. * * If the WebSocket is not connected or the identity key is missing, the method exits gracefully. * * @example * await client.leaveRoom('payment_inbox') */ leaveRoom(messageBox: string): Promise<void>; /** * @method disconnectWebSocket * @async * @returns {Promise<void>} Resolves when the WebSocket connection is successfully closed. * * @description * Gracefully disconnects the WebSocket connection to the MessageBox server. * This should be called when the client is shutting down, logging out, or no longer * needs real-time communication to conserve system resources. * * @example * await client.disconnectWebSocket() */ disconnectWebSocket(): Promise<void>; /** * @method sendMessage * @async * @param {SendMessageParams} message - Contains recipient, messageBox name, message body, optional messageId, and skipEncryption flag. * @param {string} [overrideHost] - Optional host to override overlay resolution (useful for testing or private routing). * @returns {Promise<SendMessageResponse>} - Resolves with `{ status, messageId }` on success. * * @description * Sends a message over HTTP to a recipient's messageBox. This method: * * - Derives a deterministic `messageId` using an HMAC of the message body and recipient key. * - Encrypts the message body using AES-256-GCM, derived from a shared secret using BRC-2-compliant key derivation and ECDH, unless `skipEncryption` is set to true. * - Automatically resolves the host via overlay LookupResolver unless an override is provided. * - Authenticates the request using the current identity key with `AuthFetch`. * * This is the fallback mechanism for `sendLiveMessage` when WebSocket delivery fails. * It is also used for message types that do not require real-time delivery. * * @throws {Error} If validation, encryption, HMAC, or network request fails. * * @example * await client.sendMessage({ * recipient: '03abc...', * messageBox: 'notifications', * body: { type: 'ping' } * }) */ sendMessage(message: SendMessageParams, overrideHost?: string): Promise<SendMessageResponse>; /** * @method anointHost * @async * @param {string} host - The full URL of the server you want to designate as your MessageBox host (e.g., "https://mybox.com"). * @returns {Promise<{ txid: string }>} - The transaction ID of the advertisement broadcast to the overlay network. * * @description * Broadcasts a signed overlay advertisement using a PushDrop output under the `tm_messagebox` topic. * This advertisement announces that the specified `host` is now authorized to receive and route * messages for the sender’s identity key. * * The broadcasted message includes: * - The identity key * - The chosen host URL * * This is essential for enabling overlay-based message delivery via SHIP and LookupResolver. * The recipient’s host must advertise itself for message routing to succeed in a decentralized manner. * * @throws {Error} If the URL is invalid, the PushDrop creation fails, or the overlay broadcast does not succeed. * * @example * const { txid } = await client.anointHost('https://my-messagebox.io') */ anointHost(host: string): Promise<{ txid: string; }>; /** * @method revokeHostAdvertisement * @async * @param {AdvertisementToken} advertisementToken - The advertisement token containing the messagebox host to revoke. * @returns {Promise<{ txid: string }>} - The transaction ID of the revocation broadcast to the overlay network. * * @description * Broadcasts a signed revocation transaction indicating the advertisement token should be removed * and no longer tracked by lookup services. * * @example * const { txid } = await client.revokeHost('https://my-messagebox.io') */ revokeHostAdvertisement(advertisementToken: AdvertisementToken): Promise<{ txid: string; }>; /** * @method listMessages * @async * @param {ListMessagesParams} params - Contains the name of the messageBox to read from. * @returns {Promise<PeerMessage[]>} - Returns an array of decrypted `PeerMessage` objects. * * @description * Retrieves all messages from the specified `messageBox` assigned to the current identity key. * Unless a host override is provided, messages are fetched from the resolved overlay host (via LookupResolver) or the default host if no advertisement is found. * * Each message is: * - Parsed and, if encrypted, decrypted using AES-256-GCM via BRC-2-compliant ECDH key derivation and symmetric encryption. * - Returned as a normalized `PeerMessage` with readable string body content. * * Decryption automatically derives a shared secret using the sender’s identity key and the receiver’s child private key. * If the sender is the same as the recipient, the `counterparty` is set to `'self'`. * * @throws {Error} If no messageBox is specified, the request fails, or the server returns an error. * * @example * const messages = await client.listMessages({ messageBox: 'inbox' }) * messages.forEach(msg => console.log(msg.sender, msg.body)) */ listMessages({ messageBox, host }: ListMessagesParams): Promise<PeerMessage[]>; /** * @method acknowledgeMessage * @async * @param {AcknowledgeMessageParams} params - An object containing an array of message IDs to acknowledge. * @returns {Promise<string>} - A string indicating the result, typically `'success'`. * * @description * Notifies the MessageBox server(s) that one or more messages have been * successfully received and processed by the client. Once acknowledged, these messages are removed * from the recipient's inbox on the server(s). * * This operation is essential for proper message lifecycle management and prevents duplicate * processing or delivery. * * Acknowledgment supports providing a host override, or will use overlay routing to find the appropriate server the received the given message. * * @throws {Error} If the message ID array is missing or empty, or if the request to the server fails. * * @example * await client.acknowledgeMessage({ messageIds: ['msg123', 'msg456'] }) */ acknowledgeMessage({ messageIds, host }: AcknowledgeMessageParams): Promise<string>; } export {}; //# sourceMappingURL=MessageBoxClient.d.ts.map