@bsv/message-box-client 1.2.2 → 1.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/message-box-client",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1266,20 +1266,12 @@ export class MessageBoxClient {
1266
1266
  // 6. Early‑out: no messages but at least one host succeeded → []
1267
1267
  if (dedupMap.size === 0) return []
1268
1268
 
1269
- const tryParse = (raw: string): any => {
1270
- try {
1271
- return JSON.parse(raw)
1272
- } catch {
1273
- return raw
1274
- }
1275
- }
1276
-
1277
1269
  const messages: PeerMessage[] = Array.from(dedupMap.values())
1278
1270
 
1279
1271
  for (const message of messages) {
1280
1272
  try {
1281
1273
  const parsedBody: unknown =
1282
- typeof message.body === 'string' ? tryParse(message.body) : message.body
1274
+ typeof message.body === 'string' ? this.tryParse(message.body) : message.body
1283
1275
 
1284
1276
  let messageContent: any = parsedBody
1285
1277
  let paymentData: Payment | undefined
@@ -1292,7 +1284,7 @@ export class MessageBoxClient {
1292
1284
  // Handle wrapped message format (with payment data)
1293
1285
  const wrappedMessage = (parsedBody as any).message
1294
1286
  messageContent = typeof wrappedMessage === 'string'
1295
- ? tryParse(wrappedMessage)
1287
+ ? this.tryParse(wrappedMessage)
1296
1288
  : wrappedMessage
1297
1289
  paymentData = (parsedBody as any).payment
1298
1290
  }
@@ -1365,7 +1357,7 @@ export class MessageBoxClient {
1365
1357
  }, originator)
1366
1358
 
1367
1359
  const decryptedText = Utils.toUTF8(decrypted.plaintext)
1368
- message.body = tryParse(decryptedText)
1360
+ message.body = this.tryParse(decryptedText)
1369
1361
  } else {
1370
1362
  // For non-encrypted messages, use the processed content
1371
1363
  message.body = messageContent ?? parsedBody
@@ -1388,6 +1380,216 @@ export class MessageBoxClient {
1388
1380
  return messages
1389
1381
  }
1390
1382
 
1383
+ /**
1384
+ * @method listMessagesLite
1385
+ * @async
1386
+ * @param {ListMessagesParams} params - Contains the `messageBox` to read from and the `host` to query.
1387
+ * @returns {Promise<PeerMessage[]>} - Returns an array of decrypted `PeerMessage` objects with minimal processing.
1388
+ *
1389
+ * @description
1390
+ * A lightweight variant of {@link listMessages} that fetches and decrypts messages
1391
+ * from a specific host without performing:
1392
+ * - Overlay host resolution
1393
+ * - Payment acceptance or internalization
1394
+ * - Cross-host deduplication
1395
+ *
1396
+ * This method:
1397
+ * - Sends a direct POST request to the specified host's `/listMessages` endpoint.
1398
+ * - Parses message bodies as JSON when possible.
1399
+ * - Decrypts messages if they contain an `encryptedMessage` field, using AES-256-GCM via BRC-2-compliant ECDH key derivation.
1400
+ * - Returns messages in the order provided by the host.
1401
+ *
1402
+ * This is intended for cases where you already know the host and need faster,
1403
+ * simpler retrieval without the additional processing overhead of `listMessages`.
1404
+ *
1405
+ * @throws {Error} If the host returns an error status or decryption fails.
1406
+ *
1407
+ * @example
1408
+ * const messages = await client.listMessagesLite({
1409
+ * messageBox: 'notifications',
1410
+ * host: 'https://messagebox.babbage.systems'
1411
+ * })
1412
+ * console.log(messages)
1413
+ */
1414
+ async listMessagesLite ({ messageBox, host }: ListMessagesParams): Promise<PeerMessage[]> {
1415
+ const res = await this.authFetch.fetch(`${host as string}/listMessages`, {
1416
+ method: 'POST',
1417
+ headers: { 'Content-Type': 'application/json' },
1418
+ body: JSON.stringify({ messageBox })
1419
+ })
1420
+ const data = await res.json()
1421
+ if (data.status === 'error') throw new Error(data.description ?? 'Unknown server error')
1422
+ const messages = data.messages as PeerMessage[]
1423
+ const tryParse = (raw: string): any => {
1424
+ try {
1425
+ return JSON.parse(raw)
1426
+ } catch {
1427
+ return raw
1428
+ }
1429
+ }
1430
+ for (const message of messages) {
1431
+ try {
1432
+ const parsedBody: unknown =
1433
+ typeof message.body === 'string' ? tryParse(message.body) : message.body
1434
+ let messageContent: any = parsedBody
1435
+ if (
1436
+ parsedBody != null &&
1437
+ typeof parsedBody === 'object' &&
1438
+ 'message' in parsedBody
1439
+ ) {
1440
+ // Handle wrapped message format (with payment data)
1441
+ const wrappedMessage = (parsedBody as any).message
1442
+ messageContent = typeof wrappedMessage === 'string'
1443
+ ? tryParse(wrappedMessage)
1444
+ : wrappedMessage
1445
+ }
1446
+ // Handle message decryption
1447
+ if (
1448
+ messageContent != null &&
1449
+ typeof messageContent === 'object' &&
1450
+ typeof messageContent.encryptedMessage === 'string'
1451
+ ) {
1452
+ const decrypted = await this.walletClient.decrypt({
1453
+ protocolID: [1, 'messagebox'],
1454
+ keyID: '1',
1455
+ counterparty: message.sender,
1456
+ ciphertext: Utils.toArray(
1457
+ messageContent.encryptedMessage,
1458
+ 'base64'
1459
+ )
1460
+ })
1461
+ const decryptedText = Utils.toUTF8(decrypted.plaintext)
1462
+ message.body = tryParse(decryptedText)
1463
+ } else {
1464
+ // For non-encrypted messages, use the processed content
1465
+ message.body = messageContent ?? parsedBody
1466
+ }
1467
+ } catch (err) {
1468
+ Logger.error(
1469
+ '[MB CLIENT ERROR] Failed to parse or decrypt message in list:',
1470
+ err
1471
+ )
1472
+ message.body = '[Error: Failed to decrypt or parse message]'
1473
+ }
1474
+ }
1475
+ return messages
1476
+ }
1477
+
1478
+ /**
1479
+ * @method tryParse
1480
+ * @private
1481
+ * @param {string} raw - A raw string value that may contain JSON.
1482
+ * @returns {any} - The parsed JavaScript object if valid JSON, or the original string if parsing fails.
1483
+ *
1484
+ * @description
1485
+ * Attempts to parse a string as JSON. If the string is valid JSON, returns the parsed object;
1486
+ * otherwise returns the original string unchanged.
1487
+ *
1488
+ * This method is used throughout the client to safely handle message bodies that may or may not be
1489
+ * JSON-encoded without throwing parsing errors.
1490
+ *
1491
+ * @example
1492
+ * tryParse('{"hello":"world"}') // → { hello: "world" }
1493
+ * tryParse('plain text') // → "plain text"
1494
+ */
1495
+ tryParse (raw: string): any {
1496
+ try {
1497
+ return JSON.parse(raw)
1498
+ } catch {
1499
+ return raw
1500
+ }
1501
+ }
1502
+
1503
+ /**
1504
+ * @method acknowledgeNotification
1505
+ * @async
1506
+ * @param {PeerMessage} message - The peer message object to acknowledge.
1507
+ * @returns {Promise<boolean>} - Resolves to `true` if the message included a recipient payment and it was successfully internalized, otherwise `false`.
1508
+ *
1509
+ * @description
1510
+ * Acknowledges receipt of a specific notification message and, if applicable, processes any recipient
1511
+ * payment contained within it.
1512
+ *
1513
+ * This method:
1514
+ * 1. Calls `acknowledgeMessage()` to remove the message from the server's queue.
1515
+ * 2. Checks the message body for embedded payment data.
1516
+ * 3. If a recipient payment exists, attempts to internalize it into the wallet.
1517
+ *
1518
+ * This is a convenience wrapper for acknowledgment and payment handling specifically for messages
1519
+ * representing notifications.
1520
+ *
1521
+ * @example
1522
+ * const success = await client.acknowledgeNotification(message)
1523
+ * console.log(success ? 'Payment received' : 'No payment or failed')
1524
+ */
1525
+ async acknowledgeNotification(message: PeerMessage): Promise<boolean> {
1526
+ await this.acknowledgeMessage({ messageIds: [message.messageId] })
1527
+
1528
+ const parsedBody: unknown =
1529
+ typeof message.body === 'string' ? this.tryParse(message.body) : message.body
1530
+
1531
+ let paymentData: Payment | undefined
1532
+
1533
+ if (
1534
+ parsedBody != null &&
1535
+ typeof parsedBody === 'object' &&
1536
+ 'message' in parsedBody
1537
+ ) {
1538
+ paymentData = (parsedBody as any).payment
1539
+ }
1540
+
1541
+ // Process payment if present - server now only stores recipient payments
1542
+ if (paymentData?.tx != null && paymentData.outputs != null) {
1543
+ try {
1544
+ Logger.log(
1545
+ `[MB CLIENT] Processing recipient payment in message from ${String(message.sender)}…`
1546
+ )
1547
+
1548
+ // All outputs in the stored payment data are for the recipient
1549
+ // (delivery fees are already processed by the server)
1550
+ const recipientOutputs = paymentData.outputs.filter(
1551
+ output => output.protocol === 'wallet payment'
1552
+ )
1553
+
1554
+ if (recipientOutputs.length < 1) {
1555
+ Logger.log(
1556
+ '[MB CLIENT] No wallet payment outputs found in payment data'
1557
+ )
1558
+ return false
1559
+ }
1560
+
1561
+ Logger.log(
1562
+ `[MB CLIENT] Internalizing ${recipientOutputs.length} recipient payment output(s)…`
1563
+ )
1564
+
1565
+ const internalizeResult = await this.walletClient.internalizeAction({
1566
+ tx: paymentData.tx,
1567
+ outputs: recipientOutputs,
1568
+ description: paymentData.description ?? 'MessageBox recipient payment'
1569
+ })
1570
+
1571
+ if (internalizeResult.accepted) {
1572
+ Logger.log(
1573
+ '[MB CLIENT] Successfully internalized recipient payment'
1574
+ )
1575
+ return true
1576
+ } else {
1577
+ Logger.warn(
1578
+ '[MB CLIENT] Recipient payment internalization was not accepted'
1579
+ )
1580
+ return false
1581
+ }
1582
+ } catch (paymentError) {
1583
+ Logger.error(
1584
+ '[MB CLIENT ERROR] Failed to internalize recipient payment:',
1585
+ paymentError
1586
+ )
1587
+ return false
1588
+ }
1589
+ }
1590
+ return false
1591
+ }
1592
+
1391
1593
  /**
1392
1594
  * @method acknowledgeMessage
1393
1595
  * @async
@@ -1931,14 +2133,31 @@ export class MessageBoxClient {
1931
2133
  return 'payment_required'
1932
2134
  }
1933
2135
 
1934
- /**
1935
- * Creates payment transaction for message delivery fees
1936
- * TODO: Consider consolidating payment generating logic with a util PeerPayClient can use as well.
2136
+ /**
2137
+ * @method createMessagePayment
1937
2138
  * @private
1938
- * @param {string} recipient - Recipient identity key
1939
- * @param {MessageBoxQuote} quote - Fee quote with delivery and recipient fees
1940
- * @param {string} description - Description for the payment transaction
1941
- * @returns {Promise<Payment>} Payment transaction data
2139
+ * @param {string} recipient - Recipient's identity key.
2140
+ * @param {MessageBoxQuote} quote - Quote object containing recipient and delivery fees.
2141
+ * @param {string} [description='MessageBox delivery payment'] - Description for the payment action.
2142
+ * @param {string} [originator] - Optional originator to use for wallet operations.
2143
+ * @returns {Promise<Payment>} - Payment data including the transaction and remittance outputs.
2144
+ *
2145
+ * @description
2146
+ * Constructs and signs a payment transaction covering both delivery and recipient fees for
2147
+ * message delivery, based on a previously obtained quote.
2148
+ *
2149
+ * The transaction includes:
2150
+ * - An optional delivery fee output for the MessageBox server.
2151
+ * - An optional recipient fee output for the message recipient.
2152
+ *
2153
+ * Payment remittance metadata (derivation prefix/suffix, sender identity) is embedded to allow
2154
+ * the payee to derive their private key and spend the output.
2155
+ *
2156
+ * @throws {Error} If no payment is required, key derivation fails, or the action creation fails.
2157
+ *
2158
+ * @example
2159
+ * const payment = await client.createMessagePayment(recipientKey, quote)
2160
+ * await client.sendMessage({ recipient, messageBox, body, payment })
1942
2161
  */
1943
2162
  private async createMessagePayment(
1944
2163
  recipient: string,