@bsv/message-box-client 1.1.7 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/cjs/mod.js +20 -0
  2. package/dist/cjs/mod.js.map +1 -0
  3. package/dist/cjs/package.json +69 -0
  4. package/dist/cjs/src/MessageBoxClient.js +1171 -0
  5. package/dist/cjs/src/MessageBoxClient.js.map +1 -0
  6. package/dist/cjs/src/PeerPayClient.js +309 -0
  7. package/dist/cjs/src/PeerPayClient.js.map +1 -0
  8. package/dist/cjs/src/Utils/logger.js +27 -0
  9. package/dist/cjs/src/Utils/logger.js.map +1 -0
  10. package/dist/cjs/src/__tests/MessageBoxClient.test.js +614 -0
  11. package/dist/cjs/src/__tests/MessageBoxClient.test.js.map +1 -0
  12. package/dist/cjs/src/__tests/PeerPayClientUnit.test.js +213 -0
  13. package/dist/cjs/src/__tests/PeerPayClientUnit.test.js.map +1 -0
  14. package/dist/cjs/src/__tests/integration/integrationEncrypted.test.js +84 -0
  15. package/dist/cjs/src/__tests/integration/integrationEncrypted.test.js.map +1 -0
  16. package/dist/cjs/src/__tests/integration/integrationHTTP.test.js +128 -0
  17. package/dist/cjs/src/__tests/integration/integrationHTTP.test.js.map +1 -0
  18. package/dist/cjs/src/__tests/integration/integrationOverlay.test.js +138 -0
  19. package/dist/cjs/src/__tests/integration/integrationOverlay.test.js.map +1 -0
  20. package/dist/cjs/src/__tests/integration/integrationWS.test.js +123 -0
  21. package/dist/cjs/src/__tests/integration/integrationWS.test.js.map +1 -0
  22. package/dist/cjs/src/__tests/integration/testServer.js +65 -0
  23. package/dist/cjs/src/__tests/integration/testServer.js.map +1 -0
  24. package/dist/cjs/src/types.js +3 -0
  25. package/dist/cjs/src/types.js.map +1 -0
  26. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
  27. package/dist/esm/mod.js +4 -0
  28. package/dist/esm/mod.js.map +1 -0
  29. package/dist/esm/src/MessageBoxClient.js +1165 -0
  30. package/dist/esm/src/MessageBoxClient.js.map +1 -0
  31. package/dist/esm/src/PeerPayClient.js +307 -0
  32. package/dist/esm/src/PeerPayClient.js.map +1 -0
  33. package/dist/esm/src/Utils/logger.js +23 -0
  34. package/dist/esm/src/Utils/logger.js.map +1 -0
  35. package/dist/esm/src/__tests/MessageBoxClient.test.js +603 -0
  36. package/dist/esm/src/__tests/MessageBoxClient.test.js.map +1 -0
  37. package/dist/esm/src/__tests/PeerPayClientUnit.test.js +211 -0
  38. package/dist/esm/src/__tests/PeerPayClientUnit.test.js.map +1 -0
  39. package/dist/esm/src/__tests/integration/integrationEncrypted.test.js +81 -0
  40. package/dist/esm/src/__tests/integration/integrationEncrypted.test.js.map +1 -0
  41. package/dist/esm/src/__tests/integration/integrationHTTP.test.js +126 -0
  42. package/dist/esm/src/__tests/integration/integrationHTTP.test.js.map +1 -0
  43. package/dist/esm/src/__tests/integration/integrationOverlay.test.js +135 -0
  44. package/dist/esm/src/__tests/integration/integrationOverlay.test.js.map +1 -0
  45. package/dist/esm/src/__tests/integration/integrationWS.test.js +121 -0
  46. package/dist/esm/src/__tests/integration/integrationWS.test.js.map +1 -0
  47. package/dist/esm/src/__tests/integration/testServer.js +61 -0
  48. package/dist/esm/src/__tests/integration/testServer.js.map +1 -0
  49. package/dist/esm/src/types.js +2 -0
  50. package/dist/esm/src/types.js.map +1 -0
  51. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
  52. package/dist/types/mod.d.ts +4 -0
  53. package/dist/types/mod.d.ts.map +1 -0
  54. package/dist/types/src/MessageBoxClient.d.ts +441 -0
  55. package/dist/types/src/MessageBoxClient.d.ts.map +1 -0
  56. package/dist/types/src/PeerPayClient.d.ts +144 -0
  57. package/dist/types/src/PeerPayClient.d.ts.map +1 -0
  58. package/dist/types/src/Utils/logger.d.ts +9 -0
  59. package/dist/types/src/Utils/logger.d.ts.map +1 -0
  60. package/dist/types/src/__tests/MessageBoxClient.test.d.ts +2 -0
  61. package/dist/types/src/__tests/MessageBoxClient.test.d.ts.map +1 -0
  62. package/dist/types/src/__tests/PeerPayClientUnit.test.d.ts +2 -0
  63. package/dist/types/src/__tests/PeerPayClientUnit.test.d.ts.map +1 -0
  64. package/dist/types/src/__tests/integration/integrationEncrypted.test.d.ts +2 -0
  65. package/dist/types/src/__tests/integration/integrationEncrypted.test.d.ts.map +1 -0
  66. package/dist/types/src/__tests/integration/integrationHTTP.test.d.ts +2 -0
  67. package/dist/types/src/__tests/integration/integrationHTTP.test.d.ts.map +1 -0
  68. package/dist/types/src/__tests/integration/integrationOverlay.test.d.ts +2 -0
  69. package/dist/types/src/__tests/integration/integrationOverlay.test.d.ts.map +1 -0
  70. package/dist/types/src/__tests/integration/integrationWS.test.d.ts +2 -0
  71. package/dist/types/src/__tests/integration/integrationWS.test.d.ts.map +1 -0
  72. package/dist/types/src/__tests/integration/testServer.d.ts +9 -0
  73. package/dist/types/src/__tests/integration/testServer.d.ts.map +1 -0
  74. package/dist/types/src/types.d.ts +99 -0
  75. package/dist/types/src/types.d.ts.map +1 -0
  76. package/dist/types/tsconfig.types.tsbuildinfo +1 -0
  77. package/dist/umd/bundle.js +1 -0
  78. package/package.json +5 -5
@@ -0,0 +1,1171 @@
1
+ "use strict";
2
+ /**
3
+ * @file MessageBoxClient.ts
4
+ * @description
5
+ * Provides the `MessageBoxClient` class — a secure client library for sending and receiving messages
6
+ * via a Message Box Server over HTTP and WebSocket. Messages are authenticated, optionally encrypted,
7
+ * and routed using identity-based addressing based on BRC-2/BRC-42/BRC-43 protocols.
8
+ *
9
+ * Core Features:
10
+ * - Authenticated message transport using identity keys
11
+ * - Deterministic message ID generation via HMAC (BRC-2)
12
+ * - AES-256-GCM encryption using ECDH shared secrets derived via BRC-42/BRC-43
13
+ * - Support for sending messages to self (`counterparty: 'self'`)
14
+ * - Live message streaming using WebSocket rooms
15
+ * - Optional plaintext messaging with `skipEncryption`
16
+ * - Overlay host discovery and advertisement broadcasting via SHIP
17
+ * - MessageBox-based organization and acknowledgment system
18
+ *
19
+ * See BRC-2 for details on the encryption scheme: https://github.com/bitcoin-sv/BRCs/blob/master/wallet/0002.md
20
+ *
21
+ * @module MessageBoxClient
22
+ * @author Project Babbage
23
+ * @license Open BSV License
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.MessageBoxClient = void 0;
27
+ const sdk_1 = require("@bsv/sdk");
28
+ const authsocket_client_1 = require("@bsv/authsocket-client");
29
+ const logger_js_1 = require("./Utils/logger.js");
30
+ const DEFAULT_MAINNET_HOST = 'https://messagebox.babbage.systems';
31
+ const DEFAULT_TESTNET_HOST = 'https://staging-messagebox.babbage.systems';
32
+ /**
33
+ * @class MessageBoxClient
34
+ * @description
35
+ * A secure client for sending and receiving authenticated, encrypted messages
36
+ * through a MessageBox server over HTTP and WebSocket.
37
+ *
38
+ * Core Features:
39
+ * - Identity-authenticated message transport (BRC-2)
40
+ * - AES-256-GCM end-to-end encryption with BRC-42/BRC-43 key derivation
41
+ * - HMAC-based message ID generation for deduplication
42
+ * - Live WebSocket messaging with room-based subscription management
43
+ * - Overlay network discovery and host advertisement broadcasting (SHIP protocol)
44
+ * - Fallback to HTTP messaging when WebSocket is unavailable
45
+ *
46
+ * **Important:**
47
+ * The MessageBoxClient automatically calls `await init()` if needed.
48
+ * Manual initialization is optional but still supported.
49
+ *
50
+ * You may call `await init()` manually for explicit control, but you can also use methods
51
+ * like `sendMessage()` or `listenForLiveMessages()` directly — the client will initialize itself
52
+ * automatically if not yet ready.
53
+ *
54
+ * @example
55
+ * const client = new MessageBoxClient({ walletClient, enableLogging: true })
56
+ * await client.init() // <- Required before using the client
57
+ * await client.sendMessage({ recipient, messageBox: 'payment_inbox', body: 'Hello world' })
58
+ */
59
+ class MessageBoxClient {
60
+ /**
61
+ * @constructor
62
+ * @param {Object} options - Initialization options for the MessageBoxClient.
63
+ * @param {string} [options.host] - The base URL of the MessageBox server. If omitted, defaults to mainnet/testnet hosts.
64
+ * @param {WalletClient} options.walletClient - Wallet instance used for authentication, signing, and encryption.
65
+ * @param {boolean} [options.enableLogging=false] - Whether to enable detailed debug logging to the console.
66
+ * @param {'local' | 'mainnet' | 'testnet'} [options.networkPreset='mainnet'] - Overlay network preset used for routing and advertisement lookup.
67
+ *
68
+ * @description
69
+ * Constructs a new MessageBoxClient.
70
+ *
71
+ * **Note:**
72
+ * Passing a `host` during construction sets the default server.
73
+ * If you do not manually call `await init()`, the client will automatically initialize itself on first use.
74
+ *
75
+ * @example
76
+ * const client = new MessageBoxClient({
77
+ * host: 'https://messagebox.example',
78
+ * walletClient,
79
+ * enableLogging: true,
80
+ * networkPreset: 'testnet'
81
+ * })
82
+ * await client.init()
83
+ */
84
+ constructor(options = {}) {
85
+ var _a;
86
+ this.joinedRooms = new Set();
87
+ this.initialized = false;
88
+ const { host, walletClient, enableLogging = false, networkPreset = 'mainnet' } = options;
89
+ const defaultHost = this.networkPreset === 'testnet'
90
+ ? DEFAULT_TESTNET_HOST
91
+ : DEFAULT_MAINNET_HOST;
92
+ this.host = (_a = host === null || host === void 0 ? void 0 : host.trim()) !== null && _a !== void 0 ? _a : defaultHost;
93
+ this.walletClient = walletClient !== null && walletClient !== void 0 ? walletClient : new sdk_1.WalletClient();
94
+ this.authFetch = new sdk_1.AuthFetch(this.walletClient);
95
+ this.networkPreset = networkPreset;
96
+ this.lookupResolver = new sdk_1.LookupResolver({
97
+ networkPreset
98
+ });
99
+ if (enableLogging) {
100
+ logger_js_1.Logger.enable();
101
+ }
102
+ }
103
+ /**
104
+ * @method init
105
+ * @async
106
+ * @param {string} [targetHost] - Optional host to set or override the default host.
107
+ * @returns {Promise<void>}
108
+ *
109
+ * @description
110
+ * Initializes the MessageBoxClient by setting or anointing a MessageBox host.
111
+ *
112
+ * - If the client was constructed with a host, it uses that unless a different targetHost is provided.
113
+ * - If no prior advertisement exists for the identity key and host, it automatically broadcasts a new advertisement.
114
+ * - After calling init(), the client becomes ready to send, receive, and acknowledge messages.
115
+ *
116
+ * This method can be called manually for explicit control,
117
+ * but will be automatically invoked if omitted.
118
+ * @throws {Error} If no valid host is provided, or anointing fails.
119
+ *
120
+ * @example
121
+ * const client = new MessageBoxClient({ host: 'https://mybox.example', walletClient })
122
+ * await client.init()
123
+ * await client.sendMessage({ recipient, messageBox: 'inbox', body: 'Hello' })
124
+ */
125
+ async init(targetHost = this.host) {
126
+ var _a;
127
+ const normalizedHost = targetHost === null || targetHost === void 0 ? void 0 : targetHost.trim();
128
+ if (normalizedHost === '') {
129
+ throw new Error('Cannot anoint host: No valid host provided');
130
+ }
131
+ // Check if this is an override host
132
+ if (normalizedHost !== this.host) {
133
+ this.initialized = false;
134
+ this.host = normalizedHost;
135
+ }
136
+ if (this.initialized)
137
+ return;
138
+ // 1. Get our identity key
139
+ const identityKey = await this.getIdentityKey();
140
+ // 2. Check for any matching advertisements for the given host
141
+ const [firstAdvertisement] = await this.queryAdvertisements(identityKey, normalizedHost);
142
+ // 3. If none our found, anoint this host
143
+ if (firstAdvertisement == null || ((_a = firstAdvertisement === null || firstAdvertisement === void 0 ? void 0 : firstAdvertisement.host) === null || _a === void 0 ? void 0 : _a.trim()) === '' || (firstAdvertisement === null || firstAdvertisement === void 0 ? void 0 : firstAdvertisement.host) !== normalizedHost) {
144
+ logger_js_1.Logger.log('[MB CLIENT] Anointing host:', normalizedHost);
145
+ const { txid } = await this.anointHost(normalizedHost);
146
+ if (txid == null || txid.trim() === '') {
147
+ throw new Error('Failed to anoint host: No transaction ID returned');
148
+ }
149
+ }
150
+ this.initialized = true;
151
+ }
152
+ /**
153
+ * @method assertInitialized
154
+ * @private
155
+ * @description
156
+ * Ensures that the MessageBoxClient has completed initialization before performing sensitive operations
157
+ * like sending, receiving, or acknowledging messages.
158
+ *
159
+ * If the client is not yet initialized, it will automatically call `await init()` to complete setup.
160
+ *
161
+ * Used automatically by all public methods that require initialization.
162
+ */
163
+ async assertInitialized() {
164
+ if (!this.initialized || this.host == null || this.host.trim() === '') {
165
+ await this.init();
166
+ }
167
+ }
168
+ /**
169
+ * @method getJoinedRooms
170
+ * @returns {Set<string>} A set of currently joined WebSocket room IDs
171
+ * @description
172
+ * Returns a live list of WebSocket rooms the client is subscribed to.
173
+ * Useful for inspecting state or ensuring no duplicates are joined.
174
+ */
175
+ getJoinedRooms() {
176
+ return this.joinedRooms;
177
+ }
178
+ /**
179
+ * @method getIdentityKey
180
+ * @returns {Promise<string>} The identity public key of the user
181
+ * @description
182
+ * Returns the client's identity key, used for signing, encryption, and addressing.
183
+ * If not already loaded, it will fetch and cache it.
184
+ */
185
+ async getIdentityKey() {
186
+ if (this.myIdentityKey != null && this.myIdentityKey.trim() !== '') {
187
+ return this.myIdentityKey;
188
+ }
189
+ logger_js_1.Logger.log('[MB CLIENT] Fetching identity key...');
190
+ try {
191
+ const keyResult = await this.walletClient.getPublicKey({ identityKey: true });
192
+ this.myIdentityKey = keyResult.publicKey;
193
+ logger_js_1.Logger.log(`[MB CLIENT] Identity key fetched: ${this.myIdentityKey}`);
194
+ return this.myIdentityKey;
195
+ }
196
+ catch (error) {
197
+ logger_js_1.Logger.error('[MB CLIENT ERROR] Failed to fetch identity key:', error);
198
+ throw new Error('Identity key retrieval failed');
199
+ }
200
+ }
201
+ /**
202
+ * @property testSocket
203
+ * @readonly
204
+ * @returns {AuthSocketClient | undefined} The internal WebSocket client (or undefined if not connected).
205
+ * @description
206
+ * Exposes the underlying Authenticated WebSocket client used for live messaging.
207
+ * This is primarily intended for debugging, test frameworks, or direct inspection.
208
+ *
209
+ * Note: Do not interact with the socket directly unless necessary.
210
+ * Use the provided `sendLiveMessage`, `listenForLiveMessages`, and related methods.
211
+ */
212
+ get testSocket() {
213
+ return this.socket;
214
+ }
215
+ /**
216
+ * @method initializeConnection
217
+ * @async
218
+ * @returns {Promise<void>}
219
+ * @description
220
+ * Establishes an authenticated WebSocket connection to the configured MessageBox server.
221
+ * Enables live message streaming via room-based channels tied to identity keys.
222
+ *
223
+ * This method:
224
+ * 1. Retrieves the user’s identity key if not already set
225
+ * 2. Initializes a secure AuthSocketClient WebSocket connection
226
+ * 3. Authenticates the connection using the identity key
227
+ * 4. Waits up to 5 seconds for authentication confirmation
228
+ *
229
+ * If authentication fails or times out, the connection is rejected.
230
+ *
231
+ * @throws {Error} If the identity key is unavailable or authentication fails
232
+ *
233
+ * @example
234
+ * const mb = new MessageBoxClient({ walletClient })
235
+ * await mb.initializeConnection()
236
+ * // WebSocket is now ready for use
237
+ */
238
+ async initializeConnection() {
239
+ await this.assertInitialized();
240
+ logger_js_1.Logger.log('[MB CLIENT] initializeConnection() STARTED');
241
+ if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
242
+ logger_js_1.Logger.log('[MB CLIENT] Fetching identity key...');
243
+ try {
244
+ const keyResult = await this.walletClient.getPublicKey({ identityKey: true });
245
+ this.myIdentityKey = keyResult.publicKey;
246
+ logger_js_1.Logger.log(`[MB CLIENT] Identity key fetched successfully: ${this.myIdentityKey}`);
247
+ }
248
+ catch (error) {
249
+ logger_js_1.Logger.error('[MB CLIENT ERROR] Failed to fetch identity key:', error);
250
+ throw new Error('Identity key retrieval failed');
251
+ }
252
+ }
253
+ if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
254
+ logger_js_1.Logger.error('[MB CLIENT ERROR] Identity key is still missing after retrieval!');
255
+ throw new Error('Identity key is missing');
256
+ }
257
+ logger_js_1.Logger.log('[MB CLIENT] Setting up WebSocket connection...');
258
+ if (this.socket == null) {
259
+ if (typeof this.host !== 'string' || this.host.trim() === '') {
260
+ throw new Error('Cannot initialize WebSocket: Host is not set');
261
+ }
262
+ this.socket = (0, authsocket_client_1.AuthSocketClient)(this.host, { wallet: this.walletClient });
263
+ let identitySent = false;
264
+ let authenticated = false;
265
+ this.socket.on('connect', () => {
266
+ var _a;
267
+ logger_js_1.Logger.log('[MB CLIENT] Connected to WebSocket.');
268
+ if (!identitySent) {
269
+ logger_js_1.Logger.log('[MB CLIENT] Sending authentication data:', this.myIdentityKey);
270
+ if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
271
+ logger_js_1.Logger.error('[MB CLIENT ERROR] Cannot send authentication: Identity key is missing!');
272
+ }
273
+ else {
274
+ (_a = this.socket) === null || _a === void 0 ? void 0 : _a.emit('authenticated', { identityKey: this.myIdentityKey });
275
+ identitySent = true;
276
+ }
277
+ }
278
+ });
279
+ // Listen for authentication success from the server
280
+ this.socket.on('authenticationSuccess', (data) => {
281
+ logger_js_1.Logger.log(`[MB CLIENT] WebSocket authentication successful: ${JSON.stringify(data)}`);
282
+ authenticated = true;
283
+ });
284
+ // Handle authentication failures
285
+ this.socket.on('authenticationFailed', (data) => {
286
+ logger_js_1.Logger.error(`[MB CLIENT ERROR] WebSocket authentication failed: ${JSON.stringify(data)}`);
287
+ authenticated = false;
288
+ });
289
+ this.socket.on('disconnect', () => {
290
+ logger_js_1.Logger.log('[MB CLIENT] Disconnected from MessageBox server');
291
+ this.socket = undefined;
292
+ identitySent = false;
293
+ authenticated = false;
294
+ });
295
+ this.socket.on('error', (error) => {
296
+ logger_js_1.Logger.error('[MB CLIENT ERROR] WebSocket error:', error);
297
+ });
298
+ // Wait for authentication confirmation before proceeding
299
+ await new Promise((resolve, reject) => {
300
+ setTimeout(() => {
301
+ if (authenticated) {
302
+ logger_js_1.Logger.log('[MB CLIENT] WebSocket fully authenticated and ready!');
303
+ resolve();
304
+ }
305
+ else {
306
+ reject(new Error('[MB CLIENT ERROR] WebSocket authentication timed out!'));
307
+ }
308
+ }, 5000); // Timeout after 5 seconds
309
+ });
310
+ }
311
+ }
312
+ /**
313
+ * @method resolveHostForRecipient
314
+ * @async
315
+ * @param {string} identityKey - The public identity key of the intended recipient.
316
+ * @returns {Promise<string>} - A fully qualified host URL for the recipient's MessageBox server.
317
+ *
318
+ * @description
319
+ * Attempts to resolve the most recently anointed MessageBox host for the given identity key
320
+ * using the BSV overlay network and the `ls_messagebox` LookupResolver.
321
+ *
322
+ * If no advertisements are found, or if resolution fails, the client will fall back
323
+ * to its own configured `host`. This allows seamless operation in both overlay and non-overlay environments.
324
+ *
325
+ * This method guarantees a non-null return value and should be used directly when routing messages.
326
+ *
327
+ * @example
328
+ * const host = await resolveHostForRecipient('028d...') // → returns either overlay host or this.host
329
+ */
330
+ async resolveHostForRecipient(identityKey) {
331
+ const advertisementTokens = await this.queryAdvertisements(identityKey);
332
+ if (advertisementTokens.length === 0) {
333
+ logger_js_1.Logger.warn(`[MB CLIENT] No advertisements for ${identityKey}, using default host ${this.host}`);
334
+ return this.host;
335
+ }
336
+ // Return the first host found
337
+ return advertisementTokens[0].host;
338
+ }
339
+ /**
340
+ * Core lookup: ask the LookupResolver (optionally filtered by host),
341
+ * decode every PushDrop output, and collect all the host URLs you find.
342
+ *
343
+ * @param identityKey the recipient’s public key
344
+ * @param host? if passed, only look for adverts anointed at that host
345
+ * @returns 0-length array if nothing valid was found
346
+ */
347
+ async queryAdvertisements(identityKey, host) {
348
+ const hosts = [];
349
+ try {
350
+ const query = { identityKey: identityKey !== null && identityKey !== void 0 ? identityKey : await this.getIdentityKey() };
351
+ if (host != null && host.trim() !== '')
352
+ query.host = host;
353
+ const result = await this.lookupResolver.query({
354
+ service: 'ls_messagebox',
355
+ query
356
+ });
357
+ if (result.type !== 'output-list') {
358
+ throw new Error(`Unexpected result type: ${result.type}`);
359
+ }
360
+ for (const output of result.outputs) {
361
+ try {
362
+ const tx = sdk_1.Transaction.fromBEEF(output.beef);
363
+ const script = tx.outputs[output.outputIndex].lockingScript;
364
+ const token = sdk_1.PushDrop.decode(script);
365
+ const [, hostBuf] = token.fields;
366
+ if (hostBuf == null || hostBuf.length === 0) {
367
+ throw new Error('Empty host field');
368
+ }
369
+ hosts.push({
370
+ host: sdk_1.Utils.toUTF8(hostBuf),
371
+ txid: tx.id('hex'),
372
+ outputIndex: output.outputIndex,
373
+ lockingScript: script,
374
+ beef: output.beef
375
+ });
376
+ }
377
+ catch {
378
+ // skip any malformed / non-PushDrop outputs
379
+ }
380
+ }
381
+ }
382
+ catch (err) {
383
+ logger_js_1.Logger.error('[MB CLIENT ERROR] _queryAdvertisements failed:', err);
384
+ }
385
+ return hosts;
386
+ }
387
+ /**
388
+ * @method joinRoom
389
+ * @async
390
+ * @param {string} messageBox - The name of the WebSocket room to join (e.g., "payment_inbox").
391
+ * @returns {Promise<void>}
392
+ *
393
+ * @description
394
+ * Joins a WebSocket room that corresponds to the user’s identity key and the specified message box.
395
+ * This is required to receive real-time messages via WebSocket for a specific type of communication.
396
+ *
397
+ * If the WebSocket connection is not already established, this method will first initialize the connection.
398
+ * It also ensures the room is only joined once, and tracks all joined rooms in an internal set.
399
+ *
400
+ * Room ID format: `${identityKey}-${messageBox}`
401
+ *
402
+ * @example
403
+ * await client.joinRoom('payment_inbox')
404
+ * // Now listening for real-time messages in room '028d...-payment_inbox'
405
+ */
406
+ async joinRoom(messageBox) {
407
+ var _a, _b;
408
+ await this.assertInitialized();
409
+ logger_js_1.Logger.log(`[MB CLIENT] Attempting to join WebSocket room: ${messageBox}`);
410
+ // Ensure WebSocket connection is established first
411
+ if (this.socket == null) {
412
+ logger_js_1.Logger.log('[MB CLIENT] No WebSocket connection. Initializing...');
413
+ await this.initializeConnection();
414
+ }
415
+ if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
416
+ throw new Error('[MB CLIENT ERROR] Identity key is not defined');
417
+ }
418
+ const roomId = `${(_a = this.myIdentityKey) !== null && _a !== void 0 ? _a : ''}-${messageBox}`;
419
+ if (this.joinedRooms.has(roomId)) {
420
+ logger_js_1.Logger.log(`[MB CLIENT] Already joined WebSocket room: ${roomId}`);
421
+ return;
422
+ }
423
+ try {
424
+ logger_js_1.Logger.log(`[MB CLIENT] Joining WebSocket room: ${roomId}`);
425
+ await ((_b = this.socket) === null || _b === void 0 ? void 0 : _b.emit('joinRoom', roomId));
426
+ this.joinedRooms.add(roomId);
427
+ logger_js_1.Logger.log(`[MB CLIENT] Successfully joined room: ${roomId}`);
428
+ }
429
+ catch (error) {
430
+ logger_js_1.Logger.error(`[MB CLIENT ERROR] Failed to join WebSocket room: ${roomId}`, error);
431
+ }
432
+ }
433
+ /**
434
+ * @method listenForLiveMessages
435
+ * @async
436
+ * @param {Object} params - Configuration for the live message listener.
437
+ * @param {function} params.onMessage - A callback function that will be triggered when a new message arrives.
438
+ * @param {string} params.messageBox - The messageBox name (e.g., `payment_inbox`) to listen for.
439
+ * @returns {Promise<void>}
440
+ *
441
+ * @description
442
+ * Subscribes the client to live messages over WebSocket for a specific messageBox.
443
+ *
444
+ * This method:
445
+ * - Ensures the WebSocket connection is initialized and authenticated.
446
+ * - Joins the correct room formatted as `${identityKey}-${messageBox}`.
447
+ * - Listens for messages broadcast to the room.
448
+ * - Automatically attempts to parse and decrypt message bodies.
449
+ * - Emits the final message (as a `PeerMessage`) to the supplied `onMessage` handler.
450
+ *
451
+ * If the incoming message is encrypted, the client decrypts it using AES-256-GCM via
452
+ * ECDH shared secrets derived from identity keys as defined in [BRC-2](https://github.com/bitcoin-sv/BRCs/blob/master/wallet/0002.md).
453
+ * Messages sent by the client to itself are decrypted using `counterparty = 'self'`.
454
+ *
455
+ * @example
456
+ * await client.listenForLiveMessages({
457
+ * messageBox: 'payment_inbox',
458
+ * onMessage: (msg) => console.log('Received live message:', msg)
459
+ * })
460
+ */
461
+ async listenForLiveMessages({ onMessage, messageBox }) {
462
+ var _a;
463
+ await this.assertInitialized();
464
+ logger_js_1.Logger.log(`[MB CLIENT] Setting up listener for WebSocket room: ${messageBox}`);
465
+ // Ensure WebSocket connection and room join
466
+ await this.joinRoom(messageBox);
467
+ // Ensure identity key is available before creating roomId
468
+ if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
469
+ throw new Error('[MB CLIENT ERROR] Identity key is missing. Cannot construct room ID.');
470
+ }
471
+ const roomId = `${this.myIdentityKey}-${messageBox}`;
472
+ logger_js_1.Logger.log(`[MB CLIENT] Listening for messages in room: ${roomId}`);
473
+ (_a = this.socket) === null || _a === void 0 ? void 0 : _a.on(`sendMessage-${roomId}`, (message) => {
474
+ void (async () => {
475
+ logger_js_1.Logger.log(`[MB CLIENT] Received message in room ${roomId}:`, message);
476
+ try {
477
+ let parsedBody = message.body;
478
+ if (typeof parsedBody === 'string') {
479
+ try {
480
+ parsedBody = JSON.parse(parsedBody);
481
+ }
482
+ catch {
483
+ // Leave it as-is (plain text)
484
+ }
485
+ }
486
+ if (parsedBody != null &&
487
+ typeof parsedBody === 'object' &&
488
+ typeof parsedBody.encryptedMessage === 'string') {
489
+ logger_js_1.Logger.log(`[MB CLIENT] Decrypting message from ${String(message.sender)}...`);
490
+ const decrypted = await this.walletClient.decrypt({
491
+ protocolID: [1, 'messagebox'],
492
+ keyID: '1',
493
+ counterparty: message.sender,
494
+ ciphertext: sdk_1.Utils.toArray(parsedBody.encryptedMessage, 'base64')
495
+ });
496
+ message.body = sdk_1.Utils.toUTF8(decrypted.plaintext);
497
+ }
498
+ else {
499
+ logger_js_1.Logger.log('[MB CLIENT] Message is not encrypted.');
500
+ message.body = typeof parsedBody === 'string'
501
+ ? parsedBody
502
+ : (() => { try {
503
+ return JSON.stringify(parsedBody);
504
+ }
505
+ catch {
506
+ return '[Error: Unstringifiable message]';
507
+ } })();
508
+ }
509
+ }
510
+ catch (err) {
511
+ logger_js_1.Logger.error('[MB CLIENT ERROR] Failed to parse or decrypt live message:', err);
512
+ message.body = '[Error: Failed to decrypt or parse message]';
513
+ }
514
+ onMessage(message);
515
+ })();
516
+ });
517
+ }
518
+ /**
519
+ * @method sendLiveMessage
520
+ * @async
521
+ * @param {SendMessageParams} param0 - The message parameters including recipient, box name, body, and options.
522
+ * @returns {Promise<SendMessageResponse>} A success response with the generated messageId.
523
+ *
524
+ * @description
525
+ * Sends a message in real time using WebSocket with authenticated delivery and overlay fallback.
526
+ *
527
+ * This method:
528
+ * - Ensures the WebSocket connection is open and joins the correct room.
529
+ * - Derives a unique message ID using an HMAC of the message body and counterparty identity key.
530
+ * - 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),
531
+ * unless `skipEncryption` is explicitly set to `true`.
532
+ * - Sends the message to a WebSocket room in the format `${recipient}-${messageBox}`.
533
+ * - Waits for acknowledgment (`sendMessageAck-${roomId}`).
534
+ * - If no acknowledgment is received within 10 seconds, falls back to `sendMessage()` over HTTP.
535
+ *
536
+ * This hybrid delivery strategy ensures reliability in both real-time and offline-capable environments.
537
+ *
538
+ * @throws {Error} If message validation fails, HMAC generation fails, or both WebSocket and HTTP fail to deliver.
539
+ *
540
+ * @example
541
+ * await client.sendLiveMessage({
542
+ * recipient: '028d...',
543
+ * messageBox: 'payment_inbox',
544
+ * body: { amount: 1000 }
545
+ * })
546
+ */
547
+ async sendLiveMessage({ recipient, messageBox, body, messageId, skipEncryption }) {
548
+ await this.assertInitialized();
549
+ if (recipient == null || recipient.trim() === '') {
550
+ throw new Error('[MB CLIENT ERROR] Recipient identity key is required');
551
+ }
552
+ if (messageBox == null || messageBox.trim() === '') {
553
+ throw new Error('[MB CLIENT ERROR] MessageBox is required');
554
+ }
555
+ if (body == null || (typeof body === 'string' && body.trim() === '')) {
556
+ throw new Error('[MB CLIENT ERROR] Message body cannot be empty');
557
+ }
558
+ // Ensure room is joined before sending
559
+ await this.joinRoom(messageBox);
560
+ // Fallback to HTTP if WebSocket is not connected
561
+ if (this.socket == null || !this.socket.connected) {
562
+ logger_js_1.Logger.warn('[MB CLIENT WARNING] WebSocket not connected, falling back to HTTP');
563
+ const targetHost = await this.resolveHostForRecipient(recipient);
564
+ return await this.sendMessage({ recipient, messageBox, body }, targetHost);
565
+ }
566
+ let finalMessageId;
567
+ try {
568
+ const hmac = await this.walletClient.createHmac({
569
+ data: Array.from(new TextEncoder().encode(JSON.stringify(body))),
570
+ protocolID: [1, 'messagebox'],
571
+ keyID: '1',
572
+ counterparty: recipient
573
+ });
574
+ finalMessageId = messageId !== null && messageId !== void 0 ? messageId : Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join('');
575
+ }
576
+ catch (error) {
577
+ logger_js_1.Logger.error('[MB CLIENT ERROR] Failed to generate HMAC:', error);
578
+ throw new Error('Failed to generate message identifier.');
579
+ }
580
+ const roomId = `${recipient}-${messageBox}`;
581
+ logger_js_1.Logger.log(`[MB CLIENT] Sending WebSocket message to room: ${roomId}`);
582
+ let outgoingBody;
583
+ if (skipEncryption === true) {
584
+ outgoingBody = typeof body === 'string' ? body : JSON.stringify(body);
585
+ }
586
+ else {
587
+ const encryptedMessage = await this.walletClient.encrypt({
588
+ protocolID: [1, 'messagebox'],
589
+ keyID: '1',
590
+ counterparty: recipient,
591
+ plaintext: sdk_1.Utils.toArray(typeof body === 'string' ? body : JSON.stringify(body), 'utf8')
592
+ });
593
+ outgoingBody = JSON.stringify({
594
+ encryptedMessage: sdk_1.Utils.toBase64(encryptedMessage.ciphertext)
595
+ });
596
+ }
597
+ return await new Promise((resolve, reject) => {
598
+ var _a, _b;
599
+ const ackEvent = `sendMessageAck-${roomId}`;
600
+ let handled = false;
601
+ const ackHandler = (response) => {
602
+ if (handled)
603
+ return;
604
+ handled = true;
605
+ const socketAny = this.socket;
606
+ if (typeof (socketAny === null || socketAny === void 0 ? void 0 : socketAny.off) === 'function') {
607
+ socketAny.off(ackEvent, ackHandler);
608
+ }
609
+ logger_js_1.Logger.log('[MB CLIENT] Received WebSocket acknowledgment:', response);
610
+ if (response == null || response.status !== 'success') {
611
+ logger_js_1.Logger.warn('[MB CLIENT] WebSocket message failed or returned unexpected response. Falling back to HTTP.');
612
+ const fallbackMessage = {
613
+ recipient,
614
+ messageBox,
615
+ body,
616
+ messageId: finalMessageId,
617
+ skipEncryption
618
+ };
619
+ this.resolveHostForRecipient(recipient)
620
+ .then(async (host) => {
621
+ return await this.sendMessage(fallbackMessage, host);
622
+ })
623
+ .then(resolve)
624
+ .catch(reject);
625
+ }
626
+ else {
627
+ logger_js_1.Logger.log('[MB CLIENT] Message sent successfully via WebSocket:', response);
628
+ resolve(response);
629
+ }
630
+ };
631
+ // Attach acknowledgment listener
632
+ (_a = this.socket) === null || _a === void 0 ? void 0 : _a.on(ackEvent, ackHandler);
633
+ // Emit message to room
634
+ (_b = this.socket) === null || _b === void 0 ? void 0 : _b.emit('sendMessage', {
635
+ roomId,
636
+ message: {
637
+ messageId: finalMessageId,
638
+ recipient,
639
+ body: outgoingBody
640
+ }
641
+ });
642
+ // Timeout: Fallback to HTTP if no acknowledgment received
643
+ setTimeout(() => {
644
+ if (!handled) {
645
+ handled = true;
646
+ const socketAny = this.socket;
647
+ if (typeof (socketAny === null || socketAny === void 0 ? void 0 : socketAny.off) === 'function') {
648
+ socketAny.off(ackEvent, ackHandler);
649
+ }
650
+ logger_js_1.Logger.warn('[CLIENT] WebSocket acknowledgment timed out, falling back to HTTP');
651
+ const fallbackMessage = {
652
+ recipient,
653
+ messageBox,
654
+ body,
655
+ messageId: finalMessageId,
656
+ skipEncryption
657
+ };
658
+ this.resolveHostForRecipient(recipient)
659
+ .then(async (host) => {
660
+ return await this.sendMessage(fallbackMessage, host);
661
+ })
662
+ .then(resolve)
663
+ .catch(reject);
664
+ }
665
+ }, 10000);
666
+ });
667
+ }
668
+ /**
669
+ * @method leaveRoom
670
+ * @async
671
+ * @param {string} messageBox - The name of the WebSocket room to leave (e.g., `payment_inbox`).
672
+ * @returns {Promise<void>}
673
+ *
674
+ * @description
675
+ * Leaves a previously joined WebSocket room associated with the authenticated identity key.
676
+ * This helps reduce unnecessary message traffic and memory usage.
677
+ *
678
+ * If the WebSocket is not connected or the identity key is missing, the method exits gracefully.
679
+ *
680
+ * @example
681
+ * await client.leaveRoom('payment_inbox')
682
+ */
683
+ async leaveRoom(messageBox) {
684
+ await this.assertInitialized();
685
+ if (this.socket == null) {
686
+ logger_js_1.Logger.warn('[MB CLIENT] Attempted to leave a room but WebSocket is not connected.');
687
+ return;
688
+ }
689
+ if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
690
+ throw new Error('[MB CLIENT ERROR] Identity key is not defined');
691
+ }
692
+ const roomId = `${this.myIdentityKey}-${messageBox}`;
693
+ logger_js_1.Logger.log(`[MB CLIENT] Leaving WebSocket room: ${roomId}`);
694
+ this.socket.emit('leaveRoom', roomId);
695
+ // Ensure the room is removed from tracking
696
+ this.joinedRooms.delete(roomId);
697
+ }
698
+ /**
699
+ * @method disconnectWebSocket
700
+ * @async
701
+ * @returns {Promise<void>} Resolves when the WebSocket connection is successfully closed.
702
+ *
703
+ * @description
704
+ * Gracefully disconnects the WebSocket connection to the MessageBox server.
705
+ * This should be called when the client is shutting down, logging out, or no longer
706
+ * needs real-time communication to conserve system resources.
707
+ *
708
+ * @example
709
+ * await client.disconnectWebSocket()
710
+ */
711
+ async disconnectWebSocket() {
712
+ await this.assertInitialized();
713
+ if (this.socket != null) {
714
+ logger_js_1.Logger.log('[MB CLIENT] Closing WebSocket connection...');
715
+ this.socket.disconnect();
716
+ this.socket = undefined;
717
+ }
718
+ else {
719
+ logger_js_1.Logger.log('[MB CLIENT] No active WebSocket connection to close.');
720
+ }
721
+ }
722
+ /**
723
+ * @method sendMessage
724
+ * @async
725
+ * @param {SendMessageParams} message - Contains recipient, messageBox name, message body, optional messageId, and skipEncryption flag.
726
+ * @param {string} [overrideHost] - Optional host to override overlay resolution (useful for testing or private routing).
727
+ * @returns {Promise<SendMessageResponse>} - Resolves with `{ status, messageId }` on success.
728
+ *
729
+ * @description
730
+ * Sends a message over HTTP to a recipient's messageBox. This method:
731
+ *
732
+ * - Derives a deterministic `messageId` using an HMAC of the message body and recipient key.
733
+ * - 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.
734
+ * - Automatically resolves the host via overlay LookupResolver unless an override is provided.
735
+ * - Authenticates the request using the current identity key with `AuthFetch`.
736
+ *
737
+ * This is the fallback mechanism for `sendLiveMessage` when WebSocket delivery fails.
738
+ * It is also used for message types that do not require real-time delivery.
739
+ *
740
+ * @throws {Error} If validation, encryption, HMAC, or network request fails.
741
+ *
742
+ * @example
743
+ * await client.sendMessage({
744
+ * recipient: '03abc...',
745
+ * messageBox: 'notifications',
746
+ * body: { type: 'ping' }
747
+ * })
748
+ */
749
+ async sendMessage(message, overrideHost) {
750
+ var _a, _b;
751
+ await this.assertInitialized();
752
+ if (message.recipient == null || message.recipient.trim() === '') {
753
+ throw new Error('You must provide a message recipient!');
754
+ }
755
+ if (message.messageBox == null || message.messageBox.trim() === '') {
756
+ throw new Error('You must provide a messageBox to send this message into!');
757
+ }
758
+ if (message.body == null || (typeof message.body === 'string' && message.body.trim().length === 0)) {
759
+ throw new Error('Every message must have a body!');
760
+ }
761
+ let messageId;
762
+ try {
763
+ const hmac = await this.walletClient.createHmac({
764
+ data: Array.from(new TextEncoder().encode(JSON.stringify(message.body))),
765
+ protocolID: [1, 'messagebox'],
766
+ keyID: '1',
767
+ counterparty: message.recipient
768
+ });
769
+ messageId = (_a = message.messageId) !== null && _a !== void 0 ? _a : Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join('');
770
+ }
771
+ catch (error) {
772
+ logger_js_1.Logger.error('[MB CLIENT ERROR] Failed to generate HMAC:', error);
773
+ throw new Error('Failed to generate message identifier.');
774
+ }
775
+ let finalBody;
776
+ if (message.skipEncryption === true) {
777
+ finalBody = typeof message.body === 'string' ? message.body : JSON.stringify(message.body);
778
+ }
779
+ else {
780
+ const encryptedMessage = await this.walletClient.encrypt({
781
+ protocolID: [1, 'messagebox'],
782
+ keyID: '1',
783
+ counterparty: message.recipient,
784
+ plaintext: sdk_1.Utils.toArray(typeof message.body === 'string' ? message.body : JSON.stringify(message.body), 'utf8')
785
+ });
786
+ finalBody = JSON.stringify({ encryptedMessage: sdk_1.Utils.toBase64(encryptedMessage.ciphertext) });
787
+ }
788
+ const requestBody = {
789
+ message: {
790
+ ...message,
791
+ messageId,
792
+ body: finalBody
793
+ }
794
+ };
795
+ try {
796
+ const finalHost = overrideHost !== null && overrideHost !== void 0 ? overrideHost : await this.resolveHostForRecipient(message.recipient);
797
+ logger_js_1.Logger.log('[MB CLIENT] Sending HTTP request to:', `${finalHost}/sendMessage`);
798
+ logger_js_1.Logger.log('[MB CLIENT] Request Body:', JSON.stringify(requestBody, null, 2));
799
+ if (this.myIdentityKey == null || this.myIdentityKey === '') {
800
+ try {
801
+ const keyResult = await this.walletClient.getPublicKey({ identityKey: true });
802
+ this.myIdentityKey = keyResult.publicKey;
803
+ logger_js_1.Logger.log(`[MB CLIENT] Fetched identity key before sending request: ${this.myIdentityKey}`);
804
+ }
805
+ catch (error) {
806
+ logger_js_1.Logger.error('[MB CLIENT ERROR] Failed to fetch identity key:', error);
807
+ throw new Error('Identity key retrieval failed');
808
+ }
809
+ }
810
+ const response = await this.authFetch.fetch(`${finalHost}/sendMessage`, {
811
+ method: 'POST',
812
+ headers: {
813
+ 'Content-Type': 'application/json'
814
+ },
815
+ body: JSON.stringify(requestBody)
816
+ });
817
+ if (response.bodyUsed) {
818
+ throw new Error('[MB CLIENT ERROR] Response body has already been used!');
819
+ }
820
+ const parsedResponse = await response.json();
821
+ logger_js_1.Logger.log('[MB CLIENT] Raw Response Body:', parsedResponse);
822
+ if (!response.ok) {
823
+ logger_js_1.Logger.error(`[MB CLIENT ERROR] Failed to send message. HTTP ${response.status}: ${response.statusText}`);
824
+ throw new Error(`Message sending failed: HTTP ${response.status} - ${response.statusText}`);
825
+ }
826
+ if (parsedResponse.status !== 'success') {
827
+ logger_js_1.Logger.error(`[MB CLIENT ERROR] Server returned an error: ${String(parsedResponse.description)}`);
828
+ throw new Error((_b = parsedResponse.description) !== null && _b !== void 0 ? _b : 'Unknown error from server.');
829
+ }
830
+ logger_js_1.Logger.log('[MB CLIENT] Message successfully sent.');
831
+ return { ...parsedResponse, messageId };
832
+ }
833
+ catch (error) {
834
+ logger_js_1.Logger.error('[MB CLIENT ERROR] Network or timeout error:', error);
835
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
836
+ throw new Error(`Failed to send message: ${errorMessage}`);
837
+ }
838
+ }
839
+ /**
840
+ * @method anointHost
841
+ * @async
842
+ * @param {string} host - The full URL of the server you want to designate as your MessageBox host (e.g., "https://mybox.com").
843
+ * @returns {Promise<{ txid: string }>} - The transaction ID of the advertisement broadcast to the overlay network.
844
+ *
845
+ * @description
846
+ * Broadcasts a signed overlay advertisement using a PushDrop output under the `tm_messagebox` topic.
847
+ * This advertisement announces that the specified `host` is now authorized to receive and route
848
+ * messages for the sender’s identity key.
849
+ *
850
+ * The broadcasted message includes:
851
+ * - The identity key
852
+ * - The chosen host URL
853
+ *
854
+ * This is essential for enabling overlay-based message delivery via SHIP and LookupResolver.
855
+ * The recipient’s host must advertise itself for message routing to succeed in a decentralized manner.
856
+ *
857
+ * @throws {Error} If the URL is invalid, the PushDrop creation fails, or the overlay broadcast does not succeed.
858
+ *
859
+ * @example
860
+ * const { txid } = await client.anointHost('https://my-messagebox.io')
861
+ */
862
+ async anointHost(host) {
863
+ logger_js_1.Logger.log('[MB CLIENT] Starting anointHost...');
864
+ try {
865
+ if (!host.startsWith('http')) {
866
+ throw new Error('Invalid host URL');
867
+ }
868
+ const identityKey = await this.getIdentityKey();
869
+ logger_js_1.Logger.log('[MB CLIENT] Fields - Identity:', identityKey, 'Host:', host);
870
+ const fields = [
871
+ sdk_1.Utils.toArray(identityKey, 'hex'),
872
+ sdk_1.Utils.toArray(host, 'utf8')
873
+ ];
874
+ const pushdrop = new sdk_1.PushDrop(this.walletClient);
875
+ logger_js_1.Logger.log('Fields:', fields.map(a => sdk_1.Utils.toHex(a)));
876
+ logger_js_1.Logger.log('ProtocolID:', [1, 'messagebox advertisement']);
877
+ logger_js_1.Logger.log('KeyID:', '1');
878
+ logger_js_1.Logger.log('SignAs:', 'self');
879
+ logger_js_1.Logger.log('anyoneCanSpend:', false);
880
+ logger_js_1.Logger.log('forSelf:', true);
881
+ const script = await pushdrop.lock(fields, [1, 'messagebox advertisement'], '1', 'anyone', true);
882
+ logger_js_1.Logger.log('[MB CLIENT] PushDrop script:', script.toASM());
883
+ const { tx, txid } = await this.walletClient.createAction({
884
+ description: 'Anoint host for overlay routing',
885
+ outputs: [{
886
+ basket: 'overlay advertisements',
887
+ lockingScript: script.toHex(),
888
+ satoshis: 1,
889
+ outputDescription: 'Overlay advertisement output'
890
+ }],
891
+ options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
892
+ });
893
+ logger_js_1.Logger.log('[MB CLIENT] Transaction created:', txid);
894
+ if (tx !== undefined) {
895
+ const broadcaster = new sdk_1.TopicBroadcaster(['tm_messagebox'], {
896
+ networkPreset: this.networkPreset
897
+ });
898
+ const result = await broadcaster.broadcast(sdk_1.Transaction.fromAtomicBEEF(tx));
899
+ logger_js_1.Logger.log('[MB CLIENT] Advertisement broadcast succeeded. TXID:', result.txid);
900
+ if (typeof result.txid !== 'string') {
901
+ throw new Error('Anoint failed: broadcast did not return a txid');
902
+ }
903
+ return { txid: result.txid };
904
+ }
905
+ throw new Error('Anoint failed: failed to create action!');
906
+ }
907
+ catch (err) {
908
+ logger_js_1.Logger.error('[MB CLIENT ERROR] anointHost threw:', err);
909
+ throw err;
910
+ }
911
+ }
912
+ /**
913
+ * @method revokeHostAdvertisement
914
+ * @async
915
+ * @param {AdvertisementToken} advertisementToken - The advertisement token containing the messagebox host to revoke.
916
+ * @returns {Promise<{ txid: string }>} - The transaction ID of the revocation broadcast to the overlay network.
917
+ *
918
+ * @description
919
+ * Broadcasts a signed revocation transaction indicating the advertisement token should be removed
920
+ * and no longer tracked by lookup services.
921
+ *
922
+ * @example
923
+ * const { txid } = await client.revokeHost('https://my-messagebox.io')
924
+ */
925
+ async revokeHostAdvertisement(advertisementToken) {
926
+ logger_js_1.Logger.log('[MB CLIENT] Starting revokeHost...');
927
+ const outpoint = `${advertisementToken.txid}.${advertisementToken.outputIndex}`;
928
+ try {
929
+ const { signableTransaction } = await this.walletClient.createAction({
930
+ description: 'Revoke MessageBox host advertisement',
931
+ inputBEEF: advertisementToken.beef,
932
+ inputs: [
933
+ {
934
+ outpoint,
935
+ unlockingScriptLength: 73,
936
+ inputDescription: 'Revoking host advertisement token'
937
+ }
938
+ ]
939
+ });
940
+ if (signableTransaction === undefined) {
941
+ throw new Error('Failed to create signable transaction.');
942
+ }
943
+ const partialTx = sdk_1.Transaction.fromBEEF(signableTransaction.tx);
944
+ // Prepare the unlocker
945
+ const pushdrop = new sdk_1.PushDrop(this.walletClient);
946
+ const unlocker = await pushdrop.unlock([1, 'messagebox advertisement'], '1', 'anyone', 'all', false, advertisementToken.outputIndex, advertisementToken.lockingScript);
947
+ // Convert to Transaction, apply signature
948
+ const finalUnlockScript = await unlocker.sign(partialTx, advertisementToken.outputIndex);
949
+ // Complete signing with the final unlock script
950
+ const { tx: signedTx } = await this.walletClient.signAction({
951
+ reference: signableTransaction.reference,
952
+ spends: {
953
+ [advertisementToken.outputIndex]: {
954
+ unlockingScript: finalUnlockScript.toHex()
955
+ }
956
+ },
957
+ options: {
958
+ acceptDelayedBroadcast: false
959
+ }
960
+ });
961
+ if (signedTx === undefined) {
962
+ throw new Error('Failed to finalize the transaction signature.');
963
+ }
964
+ const broadcaster = new sdk_1.TopicBroadcaster(['tm_messagebox'], {
965
+ networkPreset: this.networkPreset
966
+ });
967
+ const result = await broadcaster.broadcast(sdk_1.Transaction.fromAtomicBEEF(signedTx));
968
+ logger_js_1.Logger.log('[MB CLIENT] Revocation broadcast succeeded. TXID:', result.txid);
969
+ if (typeof result.txid !== 'string') {
970
+ throw new Error('Revoke failed: broadcast did not return a txid');
971
+ }
972
+ return { txid: result.txid };
973
+ }
974
+ catch (err) {
975
+ logger_js_1.Logger.error('[MB CLIENT ERROR] revokeHost threw:', err);
976
+ throw err;
977
+ }
978
+ }
979
+ /**
980
+ * @method listMessages
981
+ * @async
982
+ * @param {ListMessagesParams} params - Contains the name of the messageBox to read from.
983
+ * @returns {Promise<PeerMessage[]>} - Returns an array of decrypted `PeerMessage` objects.
984
+ *
985
+ * @description
986
+ * Retrieves all messages from the specified `messageBox` assigned to the current identity key.
987
+ * 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.
988
+ *
989
+ * Each message is:
990
+ * - Parsed and, if encrypted, decrypted using AES-256-GCM via BRC-2-compliant ECDH key derivation and symmetric encryption.
991
+ * - Returned as a normalized `PeerMessage` with readable string body content.
992
+ *
993
+ * Decryption automatically derives a shared secret using the sender’s identity key and the receiver’s child private key.
994
+ * If the sender is the same as the recipient, the `counterparty` is set to `'self'`.
995
+ *
996
+ * @throws {Error} If no messageBox is specified, the request fails, or the server returns an error.
997
+ *
998
+ * @example
999
+ * const messages = await client.listMessages({ messageBox: 'inbox' })
1000
+ * messages.forEach(msg => console.log(msg.sender, msg.body))
1001
+ */
1002
+ async listMessages({ messageBox, host }) {
1003
+ await this.assertInitialized();
1004
+ if (messageBox.trim() === '') {
1005
+ throw new Error('MessageBox cannot be empty');
1006
+ }
1007
+ let hosts = host != null ? [host] : [];
1008
+ if (hosts.length === 0) {
1009
+ const advertisedHosts = await this.queryAdvertisements(await this.getIdentityKey());
1010
+ hosts = Array.from(new Set([this.host, ...advertisedHosts.map(h => h.host)]));
1011
+ }
1012
+ // Query each host in parallel
1013
+ const fetchFromHost = async (host) => {
1014
+ var _a;
1015
+ try {
1016
+ logger_js_1.Logger.log(`[MB CLIENT] Listing messages from ${host}…`);
1017
+ const res = await this.authFetch.fetch(`${host}/listMessages`, {
1018
+ method: 'POST',
1019
+ headers: { 'Content-Type': 'application/json' },
1020
+ body: JSON.stringify({ messageBox })
1021
+ });
1022
+ if (!res.ok)
1023
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
1024
+ const data = await res.json();
1025
+ if (data.status === 'error')
1026
+ throw new Error((_a = data.description) !== null && _a !== void 0 ? _a : 'Unknown server error');
1027
+ return data.messages;
1028
+ }
1029
+ catch (err) {
1030
+ logger_js_1.Logger.log(`[MB CLIENT DEBUG] listMessages failed for ${host}:`, err);
1031
+ throw err; // re-throw to be caught in the settled promise
1032
+ }
1033
+ };
1034
+ const settled = await Promise.allSettled(hosts.map(fetchFromHost));
1035
+ // 3. Split successes / failures
1036
+ const messagesByHost = [];
1037
+ const errors = [];
1038
+ for (const r of settled) {
1039
+ if (r.status === 'fulfilled') {
1040
+ messagesByHost.push(r.value);
1041
+ }
1042
+ else {
1043
+ errors.push(r.reason);
1044
+ }
1045
+ }
1046
+ // 4. If *every* host failed – throw aggregated error
1047
+ if (messagesByHost.length === 0) {
1048
+ throw new Error('Failed to retrieve messages from any host');
1049
+ }
1050
+ // 5. Merge & de‑duplicate (first‑seen wins)
1051
+ const dedupMap = new Map();
1052
+ for (const messageList of messagesByHost) {
1053
+ for (const m of messageList) {
1054
+ if (!dedupMap.has(m.messageId))
1055
+ dedupMap.set(m.messageId, m);
1056
+ }
1057
+ }
1058
+ // 6. Early‑out: no messages but at least one host succeeded → []
1059
+ if (dedupMap.size === 0)
1060
+ return [];
1061
+ const tryParse = (raw) => {
1062
+ try {
1063
+ return JSON.parse(raw);
1064
+ }
1065
+ catch {
1066
+ return raw;
1067
+ }
1068
+ };
1069
+ const messages = Array.from(dedupMap.values());
1070
+ for (const message of messages) {
1071
+ try {
1072
+ const parsedBody = typeof message.body === 'string' ? tryParse(message.body) : message.body;
1073
+ if (parsedBody != null &&
1074
+ typeof parsedBody === 'object' &&
1075
+ typeof parsedBody.encryptedMessage === 'string') {
1076
+ logger_js_1.Logger.log(`[MB CLIENT] Decrypting message from ${String(message.sender)}…`);
1077
+ const decrypted = await this.walletClient.decrypt({
1078
+ protocolID: [1, 'messagebox'],
1079
+ keyID: '1',
1080
+ counterparty: message.sender,
1081
+ ciphertext: sdk_1.Utils.toArray(parsedBody.encryptedMessage, 'base64')
1082
+ });
1083
+ const decryptedText = sdk_1.Utils.toUTF8(decrypted.plaintext);
1084
+ message.body = tryParse(decryptedText);
1085
+ }
1086
+ else {
1087
+ message.body = parsedBody;
1088
+ }
1089
+ }
1090
+ catch (err) {
1091
+ logger_js_1.Logger.error('[MB CLIENT ERROR] Failed to parse or decrypt message in list:', err);
1092
+ message.body = '[Error: Failed to decrypt or parse message]';
1093
+ }
1094
+ }
1095
+ // Sort newest‑first for a deterministic order
1096
+ messages.sort((a, b) => { var _a, _b; return Number((_a = b.timestamp) !== null && _a !== void 0 ? _a : 0) - Number((_b = a.timestamp) !== null && _b !== void 0 ? _b : 0); });
1097
+ return messages;
1098
+ }
1099
+ /**
1100
+ * @method acknowledgeMessage
1101
+ * @async
1102
+ * @param {AcknowledgeMessageParams} params - An object containing an array of message IDs to acknowledge.
1103
+ * @returns {Promise<string>} - A string indicating the result, typically `'success'`.
1104
+ *
1105
+ * @description
1106
+ * Notifies the MessageBox server(s) that one or more messages have been
1107
+ * successfully received and processed by the client. Once acknowledged, these messages are removed
1108
+ * from the recipient's inbox on the server(s).
1109
+ *
1110
+ * This operation is essential for proper message lifecycle management and prevents duplicate
1111
+ * processing or delivery.
1112
+ *
1113
+ * Acknowledgment supports providing a host override, or will use overlay routing to find the appropriate server the received the given message.
1114
+ *
1115
+ * @throws {Error} If the message ID array is missing or empty, or if the request to the server fails.
1116
+ *
1117
+ * @example
1118
+ * await client.acknowledgeMessage({ messageIds: ['msg123', 'msg456'] })
1119
+ */
1120
+ async acknowledgeMessage({ messageIds, host }) {
1121
+ var _a;
1122
+ await this.assertInitialized();
1123
+ if (!Array.isArray(messageIds) || messageIds.length === 0) {
1124
+ throw new Error('Message IDs array cannot be empty');
1125
+ }
1126
+ logger_js_1.Logger.log(`[MB CLIENT] Acknowledging messages ${JSON.stringify(messageIds)}…`);
1127
+ let hosts = host != null ? [host] : [];
1128
+ if (hosts.length === 0) {
1129
+ // 1. Determine all hosts (advertised + default)
1130
+ const identityKey = await this.getIdentityKey();
1131
+ const advertisedHosts = await this.queryAdvertisements(identityKey);
1132
+ hosts = Array.from(new Set([this.host, ...advertisedHosts.map(h => h.host)]));
1133
+ }
1134
+ // 2. Dispatch parallel acknowledge requests
1135
+ const ackFromHost = async (host) => {
1136
+ try {
1137
+ const res = await this.authFetch.fetch(`${host}/acknowledgeMessage`, {
1138
+ method: 'POST',
1139
+ headers: { 'Content-Type': 'application/json' },
1140
+ body: JSON.stringify({ messageIds })
1141
+ });
1142
+ if (!res.ok)
1143
+ throw new Error(`HTTP ${res.status}`);
1144
+ const data = await res.json();
1145
+ if (data.status === 'error')
1146
+ throw new Error(data.description);
1147
+ logger_js_1.Logger.log(`[MB CLIENT] Acknowledged on ${host}`);
1148
+ return data.status;
1149
+ }
1150
+ catch (err) {
1151
+ logger_js_1.Logger.warn(`[MB CLIENT WARN] acknowledgeMessage failed for ${host}:`, err);
1152
+ return null;
1153
+ }
1154
+ };
1155
+ const settled = await Promise.allSettled(hosts.map(ackFromHost));
1156
+ const successes = settled.filter((r) => r.status === 'fulfilled');
1157
+ const firstSuccess = (_a = successes.find(s => s.value != null)) === null || _a === void 0 ? void 0 : _a.value;
1158
+ if (firstSuccess != null) {
1159
+ return firstSuccess;
1160
+ }
1161
+ // No host accepted the acknowledgement
1162
+ const errs = [];
1163
+ for (const r of settled) {
1164
+ if (r.status === 'rejected')
1165
+ errs.push(r.reason);
1166
+ }
1167
+ throw new Error(`Failed to acknowledge messages on all hosts: ${errs.map(e => String(e)).join('; ')}`);
1168
+ }
1169
+ }
1170
+ exports.MessageBoxClient = MessageBoxClient;
1171
+ //# sourceMappingURL=MessageBoxClient.js.map