@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.
- package/dist/cjs/package.json +5 -5
- package/dist/cjs/src/MessageBoxClient.js +707 -87
- package/dist/cjs/src/MessageBoxClient.js.map +1 -1
- package/dist/cjs/src/PeerPayClient.js +61 -28
- package/dist/cjs/src/PeerPayClient.js.map +1 -1
- package/dist/cjs/src/Utils/logger.js +22 -21
- package/dist/cjs/src/Utils/logger.js.map +1 -1
- package/dist/cjs/src/types/permissions.js +6 -0
- package/dist/cjs/src/types/permissions.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/MessageBoxClient.js +593 -12
- package/dist/esm/src/MessageBoxClient.js.map +1 -1
- package/dist/esm/src/PeerPayClient.js +1 -1
- package/dist/esm/src/PeerPayClient.js.map +1 -1
- package/dist/esm/src/Utils/logger.js +17 -19
- package/dist/esm/src/Utils/logger.js.map +1 -1
- package/dist/esm/src/types/permissions.js +5 -0
- package/dist/esm/src/types/permissions.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/MessageBoxClient.d.ts +218 -13
- package/dist/types/src/MessageBoxClient.d.ts.map +1 -1
- package/dist/types/src/PeerPayClient.d.ts.map +1 -1
- package/dist/types/src/Utils/logger.d.ts +5 -8
- package/dist/types/src/Utils/logger.d.ts.map +1 -1
- package/dist/types/src/types/permissions.d.ts +75 -0
- package/dist/types/src/types/permissions.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +71 -2
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/package.json +5 -5
- package/src/MessageBoxClient.ts +732 -24
- package/src/PeerPayClient.ts +11 -11
- package/src/Utils/logger.ts +17 -19
- package/src/types/permissions.ts +81 -0
- 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
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|