@bsv/message-box-client
Version:
A client for P2P messaging and payments
441 lines • 20.7 kB
TypeScript
/**
* @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