@bsv/message-box-client 1.1.7 → 1.1.8
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.
- package/dist/cjs/mod.js +20 -0
- package/dist/cjs/mod.js.map +1 -0
- package/dist/cjs/package.json +69 -0
- package/dist/cjs/src/MessageBoxClient.js +1171 -0
- package/dist/cjs/src/MessageBoxClient.js.map +1 -0
- package/dist/cjs/src/PeerPayClient.js +309 -0
- package/dist/cjs/src/PeerPayClient.js.map +1 -0
- package/dist/cjs/src/Utils/logger.js +27 -0
- package/dist/cjs/src/Utils/logger.js.map +1 -0
- package/dist/cjs/src/__tests/MessageBoxClient.test.js +614 -0
- package/dist/cjs/src/__tests/MessageBoxClient.test.js.map +1 -0
- package/dist/cjs/src/__tests/PeerPayClientUnit.test.js +213 -0
- package/dist/cjs/src/__tests/PeerPayClientUnit.test.js.map +1 -0
- package/dist/cjs/src/__tests/integration/integrationEncrypted.test.js +84 -0
- package/dist/cjs/src/__tests/integration/integrationEncrypted.test.js.map +1 -0
- package/dist/cjs/src/__tests/integration/integrationHTTP.test.js +128 -0
- package/dist/cjs/src/__tests/integration/integrationHTTP.test.js.map +1 -0
- package/dist/cjs/src/__tests/integration/integrationOverlay.test.js +138 -0
- package/dist/cjs/src/__tests/integration/integrationOverlay.test.js.map +1 -0
- package/dist/cjs/src/__tests/integration/integrationWS.test.js +123 -0
- package/dist/cjs/src/__tests/integration/integrationWS.test.js.map +1 -0
- package/dist/cjs/src/__tests/integration/testServer.js +65 -0
- package/dist/cjs/src/__tests/integration/testServer.js.map +1 -0
- package/dist/cjs/src/types.js +3 -0
- package/dist/cjs/src/types.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/esm/mod.js +4 -0
- package/dist/esm/mod.js.map +1 -0
- package/dist/esm/src/MessageBoxClient.js +1165 -0
- package/dist/esm/src/MessageBoxClient.js.map +1 -0
- package/dist/esm/src/PeerPayClient.js +307 -0
- package/dist/esm/src/PeerPayClient.js.map +1 -0
- package/dist/esm/src/Utils/logger.js +23 -0
- package/dist/esm/src/Utils/logger.js.map +1 -0
- package/dist/esm/src/__tests/MessageBoxClient.test.js +603 -0
- package/dist/esm/src/__tests/MessageBoxClient.test.js.map +1 -0
- package/dist/esm/src/__tests/PeerPayClientUnit.test.js +211 -0
- package/dist/esm/src/__tests/PeerPayClientUnit.test.js.map +1 -0
- package/dist/esm/src/__tests/integration/integrationEncrypted.test.js +81 -0
- package/dist/esm/src/__tests/integration/integrationEncrypted.test.js.map +1 -0
- package/dist/esm/src/__tests/integration/integrationHTTP.test.js +126 -0
- package/dist/esm/src/__tests/integration/integrationHTTP.test.js.map +1 -0
- package/dist/esm/src/__tests/integration/integrationOverlay.test.js +135 -0
- package/dist/esm/src/__tests/integration/integrationOverlay.test.js.map +1 -0
- package/dist/esm/src/__tests/integration/integrationWS.test.js +121 -0
- package/dist/esm/src/__tests/integration/integrationWS.test.js.map +1 -0
- package/dist/esm/src/__tests/integration/testServer.js +61 -0
- package/dist/esm/src/__tests/integration/testServer.js.map +1 -0
- package/dist/esm/src/types.js +2 -0
- package/dist/esm/src/types.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
- package/dist/types/mod.d.ts +4 -0
- package/dist/types/mod.d.ts.map +1 -0
- package/dist/types/src/MessageBoxClient.d.ts +441 -0
- package/dist/types/src/MessageBoxClient.d.ts.map +1 -0
- package/dist/types/src/PeerPayClient.d.ts +144 -0
- package/dist/types/src/PeerPayClient.d.ts.map +1 -0
- package/dist/types/src/Utils/logger.d.ts +9 -0
- package/dist/types/src/Utils/logger.d.ts.map +1 -0
- package/dist/types/src/__tests/MessageBoxClient.test.d.ts +2 -0
- package/dist/types/src/__tests/MessageBoxClient.test.d.ts.map +1 -0
- package/dist/types/src/__tests/PeerPayClientUnit.test.d.ts +2 -0
- package/dist/types/src/__tests/PeerPayClientUnit.test.d.ts.map +1 -0
- package/dist/types/src/__tests/integration/integrationEncrypted.test.d.ts +2 -0
- package/dist/types/src/__tests/integration/integrationEncrypted.test.d.ts.map +1 -0
- package/dist/types/src/__tests/integration/integrationHTTP.test.d.ts +2 -0
- package/dist/types/src/__tests/integration/integrationHTTP.test.d.ts.map +1 -0
- package/dist/types/src/__tests/integration/integrationOverlay.test.d.ts +2 -0
- package/dist/types/src/__tests/integration/integrationOverlay.test.d.ts.map +1 -0
- package/dist/types/src/__tests/integration/integrationWS.test.d.ts +2 -0
- package/dist/types/src/__tests/integration/integrationWS.test.d.ts.map +1 -0
- package/dist/types/src/__tests/integration/testServer.d.ts +9 -0
- package/dist/types/src/__tests/integration/testServer.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +99 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -0
- package/dist/umd/bundle.js +1 -0
- 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
|