@bsv/message-box-client 1.1.10 → 1.2.0
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 +4 -4
- package/dist/cjs/src/MessageBoxClient.js +747 -129
- 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 +636 -57
- 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 +235 -24
- 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 +80 -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 +4 -4
- package/src/MessageBoxClient.ts +781 -68
- package/src/PeerPayClient.ts +11 -11
- package/src/Utils/logger.ts +17 -19
- package/src/types/permissions.ts +81 -0
- package/src/types.ts +87 -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
|
*
|
|
@@ -88,12 +88,12 @@ export class MessageBoxClient {
|
|
|
88
88
|
* await client.init()
|
|
89
89
|
*/
|
|
90
90
|
constructor(options = {}) {
|
|
91
|
-
const { host, walletClient, enableLogging = false, networkPreset = 'mainnet' } = options;
|
|
91
|
+
const { host, walletClient, enableLogging = false, networkPreset = 'mainnet', originator = undefined } = options;
|
|
92
92
|
const defaultHost = this.networkPreset === 'testnet'
|
|
93
93
|
? DEFAULT_TESTNET_HOST
|
|
94
94
|
: DEFAULT_MAINNET_HOST;
|
|
95
95
|
this.host = host?.trim() ?? defaultHost;
|
|
96
|
-
this.walletClient = walletClient ?? new WalletClient();
|
|
96
|
+
this.walletClient = walletClient ?? new WalletClient('auto', originator);
|
|
97
97
|
this.authFetch = new AuthFetch(this.walletClient);
|
|
98
98
|
this.networkPreset = networkPreset;
|
|
99
99
|
this.lookupResolver = new LookupResolver({
|
|
@@ -107,6 +107,7 @@ export class MessageBoxClient {
|
|
|
107
107
|
* @method init
|
|
108
108
|
* @async
|
|
109
109
|
* @param {string} [targetHost] - Optional host to set or override the default host.
|
|
110
|
+
* @param {string} [originator] - Optional originator to use with walletClient.
|
|
110
111
|
* @returns {Promise<void>}
|
|
111
112
|
*
|
|
112
113
|
* @description
|
|
@@ -125,7 +126,7 @@ export class MessageBoxClient {
|
|
|
125
126
|
* await client.init()
|
|
126
127
|
* await client.sendMessage({ recipient, messageBox: 'inbox', body: 'Hello' })
|
|
127
128
|
*/
|
|
128
|
-
async init(targetHost = this.host) {
|
|
129
|
+
async init(targetHost = this.host, originator) {
|
|
129
130
|
const normalizedHost = targetHost?.trim();
|
|
130
131
|
if (normalizedHost === '') {
|
|
131
132
|
throw new Error('Cannot anoint host: No valid host provided');
|
|
@@ -138,13 +139,13 @@ export class MessageBoxClient {
|
|
|
138
139
|
if (this.initialized)
|
|
139
140
|
return;
|
|
140
141
|
// 1. Get our identity key
|
|
141
|
-
const identityKey = await this.getIdentityKey();
|
|
142
|
+
const identityKey = await this.getIdentityKey(originator);
|
|
142
143
|
// 2. Check for any matching advertisements for the given host
|
|
143
|
-
const [firstAdvertisement] = await this.queryAdvertisements(identityKey, normalizedHost);
|
|
144
|
+
const [firstAdvertisement] = await this.queryAdvertisements(identityKey, normalizedHost, originator);
|
|
144
145
|
// 3. If none our found, anoint this host
|
|
145
146
|
if (firstAdvertisement == null || firstAdvertisement?.host?.trim() === '' || firstAdvertisement?.host !== normalizedHost) {
|
|
146
147
|
Logger.log('[MB CLIENT] Anointing host:', normalizedHost);
|
|
147
|
-
const { txid } = await this.anointHost(normalizedHost);
|
|
148
|
+
const { txid } = await this.anointHost(normalizedHost, originator);
|
|
148
149
|
if (txid == null || txid.trim() === '') {
|
|
149
150
|
throw new Error('Failed to anoint host: No transaction ID returned');
|
|
150
151
|
}
|
|
@@ -179,18 +180,19 @@ export class MessageBoxClient {
|
|
|
179
180
|
}
|
|
180
181
|
/**
|
|
181
182
|
* @method getIdentityKey
|
|
183
|
+
* @param {string} [originator] - Optional originator to use for identity key lookup
|
|
182
184
|
* @returns {Promise<string>} The identity public key of the user
|
|
183
185
|
* @description
|
|
184
186
|
* Returns the client's identity key, used for signing, encryption, and addressing.
|
|
185
187
|
* If not already loaded, it will fetch and cache it.
|
|
186
188
|
*/
|
|
187
|
-
async getIdentityKey() {
|
|
189
|
+
async getIdentityKey(originator) {
|
|
188
190
|
if (this.myIdentityKey != null && this.myIdentityKey.trim() !== '') {
|
|
189
191
|
return this.myIdentityKey;
|
|
190
192
|
}
|
|
191
193
|
Logger.log('[MB CLIENT] Fetching identity key...');
|
|
192
194
|
try {
|
|
193
|
-
const keyResult = await this.walletClient.getPublicKey({ identityKey: true });
|
|
195
|
+
const keyResult = await this.walletClient.getPublicKey({ identityKey: true }, originator);
|
|
194
196
|
this.myIdentityKey = keyResult.publicKey;
|
|
195
197
|
Logger.log(`[MB CLIENT] Identity key fetched: ${this.myIdentityKey}`);
|
|
196
198
|
return this.myIdentityKey;
|
|
@@ -216,6 +218,7 @@ export class MessageBoxClient {
|
|
|
216
218
|
}
|
|
217
219
|
/**
|
|
218
220
|
* @method initializeConnection
|
|
221
|
+
* @param {string} [originator] - Optional originator to use for authentication.
|
|
219
222
|
* @async
|
|
220
223
|
* @returns {Promise<void>}
|
|
221
224
|
* @description
|
|
@@ -237,20 +240,11 @@ export class MessageBoxClient {
|
|
|
237
240
|
* await mb.initializeConnection()
|
|
238
241
|
* // WebSocket is now ready for use
|
|
239
242
|
*/
|
|
240
|
-
async initializeConnection() {
|
|
243
|
+
async initializeConnection(originator) {
|
|
241
244
|
await this.assertInitialized();
|
|
242
245
|
Logger.log('[MB CLIENT] initializeConnection() STARTED');
|
|
243
246
|
if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
const keyResult = await this.walletClient.getPublicKey({ identityKey: true });
|
|
247
|
-
this.myIdentityKey = keyResult.publicKey;
|
|
248
|
-
Logger.log(`[MB CLIENT] Identity key fetched successfully: ${this.myIdentityKey}`);
|
|
249
|
-
}
|
|
250
|
-
catch (error) {
|
|
251
|
-
Logger.error('[MB CLIENT ERROR] Failed to fetch identity key:', error);
|
|
252
|
-
throw new Error('Identity key retrieval failed');
|
|
253
|
-
}
|
|
247
|
+
await this.getIdentityKey(originator);
|
|
254
248
|
}
|
|
255
249
|
if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
|
|
256
250
|
Logger.error('[MB CLIENT ERROR] Identity key is still missing after retrieval!');
|
|
@@ -314,6 +308,7 @@ export class MessageBoxClient {
|
|
|
314
308
|
* @method resolveHostForRecipient
|
|
315
309
|
* @async
|
|
316
310
|
* @param {string} identityKey - The public identity key of the intended recipient.
|
|
311
|
+
* @param {string} [originator] - The originator to use for the WalletClient.
|
|
317
312
|
* @returns {Promise<string>} - A fully qualified host URL for the recipient's MessageBox server.
|
|
318
313
|
*
|
|
319
314
|
* @description
|
|
@@ -328,8 +323,8 @@ export class MessageBoxClient {
|
|
|
328
323
|
* @example
|
|
329
324
|
* const host = await resolveHostForRecipient('028d...') // → returns either overlay host or this.host
|
|
330
325
|
*/
|
|
331
|
-
async resolveHostForRecipient(identityKey) {
|
|
332
|
-
const advertisementTokens = await this.queryAdvertisements(identityKey);
|
|
326
|
+
async resolveHostForRecipient(identityKey, originator) {
|
|
327
|
+
const advertisementTokens = await this.queryAdvertisements(identityKey, undefined, originator);
|
|
333
328
|
if (advertisementTokens.length === 0) {
|
|
334
329
|
Logger.warn(`[MB CLIENT] No advertisements for ${identityKey}, using default host ${this.host}`);
|
|
335
330
|
return this.host;
|
|
@@ -345,10 +340,10 @@ export class MessageBoxClient {
|
|
|
345
340
|
* @param host? if passed, only look for adverts anointed at that host
|
|
346
341
|
* @returns 0-length array if nothing valid was found
|
|
347
342
|
*/
|
|
348
|
-
async queryAdvertisements(identityKey, host) {
|
|
343
|
+
async queryAdvertisements(identityKey, host, originator) {
|
|
349
344
|
const hosts = [];
|
|
350
345
|
try {
|
|
351
|
-
const query = { identityKey: identityKey ?? await this.getIdentityKey() };
|
|
346
|
+
const query = { identityKey: identityKey ?? await this.getIdentityKey(originator) };
|
|
352
347
|
if (host != null && host.trim() !== '')
|
|
353
348
|
query.host = host;
|
|
354
349
|
const result = await this.lookupResolver.query({
|
|
@@ -356,7 +351,7 @@ export class MessageBoxClient {
|
|
|
356
351
|
query
|
|
357
352
|
});
|
|
358
353
|
if (result.type !== 'output-list') {
|
|
359
|
-
throw new Error(`Unexpected result type: ${result.type}`);
|
|
354
|
+
throw new Error(`Unexpected result type: ${String(result.type)}`);
|
|
360
355
|
}
|
|
361
356
|
for (const output of result.outputs) {
|
|
362
357
|
try {
|
|
@@ -458,7 +453,7 @@ export class MessageBoxClient {
|
|
|
458
453
|
* onMessage: (msg) => console.log('Received live message:', msg)
|
|
459
454
|
* })
|
|
460
455
|
*/
|
|
461
|
-
async listenForLiveMessages({ onMessage, messageBox }) {
|
|
456
|
+
async listenForLiveMessages({ onMessage, messageBox, originator }) {
|
|
462
457
|
await this.assertInitialized();
|
|
463
458
|
Logger.log(`[MB CLIENT] Setting up listener for WebSocket room: ${messageBox}`);
|
|
464
459
|
// Ensure WebSocket connection and room join
|
|
@@ -491,7 +486,7 @@ export class MessageBoxClient {
|
|
|
491
486
|
keyID: '1',
|
|
492
487
|
counterparty: message.sender,
|
|
493
488
|
ciphertext: Utils.toArray(parsedBody.encryptedMessage, 'base64')
|
|
494
|
-
});
|
|
489
|
+
}, originator);
|
|
495
490
|
message.body = Utils.toUTF8(decrypted.plaintext);
|
|
496
491
|
}
|
|
497
492
|
else {
|
|
@@ -543,7 +538,7 @@ export class MessageBoxClient {
|
|
|
543
538
|
* body: { amount: 1000 }
|
|
544
539
|
* })
|
|
545
540
|
*/
|
|
546
|
-
async sendLiveMessage({ recipient, messageBox, body, messageId, skipEncryption }) {
|
|
541
|
+
async sendLiveMessage({ recipient, messageBox, body, messageId, skipEncryption, checkPermissions, originator }) {
|
|
547
542
|
await this.assertInitialized();
|
|
548
543
|
if (recipient == null || recipient.trim() === '') {
|
|
549
544
|
throw new Error('[MB CLIENT ERROR] Recipient identity key is required');
|
|
@@ -569,7 +564,7 @@ export class MessageBoxClient {
|
|
|
569
564
|
protocolID: [1, 'messagebox'],
|
|
570
565
|
keyID: '1',
|
|
571
566
|
counterparty: recipient
|
|
572
|
-
});
|
|
567
|
+
}, originator);
|
|
573
568
|
finalMessageId = messageId ?? Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
574
569
|
}
|
|
575
570
|
catch (error) {
|
|
@@ -588,7 +583,7 @@ export class MessageBoxClient {
|
|
|
588
583
|
keyID: '1',
|
|
589
584
|
counterparty: recipient,
|
|
590
585
|
plaintext: Utils.toArray(typeof body === 'string' ? body : JSON.stringify(body), 'utf8')
|
|
591
|
-
});
|
|
586
|
+
}, originator);
|
|
592
587
|
outgoingBody = JSON.stringify({
|
|
593
588
|
encryptedMessage: Utils.toBase64(encryptedMessage.ciphertext)
|
|
594
589
|
});
|
|
@@ -612,7 +607,8 @@ export class MessageBoxClient {
|
|
|
612
607
|
messageBox,
|
|
613
608
|
body,
|
|
614
609
|
messageId: finalMessageId,
|
|
615
|
-
skipEncryption
|
|
610
|
+
skipEncryption,
|
|
611
|
+
checkPermissions
|
|
616
612
|
};
|
|
617
613
|
this.resolveHostForRecipient(recipient)
|
|
618
614
|
.then(async (host) => {
|
|
@@ -651,7 +647,8 @@ export class MessageBoxClient {
|
|
|
651
647
|
messageBox,
|
|
652
648
|
body,
|
|
653
649
|
messageId: finalMessageId,
|
|
654
|
-
skipEncryption
|
|
650
|
+
skipEncryption,
|
|
651
|
+
checkPermissions
|
|
655
652
|
};
|
|
656
653
|
this.resolveHostForRecipient(recipient)
|
|
657
654
|
.then(async (host) => {
|
|
@@ -744,7 +741,7 @@ export class MessageBoxClient {
|
|
|
744
741
|
* body: { type: 'ping' }
|
|
745
742
|
* })
|
|
746
743
|
*/
|
|
747
|
-
async sendMessage(message, overrideHost) {
|
|
744
|
+
async sendMessage(message, overrideHost, originator) {
|
|
748
745
|
await this.assertInitialized();
|
|
749
746
|
if (message.recipient == null || message.recipient.trim() === '') {
|
|
750
747
|
throw new Error('You must provide a message recipient!');
|
|
@@ -755,6 +752,33 @@ export class MessageBoxClient {
|
|
|
755
752
|
if (message.body == null || (typeof message.body === 'string' && message.body.trim().length === 0)) {
|
|
756
753
|
throw new Error('Every message must have a body!');
|
|
757
754
|
}
|
|
755
|
+
// Optional permission checking for backwards compatibility
|
|
756
|
+
let paymentData;
|
|
757
|
+
if (message.checkPermissions === true) {
|
|
758
|
+
try {
|
|
759
|
+
Logger.log('[MB CLIENT] Checking permissions and fees for message...');
|
|
760
|
+
// Get quote to check if payment is required
|
|
761
|
+
const quote = await this.getMessageBoxQuote({
|
|
762
|
+
recipient: message.recipient,
|
|
763
|
+
messageBox: message.messageBox
|
|
764
|
+
});
|
|
765
|
+
if (quote.recipientFee === -1) {
|
|
766
|
+
throw new Error('You have been blocked from sending messages to this recipient.');
|
|
767
|
+
}
|
|
768
|
+
if (quote.recipientFee > 0 || quote.deliveryFee > 0) {
|
|
769
|
+
const requiredPayment = quote.recipientFee + quote.deliveryFee;
|
|
770
|
+
if (requiredPayment > 0) {
|
|
771
|
+
Logger.log(`[MB CLIENT] Creating payment of ${requiredPayment} sats for message...`);
|
|
772
|
+
// Create payment using helper method
|
|
773
|
+
paymentData = await this.createMessagePayment(message.recipient, quote, overrideHost);
|
|
774
|
+
Logger.log('[MB CLIENT] Payment data prepared:', paymentData);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
throw new Error(`Permission check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
758
782
|
let messageId;
|
|
759
783
|
try {
|
|
760
784
|
const hmac = await this.walletClient.createHmac({
|
|
@@ -762,7 +786,7 @@ export class MessageBoxClient {
|
|
|
762
786
|
protocolID: [1, 'messagebox'],
|
|
763
787
|
keyID: '1',
|
|
764
788
|
counterparty: message.recipient
|
|
765
|
-
});
|
|
789
|
+
}, originator);
|
|
766
790
|
messageId = message.messageId ?? Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
767
791
|
}
|
|
768
792
|
catch (error) {
|
|
@@ -779,7 +803,7 @@ export class MessageBoxClient {
|
|
|
779
803
|
keyID: '1',
|
|
780
804
|
counterparty: message.recipient,
|
|
781
805
|
plaintext: Utils.toArray(typeof message.body === 'string' ? message.body : JSON.stringify(message.body), 'utf8')
|
|
782
|
-
});
|
|
806
|
+
}, originator);
|
|
783
807
|
finalBody = JSON.stringify({ encryptedMessage: Utils.toBase64(encryptedMessage.ciphertext) });
|
|
784
808
|
}
|
|
785
809
|
const requestBody = {
|
|
@@ -787,7 +811,8 @@ export class MessageBoxClient {
|
|
|
787
811
|
...message,
|
|
788
812
|
messageId,
|
|
789
813
|
body: finalBody
|
|
790
|
-
}
|
|
814
|
+
},
|
|
815
|
+
...(paymentData != null && { payment: paymentData })
|
|
791
816
|
};
|
|
792
817
|
try {
|
|
793
818
|
const finalHost = overrideHost ?? await this.resolveHostForRecipient(message.recipient);
|
|
@@ -795,7 +820,7 @@ export class MessageBoxClient {
|
|
|
795
820
|
Logger.log('[MB CLIENT] Request Body:', JSON.stringify(requestBody, null, 2));
|
|
796
821
|
if (this.myIdentityKey == null || this.myIdentityKey === '') {
|
|
797
822
|
try {
|
|
798
|
-
const keyResult = await this.walletClient.getPublicKey({ identityKey: true });
|
|
823
|
+
const keyResult = await this.walletClient.getPublicKey({ identityKey: true }, originator);
|
|
799
824
|
this.myIdentityKey = keyResult.publicKey;
|
|
800
825
|
Logger.log(`[MB CLIENT] Fetched identity key before sending request: ${this.myIdentityKey}`);
|
|
801
826
|
}
|
|
@@ -856,19 +881,19 @@ export class MessageBoxClient {
|
|
|
856
881
|
* @example
|
|
857
882
|
* const { txid } = await client.anointHost('https://my-messagebox.io')
|
|
858
883
|
*/
|
|
859
|
-
async anointHost(host) {
|
|
884
|
+
async anointHost(host, originator) {
|
|
860
885
|
Logger.log('[MB CLIENT] Starting anointHost...');
|
|
861
886
|
try {
|
|
862
887
|
if (!host.startsWith('http')) {
|
|
863
888
|
throw new Error('Invalid host URL');
|
|
864
889
|
}
|
|
865
|
-
const identityKey = await this.getIdentityKey();
|
|
890
|
+
const identityKey = await this.getIdentityKey(originator);
|
|
866
891
|
Logger.log('[MB CLIENT] Fields - Identity:', identityKey, 'Host:', host);
|
|
867
892
|
const fields = [
|
|
868
893
|
Utils.toArray(identityKey, 'hex'),
|
|
869
894
|
Utils.toArray(host, 'utf8')
|
|
870
895
|
];
|
|
871
|
-
const pushdrop = new PushDrop(this.walletClient);
|
|
896
|
+
const pushdrop = new PushDrop(this.walletClient, originator);
|
|
872
897
|
Logger.log('Fields:', fields.map(a => Utils.toHex(a)));
|
|
873
898
|
Logger.log('ProtocolID:', [1, 'messagebox advertisement']);
|
|
874
899
|
Logger.log('KeyID:', '1');
|
|
@@ -886,7 +911,7 @@ export class MessageBoxClient {
|
|
|
886
911
|
outputDescription: 'Overlay advertisement output'
|
|
887
912
|
}],
|
|
888
913
|
options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
|
|
889
|
-
});
|
|
914
|
+
}, originator);
|
|
890
915
|
Logger.log('[MB CLIENT] Transaction created:', txid);
|
|
891
916
|
if (tx !== undefined) {
|
|
892
917
|
const broadcaster = new TopicBroadcaster(['tm_messagebox'], {
|
|
@@ -910,6 +935,7 @@ export class MessageBoxClient {
|
|
|
910
935
|
* @method revokeHostAdvertisement
|
|
911
936
|
* @async
|
|
912
937
|
* @param {AdvertisementToken} advertisementToken - The advertisement token containing the messagebox host to revoke.
|
|
938
|
+
* @param {string} [originator] - Optional originator to use with walletClient.
|
|
913
939
|
* @returns {Promise<{ txid: string }>} - The transaction ID of the revocation broadcast to the overlay network.
|
|
914
940
|
*
|
|
915
941
|
* @description
|
|
@@ -919,7 +945,7 @@ export class MessageBoxClient {
|
|
|
919
945
|
* @example
|
|
920
946
|
* const { txid } = await client.revokeHost('https://my-messagebox.io')
|
|
921
947
|
*/
|
|
922
|
-
async revokeHostAdvertisement(advertisementToken) {
|
|
948
|
+
async revokeHostAdvertisement(advertisementToken, originator) {
|
|
923
949
|
Logger.log('[MB CLIENT] Starting revokeHost...');
|
|
924
950
|
const outpoint = `${advertisementToken.txid}.${advertisementToken.outputIndex}`;
|
|
925
951
|
try {
|
|
@@ -933,13 +959,13 @@ export class MessageBoxClient {
|
|
|
933
959
|
inputDescription: 'Revoking host advertisement token'
|
|
934
960
|
}
|
|
935
961
|
]
|
|
936
|
-
});
|
|
962
|
+
}, originator);
|
|
937
963
|
if (signableTransaction === undefined) {
|
|
938
964
|
throw new Error('Failed to create signable transaction.');
|
|
939
965
|
}
|
|
940
966
|
const partialTx = Transaction.fromBEEF(signableTransaction.tx);
|
|
941
967
|
// Prepare the unlocker
|
|
942
|
-
const pushdrop = new PushDrop(this.walletClient);
|
|
968
|
+
const pushdrop = new PushDrop(this.walletClient, originator);
|
|
943
969
|
const unlocker = await pushdrop.unlock([1, 'messagebox advertisement'], '1', 'anyone', 'all', false, advertisementToken.outputIndex, advertisementToken.lockingScript);
|
|
944
970
|
// Convert to Transaction, apply signature
|
|
945
971
|
const finalUnlockScript = await unlocker.sign(partialTx, advertisementToken.outputIndex);
|
|
@@ -954,7 +980,7 @@ export class MessageBoxClient {
|
|
|
954
980
|
options: {
|
|
955
981
|
acceptDelayedBroadcast: false
|
|
956
982
|
}
|
|
957
|
-
});
|
|
983
|
+
}, originator);
|
|
958
984
|
if (signedTx === undefined) {
|
|
959
985
|
throw new Error('Failed to finalize the transaction signature.');
|
|
960
986
|
}
|
|
@@ -985,9 +1011,16 @@ export class MessageBoxClient {
|
|
|
985
1011
|
*
|
|
986
1012
|
* Each message is:
|
|
987
1013
|
* - Parsed and, if encrypted, decrypted using AES-256-GCM via BRC-2-compliant ECDH key derivation and symmetric encryption.
|
|
1014
|
+
* - Automatically processed for payments: if the message includes recipient fee payments, they are internalized using `walletClient.internalizeAction()`.
|
|
988
1015
|
* - Returned as a normalized `PeerMessage` with readable string body content.
|
|
989
1016
|
*
|
|
990
|
-
*
|
|
1017
|
+
* Payment Processing:
|
|
1018
|
+
* - Detects messages that include payment data (from paid message delivery).
|
|
1019
|
+
* - Automatically internalizes recipient payment outputs, allowing you to receive payments without additional API calls.
|
|
1020
|
+
* - Only recipient payments are stored with messages - delivery fees are already processed by the server.
|
|
1021
|
+
* - Continues processing messages even if payment internalization fails.
|
|
1022
|
+
*
|
|
1023
|
+
* Decryption automatically derives a shared secret using the sender's identity key and the receiver's child private key.
|
|
991
1024
|
* If the sender is the same as the recipient, the `counterparty` is set to `'self'`.
|
|
992
1025
|
*
|
|
993
1026
|
* @throws {Error} If no messageBox is specified, the request fails, or the server returns an error.
|
|
@@ -995,15 +1028,16 @@ export class MessageBoxClient {
|
|
|
995
1028
|
* @example
|
|
996
1029
|
* const messages = await client.listMessages({ messageBox: 'inbox' })
|
|
997
1030
|
* messages.forEach(msg => console.log(msg.sender, msg.body))
|
|
1031
|
+
* // Payments included with messages are automatically received
|
|
998
1032
|
*/
|
|
999
|
-
async listMessages({ messageBox, host }) {
|
|
1033
|
+
async listMessages({ messageBox, host, originator }) {
|
|
1000
1034
|
await this.assertInitialized();
|
|
1001
1035
|
if (messageBox.trim() === '') {
|
|
1002
1036
|
throw new Error('MessageBox cannot be empty');
|
|
1003
1037
|
}
|
|
1004
1038
|
let hosts = host != null ? [host] : [];
|
|
1005
1039
|
if (hosts.length === 0) {
|
|
1006
|
-
const advertisedHosts = await this.queryAdvertisements(await this.getIdentityKey());
|
|
1040
|
+
const advertisedHosts = await this.queryAdvertisements(await this.getIdentityKey(originator), originator);
|
|
1007
1041
|
hosts = Array.from(new Set([this.host, ...advertisedHosts.map(h => h.host)]));
|
|
1008
1042
|
}
|
|
1009
1043
|
// Query each host in parallel
|
|
@@ -1066,21 +1100,65 @@ export class MessageBoxClient {
|
|
|
1066
1100
|
for (const message of messages) {
|
|
1067
1101
|
try {
|
|
1068
1102
|
const parsedBody = typeof message.body === 'string' ? tryParse(message.body) : message.body;
|
|
1103
|
+
let messageContent = parsedBody;
|
|
1104
|
+
let paymentData;
|
|
1069
1105
|
if (parsedBody != null &&
|
|
1070
1106
|
typeof parsedBody === 'object' &&
|
|
1071
|
-
|
|
1107
|
+
'message' in parsedBody) {
|
|
1108
|
+
// Handle wrapped message format (with payment data)
|
|
1109
|
+
const wrappedMessage = parsedBody.message;
|
|
1110
|
+
messageContent = typeof wrappedMessage === 'string'
|
|
1111
|
+
? tryParse(wrappedMessage)
|
|
1112
|
+
: wrappedMessage;
|
|
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
|
+
}, originator);
|
|
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(
|
|
1078
|
-
});
|
|
1154
|
+
ciphertext: Utils.toArray(messageContent.encryptedMessage, 'base64')
|
|
1155
|
+
}, originator);
|
|
1079
1156
|
const decryptedText = Utils.toUTF8(decrypted.plaintext);
|
|
1080
1157
|
message.body = tryParse(decryptedText);
|
|
1081
1158
|
}
|
|
1082
1159
|
else {
|
|
1083
|
-
|
|
1160
|
+
// For non-encrypted messages, use the processed content
|
|
1161
|
+
message.body = messageContent ?? parsedBody;
|
|
1084
1162
|
}
|
|
1085
1163
|
}
|
|
1086
1164
|
catch (err) {
|
|
@@ -1113,7 +1191,7 @@ export class MessageBoxClient {
|
|
|
1113
1191
|
* @example
|
|
1114
1192
|
* await client.acknowledgeMessage({ messageIds: ['msg123', 'msg456'] })
|
|
1115
1193
|
*/
|
|
1116
|
-
async acknowledgeMessage({ messageIds, host }) {
|
|
1194
|
+
async acknowledgeMessage({ messageIds, host, originator }) {
|
|
1117
1195
|
await this.assertInitialized();
|
|
1118
1196
|
if (!Array.isArray(messageIds) || messageIds.length === 0) {
|
|
1119
1197
|
throw new Error('Message IDs array cannot be empty');
|
|
@@ -1122,8 +1200,8 @@ export class MessageBoxClient {
|
|
|
1122
1200
|
let hosts = host != null ? [host] : [];
|
|
1123
1201
|
if (hosts.length === 0) {
|
|
1124
1202
|
// 1. Determine all hosts (advertised + default)
|
|
1125
|
-
const identityKey = await this.getIdentityKey();
|
|
1126
|
-
const advertisedHosts = await this.queryAdvertisements(identityKey);
|
|
1203
|
+
const identityKey = await this.getIdentityKey(originator);
|
|
1204
|
+
const advertisedHosts = await this.queryAdvertisements(identityKey, undefined, originator);
|
|
1127
1205
|
hosts = Array.from(new Set([this.host, ...advertisedHosts.map(h => h.host)]));
|
|
1128
1206
|
}
|
|
1129
1207
|
// 2. Dispatch parallel acknowledge requests
|
|
@@ -1161,5 +1239,506 @@ export class MessageBoxClient {
|
|
|
1161
1239
|
}
|
|
1162
1240
|
throw new Error(`Failed to acknowledge messages on all hosts: ${errs.map(e => String(e)).join('; ')}`);
|
|
1163
1241
|
}
|
|
1242
|
+
// ===========================
|
|
1243
|
+
// PERMISSION MANAGEMENT METHODS
|
|
1244
|
+
// ===========================
|
|
1245
|
+
/**
|
|
1246
|
+
* @method setMessageBoxPermission
|
|
1247
|
+
* @async
|
|
1248
|
+
* @param {SetMessageBoxPermissionParams} params - Permission configuration
|
|
1249
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1250
|
+
* @returns {Promise<void>} Permission status after setting
|
|
1251
|
+
*
|
|
1252
|
+
* @description
|
|
1253
|
+
* Sets permission for receiving messages in a specific messageBox.
|
|
1254
|
+
* Can set sender-specific permissions or box-wide defaults.
|
|
1255
|
+
*
|
|
1256
|
+
* @example
|
|
1257
|
+
* // Set box-wide default: allow notifications for 10 sats
|
|
1258
|
+
* await client.setMessageBoxPermission({ messageBox: 'notifications', recipientFee: 10 })
|
|
1259
|
+
*
|
|
1260
|
+
* // Block specific sender
|
|
1261
|
+
* await client.setMessageBoxPermission({
|
|
1262
|
+
* messageBox: 'notifications',
|
|
1263
|
+
* sender: '03abc123...',
|
|
1264
|
+
* recipientFee: -1
|
|
1265
|
+
* })
|
|
1266
|
+
*/
|
|
1267
|
+
async setMessageBoxPermission(params, overrideHost) {
|
|
1268
|
+
await this.assertInitialized();
|
|
1269
|
+
const finalHost = overrideHost ?? this.host;
|
|
1270
|
+
Logger.log('[MB CLIENT] Setting messageBox permission...');
|
|
1271
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/set`, {
|
|
1272
|
+
method: 'POST',
|
|
1273
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1274
|
+
body: JSON.stringify({
|
|
1275
|
+
messageBox: params.messageBox,
|
|
1276
|
+
recipientFee: params.recipientFee,
|
|
1277
|
+
...(params.sender != null && { sender: params.sender })
|
|
1278
|
+
})
|
|
1279
|
+
});
|
|
1280
|
+
if (!response.ok) {
|
|
1281
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1282
|
+
throw new Error(`Failed to set permission: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`);
|
|
1283
|
+
}
|
|
1284
|
+
const { status, description } = await response.json();
|
|
1285
|
+
if (status === 'error') {
|
|
1286
|
+
throw new Error(description ?? 'Failed to set permission');
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* @method getMessageBoxPermission
|
|
1291
|
+
* @async
|
|
1292
|
+
* @param {GetMessageBoxPermissionParams} params - Permission query parameters
|
|
1293
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1294
|
+
* @returns {Promise<MessageBoxPermission | null>} Permission data (null if not set)
|
|
1295
|
+
*
|
|
1296
|
+
* @description
|
|
1297
|
+
* Gets current permission data for a sender/messageBox combination.
|
|
1298
|
+
* Returns null if no permission is set.
|
|
1299
|
+
*
|
|
1300
|
+
* @example
|
|
1301
|
+
* const status = await client.getMessageBoxPermission({
|
|
1302
|
+
* recipient: '03def456...',
|
|
1303
|
+
* messageBox: 'notifications',
|
|
1304
|
+
* sender: '03abc123...'
|
|
1305
|
+
* })
|
|
1306
|
+
*/
|
|
1307
|
+
async getMessageBoxPermission(params, overrideHost) {
|
|
1308
|
+
await this.assertInitialized();
|
|
1309
|
+
const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient);
|
|
1310
|
+
const queryParams = new URLSearchParams({
|
|
1311
|
+
recipient: params.recipient,
|
|
1312
|
+
messageBox: params.messageBox,
|
|
1313
|
+
...(params.sender != null && { sender: params.sender })
|
|
1314
|
+
});
|
|
1315
|
+
Logger.log('[MB CLIENT] Getting messageBox permission...');
|
|
1316
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/get?${queryParams.toString()}`, {
|
|
1317
|
+
method: 'GET'
|
|
1318
|
+
});
|
|
1319
|
+
if (!response.ok) {
|
|
1320
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1321
|
+
throw new Error(`Failed to get permission: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`);
|
|
1322
|
+
}
|
|
1323
|
+
const data = await response.json();
|
|
1324
|
+
if (data.status === 'error') {
|
|
1325
|
+
throw new Error(data.description ?? 'Failed to get permission');
|
|
1326
|
+
}
|
|
1327
|
+
return data.permission;
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* @method getMessageBoxQuote
|
|
1331
|
+
* @async
|
|
1332
|
+
* @param {GetQuoteParams} params - Quote request parameters
|
|
1333
|
+
* @returns {Promise<MessageBoxQuote>} Fee quote and permission status
|
|
1334
|
+
*
|
|
1335
|
+
* @description
|
|
1336
|
+
* Gets a fee quote for sending a message, including delivery and recipient fees.
|
|
1337
|
+
*
|
|
1338
|
+
* @example
|
|
1339
|
+
* const quote = await client.getMessageBoxQuote({
|
|
1340
|
+
* recipient: '03def456...',
|
|
1341
|
+
* messageBox: 'notifications'
|
|
1342
|
+
* })
|
|
1343
|
+
*/
|
|
1344
|
+
async getMessageBoxQuote(params, overrideHost) {
|
|
1345
|
+
await this.assertInitialized();
|
|
1346
|
+
const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient);
|
|
1347
|
+
const queryParams = new URLSearchParams({
|
|
1348
|
+
recipient: params.recipient,
|
|
1349
|
+
messageBox: params.messageBox
|
|
1350
|
+
});
|
|
1351
|
+
Logger.log('[MB CLIENT] Getting messageBox quote...');
|
|
1352
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/quote?${queryParams.toString()}`, {
|
|
1353
|
+
method: 'GET'
|
|
1354
|
+
});
|
|
1355
|
+
if (!response.ok) {
|
|
1356
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1357
|
+
throw new Error(`Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`);
|
|
1358
|
+
}
|
|
1359
|
+
const { status, description, quote } = await response.json();
|
|
1360
|
+
if (status === 'error') {
|
|
1361
|
+
throw new Error(description ?? 'Failed to get quote');
|
|
1362
|
+
}
|
|
1363
|
+
const deliveryAgentIdentityKey = response.headers.get('x-bsv-auth-identity-key');
|
|
1364
|
+
if (deliveryAgentIdentityKey == null) {
|
|
1365
|
+
throw new Error('Failed to get quote: Delivery agent did not provide their identity key');
|
|
1366
|
+
}
|
|
1367
|
+
return {
|
|
1368
|
+
recipientFee: quote.recipientFee,
|
|
1369
|
+
deliveryFee: quote.deliveryFee,
|
|
1370
|
+
deliveryAgentIdentityKey
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* @method listMessageBoxPermissions
|
|
1375
|
+
* @async
|
|
1376
|
+
* @param {ListPermissionsParams} [params] - Optional filtering and pagination parameters
|
|
1377
|
+
* @returns {Promise<MessageBoxPermission[]>} List of current permissions
|
|
1378
|
+
*
|
|
1379
|
+
* @description
|
|
1380
|
+
* Lists permissions for the authenticated user's messageBoxes with optional pagination.
|
|
1381
|
+
*
|
|
1382
|
+
* @example
|
|
1383
|
+
* // List all permissions
|
|
1384
|
+
* const all = await client.listMessageBoxPermissions()
|
|
1385
|
+
*
|
|
1386
|
+
* // List only notification permissions with pagination
|
|
1387
|
+
* const notifications = await client.listMessageBoxPermissions({
|
|
1388
|
+
* messageBox: 'notifications',
|
|
1389
|
+
* limit: 50,
|
|
1390
|
+
* offset: 0
|
|
1391
|
+
* })
|
|
1392
|
+
*/
|
|
1393
|
+
async listMessageBoxPermissions(params, overrideHost) {
|
|
1394
|
+
await this.assertInitialized();
|
|
1395
|
+
const finalHost = overrideHost ?? this.host;
|
|
1396
|
+
const queryParams = new URLSearchParams();
|
|
1397
|
+
if (params?.messageBox != null) {
|
|
1398
|
+
queryParams.set('message_box', params.messageBox);
|
|
1399
|
+
}
|
|
1400
|
+
if (params?.limit !== undefined) {
|
|
1401
|
+
queryParams.set('limit', params.limit.toString());
|
|
1402
|
+
}
|
|
1403
|
+
if (params?.offset !== undefined) {
|
|
1404
|
+
queryParams.set('offset', params.offset.toString());
|
|
1405
|
+
}
|
|
1406
|
+
Logger.log('[MB CLIENT] Listing messageBox permissions with params:', queryParams.toString());
|
|
1407
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/list?${queryParams.toString()}`, {
|
|
1408
|
+
method: 'GET'
|
|
1409
|
+
});
|
|
1410
|
+
if (!response.ok) {
|
|
1411
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1412
|
+
throw new Error(`Failed to list permissions: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`);
|
|
1413
|
+
}
|
|
1414
|
+
const data = await response.json();
|
|
1415
|
+
if (data.status === 'error') {
|
|
1416
|
+
throw new Error(data.description ?? 'Failed to list permissions');
|
|
1417
|
+
}
|
|
1418
|
+
return data.permissions.map((p) => ({
|
|
1419
|
+
sender: p.sender,
|
|
1420
|
+
messageBox: p.message_box,
|
|
1421
|
+
recipientFee: p.recipient_fee,
|
|
1422
|
+
status: MessageBoxClient.getStatusFromFee(p.recipient_fee),
|
|
1423
|
+
createdAt: p.created_at,
|
|
1424
|
+
updatedAt: p.updated_at
|
|
1425
|
+
}));
|
|
1426
|
+
}
|
|
1427
|
+
// ===========================
|
|
1428
|
+
// NOTIFICATION CONVENIENCE METHODS
|
|
1429
|
+
// ===========================
|
|
1430
|
+
/**
|
|
1431
|
+
* @method allowNotificationsFromPeer
|
|
1432
|
+
* @async
|
|
1433
|
+
* @param {PubKeyHex} identityKey - Sender's identity key to allow
|
|
1434
|
+
* @param {number} [recipientFee=0] - Fee to charge (0 for always allow)
|
|
1435
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1436
|
+
* @returns {Promise<void>} Permission status after allowing
|
|
1437
|
+
*
|
|
1438
|
+
* @description
|
|
1439
|
+
* Convenience method to allow notifications from a specific peer.
|
|
1440
|
+
*
|
|
1441
|
+
* @example
|
|
1442
|
+
* await client.allowNotificationsFromPeer('03abc123...') // Always allow
|
|
1443
|
+
* await client.allowNotificationsFromPeer('03def456...', 5) // Allow for 5 sats
|
|
1444
|
+
*/
|
|
1445
|
+
async allowNotificationsFromPeer(identityKey, recipientFee = 0, overrideHost) {
|
|
1446
|
+
await this.setMessageBoxPermission({
|
|
1447
|
+
messageBox: 'notifications',
|
|
1448
|
+
sender: identityKey,
|
|
1449
|
+
recipientFee
|
|
1450
|
+
}, overrideHost);
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* @method denyNotificationsFromPeer
|
|
1454
|
+
* @async
|
|
1455
|
+
* @param {PubKeyHex} identityKey - Sender's identity key to block
|
|
1456
|
+
* @returns {Promise<void>} Permission status after denying
|
|
1457
|
+
*
|
|
1458
|
+
* @description
|
|
1459
|
+
* Convenience method to block notifications from a specific peer.
|
|
1460
|
+
*
|
|
1461
|
+
* @example
|
|
1462
|
+
* await client.denyNotificationsFromPeer('03spam123...')
|
|
1463
|
+
*/
|
|
1464
|
+
async denyNotificationsFromPeer(identityKey, overrideHost) {
|
|
1465
|
+
await this.setMessageBoxPermission({
|
|
1466
|
+
messageBox: 'notifications',
|
|
1467
|
+
sender: identityKey,
|
|
1468
|
+
recipientFee: -1
|
|
1469
|
+
}, overrideHost);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* @method checkPeerNotificationStatus
|
|
1473
|
+
* @async
|
|
1474
|
+
* @param {PubKeyHex} identityKey - Sender's identity key to check
|
|
1475
|
+
* @returns {Promise<MessageBoxPermission>} Current permission status
|
|
1476
|
+
*
|
|
1477
|
+
* @description
|
|
1478
|
+
* Convenience method to check notification permission for a specific peer.
|
|
1479
|
+
*
|
|
1480
|
+
* @example
|
|
1481
|
+
* const status = await client.checkPeerNotificationStatus('03abc123...')
|
|
1482
|
+
* console.log(status.allowed) // true/false
|
|
1483
|
+
*/
|
|
1484
|
+
async checkPeerNotificationStatus(identityKey, overrideHost) {
|
|
1485
|
+
const myIdentityKey = await this.getIdentityKey();
|
|
1486
|
+
return await this.getMessageBoxPermission({
|
|
1487
|
+
recipient: myIdentityKey,
|
|
1488
|
+
messageBox: 'notifications',
|
|
1489
|
+
sender: identityKey
|
|
1490
|
+
}, overrideHost);
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* @method listPeerNotifications
|
|
1494
|
+
* @async
|
|
1495
|
+
* @returns {Promise<MessageBoxPermission[]>} List of notification permissions
|
|
1496
|
+
*
|
|
1497
|
+
* @description
|
|
1498
|
+
* Convenience method to list all notification permissions.
|
|
1499
|
+
*
|
|
1500
|
+
* @example
|
|
1501
|
+
* const notifications = await client.listPeerNotifications()
|
|
1502
|
+
*/
|
|
1503
|
+
async listPeerNotifications(overrideHost) {
|
|
1504
|
+
return await this.listMessageBoxPermissions({ messageBox: 'notifications' }, overrideHost);
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* @method sendNotification
|
|
1508
|
+
* @async
|
|
1509
|
+
* @param {PubKeyHex} recipient - Recipient's identity key
|
|
1510
|
+
* @param {string | object} body - Notification content
|
|
1511
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1512
|
+
* @returns {Promise<SendMessageResponse>} Send result
|
|
1513
|
+
*
|
|
1514
|
+
* @description
|
|
1515
|
+
* Convenience method to send a notification with automatic quote fetching and payment handling.
|
|
1516
|
+
* Automatically determines the required payment amount and creates the payment if needed.
|
|
1517
|
+
*
|
|
1518
|
+
* @example
|
|
1519
|
+
* // Send notification (auto-determines payment needed)
|
|
1520
|
+
* await client.sendNotification('03def456...', 'Hello!')
|
|
1521
|
+
*
|
|
1522
|
+
* // Send with maximum payment limit for safety
|
|
1523
|
+
* await client.sendNotification('03def456...', { title: 'Alert', body: 'Important update' }, 50)
|
|
1524
|
+
*/
|
|
1525
|
+
async sendNotification(recipient, body, overrideHost) {
|
|
1526
|
+
await this.assertInitialized();
|
|
1527
|
+
// Use sendMessage with permission checking enabled
|
|
1528
|
+
// This eliminates duplication of quote fetching and payment logic
|
|
1529
|
+
return await this.sendMessage({
|
|
1530
|
+
recipient,
|
|
1531
|
+
messageBox: 'notifications',
|
|
1532
|
+
body,
|
|
1533
|
+
checkPermissions: true
|
|
1534
|
+
}, overrideHost);
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Register a device for FCM push notifications.
|
|
1538
|
+
*
|
|
1539
|
+
* @async
|
|
1540
|
+
* @param {DeviceRegistrationParams} params - Device registration parameters
|
|
1541
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1542
|
+
* @returns {Promise<DeviceRegistrationResponse>} Registration response
|
|
1543
|
+
*
|
|
1544
|
+
* @description
|
|
1545
|
+
* Registers a device with the message box server to receive FCM push notifications.
|
|
1546
|
+
* The FCM token is obtained from Firebase SDK on the client side.
|
|
1547
|
+
*
|
|
1548
|
+
* @example
|
|
1549
|
+
* const result = await client.registerDevice({
|
|
1550
|
+
* fcmToken: 'eBo8F...',
|
|
1551
|
+
* platform: 'ios',
|
|
1552
|
+
* deviceId: 'iPhone15Pro'
|
|
1553
|
+
* })
|
|
1554
|
+
*/
|
|
1555
|
+
async registerDevice(params, overrideHost) {
|
|
1556
|
+
await this.assertInitialized();
|
|
1557
|
+
if (params.fcmToken == null || params.fcmToken.trim() === '') {
|
|
1558
|
+
throw new Error('fcmToken is required and must be a non-empty string');
|
|
1559
|
+
}
|
|
1560
|
+
// Validate platform if provided
|
|
1561
|
+
const validPlatforms = ['ios', 'android', 'web'];
|
|
1562
|
+
if (params.platform != null && !validPlatforms.includes(params.platform)) {
|
|
1563
|
+
throw new Error('platform must be one of: ios, android, web');
|
|
1564
|
+
}
|
|
1565
|
+
const finalHost = overrideHost ?? this.host;
|
|
1566
|
+
Logger.log('[MB CLIENT] Registering device for FCM notifications...');
|
|
1567
|
+
const response = await this.authFetch.fetch(`${finalHost}/registerDevice`, {
|
|
1568
|
+
method: 'POST',
|
|
1569
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1570
|
+
body: JSON.stringify({
|
|
1571
|
+
fcmToken: params.fcmToken.trim(),
|
|
1572
|
+
deviceId: params.deviceId?.trim() ?? undefined,
|
|
1573
|
+
platform: params.platform ?? undefined
|
|
1574
|
+
})
|
|
1575
|
+
});
|
|
1576
|
+
if (!response.ok) {
|
|
1577
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1578
|
+
const description = String(errorData.description) ?? response.statusText;
|
|
1579
|
+
throw new Error(`Failed to register device: HTTP ${response.status} - ${description}`);
|
|
1580
|
+
}
|
|
1581
|
+
const data = await response.json();
|
|
1582
|
+
if (data.status === 'error') {
|
|
1583
|
+
throw new Error(data.description ?? 'Failed to register device');
|
|
1584
|
+
}
|
|
1585
|
+
Logger.log('[MB CLIENT] Device registered successfully');
|
|
1586
|
+
return {
|
|
1587
|
+
status: data.status,
|
|
1588
|
+
message: data.message,
|
|
1589
|
+
deviceId: data.deviceId
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* List all registered devices for push notifications.
|
|
1594
|
+
*
|
|
1595
|
+
* @async
|
|
1596
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1597
|
+
* @returns {Promise<RegisteredDevice[]>} Array of registered devices
|
|
1598
|
+
*
|
|
1599
|
+
* @description
|
|
1600
|
+
* Retrieves all devices registered by the authenticated user for FCM push notifications.
|
|
1601
|
+
* Only shows devices belonging to the current user (authenticated via AuthFetch).
|
|
1602
|
+
*
|
|
1603
|
+
* @example
|
|
1604
|
+
* const devices = await client.listRegisteredDevices()
|
|
1605
|
+
* console.log(`Found ${devices.length} registered devices`)
|
|
1606
|
+
* devices.forEach(device => {
|
|
1607
|
+
* console.log(`Device: ${device.platform} - ${device.fcmToken}`)
|
|
1608
|
+
* })
|
|
1609
|
+
*/
|
|
1610
|
+
async listRegisteredDevices(overrideHost) {
|
|
1611
|
+
await this.assertInitialized();
|
|
1612
|
+
const finalHost = overrideHost ?? this.host;
|
|
1613
|
+
Logger.log('[MB CLIENT] Listing registered devices...');
|
|
1614
|
+
const response = await this.authFetch.fetch(`${finalHost}/devices`, {
|
|
1615
|
+
method: 'GET'
|
|
1616
|
+
});
|
|
1617
|
+
if (!response.ok) {
|
|
1618
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1619
|
+
const description = String(errorData.description) ?? response.statusText;
|
|
1620
|
+
throw new Error(`Failed to list devices: HTTP ${response.status} - ${description}`);
|
|
1621
|
+
}
|
|
1622
|
+
const data = await response.json();
|
|
1623
|
+
if (data.status === 'error') {
|
|
1624
|
+
throw new Error(data.description ?? 'Failed to list devices');
|
|
1625
|
+
}
|
|
1626
|
+
Logger.log(`[MB CLIENT] Found ${data.devices.length} registered devices`);
|
|
1627
|
+
return data.devices;
|
|
1628
|
+
}
|
|
1629
|
+
// ===========================
|
|
1630
|
+
// PRIVATE HELPER METHODS
|
|
1631
|
+
// ===========================
|
|
1632
|
+
static getStatusFromFee(fee) {
|
|
1633
|
+
if (fee === -1)
|
|
1634
|
+
return 'blocked';
|
|
1635
|
+
if (fee === 0)
|
|
1636
|
+
return 'always_allow';
|
|
1637
|
+
return 'payment_required';
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Creates payment transaction for message delivery fees
|
|
1641
|
+
* TODO: Consider consolidating payment generating logic with a util PeerPayClient can use as well.
|
|
1642
|
+
* @private
|
|
1643
|
+
* @param {string} recipient - Recipient identity key
|
|
1644
|
+
* @param {MessageBoxQuote} quote - Fee quote with delivery and recipient fees
|
|
1645
|
+
* @param {string} description - Description for the payment transaction
|
|
1646
|
+
* @returns {Promise<Payment>} Payment transaction data
|
|
1647
|
+
*/
|
|
1648
|
+
async createMessagePayment(recipient, quote, description = 'MessageBox delivery payment', originator) {
|
|
1649
|
+
if (quote.recipientFee <= 0 && quote.deliveryFee <= 0) {
|
|
1650
|
+
throw new Error('No payment required');
|
|
1651
|
+
}
|
|
1652
|
+
Logger.log(`[MB CLIENT] Creating payment transaction for ${quote.recipientFee} sats (delivery: ${quote.deliveryFee}, recipient: ${quote.recipientFee})`);
|
|
1653
|
+
const outputs = [];
|
|
1654
|
+
const createActionOutputs = [];
|
|
1655
|
+
// Get sender identity key for remittance data
|
|
1656
|
+
const senderIdentityKey = await this.getIdentityKey();
|
|
1657
|
+
// Add server delivery fee output if > 0
|
|
1658
|
+
let outputIndex = 0;
|
|
1659
|
+
if (quote.deliveryFee > 0) {
|
|
1660
|
+
const derivationPrefix = Utils.toBase64(Random(32));
|
|
1661
|
+
const derivationSuffix = Utils.toBase64(Random(32));
|
|
1662
|
+
// Get host's derived public key
|
|
1663
|
+
const { publicKey: derivedKeyResult } = await this.walletClient.getPublicKey({
|
|
1664
|
+
protocolID: [2, '3241645161d8'],
|
|
1665
|
+
keyID: `${derivationPrefix} ${derivationSuffix}`,
|
|
1666
|
+
counterparty: quote.deliveryAgentIdentityKey
|
|
1667
|
+
}, originator);
|
|
1668
|
+
// Create locking script using host's public key
|
|
1669
|
+
const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex();
|
|
1670
|
+
// Add to createAction outputs
|
|
1671
|
+
createActionOutputs.push({
|
|
1672
|
+
satoshis: quote.deliveryFee,
|
|
1673
|
+
lockingScript,
|
|
1674
|
+
outputDescription: 'MessageBox server delivery fee',
|
|
1675
|
+
customInstructions: JSON.stringify({
|
|
1676
|
+
derivationPrefix,
|
|
1677
|
+
derivationSuffix,
|
|
1678
|
+
recipientIdentityKey: quote.deliveryAgentIdentityKey
|
|
1679
|
+
})
|
|
1680
|
+
});
|
|
1681
|
+
outputs.push({
|
|
1682
|
+
outputIndex: outputIndex++,
|
|
1683
|
+
protocol: 'wallet payment',
|
|
1684
|
+
paymentRemittance: {
|
|
1685
|
+
derivationPrefix,
|
|
1686
|
+
derivationSuffix,
|
|
1687
|
+
senderIdentityKey
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
// Add recipient fee output if > 0
|
|
1692
|
+
if (quote.recipientFee > 0) {
|
|
1693
|
+
const derivationPrefix = Utils.toBase64(Random(32));
|
|
1694
|
+
const derivationSuffix = Utils.toBase64(Random(32));
|
|
1695
|
+
// Get a derived public key for the recipient that "anyone" can verify
|
|
1696
|
+
const anyoneWallet = new ProtoWallet('anyone');
|
|
1697
|
+
const { publicKey: derivedKeyResult } = await anyoneWallet.getPublicKey({
|
|
1698
|
+
protocolID: [2, '3241645161d8'],
|
|
1699
|
+
keyID: `${derivationPrefix} ${derivationSuffix}`,
|
|
1700
|
+
counterparty: recipient
|
|
1701
|
+
});
|
|
1702
|
+
if (derivedKeyResult == null || derivedKeyResult.trim() === '') {
|
|
1703
|
+
throw new Error('Failed to derive recipient\'s public key');
|
|
1704
|
+
}
|
|
1705
|
+
// Create locking script using recipient's public key
|
|
1706
|
+
const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex();
|
|
1707
|
+
// Add to createAction outputs
|
|
1708
|
+
createActionOutputs.push({
|
|
1709
|
+
satoshis: quote.recipientFee,
|
|
1710
|
+
lockingScript,
|
|
1711
|
+
outputDescription: 'Recipient message fee',
|
|
1712
|
+
customInstructions: JSON.stringify({
|
|
1713
|
+
derivationPrefix,
|
|
1714
|
+
derivationSuffix,
|
|
1715
|
+
recipientIdentityKey: recipient
|
|
1716
|
+
})
|
|
1717
|
+
});
|
|
1718
|
+
outputs.push({
|
|
1719
|
+
outputIndex: outputIndex++,
|
|
1720
|
+
protocol: 'wallet payment',
|
|
1721
|
+
paymentRemittance: {
|
|
1722
|
+
derivationPrefix,
|
|
1723
|
+
derivationSuffix,
|
|
1724
|
+
senderIdentityKey: (await anyoneWallet.getPublicKey({ identityKey: true })).publicKey
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
const { tx } = await this.walletClient.createAction({
|
|
1729
|
+
description,
|
|
1730
|
+
outputs: createActionOutputs,
|
|
1731
|
+
options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
|
|
1732
|
+
}, originator);
|
|
1733
|
+
if (tx == null) {
|
|
1734
|
+
throw new Error('Failed to create payment transaction');
|
|
1735
|
+
}
|
|
1736
|
+
return {
|
|
1737
|
+
tx,
|
|
1738
|
+
outputs,
|
|
1739
|
+
description
|
|
1740
|
+
// labels
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1164
1743
|
}
|
|
1165
1744
|
//# sourceMappingURL=MessageBoxClient.js.map
|