@bsv/message-box-client 1.1.9 → 1.1.11

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 (36) hide show
  1. package/dist/cjs/package.json +5 -5
  2. package/dist/cjs/src/MessageBoxClient.js +707 -87
  3. package/dist/cjs/src/MessageBoxClient.js.map +1 -1
  4. package/dist/cjs/src/PeerPayClient.js +61 -28
  5. package/dist/cjs/src/PeerPayClient.js.map +1 -1
  6. package/dist/cjs/src/Utils/logger.js +22 -21
  7. package/dist/cjs/src/Utils/logger.js.map +1 -1
  8. package/dist/cjs/src/types/permissions.js +6 -0
  9. package/dist/cjs/src/types/permissions.js.map +1 -0
  10. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  11. package/dist/esm/src/MessageBoxClient.js +593 -12
  12. package/dist/esm/src/MessageBoxClient.js.map +1 -1
  13. package/dist/esm/src/PeerPayClient.js +1 -1
  14. package/dist/esm/src/PeerPayClient.js.map +1 -1
  15. package/dist/esm/src/Utils/logger.js +17 -19
  16. package/dist/esm/src/Utils/logger.js.map +1 -1
  17. package/dist/esm/src/types/permissions.js +5 -0
  18. package/dist/esm/src/types/permissions.js.map +1 -0
  19. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  20. package/dist/types/src/MessageBoxClient.d.ts +218 -13
  21. package/dist/types/src/MessageBoxClient.d.ts.map +1 -1
  22. package/dist/types/src/PeerPayClient.d.ts.map +1 -1
  23. package/dist/types/src/Utils/logger.d.ts +5 -8
  24. package/dist/types/src/Utils/logger.d.ts.map +1 -1
  25. package/dist/types/src/types/permissions.d.ts +75 -0
  26. package/dist/types/src/types/permissions.d.ts.map +1 -0
  27. package/dist/types/src/types.d.ts +71 -2
  28. package/dist/types/src/types.d.ts.map +1 -1
  29. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  30. package/dist/umd/bundle.js +1 -1
  31. package/package.json +5 -5
  32. package/src/MessageBoxClient.ts +732 -24
  33. package/src/PeerPayClient.ts +11 -11
  34. package/src/Utils/logger.ts +17 -19
  35. package/src/types/permissions.ts +81 -0
  36. package/src/types.ts +77 -2
@@ -21,9 +21,9 @@
21
21
  * @author Project Babbage
22
22
  * @license Open BSV License
23
23
  */
24
- import { WalletClient, AuthFetch, LookupResolver, TopicBroadcaster, Utils, Transaction, PushDrop } from '@bsv/sdk';
24
+ import { WalletClient, AuthFetch, LookupResolver, TopicBroadcaster, Utils, Transaction, PushDrop, P2PKH, PublicKey, ProtoWallet, Random } from '@bsv/sdk';
25
25
  import { AuthSocketClient } from '@bsv/authsocket-client';
26
- import { Logger } from './Utils/logger.js';
26
+ import * as Logger from './Utils/logger.js';
27
27
  const DEFAULT_MAINNET_HOST = 'https://messagebox.babbage.systems';
28
28
  const DEFAULT_TESTNET_HOST = 'https://staging-messagebox.babbage.systems';
29
29
  /**
@@ -67,7 +67,7 @@ export class MessageBoxClient {
67
67
  * @constructor
68
68
  * @param {Object} options - Initialization options for the MessageBoxClient.
69
69
  * @param {string} [options.host] - The base URL of the MessageBox server. If omitted, defaults to mainnet/testnet hosts.
70
- * @param {WalletClient} options.walletClient - Wallet instance used for authentication, signing, and encryption.
70
+ * @param {WalletInterface} options.walletClient - Wallet instance used for authentication, signing, and encryption.
71
71
  * @param {boolean} [options.enableLogging=false] - Whether to enable detailed debug logging to the console.
72
72
  * @param {'local' | 'mainnet' | 'testnet'} [options.networkPreset='mainnet'] - Overlay network preset used for routing and advertisement lookup.
73
73
  *
@@ -356,7 +356,7 @@ export class MessageBoxClient {
356
356
  query
357
357
  });
358
358
  if (result.type !== 'output-list') {
359
- throw new Error(`Unexpected result type: ${result.type}`);
359
+ throw new Error(`Unexpected result type: ${String(result.type)}`);
360
360
  }
361
361
  for (const output of result.outputs) {
362
362
  try {
@@ -543,7 +543,7 @@ export class MessageBoxClient {
543
543
  * body: { amount: 1000 }
544
544
  * })
545
545
  */
546
- async sendLiveMessage({ recipient, messageBox, body, messageId, skipEncryption }) {
546
+ async sendLiveMessage({ recipient, messageBox, body, messageId, skipEncryption, checkPermissions }) {
547
547
  await this.assertInitialized();
548
548
  if (recipient == null || recipient.trim() === '') {
549
549
  throw new Error('[MB CLIENT ERROR] Recipient identity key is required');
@@ -612,7 +612,8 @@ export class MessageBoxClient {
612
612
  messageBox,
613
613
  body,
614
614
  messageId: finalMessageId,
615
- skipEncryption
615
+ skipEncryption,
616
+ checkPermissions
616
617
  };
617
618
  this.resolveHostForRecipient(recipient)
618
619
  .then(async (host) => {
@@ -651,7 +652,8 @@ export class MessageBoxClient {
651
652
  messageBox,
652
653
  body,
653
654
  messageId: finalMessageId,
654
- skipEncryption
655
+ skipEncryption,
656
+ checkPermissions
655
657
  };
656
658
  this.resolveHostForRecipient(recipient)
657
659
  .then(async (host) => {
@@ -755,6 +757,33 @@ export class MessageBoxClient {
755
757
  if (message.body == null || (typeof message.body === 'string' && message.body.trim().length === 0)) {
756
758
  throw new Error('Every message must have a body!');
757
759
  }
760
+ // Optional permission checking for backwards compatibility
761
+ let paymentData;
762
+ if (message.checkPermissions === true) {
763
+ try {
764
+ Logger.log('[MB CLIENT] Checking permissions and fees for message...');
765
+ // Get quote to check if payment is required
766
+ const quote = await this.getMessageBoxQuote({
767
+ recipient: message.recipient,
768
+ messageBox: message.messageBox
769
+ });
770
+ if (quote.recipientFee === -1) {
771
+ throw new Error('You have been blocked from sending messages to this recipient.');
772
+ }
773
+ if (quote.recipientFee > 0 || quote.deliveryFee > 0) {
774
+ const requiredPayment = quote.recipientFee + quote.deliveryFee;
775
+ if (requiredPayment > 0) {
776
+ Logger.log(`[MB CLIENT] Creating payment of ${requiredPayment} sats for message...`);
777
+ // Create payment using helper method
778
+ paymentData = await this.createMessagePayment(message.recipient, quote, overrideHost);
779
+ Logger.log('[MB CLIENT] Payment data prepared:', paymentData);
780
+ }
781
+ }
782
+ }
783
+ catch (error) {
784
+ throw new Error(`Permission check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
785
+ }
786
+ }
758
787
  let messageId;
759
788
  try {
760
789
  const hmac = await this.walletClient.createHmac({
@@ -787,7 +816,8 @@ export class MessageBoxClient {
787
816
  ...message,
788
817
  messageId,
789
818
  body: finalBody
790
- }
819
+ },
820
+ ...(paymentData != null && { payment: paymentData })
791
821
  };
792
822
  try {
793
823
  const finalHost = overrideHost ?? await this.resolveHostForRecipient(message.recipient);
@@ -985,9 +1015,16 @@ export class MessageBoxClient {
985
1015
  *
986
1016
  * Each message is:
987
1017
  * - Parsed and, if encrypted, decrypted using AES-256-GCM via BRC-2-compliant ECDH key derivation and symmetric encryption.
1018
+ * - Automatically processed for payments: if the message includes recipient fee payments, they are internalized using `walletClient.internalizeAction()`.
988
1019
  * - Returned as a normalized `PeerMessage` with readable string body content.
989
1020
  *
990
- * Decryption automatically derives a shared secret using the sender’s identity key and the receiver’s child private key.
1021
+ * Payment Processing:
1022
+ * - Detects messages that include payment data (from paid message delivery).
1023
+ * - Automatically internalizes recipient payment outputs, allowing you to receive payments without additional API calls.
1024
+ * - Only recipient payments are stored with messages - delivery fees are already processed by the server.
1025
+ * - Continues processing messages even if payment internalization fails.
1026
+ *
1027
+ * Decryption automatically derives a shared secret using the sender's identity key and the receiver's child private key.
991
1028
  * If the sender is the same as the recipient, the `counterparty` is set to `'self'`.
992
1029
  *
993
1030
  * @throws {Error} If no messageBox is specified, the request fails, or the server returns an error.
@@ -995,6 +1032,7 @@ export class MessageBoxClient {
995
1032
  * @example
996
1033
  * const messages = await client.listMessages({ messageBox: 'inbox' })
997
1034
  * messages.forEach(msg => console.log(msg.sender, msg.body))
1035
+ * // Payments included with messages are automatically received
998
1036
  */
999
1037
  async listMessages({ messageBox, host }) {
1000
1038
  await this.assertInitialized();
@@ -1066,21 +1104,63 @@ export class MessageBoxClient {
1066
1104
  for (const message of messages) {
1067
1105
  try {
1068
1106
  const parsedBody = typeof message.body === 'string' ? tryParse(message.body) : message.body;
1107
+ let messageContent = parsedBody;
1108
+ let paymentData;
1069
1109
  if (parsedBody != null &&
1070
1110
  typeof parsedBody === 'object' &&
1071
- typeof parsedBody.encryptedMessage === 'string') {
1111
+ 'message' in parsedBody) {
1112
+ messageContent = parsedBody.message?.body;
1113
+ paymentData = parsedBody.payment;
1114
+ }
1115
+ // Process payment if present - server now only stores recipient payments
1116
+ if (paymentData?.tx != null && paymentData.outputs != null) {
1117
+ try {
1118
+ Logger.log(`[MB CLIENT] Processing recipient payment in message from ${String(message.sender)}…`);
1119
+ // All outputs in the stored payment data are for the recipient
1120
+ // (delivery fees are already processed by the server)
1121
+ const recipientOutputs = paymentData.outputs.filter(output => output.protocol === 'wallet payment');
1122
+ if (recipientOutputs.length > 0) {
1123
+ Logger.log(`[MB CLIENT] Internalizing ${recipientOutputs.length} recipient payment output(s)…`);
1124
+ const internalizeResult = await this.walletClient.internalizeAction({
1125
+ tx: paymentData.tx,
1126
+ outputs: recipientOutputs,
1127
+ description: paymentData.description ?? 'MessageBox recipient payment'
1128
+ });
1129
+ if (internalizeResult.accepted) {
1130
+ Logger.log('[MB CLIENT] Successfully internalized recipient payment');
1131
+ }
1132
+ else {
1133
+ Logger.warn('[MB CLIENT] Recipient payment internalization was not accepted');
1134
+ }
1135
+ }
1136
+ else {
1137
+ Logger.log('[MB CLIENT] No wallet payment outputs found in payment data');
1138
+ }
1139
+ }
1140
+ catch (paymentError) {
1141
+ Logger.error('[MB CLIENT ERROR] Failed to internalize recipient payment:', paymentError);
1142
+ // Continue processing the message even if payment fails
1143
+ }
1144
+ }
1145
+ // Handle message decryption
1146
+ if (messageContent != null &&
1147
+ typeof messageContent === 'object' &&
1148
+ typeof messageContent.encryptedMessage === 'string') {
1072
1149
  Logger.log(`[MB CLIENT] Decrypting message from ${String(message.sender)}…`);
1073
1150
  const decrypted = await this.walletClient.decrypt({
1074
1151
  protocolID: [1, 'messagebox'],
1075
1152
  keyID: '1',
1076
1153
  counterparty: message.sender,
1077
- ciphertext: Utils.toArray(parsedBody.encryptedMessage, 'base64')
1154
+ ciphertext: Utils.toArray(messageContent.encryptedMessage, 'base64')
1078
1155
  });
1079
1156
  const decryptedText = Utils.toUTF8(decrypted.plaintext);
1080
1157
  message.body = tryParse(decryptedText);
1081
1158
  }
1082
1159
  else {
1083
- message.body = parsedBody;
1160
+ // Handle both old format (direct content) and new format (message.body)
1161
+ message.body = messageContent != null
1162
+ ? (typeof messageContent === 'string' ? messageContent : messageContent)
1163
+ : parsedBody;
1084
1164
  }
1085
1165
  }
1086
1166
  catch (err) {
@@ -1161,5 +1241,506 @@ export class MessageBoxClient {
1161
1241
  }
1162
1242
  throw new Error(`Failed to acknowledge messages on all hosts: ${errs.map(e => String(e)).join('; ')}`);
1163
1243
  }
1244
+ // ===========================
1245
+ // PERMISSION MANAGEMENT METHODS
1246
+ // ===========================
1247
+ /**
1248
+ * @method setMessageBoxPermission
1249
+ * @async
1250
+ * @param {SetMessageBoxPermissionParams} params - Permission configuration
1251
+ * @param {string} [overrideHost] - Optional host override
1252
+ * @returns {Promise<void>} Permission status after setting
1253
+ *
1254
+ * @description
1255
+ * Sets permission for receiving messages in a specific messageBox.
1256
+ * Can set sender-specific permissions or box-wide defaults.
1257
+ *
1258
+ * @example
1259
+ * // Set box-wide default: allow notifications for 10 sats
1260
+ * await client.setMessageBoxPermission({ messageBox: 'notifications', recipientFee: 10 })
1261
+ *
1262
+ * // Block specific sender
1263
+ * await client.setMessageBoxPermission({
1264
+ * messageBox: 'notifications',
1265
+ * sender: '03abc123...',
1266
+ * recipientFee: -1
1267
+ * })
1268
+ */
1269
+ async setMessageBoxPermission(params, overrideHost) {
1270
+ await this.assertInitialized();
1271
+ const finalHost = overrideHost ?? this.host;
1272
+ Logger.log('[MB CLIENT] Setting messageBox permission...');
1273
+ const response = await this.authFetch.fetch(`${finalHost}/permissions/set`, {
1274
+ method: 'POST',
1275
+ headers: { 'Content-Type': 'application/json' },
1276
+ body: JSON.stringify({
1277
+ messageBox: params.messageBox,
1278
+ recipientFee: params.recipientFee,
1279
+ ...(params.sender != null && { sender: params.sender })
1280
+ })
1281
+ });
1282
+ if (!response.ok) {
1283
+ const errorData = await response.json().catch(() => ({}));
1284
+ throw new Error(`Failed to set permission: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`);
1285
+ }
1286
+ const { status, description } = await response.json();
1287
+ if (status === 'error') {
1288
+ throw new Error(description ?? 'Failed to set permission');
1289
+ }
1290
+ }
1291
+ /**
1292
+ * @method getMessageBoxPermission
1293
+ * @async
1294
+ * @param {GetMessageBoxPermissionParams} params - Permission query parameters
1295
+ * @param {string} [overrideHost] - Optional host override
1296
+ * @returns {Promise<MessageBoxPermission | null>} Permission data (null if not set)
1297
+ *
1298
+ * @description
1299
+ * Gets current permission data for a sender/messageBox combination.
1300
+ * Returns null if no permission is set.
1301
+ *
1302
+ * @example
1303
+ * const status = await client.getMessageBoxPermission({
1304
+ * recipient: '03def456...',
1305
+ * messageBox: 'notifications',
1306
+ * sender: '03abc123...'
1307
+ * })
1308
+ */
1309
+ async getMessageBoxPermission(params, overrideHost) {
1310
+ await this.assertInitialized();
1311
+ const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient);
1312
+ const queryParams = new URLSearchParams({
1313
+ recipient: params.recipient,
1314
+ messageBox: params.messageBox,
1315
+ ...(params.sender != null && { sender: params.sender })
1316
+ });
1317
+ Logger.log('[MB CLIENT] Getting messageBox permission...');
1318
+ const response = await this.authFetch.fetch(`${finalHost}/permissions/get?${queryParams.toString()}`, {
1319
+ method: 'GET'
1320
+ });
1321
+ if (!response.ok) {
1322
+ const errorData = await response.json().catch(() => ({}));
1323
+ throw new Error(`Failed to get permission: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`);
1324
+ }
1325
+ const data = await response.json();
1326
+ if (data.status === 'error') {
1327
+ throw new Error(data.description ?? 'Failed to get permission');
1328
+ }
1329
+ return data.permission;
1330
+ }
1331
+ /**
1332
+ * @method getMessageBoxQuote
1333
+ * @async
1334
+ * @param {GetQuoteParams} params - Quote request parameters
1335
+ * @returns {Promise<MessageBoxQuote>} Fee quote and permission status
1336
+ *
1337
+ * @description
1338
+ * Gets a fee quote for sending a message, including delivery and recipient fees.
1339
+ *
1340
+ * @example
1341
+ * const quote = await client.getMessageBoxQuote({
1342
+ * recipient: '03def456...',
1343
+ * messageBox: 'notifications'
1344
+ * })
1345
+ */
1346
+ async getMessageBoxQuote(params, overrideHost) {
1347
+ await this.assertInitialized();
1348
+ const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient);
1349
+ const queryParams = new URLSearchParams({
1350
+ recipient: params.recipient,
1351
+ messageBox: params.messageBox
1352
+ });
1353
+ Logger.log('[MB CLIENT] Getting messageBox quote...');
1354
+ const response = await this.authFetch.fetch(`${finalHost}/permissions/quote?${queryParams.toString()}`, {
1355
+ method: 'GET'
1356
+ });
1357
+ if (!response.ok) {
1358
+ const errorData = await response.json().catch(() => ({}));
1359
+ throw new Error(`Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`);
1360
+ }
1361
+ const { status, description, quote } = await response.json();
1362
+ if (status === 'error') {
1363
+ throw new Error(description ?? 'Failed to get quote');
1364
+ }
1365
+ const deliveryAgentIdentityKey = response.headers.get('x-bsv-auth-identity-key');
1366
+ if (deliveryAgentIdentityKey == null) {
1367
+ throw new Error('Failed to get quote: Delivery agent did not provide their identity key');
1368
+ }
1369
+ return {
1370
+ recipientFee: quote.recipientFee,
1371
+ deliveryFee: quote.deliveryFee,
1372
+ deliveryAgentIdentityKey
1373
+ };
1374
+ }
1375
+ /**
1376
+ * @method listMessageBoxPermissions
1377
+ * @async
1378
+ * @param {ListPermissionsParams} [params] - Optional filtering and pagination parameters
1379
+ * @returns {Promise<MessageBoxPermission[]>} List of current permissions
1380
+ *
1381
+ * @description
1382
+ * Lists permissions for the authenticated user's messageBoxes with optional pagination.
1383
+ *
1384
+ * @example
1385
+ * // List all permissions
1386
+ * const all = await client.listMessageBoxPermissions()
1387
+ *
1388
+ * // List only notification permissions with pagination
1389
+ * const notifications = await client.listMessageBoxPermissions({
1390
+ * messageBox: 'notifications',
1391
+ * limit: 50,
1392
+ * offset: 0
1393
+ * })
1394
+ */
1395
+ async listMessageBoxPermissions(params, overrideHost) {
1396
+ await this.assertInitialized();
1397
+ const finalHost = overrideHost ?? this.host;
1398
+ const queryParams = new URLSearchParams();
1399
+ if (params?.messageBox != null) {
1400
+ queryParams.set('message_box', params.messageBox);
1401
+ }
1402
+ if (params?.limit !== undefined) {
1403
+ queryParams.set('limit', params.limit.toString());
1404
+ }
1405
+ if (params?.offset !== undefined) {
1406
+ queryParams.set('offset', params.offset.toString());
1407
+ }
1408
+ Logger.log('[MB CLIENT] Listing messageBox permissions with params:', queryParams.toString());
1409
+ const response = await this.authFetch.fetch(`${finalHost}/permissions/list?${queryParams.toString()}`, {
1410
+ method: 'GET'
1411
+ });
1412
+ if (!response.ok) {
1413
+ const errorData = await response.json().catch(() => ({}));
1414
+ throw new Error(`Failed to list permissions: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`);
1415
+ }
1416
+ const data = await response.json();
1417
+ if (data.status === 'error') {
1418
+ throw new Error(data.description ?? 'Failed to list permissions');
1419
+ }
1420
+ return data.permissions.map((p) => ({
1421
+ sender: p.sender,
1422
+ messageBox: p.message_box,
1423
+ recipientFee: p.recipient_fee,
1424
+ status: MessageBoxClient.getStatusFromFee(p.recipient_fee),
1425
+ createdAt: p.created_at,
1426
+ updatedAt: p.updated_at
1427
+ }));
1428
+ }
1429
+ // ===========================
1430
+ // NOTIFICATION CONVENIENCE METHODS
1431
+ // ===========================
1432
+ /**
1433
+ * @method allowNotificationsFromPeer
1434
+ * @async
1435
+ * @param {PubKeyHex} identityKey - Sender's identity key to allow
1436
+ * @param {number} [recipientFee=0] - Fee to charge (0 for always allow)
1437
+ * @param {string} [overrideHost] - Optional host override
1438
+ * @returns {Promise<void>} Permission status after allowing
1439
+ *
1440
+ * @description
1441
+ * Convenience method to allow notifications from a specific peer.
1442
+ *
1443
+ * @example
1444
+ * await client.allowNotificationsFromPeer('03abc123...') // Always allow
1445
+ * await client.allowNotificationsFromPeer('03def456...', 5) // Allow for 5 sats
1446
+ */
1447
+ async allowNotificationsFromPeer(identityKey, recipientFee = 0, overrideHost) {
1448
+ await this.setMessageBoxPermission({
1449
+ messageBox: 'notifications',
1450
+ sender: identityKey,
1451
+ recipientFee
1452
+ }, overrideHost);
1453
+ }
1454
+ /**
1455
+ * @method denyNotificationsFromPeer
1456
+ * @async
1457
+ * @param {PubKeyHex} identityKey - Sender's identity key to block
1458
+ * @returns {Promise<void>} Permission status after denying
1459
+ *
1460
+ * @description
1461
+ * Convenience method to block notifications from a specific peer.
1462
+ *
1463
+ * @example
1464
+ * await client.denyNotificationsFromPeer('03spam123...')
1465
+ */
1466
+ async denyNotificationsFromPeer(identityKey, overrideHost) {
1467
+ await this.setMessageBoxPermission({
1468
+ messageBox: 'notifications',
1469
+ sender: identityKey,
1470
+ recipientFee: -1
1471
+ }, overrideHost);
1472
+ }
1473
+ /**
1474
+ * @method checkPeerNotificationStatus
1475
+ * @async
1476
+ * @param {PubKeyHex} identityKey - Sender's identity key to check
1477
+ * @returns {Promise<MessageBoxPermission>} Current permission status
1478
+ *
1479
+ * @description
1480
+ * Convenience method to check notification permission for a specific peer.
1481
+ *
1482
+ * @example
1483
+ * const status = await client.checkPeerNotificationStatus('03abc123...')
1484
+ * console.log(status.allowed) // true/false
1485
+ */
1486
+ async checkPeerNotificationStatus(identityKey, overrideHost) {
1487
+ const myIdentityKey = await this.getIdentityKey();
1488
+ return await this.getMessageBoxPermission({
1489
+ recipient: myIdentityKey,
1490
+ messageBox: 'notifications',
1491
+ sender: identityKey
1492
+ }, overrideHost);
1493
+ }
1494
+ /**
1495
+ * @method listPeerNotifications
1496
+ * @async
1497
+ * @returns {Promise<MessageBoxPermission[]>} List of notification permissions
1498
+ *
1499
+ * @description
1500
+ * Convenience method to list all notification permissions.
1501
+ *
1502
+ * @example
1503
+ * const notifications = await client.listPeerNotifications()
1504
+ */
1505
+ async listPeerNotifications(overrideHost) {
1506
+ return await this.listMessageBoxPermissions({ messageBox: 'notifications' }, overrideHost);
1507
+ }
1508
+ /**
1509
+ * @method sendNotification
1510
+ * @async
1511
+ * @param {PubKeyHex} recipient - Recipient's identity key
1512
+ * @param {string | object} body - Notification content
1513
+ * @param {string} [overrideHost] - Optional host override
1514
+ * @returns {Promise<SendMessageResponse>} Send result
1515
+ *
1516
+ * @description
1517
+ * Convenience method to send a notification with automatic quote fetching and payment handling.
1518
+ * Automatically determines the required payment amount and creates the payment if needed.
1519
+ *
1520
+ * @example
1521
+ * // Send notification (auto-determines payment needed)
1522
+ * await client.sendNotification('03def456...', 'Hello!')
1523
+ *
1524
+ * // Send with maximum payment limit for safety
1525
+ * await client.sendNotification('03def456...', { title: 'Alert', body: 'Important update' }, 50)
1526
+ */
1527
+ async sendNotification(recipient, body, overrideHost) {
1528
+ await this.assertInitialized();
1529
+ // Use sendMessage with permission checking enabled
1530
+ // This eliminates duplication of quote fetching and payment logic
1531
+ return await this.sendMessage({
1532
+ recipient,
1533
+ messageBox: 'notifications',
1534
+ body,
1535
+ checkPermissions: true
1536
+ }, overrideHost);
1537
+ }
1538
+ /**
1539
+ * Register a device for FCM push notifications.
1540
+ *
1541
+ * @async
1542
+ * @param {DeviceRegistrationParams} params - Device registration parameters
1543
+ * @param {string} [overrideHost] - Optional host override
1544
+ * @returns {Promise<DeviceRegistrationResponse>} Registration response
1545
+ *
1546
+ * @description
1547
+ * Registers a device with the message box server to receive FCM push notifications.
1548
+ * The FCM token is obtained from Firebase SDK on the client side.
1549
+ *
1550
+ * @example
1551
+ * const result = await client.registerDevice({
1552
+ * fcmToken: 'eBo8F...',
1553
+ * platform: 'ios',
1554
+ * deviceId: 'iPhone15Pro'
1555
+ * })
1556
+ */
1557
+ async registerDevice(params, overrideHost) {
1558
+ await this.assertInitialized();
1559
+ if (params.fcmToken == null || params.fcmToken.trim() === '') {
1560
+ throw new Error('fcmToken is required and must be a non-empty string');
1561
+ }
1562
+ // Validate platform if provided
1563
+ const validPlatforms = ['ios', 'android', 'web'];
1564
+ if (params.platform != null && !validPlatforms.includes(params.platform)) {
1565
+ throw new Error('platform must be one of: ios, android, web');
1566
+ }
1567
+ const finalHost = overrideHost ?? this.host;
1568
+ Logger.log('[MB CLIENT] Registering device for FCM notifications...');
1569
+ const response = await this.authFetch.fetch(`${finalHost}/registerDevice`, {
1570
+ method: 'POST',
1571
+ headers: { 'Content-Type': 'application/json' },
1572
+ body: JSON.stringify({
1573
+ fcmToken: params.fcmToken.trim(),
1574
+ deviceId: params.deviceId?.trim() ?? undefined,
1575
+ platform: params.platform ?? undefined
1576
+ })
1577
+ });
1578
+ if (!response.ok) {
1579
+ const errorData = await response.json().catch(() => ({}));
1580
+ const description = String(errorData.description) ?? response.statusText;
1581
+ throw new Error(`Failed to register device: HTTP ${response.status} - ${description}`);
1582
+ }
1583
+ const data = await response.json();
1584
+ if (data.status === 'error') {
1585
+ throw new Error(data.description ?? 'Failed to register device');
1586
+ }
1587
+ Logger.log('[MB CLIENT] Device registered successfully');
1588
+ return {
1589
+ status: data.status,
1590
+ message: data.message,
1591
+ deviceId: data.deviceId
1592
+ };
1593
+ }
1594
+ /**
1595
+ * List all registered devices for push notifications.
1596
+ *
1597
+ * @async
1598
+ * @param {string} [overrideHost] - Optional host override
1599
+ * @returns {Promise<RegisteredDevice[]>} Array of registered devices
1600
+ *
1601
+ * @description
1602
+ * Retrieves all devices registered by the authenticated user for FCM push notifications.
1603
+ * Only shows devices belonging to the current user (authenticated via AuthFetch).
1604
+ *
1605
+ * @example
1606
+ * const devices = await client.listRegisteredDevices()
1607
+ * console.log(`Found ${devices.length} registered devices`)
1608
+ * devices.forEach(device => {
1609
+ * console.log(`Device: ${device.platform} - ${device.fcmToken}`)
1610
+ * })
1611
+ */
1612
+ async listRegisteredDevices(overrideHost) {
1613
+ await this.assertInitialized();
1614
+ const finalHost = overrideHost ?? this.host;
1615
+ Logger.log('[MB CLIENT] Listing registered devices...');
1616
+ const response = await this.authFetch.fetch(`${finalHost}/devices`, {
1617
+ method: 'GET'
1618
+ });
1619
+ if (!response.ok) {
1620
+ const errorData = await response.json().catch(() => ({}));
1621
+ const description = String(errorData.description) ?? response.statusText;
1622
+ throw new Error(`Failed to list devices: HTTP ${response.status} - ${description}`);
1623
+ }
1624
+ const data = await response.json();
1625
+ if (data.status === 'error') {
1626
+ throw new Error(data.description ?? 'Failed to list devices');
1627
+ }
1628
+ Logger.log(`[MB CLIENT] Found ${data.devices.length} registered devices`);
1629
+ return data.devices;
1630
+ }
1631
+ // ===========================
1632
+ // PRIVATE HELPER METHODS
1633
+ // ===========================
1634
+ static getStatusFromFee(fee) {
1635
+ if (fee === -1)
1636
+ return 'blocked';
1637
+ if (fee === 0)
1638
+ return 'always_allow';
1639
+ return 'payment_required';
1640
+ }
1641
+ /**
1642
+ * Creates payment transaction for message delivery fees
1643
+ * TODO: Consider consolidating payment generating logic with a util PeerPayClient can use as well.
1644
+ * @private
1645
+ * @param {string} recipient - Recipient identity key
1646
+ * @param {MessageBoxQuote} quote - Fee quote with delivery and recipient fees
1647
+ * @param {string} description - Description for the payment transaction
1648
+ * @returns {Promise<Payment>} Payment transaction data
1649
+ */
1650
+ async createMessagePayment(recipient, quote, description = 'MessageBox delivery payment') {
1651
+ if (quote.recipientFee <= 0 && quote.deliveryFee <= 0) {
1652
+ throw new Error('No payment required');
1653
+ }
1654
+ Logger.log(`[MB CLIENT] Creating payment transaction for ${quote.recipientFee} sats (delivery: ${quote.deliveryFee}, recipient: ${quote.recipientFee})`);
1655
+ const outputs = [];
1656
+ const createActionOutputs = [];
1657
+ // Get sender identity key for remittance data
1658
+ const senderIdentityKey = await this.getIdentityKey();
1659
+ // Add server delivery fee output if > 0
1660
+ let outputIndex = 0;
1661
+ if (quote.deliveryFee > 0) {
1662
+ const derivationPrefix = Utils.toBase64(Random(32));
1663
+ const derivationSuffix = Utils.toBase64(Random(32));
1664
+ // Get host's derived public key
1665
+ const { publicKey: derivedKeyResult } = await this.walletClient.getPublicKey({
1666
+ protocolID: [2, '3241645161d8'],
1667
+ keyID: `${derivationPrefix} ${derivationSuffix}`,
1668
+ counterparty: quote.deliveryAgentIdentityKey
1669
+ });
1670
+ // Create locking script using host's public key
1671
+ const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex();
1672
+ // Add to createAction outputs
1673
+ createActionOutputs.push({
1674
+ satoshis: quote.deliveryFee,
1675
+ lockingScript,
1676
+ outputDescription: 'MessageBox server delivery fee',
1677
+ customInstructions: JSON.stringify({
1678
+ derivationPrefix,
1679
+ derivationSuffix,
1680
+ recipientIdentityKey: quote.deliveryAgentIdentityKey
1681
+ })
1682
+ });
1683
+ outputs.push({
1684
+ outputIndex: outputIndex++,
1685
+ protocol: 'wallet payment',
1686
+ paymentRemittance: {
1687
+ derivationPrefix,
1688
+ derivationSuffix,
1689
+ senderIdentityKey
1690
+ }
1691
+ });
1692
+ }
1693
+ // Add recipient fee output if > 0
1694
+ if (quote.recipientFee > 0) {
1695
+ const derivationPrefix = Utils.toBase64(Random(32));
1696
+ const derivationSuffix = Utils.toBase64(Random(32));
1697
+ // Get a derived public key for the recipient that "anyone" can verify
1698
+ const anyoneWallet = new ProtoWallet('anyone');
1699
+ const { publicKey: derivedKeyResult } = await anyoneWallet.getPublicKey({
1700
+ protocolID: [2, '3241645161d8'],
1701
+ keyID: `${derivationPrefix} ${derivationSuffix}`,
1702
+ counterparty: recipient
1703
+ });
1704
+ if (derivedKeyResult == null || derivedKeyResult.trim() === '') {
1705
+ throw new Error('Failed to derive recipient\'s public key');
1706
+ }
1707
+ // Create locking script using recipient's public key
1708
+ const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex();
1709
+ // Add to createAction outputs
1710
+ createActionOutputs.push({
1711
+ satoshis: quote.recipientFee,
1712
+ lockingScript,
1713
+ outputDescription: 'Recipient message fee',
1714
+ customInstructions: JSON.stringify({
1715
+ derivationPrefix,
1716
+ derivationSuffix,
1717
+ recipientIdentityKey: recipient
1718
+ })
1719
+ });
1720
+ outputs.push({
1721
+ outputIndex: outputIndex++,
1722
+ protocol: 'wallet payment',
1723
+ paymentRemittance: {
1724
+ derivationPrefix,
1725
+ derivationSuffix,
1726
+ senderIdentityKey
1727
+ }
1728
+ });
1729
+ }
1730
+ const { tx } = await this.walletClient.createAction({
1731
+ description,
1732
+ outputs: createActionOutputs,
1733
+ options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
1734
+ });
1735
+ if (tx == null) {
1736
+ throw new Error('Failed to create payment transaction');
1737
+ }
1738
+ return {
1739
+ tx,
1740
+ outputs,
1741
+ description
1742
+ // labels
1743
+ };
1744
+ }
1164
1745
  }
1165
1746
  //# sourceMappingURL=MessageBoxClient.js.map