@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
package/src/MessageBoxClient.ts
CHANGED
|
@@ -30,25 +30,30 @@ import {
|
|
|
30
30
|
Utils,
|
|
31
31
|
Transaction,
|
|
32
32
|
PushDrop,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
PubKeyHex,
|
|
34
|
+
P2PKH,
|
|
35
|
+
PublicKey,
|
|
36
|
+
CreateActionOutput,
|
|
37
|
+
WalletInterface,
|
|
38
|
+
ProtoWallet,
|
|
39
|
+
InternalizeOutput,
|
|
40
|
+
Random
|
|
36
41
|
} from '@bsv/sdk'
|
|
37
42
|
import { AuthSocketClient } from '@bsv/authsocket-client'
|
|
38
|
-
import
|
|
39
|
-
import { AcknowledgeMessageParams, EncryptedMessage, ListMessagesParams, MessageBoxClientOptions, PeerMessage, SendMessageParams, SendMessageResponse } from './types.js'
|
|
43
|
+
import * as Logger from './Utils/logger.js'
|
|
44
|
+
import { AcknowledgeMessageParams, AdvertisementToken, EncryptedMessage, ListMessagesParams, MessageBoxClientOptions, Payment, PeerMessage, SendMessageParams, SendMessageResponse, DeviceRegistrationParams, DeviceRegistrationResponse, RegisteredDevice, ListDevicesResponse } from './types.js'
|
|
45
|
+
import {
|
|
46
|
+
SetMessageBoxPermissionParams,
|
|
47
|
+
GetMessageBoxPermissionParams,
|
|
48
|
+
MessageBoxPermission,
|
|
49
|
+
MessageBoxQuote,
|
|
50
|
+
ListPermissionsParams,
|
|
51
|
+
GetQuoteParams
|
|
52
|
+
} from './types/permissions.js'
|
|
40
53
|
|
|
41
54
|
const DEFAULT_MAINNET_HOST = 'https://messagebox.babbage.systems'
|
|
42
55
|
const DEFAULT_TESTNET_HOST = 'https://staging-messagebox.babbage.systems'
|
|
43
56
|
|
|
44
|
-
interface AdvertisementToken {
|
|
45
|
-
host: string
|
|
46
|
-
txid: HexString
|
|
47
|
-
outputIndex: number
|
|
48
|
-
lockingScript: LockingScript
|
|
49
|
-
beef: BEEF
|
|
50
|
-
}
|
|
51
|
-
|
|
52
57
|
/**
|
|
53
58
|
* @class MessageBoxClient
|
|
54
59
|
* @description
|
|
@@ -79,7 +84,7 @@ interface AdvertisementToken {
|
|
|
79
84
|
export class MessageBoxClient {
|
|
80
85
|
private host: string
|
|
81
86
|
public readonly authFetch: AuthFetch
|
|
82
|
-
private readonly walletClient:
|
|
87
|
+
private readonly walletClient: WalletInterface
|
|
83
88
|
private socket?: ReturnType<typeof AuthSocketClient>
|
|
84
89
|
private myIdentityKey?: string
|
|
85
90
|
private readonly joinedRooms: Set<string> = new Set()
|
|
@@ -91,7 +96,7 @@ export class MessageBoxClient {
|
|
|
91
96
|
* @constructor
|
|
92
97
|
* @param {Object} options - Initialization options for the MessageBoxClient.
|
|
93
98
|
* @param {string} [options.host] - The base URL of the MessageBox server. If omitted, defaults to mainnet/testnet hosts.
|
|
94
|
-
* @param {
|
|
99
|
+
* @param {WalletInterface} options.walletClient - Wallet instance used for authentication, signing, and encryption.
|
|
95
100
|
* @param {boolean} [options.enableLogging=false] - Whether to enable detailed debug logging to the console.
|
|
96
101
|
* @param {'local' | 'mainnet' | 'testnet'} [options.networkPreset='mainnet'] - Overlay network preset used for routing and advertisement lookup.
|
|
97
102
|
*
|
|
@@ -116,7 +121,8 @@ export class MessageBoxClient {
|
|
|
116
121
|
host,
|
|
117
122
|
walletClient,
|
|
118
123
|
enableLogging = false,
|
|
119
|
-
networkPreset = 'mainnet'
|
|
124
|
+
networkPreset = 'mainnet',
|
|
125
|
+
originator = undefined
|
|
120
126
|
} = options
|
|
121
127
|
|
|
122
128
|
const defaultHost =
|
|
@@ -126,7 +132,7 @@ export class MessageBoxClient {
|
|
|
126
132
|
|
|
127
133
|
this.host = host?.trim() ?? defaultHost
|
|
128
134
|
|
|
129
|
-
this.walletClient = walletClient ?? new WalletClient()
|
|
135
|
+
this.walletClient = walletClient ?? new WalletClient('auto', originator)
|
|
130
136
|
this.authFetch = new AuthFetch(this.walletClient)
|
|
131
137
|
this.networkPreset = networkPreset
|
|
132
138
|
|
|
@@ -143,6 +149,7 @@ export class MessageBoxClient {
|
|
|
143
149
|
* @method init
|
|
144
150
|
* @async
|
|
145
151
|
* @param {string} [targetHost] - Optional host to set or override the default host.
|
|
152
|
+
* @param {string} [originator] - Optional originator to use with walletClient.
|
|
146
153
|
* @returns {Promise<void>}
|
|
147
154
|
*
|
|
148
155
|
* @description
|
|
@@ -161,7 +168,7 @@ export class MessageBoxClient {
|
|
|
161
168
|
* await client.init()
|
|
162
169
|
* await client.sendMessage({ recipient, messageBox: 'inbox', body: 'Hello' })
|
|
163
170
|
*/
|
|
164
|
-
async init(targetHost: string = this.host): Promise<void> {
|
|
171
|
+
async init(targetHost: string = this.host, originator?: string): Promise<void> {
|
|
165
172
|
const normalizedHost = targetHost?.trim()
|
|
166
173
|
if (normalizedHost === '') {
|
|
167
174
|
throw new Error('Cannot anoint host: No valid host provided')
|
|
@@ -176,13 +183,13 @@ export class MessageBoxClient {
|
|
|
176
183
|
if (this.initialized) return
|
|
177
184
|
|
|
178
185
|
// 1. Get our identity key
|
|
179
|
-
const identityKey = await this.getIdentityKey()
|
|
186
|
+
const identityKey = await this.getIdentityKey(originator)
|
|
180
187
|
// 2. Check for any matching advertisements for the given host
|
|
181
|
-
const [firstAdvertisement] = await this.queryAdvertisements(identityKey, normalizedHost)
|
|
188
|
+
const [firstAdvertisement] = await this.queryAdvertisements(identityKey, normalizedHost, originator)
|
|
182
189
|
// 3. If none our found, anoint this host
|
|
183
190
|
if (firstAdvertisement == null || firstAdvertisement?.host?.trim() === '' || firstAdvertisement?.host !== normalizedHost) {
|
|
184
191
|
Logger.log('[MB CLIENT] Anointing host:', normalizedHost)
|
|
185
|
-
const { txid } = await this.anointHost(normalizedHost)
|
|
192
|
+
const { txid } = await this.anointHost(normalizedHost, originator)
|
|
186
193
|
if (txid == null || txid.trim() === '') {
|
|
187
194
|
throw new Error('Failed to anoint host: No transaction ID returned')
|
|
188
195
|
}
|
|
@@ -220,19 +227,20 @@ export class MessageBoxClient {
|
|
|
220
227
|
|
|
221
228
|
/**
|
|
222
229
|
* @method getIdentityKey
|
|
230
|
+
* @param {string} [originator] - Optional originator to use for identity key lookup
|
|
223
231
|
* @returns {Promise<string>} The identity public key of the user
|
|
224
232
|
* @description
|
|
225
233
|
* Returns the client's identity key, used for signing, encryption, and addressing.
|
|
226
234
|
* If not already loaded, it will fetch and cache it.
|
|
227
235
|
*/
|
|
228
|
-
public async getIdentityKey(): Promise<string> {
|
|
236
|
+
public async getIdentityKey(originator?: string): Promise<string> {
|
|
229
237
|
if (this.myIdentityKey != null && this.myIdentityKey.trim() !== '') {
|
|
230
238
|
return this.myIdentityKey
|
|
231
239
|
}
|
|
232
240
|
|
|
233
241
|
Logger.log('[MB CLIENT] Fetching identity key...')
|
|
234
242
|
try {
|
|
235
|
-
const keyResult = await this.walletClient.getPublicKey({ identityKey: true })
|
|
243
|
+
const keyResult = await this.walletClient.getPublicKey({ identityKey: true }, originator)
|
|
236
244
|
this.myIdentityKey = keyResult.publicKey
|
|
237
245
|
Logger.log(`[MB CLIENT] Identity key fetched: ${this.myIdentityKey}`)
|
|
238
246
|
return this.myIdentityKey
|
|
@@ -259,6 +267,7 @@ export class MessageBoxClient {
|
|
|
259
267
|
|
|
260
268
|
/**
|
|
261
269
|
* @method initializeConnection
|
|
270
|
+
* @param {string} [originator] - Optional originator to use for authentication.
|
|
262
271
|
* @async
|
|
263
272
|
* @returns {Promise<void>}
|
|
264
273
|
* @description
|
|
@@ -280,20 +289,12 @@ export class MessageBoxClient {
|
|
|
280
289
|
* await mb.initializeConnection()
|
|
281
290
|
* // WebSocket is now ready for use
|
|
282
291
|
*/
|
|
283
|
-
async initializeConnection(): Promise<void> {
|
|
292
|
+
async initializeConnection(originator?: string): Promise<void> {
|
|
284
293
|
await this.assertInitialized()
|
|
285
294
|
Logger.log('[MB CLIENT] initializeConnection() STARTED')
|
|
286
295
|
|
|
287
296
|
if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
const keyResult = await this.walletClient.getPublicKey({ identityKey: true })
|
|
291
|
-
this.myIdentityKey = keyResult.publicKey
|
|
292
|
-
Logger.log(`[MB CLIENT] Identity key fetched successfully: ${this.myIdentityKey}`)
|
|
293
|
-
} catch (error) {
|
|
294
|
-
Logger.error('[MB CLIENT ERROR] Failed to fetch identity key:', error)
|
|
295
|
-
throw new Error('Identity key retrieval failed')
|
|
296
|
-
}
|
|
297
|
+
await this.getIdentityKey(originator)
|
|
297
298
|
}
|
|
298
299
|
|
|
299
300
|
if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
|
|
@@ -367,6 +368,7 @@ export class MessageBoxClient {
|
|
|
367
368
|
* @method resolveHostForRecipient
|
|
368
369
|
* @async
|
|
369
370
|
* @param {string} identityKey - The public identity key of the intended recipient.
|
|
371
|
+
* @param {string} [originator] - The originator to use for the WalletClient.
|
|
370
372
|
* @returns {Promise<string>} - A fully qualified host URL for the recipient's MessageBox server.
|
|
371
373
|
*
|
|
372
374
|
* @description
|
|
@@ -381,8 +383,8 @@ export class MessageBoxClient {
|
|
|
381
383
|
* @example
|
|
382
384
|
* const host = await resolveHostForRecipient('028d...') // → returns either overlay host or this.host
|
|
383
385
|
*/
|
|
384
|
-
async resolveHostForRecipient(identityKey: string): Promise<string> {
|
|
385
|
-
const advertisementTokens = await this.queryAdvertisements(identityKey)
|
|
386
|
+
async resolveHostForRecipient(identityKey: string, originator?: string): Promise<string> {
|
|
387
|
+
const advertisementTokens = await this.queryAdvertisements(identityKey, undefined, originator)
|
|
386
388
|
if (advertisementTokens.length === 0) {
|
|
387
389
|
Logger.warn(`[MB CLIENT] No advertisements for ${identityKey}, using default host ${this.host}`)
|
|
388
390
|
return this.host
|
|
@@ -401,11 +403,12 @@ export class MessageBoxClient {
|
|
|
401
403
|
*/
|
|
402
404
|
async queryAdvertisements(
|
|
403
405
|
identityKey?: string,
|
|
404
|
-
host?: string
|
|
406
|
+
host?: string,
|
|
407
|
+
originator?: string
|
|
405
408
|
): Promise<AdvertisementToken[]> {
|
|
406
409
|
const hosts: AdvertisementToken[] = []
|
|
407
410
|
try {
|
|
408
|
-
const query: Record<string, string> = { identityKey: identityKey ?? await this.getIdentityKey() }
|
|
411
|
+
const query: Record<string, string> = { identityKey: identityKey ?? await this.getIdentityKey(originator) }
|
|
409
412
|
if (host != null && host.trim() !== '') query.host = host
|
|
410
413
|
|
|
411
414
|
const result = await this.lookupResolver.query({
|
|
@@ -413,7 +416,7 @@ export class MessageBoxClient {
|
|
|
413
416
|
query
|
|
414
417
|
})
|
|
415
418
|
if (result.type !== 'output-list') {
|
|
416
|
-
throw new Error(`Unexpected result type: ${result.type}`)
|
|
419
|
+
throw new Error(`Unexpected result type: ${String(result.type)}`)
|
|
417
420
|
}
|
|
418
421
|
|
|
419
422
|
for (const output of result.outputs) {
|
|
@@ -524,10 +527,12 @@ export class MessageBoxClient {
|
|
|
524
527
|
*/
|
|
525
528
|
async listenForLiveMessages({
|
|
526
529
|
onMessage,
|
|
527
|
-
messageBox
|
|
530
|
+
messageBox,
|
|
531
|
+
originator
|
|
528
532
|
}: {
|
|
529
533
|
onMessage: (message: PeerMessage) => void
|
|
530
534
|
messageBox: string
|
|
535
|
+
originator?: string
|
|
531
536
|
}): Promise<void> {
|
|
532
537
|
await this.assertInitialized()
|
|
533
538
|
Logger.log(`[MB CLIENT] Setting up listener for WebSocket room: ${messageBox}`)
|
|
@@ -570,7 +575,7 @@ export class MessageBoxClient {
|
|
|
570
575
|
keyID: '1',
|
|
571
576
|
counterparty: message.sender,
|
|
572
577
|
ciphertext: Utils.toArray((parsedBody as any).encryptedMessage, 'base64')
|
|
573
|
-
})
|
|
578
|
+
}, originator)
|
|
574
579
|
|
|
575
580
|
message.body = Utils.toUTF8(decrypted.plaintext)
|
|
576
581
|
} else {
|
|
@@ -623,7 +628,9 @@ export class MessageBoxClient {
|
|
|
623
628
|
messageBox,
|
|
624
629
|
body,
|
|
625
630
|
messageId,
|
|
626
|
-
skipEncryption
|
|
631
|
+
skipEncryption,
|
|
632
|
+
checkPermissions,
|
|
633
|
+
originator
|
|
627
634
|
}: SendMessageParams): Promise<SendMessageResponse> {
|
|
628
635
|
await this.assertInitialized()
|
|
629
636
|
if (recipient == null || recipient.trim() === '') {
|
|
@@ -653,7 +660,7 @@ export class MessageBoxClient {
|
|
|
653
660
|
protocolID: [1, 'messagebox'],
|
|
654
661
|
keyID: '1',
|
|
655
662
|
counterparty: recipient
|
|
656
|
-
})
|
|
663
|
+
}, originator)
|
|
657
664
|
finalMessageId = messageId ?? Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
658
665
|
} catch (error) {
|
|
659
666
|
Logger.error('[MB CLIENT ERROR] Failed to generate HMAC:', error)
|
|
@@ -672,7 +679,7 @@ export class MessageBoxClient {
|
|
|
672
679
|
keyID: '1',
|
|
673
680
|
counterparty: recipient,
|
|
674
681
|
plaintext: Utils.toArray(typeof body === 'string' ? body : JSON.stringify(body), 'utf8')
|
|
675
|
-
})
|
|
682
|
+
}, originator)
|
|
676
683
|
|
|
677
684
|
outgoingBody = JSON.stringify({
|
|
678
685
|
encryptedMessage: Utils.toBase64(encryptedMessage.ciphertext)
|
|
@@ -701,7 +708,8 @@ export class MessageBoxClient {
|
|
|
701
708
|
messageBox,
|
|
702
709
|
body,
|
|
703
710
|
messageId: finalMessageId,
|
|
704
|
-
skipEncryption
|
|
711
|
+
skipEncryption,
|
|
712
|
+
checkPermissions
|
|
705
713
|
}
|
|
706
714
|
|
|
707
715
|
this.resolveHostForRecipient(recipient)
|
|
@@ -743,7 +751,8 @@ export class MessageBoxClient {
|
|
|
743
751
|
messageBox,
|
|
744
752
|
body,
|
|
745
753
|
messageId: finalMessageId,
|
|
746
|
-
skipEncryption
|
|
754
|
+
skipEncryption,
|
|
755
|
+
checkPermissions
|
|
747
756
|
}
|
|
748
757
|
|
|
749
758
|
this.resolveHostForRecipient(recipient)
|
|
@@ -844,7 +853,8 @@ export class MessageBoxClient {
|
|
|
844
853
|
*/
|
|
845
854
|
async sendMessage(
|
|
846
855
|
message: SendMessageParams,
|
|
847
|
-
overrideHost?: string
|
|
856
|
+
overrideHost?: string,
|
|
857
|
+
originator?: string
|
|
848
858
|
): Promise<SendMessageResponse> {
|
|
849
859
|
await this.assertInitialized()
|
|
850
860
|
if (message.recipient == null || message.recipient.trim() === '') {
|
|
@@ -857,6 +867,43 @@ export class MessageBoxClient {
|
|
|
857
867
|
throw new Error('Every message must have a body!')
|
|
858
868
|
}
|
|
859
869
|
|
|
870
|
+
// Optional permission checking for backwards compatibility
|
|
871
|
+
let paymentData: Payment | undefined
|
|
872
|
+
if (message.checkPermissions === true) {
|
|
873
|
+
try {
|
|
874
|
+
Logger.log('[MB CLIENT] Checking permissions and fees for message...')
|
|
875
|
+
|
|
876
|
+
// Get quote to check if payment is required
|
|
877
|
+
const quote = await this.getMessageBoxQuote({
|
|
878
|
+
recipient: message.recipient,
|
|
879
|
+
messageBox: message.messageBox
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
if (quote.recipientFee === -1) {
|
|
883
|
+
throw new Error('You have been blocked from sending messages to this recipient.')
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (quote.recipientFee > 0 || quote.deliveryFee > 0) {
|
|
887
|
+
const requiredPayment = quote.recipientFee + quote.deliveryFee
|
|
888
|
+
|
|
889
|
+
if (requiredPayment > 0) {
|
|
890
|
+
Logger.log(`[MB CLIENT] Creating payment of ${requiredPayment} sats for message...`)
|
|
891
|
+
|
|
892
|
+
// Create payment using helper method
|
|
893
|
+
paymentData = await this.createMessagePayment(
|
|
894
|
+
message.recipient,
|
|
895
|
+
quote,
|
|
896
|
+
overrideHost
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
Logger.log('[MB CLIENT] Payment data prepared:', paymentData)
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
} catch (error) {
|
|
903
|
+
throw new Error(`Permission check failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
860
907
|
let messageId: string
|
|
861
908
|
try {
|
|
862
909
|
const hmac = await this.walletClient.createHmac({
|
|
@@ -864,7 +911,7 @@ export class MessageBoxClient {
|
|
|
864
911
|
protocolID: [1, 'messagebox'],
|
|
865
912
|
keyID: '1',
|
|
866
913
|
counterparty: message.recipient
|
|
867
|
-
})
|
|
914
|
+
}, originator)
|
|
868
915
|
messageId = message.messageId ?? Array.from(hmac.hmac).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
869
916
|
} catch (error) {
|
|
870
917
|
Logger.error('[MB CLIENT ERROR] Failed to generate HMAC:', error)
|
|
@@ -880,7 +927,7 @@ export class MessageBoxClient {
|
|
|
880
927
|
keyID: '1',
|
|
881
928
|
counterparty: message.recipient,
|
|
882
929
|
plaintext: Utils.toArray(typeof message.body === 'string' ? message.body : JSON.stringify(message.body), 'utf8')
|
|
883
|
-
})
|
|
930
|
+
}, originator)
|
|
884
931
|
|
|
885
932
|
finalBody = JSON.stringify({ encryptedMessage: Utils.toBase64(encryptedMessage.ciphertext) })
|
|
886
933
|
}
|
|
@@ -890,7 +937,8 @@ export class MessageBoxClient {
|
|
|
890
937
|
...message,
|
|
891
938
|
messageId,
|
|
892
939
|
body: finalBody
|
|
893
|
-
}
|
|
940
|
+
},
|
|
941
|
+
...(paymentData != null && { payment: paymentData })
|
|
894
942
|
}
|
|
895
943
|
|
|
896
944
|
try {
|
|
@@ -901,7 +949,7 @@ export class MessageBoxClient {
|
|
|
901
949
|
|
|
902
950
|
if (this.myIdentityKey == null || this.myIdentityKey === '') {
|
|
903
951
|
try {
|
|
904
|
-
const keyResult = await this.walletClient.getPublicKey({ identityKey: true })
|
|
952
|
+
const keyResult = await this.walletClient.getPublicKey({ identityKey: true }, originator)
|
|
905
953
|
this.myIdentityKey = keyResult.publicKey
|
|
906
954
|
Logger.log(`[MB CLIENT] Fetched identity key before sending request: ${this.myIdentityKey}`)
|
|
907
955
|
} catch (error) {
|
|
@@ -967,14 +1015,14 @@ export class MessageBoxClient {
|
|
|
967
1015
|
* @example
|
|
968
1016
|
* const { txid } = await client.anointHost('https://my-messagebox.io')
|
|
969
1017
|
*/
|
|
970
|
-
async anointHost(host: string): Promise<{ txid: string }> {
|
|
1018
|
+
async anointHost(host: string, originator?: string): Promise<{ txid: string }> {
|
|
971
1019
|
Logger.log('[MB CLIENT] Starting anointHost...')
|
|
972
1020
|
try {
|
|
973
1021
|
if (!host.startsWith('http')) {
|
|
974
1022
|
throw new Error('Invalid host URL')
|
|
975
1023
|
}
|
|
976
1024
|
|
|
977
|
-
const identityKey = await this.getIdentityKey()
|
|
1025
|
+
const identityKey = await this.getIdentityKey(originator)
|
|
978
1026
|
|
|
979
1027
|
Logger.log('[MB CLIENT] Fields - Identity:', identityKey, 'Host:', host)
|
|
980
1028
|
|
|
@@ -983,7 +1031,7 @@ export class MessageBoxClient {
|
|
|
983
1031
|
Utils.toArray(host, 'utf8')
|
|
984
1032
|
]
|
|
985
1033
|
|
|
986
|
-
const pushdrop = new PushDrop(this.walletClient)
|
|
1034
|
+
const pushdrop = new PushDrop(this.walletClient, originator)
|
|
987
1035
|
Logger.log('Fields:', fields.map(a => Utils.toHex(a)))
|
|
988
1036
|
Logger.log('ProtocolID:', [1, 'messagebox advertisement'])
|
|
989
1037
|
Logger.log('KeyID:', '1')
|
|
@@ -1009,7 +1057,7 @@ export class MessageBoxClient {
|
|
|
1009
1057
|
outputDescription: 'Overlay advertisement output'
|
|
1010
1058
|
}],
|
|
1011
1059
|
options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
|
|
1012
|
-
})
|
|
1060
|
+
}, originator)
|
|
1013
1061
|
|
|
1014
1062
|
Logger.log('[MB CLIENT] Transaction created:', txid)
|
|
1015
1063
|
|
|
@@ -1039,6 +1087,7 @@ export class MessageBoxClient {
|
|
|
1039
1087
|
* @method revokeHostAdvertisement
|
|
1040
1088
|
* @async
|
|
1041
1089
|
* @param {AdvertisementToken} advertisementToken - The advertisement token containing the messagebox host to revoke.
|
|
1090
|
+
* @param {string} [originator] - Optional originator to use with walletClient.
|
|
1042
1091
|
* @returns {Promise<{ txid: string }>} - The transaction ID of the revocation broadcast to the overlay network.
|
|
1043
1092
|
*
|
|
1044
1093
|
* @description
|
|
@@ -1048,7 +1097,7 @@ export class MessageBoxClient {
|
|
|
1048
1097
|
* @example
|
|
1049
1098
|
* const { txid } = await client.revokeHost('https://my-messagebox.io')
|
|
1050
1099
|
*/
|
|
1051
|
-
async revokeHostAdvertisement(advertisementToken: AdvertisementToken): Promise<{ txid: string }> {
|
|
1100
|
+
async revokeHostAdvertisement(advertisementToken: AdvertisementToken, originator?: string): Promise<{ txid: string }> {
|
|
1052
1101
|
Logger.log('[MB CLIENT] Starting revokeHost...')
|
|
1053
1102
|
const outpoint = `${advertisementToken.txid}.${advertisementToken.outputIndex}`
|
|
1054
1103
|
try {
|
|
@@ -1062,7 +1111,7 @@ export class MessageBoxClient {
|
|
|
1062
1111
|
inputDescription: 'Revoking host advertisement token'
|
|
1063
1112
|
}
|
|
1064
1113
|
]
|
|
1065
|
-
})
|
|
1114
|
+
}, originator)
|
|
1066
1115
|
|
|
1067
1116
|
if (signableTransaction === undefined) {
|
|
1068
1117
|
throw new Error('Failed to create signable transaction.')
|
|
@@ -1071,7 +1120,7 @@ export class MessageBoxClient {
|
|
|
1071
1120
|
const partialTx = Transaction.fromBEEF(signableTransaction.tx)
|
|
1072
1121
|
|
|
1073
1122
|
// Prepare the unlocker
|
|
1074
|
-
const pushdrop = new PushDrop(this.walletClient)
|
|
1123
|
+
const pushdrop = new PushDrop(this.walletClient, originator)
|
|
1075
1124
|
const unlocker = await pushdrop.unlock(
|
|
1076
1125
|
[1, 'messagebox advertisement'],
|
|
1077
1126
|
'1',
|
|
@@ -1096,7 +1145,7 @@ export class MessageBoxClient {
|
|
|
1096
1145
|
options: {
|
|
1097
1146
|
acceptDelayedBroadcast: false
|
|
1098
1147
|
}
|
|
1099
|
-
})
|
|
1148
|
+
}, originator)
|
|
1100
1149
|
|
|
1101
1150
|
if (signedTx === undefined) {
|
|
1102
1151
|
throw new Error('Failed to finalize the transaction signature.')
|
|
@@ -1132,9 +1181,16 @@ export class MessageBoxClient {
|
|
|
1132
1181
|
*
|
|
1133
1182
|
* Each message is:
|
|
1134
1183
|
* - Parsed and, if encrypted, decrypted using AES-256-GCM via BRC-2-compliant ECDH key derivation and symmetric encryption.
|
|
1184
|
+
* - Automatically processed for payments: if the message includes recipient fee payments, they are internalized using `walletClient.internalizeAction()`.
|
|
1135
1185
|
* - Returned as a normalized `PeerMessage` with readable string body content.
|
|
1136
1186
|
*
|
|
1137
|
-
*
|
|
1187
|
+
* Payment Processing:
|
|
1188
|
+
* - Detects messages that include payment data (from paid message delivery).
|
|
1189
|
+
* - Automatically internalizes recipient payment outputs, allowing you to receive payments without additional API calls.
|
|
1190
|
+
* - Only recipient payments are stored with messages - delivery fees are already processed by the server.
|
|
1191
|
+
* - Continues processing messages even if payment internalization fails.
|
|
1192
|
+
*
|
|
1193
|
+
* Decryption automatically derives a shared secret using the sender's identity key and the receiver's child private key.
|
|
1138
1194
|
* If the sender is the same as the recipient, the `counterparty` is set to `'self'`.
|
|
1139
1195
|
*
|
|
1140
1196
|
* @throws {Error} If no messageBox is specified, the request fails, or the server returns an error.
|
|
@@ -1142,8 +1198,9 @@ export class MessageBoxClient {
|
|
|
1142
1198
|
* @example
|
|
1143
1199
|
* const messages = await client.listMessages({ messageBox: 'inbox' })
|
|
1144
1200
|
* messages.forEach(msg => console.log(msg.sender, msg.body))
|
|
1201
|
+
* // Payments included with messages are automatically received
|
|
1145
1202
|
*/
|
|
1146
|
-
async listMessages({ messageBox, host }: ListMessagesParams): Promise<PeerMessage[]> {
|
|
1203
|
+
async listMessages({ messageBox, host, originator }: ListMessagesParams): Promise<PeerMessage[]> {
|
|
1147
1204
|
await this.assertInitialized()
|
|
1148
1205
|
if (messageBox.trim() === '') {
|
|
1149
1206
|
throw new Error('MessageBox cannot be empty')
|
|
@@ -1151,7 +1208,7 @@ export class MessageBoxClient {
|
|
|
1151
1208
|
|
|
1152
1209
|
let hosts: string[] = host != null ? [host] : []
|
|
1153
1210
|
if (hosts.length === 0) {
|
|
1154
|
-
const advertisedHosts = await this.queryAdvertisements(await this.getIdentityKey())
|
|
1211
|
+
const advertisedHosts = await this.queryAdvertisements(await this.getIdentityKey(originator), originator)
|
|
1155
1212
|
hosts = Array.from(new Set([this.host, ...advertisedHosts.map(h => h.host)]))
|
|
1156
1213
|
}
|
|
1157
1214
|
|
|
@@ -1219,10 +1276,74 @@ export class MessageBoxClient {
|
|
|
1219
1276
|
const parsedBody: unknown =
|
|
1220
1277
|
typeof message.body === 'string' ? tryParse(message.body) : message.body
|
|
1221
1278
|
|
|
1279
|
+
let messageContent: any = parsedBody
|
|
1280
|
+
let paymentData: Payment | undefined
|
|
1281
|
+
|
|
1222
1282
|
if (
|
|
1223
1283
|
parsedBody != null &&
|
|
1224
1284
|
typeof parsedBody === 'object' &&
|
|
1225
|
-
|
|
1285
|
+
'message' in parsedBody
|
|
1286
|
+
) {
|
|
1287
|
+
// Handle wrapped message format (with payment data)
|
|
1288
|
+
const wrappedMessage = (parsedBody as any).message
|
|
1289
|
+
messageContent = typeof wrappedMessage === 'string'
|
|
1290
|
+
? tryParse(wrappedMessage)
|
|
1291
|
+
: wrappedMessage
|
|
1292
|
+
paymentData = (parsedBody as any).payment
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Process payment if present - server now only stores recipient payments
|
|
1296
|
+
if (paymentData?.tx != null && paymentData.outputs != null) {
|
|
1297
|
+
try {
|
|
1298
|
+
Logger.log(
|
|
1299
|
+
`[MB CLIENT] Processing recipient payment in message from ${String(message.sender)}…`
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
// All outputs in the stored payment data are for the recipient
|
|
1303
|
+
// (delivery fees are already processed by the server)
|
|
1304
|
+
const recipientOutputs = paymentData.outputs.filter(
|
|
1305
|
+
output => output.protocol === 'wallet payment'
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
if (recipientOutputs.length > 0) {
|
|
1309
|
+
Logger.log(
|
|
1310
|
+
`[MB CLIENT] Internalizing ${recipientOutputs.length} recipient payment output(s)…`
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
const internalizeResult = await this.walletClient.internalizeAction({
|
|
1314
|
+
tx: paymentData.tx,
|
|
1315
|
+
outputs: recipientOutputs,
|
|
1316
|
+
description: paymentData.description ?? 'MessageBox recipient payment'
|
|
1317
|
+
}, originator)
|
|
1318
|
+
|
|
1319
|
+
if (internalizeResult.accepted) {
|
|
1320
|
+
Logger.log(
|
|
1321
|
+
'[MB CLIENT] Successfully internalized recipient payment'
|
|
1322
|
+
)
|
|
1323
|
+
} else {
|
|
1324
|
+
Logger.warn(
|
|
1325
|
+
'[MB CLIENT] Recipient payment internalization was not accepted'
|
|
1326
|
+
)
|
|
1327
|
+
}
|
|
1328
|
+
} else {
|
|
1329
|
+
Logger.log(
|
|
1330
|
+
'[MB CLIENT] No wallet payment outputs found in payment data'
|
|
1331
|
+
)
|
|
1332
|
+
}
|
|
1333
|
+
} catch (paymentError) {
|
|
1334
|
+
Logger.error(
|
|
1335
|
+
'[MB CLIENT ERROR] Failed to internalize recipient payment:',
|
|
1336
|
+
paymentError
|
|
1337
|
+
)
|
|
1338
|
+
// Continue processing the message even if payment fails
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Handle message decryption
|
|
1343
|
+
if (
|
|
1344
|
+
messageContent != null &&
|
|
1345
|
+
typeof messageContent === 'object' &&
|
|
1346
|
+
typeof (messageContent as any).encryptedMessage === 'string'
|
|
1226
1347
|
) {
|
|
1227
1348
|
Logger.log(
|
|
1228
1349
|
`[MB CLIENT] Decrypting message from ${String(message.sender)}…`
|
|
@@ -1233,15 +1354,16 @@ export class MessageBoxClient {
|
|
|
1233
1354
|
keyID: '1',
|
|
1234
1355
|
counterparty: message.sender,
|
|
1235
1356
|
ciphertext: Utils.toArray(
|
|
1236
|
-
|
|
1357
|
+
messageContent.encryptedMessage,
|
|
1237
1358
|
'base64'
|
|
1238
1359
|
)
|
|
1239
|
-
})
|
|
1360
|
+
}, originator)
|
|
1240
1361
|
|
|
1241
1362
|
const decryptedText = Utils.toUTF8(decrypted.plaintext)
|
|
1242
1363
|
message.body = tryParse(decryptedText)
|
|
1243
1364
|
} else {
|
|
1244
|
-
|
|
1365
|
+
// For non-encrypted messages, use the processed content
|
|
1366
|
+
message.body = messageContent ?? parsedBody
|
|
1245
1367
|
}
|
|
1246
1368
|
} catch (err) {
|
|
1247
1369
|
Logger.error(
|
|
@@ -1282,7 +1404,7 @@ export class MessageBoxClient {
|
|
|
1282
1404
|
* @example
|
|
1283
1405
|
* await client.acknowledgeMessage({ messageIds: ['msg123', 'msg456'] })
|
|
1284
1406
|
*/
|
|
1285
|
-
async acknowledgeMessage({ messageIds, host }: AcknowledgeMessageParams): Promise<string> {
|
|
1407
|
+
async acknowledgeMessage({ messageIds, host, originator }: AcknowledgeMessageParams): Promise<string> {
|
|
1286
1408
|
await this.assertInitialized()
|
|
1287
1409
|
if (!Array.isArray(messageIds) || messageIds.length === 0) {
|
|
1288
1410
|
throw new Error('Message IDs array cannot be empty')
|
|
@@ -1293,8 +1415,8 @@ export class MessageBoxClient {
|
|
|
1293
1415
|
let hosts: string[] = host != null ? [host] : []
|
|
1294
1416
|
if (hosts.length === 0) {
|
|
1295
1417
|
// 1. Determine all hosts (advertised + default)
|
|
1296
|
-
const identityKey = await this.getIdentityKey()
|
|
1297
|
-
const advertisedHosts = await this.queryAdvertisements(identityKey)
|
|
1418
|
+
const identityKey = await this.getIdentityKey(originator)
|
|
1419
|
+
const advertisedHosts = await this.queryAdvertisements(identityKey, undefined, originator)
|
|
1298
1420
|
hosts = Array.from(new Set([this.host, ...advertisedHosts.map(h => h.host)]))
|
|
1299
1421
|
}
|
|
1300
1422
|
|
|
@@ -1338,4 +1460,595 @@ export class MessageBoxClient {
|
|
|
1338
1460
|
`Failed to acknowledge messages on all hosts: ${errs.map(e => String(e)).join('; ')}`
|
|
1339
1461
|
)
|
|
1340
1462
|
}
|
|
1463
|
+
|
|
1464
|
+
// ===========================
|
|
1465
|
+
// PERMISSION MANAGEMENT METHODS
|
|
1466
|
+
// ===========================
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* @method setMessageBoxPermission
|
|
1470
|
+
* @async
|
|
1471
|
+
* @param {SetMessageBoxPermissionParams} params - Permission configuration
|
|
1472
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1473
|
+
* @returns {Promise<void>} Permission status after setting
|
|
1474
|
+
*
|
|
1475
|
+
* @description
|
|
1476
|
+
* Sets permission for receiving messages in a specific messageBox.
|
|
1477
|
+
* Can set sender-specific permissions or box-wide defaults.
|
|
1478
|
+
*
|
|
1479
|
+
* @example
|
|
1480
|
+
* // Set box-wide default: allow notifications for 10 sats
|
|
1481
|
+
* await client.setMessageBoxPermission({ messageBox: 'notifications', recipientFee: 10 })
|
|
1482
|
+
*
|
|
1483
|
+
* // Block specific sender
|
|
1484
|
+
* await client.setMessageBoxPermission({
|
|
1485
|
+
* messageBox: 'notifications',
|
|
1486
|
+
* sender: '03abc123...',
|
|
1487
|
+
* recipientFee: -1
|
|
1488
|
+
* })
|
|
1489
|
+
*/
|
|
1490
|
+
async setMessageBoxPermission(
|
|
1491
|
+
params: SetMessageBoxPermissionParams,
|
|
1492
|
+
overrideHost?: string
|
|
1493
|
+
): Promise<void> {
|
|
1494
|
+
await this.assertInitialized()
|
|
1495
|
+
const finalHost = overrideHost ?? this.host
|
|
1496
|
+
|
|
1497
|
+
Logger.log('[MB CLIENT] Setting messageBox permission...')
|
|
1498
|
+
|
|
1499
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/set`, {
|
|
1500
|
+
method: 'POST',
|
|
1501
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1502
|
+
body: JSON.stringify({
|
|
1503
|
+
messageBox: params.messageBox,
|
|
1504
|
+
recipientFee: params.recipientFee,
|
|
1505
|
+
...(params.sender != null && { sender: params.sender })
|
|
1506
|
+
})
|
|
1507
|
+
})
|
|
1508
|
+
|
|
1509
|
+
if (!response.ok) {
|
|
1510
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1511
|
+
throw new Error(`Failed to set permission: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`)
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
const { status, description } = await response.json()
|
|
1515
|
+
if (status === 'error') {
|
|
1516
|
+
throw new Error(description ?? 'Failed to set permission')
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* @method getMessageBoxPermission
|
|
1522
|
+
* @async
|
|
1523
|
+
* @param {GetMessageBoxPermissionParams} params - Permission query parameters
|
|
1524
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1525
|
+
* @returns {Promise<MessageBoxPermission | null>} Permission data (null if not set)
|
|
1526
|
+
*
|
|
1527
|
+
* @description
|
|
1528
|
+
* Gets current permission data for a sender/messageBox combination.
|
|
1529
|
+
* Returns null if no permission is set.
|
|
1530
|
+
*
|
|
1531
|
+
* @example
|
|
1532
|
+
* const status = await client.getMessageBoxPermission({
|
|
1533
|
+
* recipient: '03def456...',
|
|
1534
|
+
* messageBox: 'notifications',
|
|
1535
|
+
* sender: '03abc123...'
|
|
1536
|
+
* })
|
|
1537
|
+
*/
|
|
1538
|
+
async getMessageBoxPermission(
|
|
1539
|
+
params: GetMessageBoxPermissionParams,
|
|
1540
|
+
overrideHost?: string
|
|
1541
|
+
): Promise<MessageBoxPermission | null> {
|
|
1542
|
+
await this.assertInitialized()
|
|
1543
|
+
|
|
1544
|
+
const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient)
|
|
1545
|
+
const queryParams = new URLSearchParams({
|
|
1546
|
+
recipient: params.recipient,
|
|
1547
|
+
messageBox: params.messageBox,
|
|
1548
|
+
...(params.sender != null && { sender: params.sender })
|
|
1549
|
+
})
|
|
1550
|
+
|
|
1551
|
+
Logger.log('[MB CLIENT] Getting messageBox permission...')
|
|
1552
|
+
|
|
1553
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/get?${queryParams.toString()}`, {
|
|
1554
|
+
method: 'GET'
|
|
1555
|
+
})
|
|
1556
|
+
|
|
1557
|
+
if (!response.ok) {
|
|
1558
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1559
|
+
throw new Error(`Failed to get permission: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`)
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const data = await response.json()
|
|
1563
|
+
if (data.status === 'error') {
|
|
1564
|
+
throw new Error(data.description ?? 'Failed to get permission')
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
return data.permission
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* @method getMessageBoxQuote
|
|
1572
|
+
* @async
|
|
1573
|
+
* @param {GetQuoteParams} params - Quote request parameters
|
|
1574
|
+
* @returns {Promise<MessageBoxQuote>} Fee quote and permission status
|
|
1575
|
+
*
|
|
1576
|
+
* @description
|
|
1577
|
+
* Gets a fee quote for sending a message, including delivery and recipient fees.
|
|
1578
|
+
*
|
|
1579
|
+
* @example
|
|
1580
|
+
* const quote = await client.getMessageBoxQuote({
|
|
1581
|
+
* recipient: '03def456...',
|
|
1582
|
+
* messageBox: 'notifications'
|
|
1583
|
+
* })
|
|
1584
|
+
*/
|
|
1585
|
+
async getMessageBoxQuote(params: GetQuoteParams, overrideHost?: string): Promise<MessageBoxQuote> {
|
|
1586
|
+
await this.assertInitialized()
|
|
1587
|
+
|
|
1588
|
+
const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient)
|
|
1589
|
+
const queryParams = new URLSearchParams({
|
|
1590
|
+
recipient: params.recipient,
|
|
1591
|
+
messageBox: params.messageBox
|
|
1592
|
+
})
|
|
1593
|
+
|
|
1594
|
+
Logger.log('[MB CLIENT] Getting messageBox quote...')
|
|
1595
|
+
|
|
1596
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/quote?${queryParams.toString()}`, {
|
|
1597
|
+
method: 'GET'
|
|
1598
|
+
})
|
|
1599
|
+
|
|
1600
|
+
if (!response.ok) {
|
|
1601
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1602
|
+
throw new Error(`Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`)
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const { status, description, quote } = await response.json()
|
|
1606
|
+
if (status === 'error') {
|
|
1607
|
+
throw new Error(description ?? 'Failed to get quote')
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const deliveryAgentIdentityKey = response.headers.get('x-bsv-auth-identity-key')
|
|
1611
|
+
|
|
1612
|
+
if (deliveryAgentIdentityKey == null) {
|
|
1613
|
+
throw new Error('Failed to get quote: Delivery agent did not provide their identity key')
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
return {
|
|
1617
|
+
recipientFee: quote.recipientFee,
|
|
1618
|
+
deliveryFee: quote.deliveryFee,
|
|
1619
|
+
deliveryAgentIdentityKey
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/**
|
|
1624
|
+
* @method listMessageBoxPermissions
|
|
1625
|
+
* @async
|
|
1626
|
+
* @param {ListPermissionsParams} [params] - Optional filtering and pagination parameters
|
|
1627
|
+
* @returns {Promise<MessageBoxPermission[]>} List of current permissions
|
|
1628
|
+
*
|
|
1629
|
+
* @description
|
|
1630
|
+
* Lists permissions for the authenticated user's messageBoxes with optional pagination.
|
|
1631
|
+
*
|
|
1632
|
+
* @example
|
|
1633
|
+
* // List all permissions
|
|
1634
|
+
* const all = await client.listMessageBoxPermissions()
|
|
1635
|
+
*
|
|
1636
|
+
* // List only notification permissions with pagination
|
|
1637
|
+
* const notifications = await client.listMessageBoxPermissions({
|
|
1638
|
+
* messageBox: 'notifications',
|
|
1639
|
+
* limit: 50,
|
|
1640
|
+
* offset: 0
|
|
1641
|
+
* })
|
|
1642
|
+
*/
|
|
1643
|
+
async listMessageBoxPermissions(params?: ListPermissionsParams, overrideHost?: string): Promise<MessageBoxPermission[]> {
|
|
1644
|
+
await this.assertInitialized()
|
|
1645
|
+
|
|
1646
|
+
const finalHost = overrideHost ?? this.host
|
|
1647
|
+
const queryParams = new URLSearchParams()
|
|
1648
|
+
|
|
1649
|
+
if (params?.messageBox != null) {
|
|
1650
|
+
queryParams.set('message_box', params.messageBox)
|
|
1651
|
+
}
|
|
1652
|
+
if (params?.limit !== undefined) {
|
|
1653
|
+
queryParams.set('limit', params.limit.toString())
|
|
1654
|
+
}
|
|
1655
|
+
if (params?.offset !== undefined) {
|
|
1656
|
+
queryParams.set('offset', params.offset.toString())
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
Logger.log('[MB CLIENT] Listing messageBox permissions with params:', queryParams.toString())
|
|
1660
|
+
|
|
1661
|
+
const response = await this.authFetch.fetch(`${finalHost}/permissions/list?${queryParams.toString()}`, {
|
|
1662
|
+
method: 'GET'
|
|
1663
|
+
})
|
|
1664
|
+
|
|
1665
|
+
if (!response.ok) {
|
|
1666
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1667
|
+
throw new Error(`Failed to list permissions: HTTP ${response.status} - ${String(errorData.description) !== '' ? String(errorData.description) : response.statusText}`)
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
const data = await response.json()
|
|
1671
|
+
if (data.status === 'error') {
|
|
1672
|
+
throw new Error(data.description ?? 'Failed to list permissions')
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
return data.permissions.map((p: any) => ({
|
|
1676
|
+
sender: p.sender,
|
|
1677
|
+
messageBox: p.message_box,
|
|
1678
|
+
recipientFee: p.recipient_fee,
|
|
1679
|
+
status: MessageBoxClient.getStatusFromFee(p.recipient_fee),
|
|
1680
|
+
createdAt: p.created_at,
|
|
1681
|
+
updatedAt: p.updated_at
|
|
1682
|
+
}))
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// ===========================
|
|
1686
|
+
// NOTIFICATION CONVENIENCE METHODS
|
|
1687
|
+
// ===========================
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* @method allowNotificationsFromPeer
|
|
1691
|
+
* @async
|
|
1692
|
+
* @param {PubKeyHex} identityKey - Sender's identity key to allow
|
|
1693
|
+
* @param {number} [recipientFee=0] - Fee to charge (0 for always allow)
|
|
1694
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1695
|
+
* @returns {Promise<void>} Permission status after allowing
|
|
1696
|
+
*
|
|
1697
|
+
* @description
|
|
1698
|
+
* Convenience method to allow notifications from a specific peer.
|
|
1699
|
+
*
|
|
1700
|
+
* @example
|
|
1701
|
+
* await client.allowNotificationsFromPeer('03abc123...') // Always allow
|
|
1702
|
+
* await client.allowNotificationsFromPeer('03def456...', 5) // Allow for 5 sats
|
|
1703
|
+
*/
|
|
1704
|
+
async allowNotificationsFromPeer(identityKey: PubKeyHex, recipientFee: number = 0, overrideHost?: string): Promise<void> {
|
|
1705
|
+
await this.setMessageBoxPermission({
|
|
1706
|
+
messageBox: 'notifications',
|
|
1707
|
+
sender: identityKey,
|
|
1708
|
+
recipientFee
|
|
1709
|
+
}, overrideHost)
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
/**
|
|
1713
|
+
* @method denyNotificationsFromPeer
|
|
1714
|
+
* @async
|
|
1715
|
+
* @param {PubKeyHex} identityKey - Sender's identity key to block
|
|
1716
|
+
* @returns {Promise<void>} Permission status after denying
|
|
1717
|
+
*
|
|
1718
|
+
* @description
|
|
1719
|
+
* Convenience method to block notifications from a specific peer.
|
|
1720
|
+
*
|
|
1721
|
+
* @example
|
|
1722
|
+
* await client.denyNotificationsFromPeer('03spam123...')
|
|
1723
|
+
*/
|
|
1724
|
+
async denyNotificationsFromPeer(identityKey: PubKeyHex, overrideHost?: string): Promise<void> {
|
|
1725
|
+
await this.setMessageBoxPermission({
|
|
1726
|
+
messageBox: 'notifications',
|
|
1727
|
+
sender: identityKey,
|
|
1728
|
+
recipientFee: -1
|
|
1729
|
+
}, overrideHost)
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* @method checkPeerNotificationStatus
|
|
1734
|
+
* @async
|
|
1735
|
+
* @param {PubKeyHex} identityKey - Sender's identity key to check
|
|
1736
|
+
* @returns {Promise<MessageBoxPermission>} Current permission status
|
|
1737
|
+
*
|
|
1738
|
+
* @description
|
|
1739
|
+
* Convenience method to check notification permission for a specific peer.
|
|
1740
|
+
*
|
|
1741
|
+
* @example
|
|
1742
|
+
* const status = await client.checkPeerNotificationStatus('03abc123...')
|
|
1743
|
+
* console.log(status.allowed) // true/false
|
|
1744
|
+
*/
|
|
1745
|
+
async checkPeerNotificationStatus(identityKey: PubKeyHex, overrideHost?: string): Promise<MessageBoxPermission | null> {
|
|
1746
|
+
const myIdentityKey = await this.getIdentityKey()
|
|
1747
|
+
return await this.getMessageBoxPermission({
|
|
1748
|
+
recipient: myIdentityKey,
|
|
1749
|
+
messageBox: 'notifications',
|
|
1750
|
+
sender: identityKey
|
|
1751
|
+
}, overrideHost)
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* @method listPeerNotifications
|
|
1756
|
+
* @async
|
|
1757
|
+
* @returns {Promise<MessageBoxPermission[]>} List of notification permissions
|
|
1758
|
+
*
|
|
1759
|
+
* @description
|
|
1760
|
+
* Convenience method to list all notification permissions.
|
|
1761
|
+
*
|
|
1762
|
+
* @example
|
|
1763
|
+
* const notifications = await client.listPeerNotifications()
|
|
1764
|
+
*/
|
|
1765
|
+
async listPeerNotifications(overrideHost?: string): Promise<MessageBoxPermission[]> {
|
|
1766
|
+
return await this.listMessageBoxPermissions({ messageBox: 'notifications' }, overrideHost)
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* @method sendNotification
|
|
1771
|
+
* @async
|
|
1772
|
+
* @param {PubKeyHex} recipient - Recipient's identity key
|
|
1773
|
+
* @param {string | object} body - Notification content
|
|
1774
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1775
|
+
* @returns {Promise<SendMessageResponse>} Send result
|
|
1776
|
+
*
|
|
1777
|
+
* @description
|
|
1778
|
+
* Convenience method to send a notification with automatic quote fetching and payment handling.
|
|
1779
|
+
* Automatically determines the required payment amount and creates the payment if needed.
|
|
1780
|
+
*
|
|
1781
|
+
* @example
|
|
1782
|
+
* // Send notification (auto-determines payment needed)
|
|
1783
|
+
* await client.sendNotification('03def456...', 'Hello!')
|
|
1784
|
+
*
|
|
1785
|
+
* // Send with maximum payment limit for safety
|
|
1786
|
+
* await client.sendNotification('03def456...', { title: 'Alert', body: 'Important update' }, 50)
|
|
1787
|
+
*/
|
|
1788
|
+
async sendNotification(
|
|
1789
|
+
recipient: PubKeyHex,
|
|
1790
|
+
body: string | object,
|
|
1791
|
+
overrideHost?: string
|
|
1792
|
+
): Promise<SendMessageResponse> {
|
|
1793
|
+
await this.assertInitialized()
|
|
1794
|
+
|
|
1795
|
+
// Use sendMessage with permission checking enabled
|
|
1796
|
+
// This eliminates duplication of quote fetching and payment logic
|
|
1797
|
+
return await this.sendMessage({
|
|
1798
|
+
recipient,
|
|
1799
|
+
messageBox: 'notifications',
|
|
1800
|
+
body,
|
|
1801
|
+
checkPermissions: true
|
|
1802
|
+
}, overrideHost)
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Register a device for FCM push notifications.
|
|
1807
|
+
*
|
|
1808
|
+
* @async
|
|
1809
|
+
* @param {DeviceRegistrationParams} params - Device registration parameters
|
|
1810
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1811
|
+
* @returns {Promise<DeviceRegistrationResponse>} Registration response
|
|
1812
|
+
*
|
|
1813
|
+
* @description
|
|
1814
|
+
* Registers a device with the message box server to receive FCM push notifications.
|
|
1815
|
+
* The FCM token is obtained from Firebase SDK on the client side.
|
|
1816
|
+
*
|
|
1817
|
+
* @example
|
|
1818
|
+
* const result = await client.registerDevice({
|
|
1819
|
+
* fcmToken: 'eBo8F...',
|
|
1820
|
+
* platform: 'ios',
|
|
1821
|
+
* deviceId: 'iPhone15Pro'
|
|
1822
|
+
* })
|
|
1823
|
+
*/
|
|
1824
|
+
async registerDevice(
|
|
1825
|
+
params: DeviceRegistrationParams,
|
|
1826
|
+
overrideHost?: string
|
|
1827
|
+
): Promise<DeviceRegistrationResponse> {
|
|
1828
|
+
await this.assertInitialized()
|
|
1829
|
+
|
|
1830
|
+
if (params.fcmToken == null || params.fcmToken.trim() === '') {
|
|
1831
|
+
throw new Error('fcmToken is required and must be a non-empty string')
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// Validate platform if provided
|
|
1835
|
+
const validPlatforms = ['ios', 'android', 'web']
|
|
1836
|
+
if (params.platform != null && !validPlatforms.includes(params.platform)) {
|
|
1837
|
+
throw new Error('platform must be one of: ios, android, web')
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
const finalHost = overrideHost ?? this.host
|
|
1841
|
+
|
|
1842
|
+
Logger.log('[MB CLIENT] Registering device for FCM notifications...')
|
|
1843
|
+
|
|
1844
|
+
const response = await this.authFetch.fetch(`${finalHost}/registerDevice`, {
|
|
1845
|
+
method: 'POST',
|
|
1846
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1847
|
+
body: JSON.stringify({
|
|
1848
|
+
fcmToken: params.fcmToken.trim(),
|
|
1849
|
+
deviceId: params.deviceId?.trim() ?? undefined,
|
|
1850
|
+
platform: params.platform ?? undefined
|
|
1851
|
+
})
|
|
1852
|
+
})
|
|
1853
|
+
|
|
1854
|
+
if (!response.ok) {
|
|
1855
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1856
|
+
const description = String(errorData.description) ?? response.statusText
|
|
1857
|
+
throw new Error(`Failed to register device: HTTP ${response.status} - ${description}`)
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
const data = await response.json()
|
|
1861
|
+
if (data.status === 'error') {
|
|
1862
|
+
throw new Error(data.description ?? 'Failed to register device')
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
Logger.log('[MB CLIENT] Device registered successfully')
|
|
1866
|
+
return {
|
|
1867
|
+
status: data.status,
|
|
1868
|
+
message: data.message,
|
|
1869
|
+
deviceId: data.deviceId
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
/**
|
|
1874
|
+
* List all registered devices for push notifications.
|
|
1875
|
+
*
|
|
1876
|
+
* @async
|
|
1877
|
+
* @param {string} [overrideHost] - Optional host override
|
|
1878
|
+
* @returns {Promise<RegisteredDevice[]>} Array of registered devices
|
|
1879
|
+
*
|
|
1880
|
+
* @description
|
|
1881
|
+
* Retrieves all devices registered by the authenticated user for FCM push notifications.
|
|
1882
|
+
* Only shows devices belonging to the current user (authenticated via AuthFetch).
|
|
1883
|
+
*
|
|
1884
|
+
* @example
|
|
1885
|
+
* const devices = await client.listRegisteredDevices()
|
|
1886
|
+
* console.log(`Found ${devices.length} registered devices`)
|
|
1887
|
+
* devices.forEach(device => {
|
|
1888
|
+
* console.log(`Device: ${device.platform} - ${device.fcmToken}`)
|
|
1889
|
+
* })
|
|
1890
|
+
*/
|
|
1891
|
+
async listRegisteredDevices(
|
|
1892
|
+
overrideHost?: string
|
|
1893
|
+
): Promise<RegisteredDevice[]> {
|
|
1894
|
+
await this.assertInitialized()
|
|
1895
|
+
|
|
1896
|
+
const finalHost = overrideHost ?? this.host
|
|
1897
|
+
|
|
1898
|
+
Logger.log('[MB CLIENT] Listing registered devices...')
|
|
1899
|
+
|
|
1900
|
+
const response = await this.authFetch.fetch(`${finalHost}/devices`, {
|
|
1901
|
+
method: 'GET'
|
|
1902
|
+
})
|
|
1903
|
+
|
|
1904
|
+
if (!response.ok) {
|
|
1905
|
+
const errorData = await response.json().catch(() => ({}))
|
|
1906
|
+
const description = String(errorData.description) ?? response.statusText
|
|
1907
|
+
throw new Error(`Failed to list devices: HTTP ${response.status} - ${description}`)
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
const data: ListDevicesResponse = await response.json()
|
|
1911
|
+
if (data.status === 'error') {
|
|
1912
|
+
throw new Error(data.description ?? 'Failed to list devices')
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
Logger.log(`[MB CLIENT] Found ${data.devices.length} registered devices`)
|
|
1916
|
+
return data.devices
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// ===========================
|
|
1920
|
+
// PRIVATE HELPER METHODS
|
|
1921
|
+
// ===========================
|
|
1922
|
+
|
|
1923
|
+
private static getStatusFromFee(fee: number): 'always_allow' | 'blocked' | 'payment_required' {
|
|
1924
|
+
if (fee === -1) return 'blocked'
|
|
1925
|
+
if (fee === 0) return 'always_allow'
|
|
1926
|
+
return 'payment_required'
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
/**
|
|
1930
|
+
* Creates payment transaction for message delivery fees
|
|
1931
|
+
* TODO: Consider consolidating payment generating logic with a util PeerPayClient can use as well.
|
|
1932
|
+
* @private
|
|
1933
|
+
* @param {string} recipient - Recipient identity key
|
|
1934
|
+
* @param {MessageBoxQuote} quote - Fee quote with delivery and recipient fees
|
|
1935
|
+
* @param {string} description - Description for the payment transaction
|
|
1936
|
+
* @returns {Promise<Payment>} Payment transaction data
|
|
1937
|
+
*/
|
|
1938
|
+
private async createMessagePayment(
|
|
1939
|
+
recipient: string,
|
|
1940
|
+
quote: MessageBoxQuote,
|
|
1941
|
+
description: string = 'MessageBox delivery payment',
|
|
1942
|
+
originator?: string
|
|
1943
|
+
): Promise<Payment> {
|
|
1944
|
+
if (quote.recipientFee <= 0 && quote.deliveryFee <= 0) {
|
|
1945
|
+
throw new Error('No payment required')
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
Logger.log(`[MB CLIENT] Creating payment transaction for ${quote.recipientFee} sats (delivery: ${quote.deliveryFee}, recipient: ${quote.recipientFee})`)
|
|
1949
|
+
|
|
1950
|
+
const outputs: InternalizeOutput[] = []
|
|
1951
|
+
const createActionOutputs: CreateActionOutput[] = []
|
|
1952
|
+
|
|
1953
|
+
// Get sender identity key for remittance data
|
|
1954
|
+
const senderIdentityKey = await this.getIdentityKey()
|
|
1955
|
+
|
|
1956
|
+
// Add server delivery fee output if > 0
|
|
1957
|
+
let outputIndex = 0
|
|
1958
|
+
if (quote.deliveryFee > 0) {
|
|
1959
|
+
const derivationPrefix = Utils.toBase64(Random(32))
|
|
1960
|
+
const derivationSuffix = Utils.toBase64(Random(32))
|
|
1961
|
+
|
|
1962
|
+
// Get host's derived public key
|
|
1963
|
+
const { publicKey: derivedKeyResult } = await this.walletClient.getPublicKey({
|
|
1964
|
+
protocolID: [2, '3241645161d8'],
|
|
1965
|
+
keyID: `${derivationPrefix} ${derivationSuffix}`,
|
|
1966
|
+
counterparty: quote.deliveryAgentIdentityKey
|
|
1967
|
+
}, originator)
|
|
1968
|
+
|
|
1969
|
+
// Create locking script using host's public key
|
|
1970
|
+
const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex()
|
|
1971
|
+
|
|
1972
|
+
// Add to createAction outputs
|
|
1973
|
+
createActionOutputs.push({
|
|
1974
|
+
satoshis: quote.deliveryFee,
|
|
1975
|
+
lockingScript,
|
|
1976
|
+
outputDescription: 'MessageBox server delivery fee',
|
|
1977
|
+
customInstructions: JSON.stringify({
|
|
1978
|
+
derivationPrefix,
|
|
1979
|
+
derivationSuffix,
|
|
1980
|
+
recipientIdentityKey: quote.deliveryAgentIdentityKey
|
|
1981
|
+
})
|
|
1982
|
+
})
|
|
1983
|
+
|
|
1984
|
+
outputs.push({
|
|
1985
|
+
outputIndex: outputIndex++,
|
|
1986
|
+
protocol: 'wallet payment',
|
|
1987
|
+
paymentRemittance: {
|
|
1988
|
+
derivationPrefix,
|
|
1989
|
+
derivationSuffix,
|
|
1990
|
+
senderIdentityKey
|
|
1991
|
+
}
|
|
1992
|
+
})
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// Add recipient fee output if > 0
|
|
1996
|
+
if (quote.recipientFee > 0) {
|
|
1997
|
+
const derivationPrefix = Utils.toBase64(Random(32))
|
|
1998
|
+
const derivationSuffix = Utils.toBase64(Random(32))
|
|
1999
|
+
// Get a derived public key for the recipient that "anyone" can verify
|
|
2000
|
+
const anyoneWallet = new ProtoWallet('anyone')
|
|
2001
|
+
const { publicKey: derivedKeyResult } = await anyoneWallet.getPublicKey({
|
|
2002
|
+
protocolID: [2, '3241645161d8'],
|
|
2003
|
+
keyID: `${derivationPrefix} ${derivationSuffix}`,
|
|
2004
|
+
counterparty: recipient
|
|
2005
|
+
})
|
|
2006
|
+
|
|
2007
|
+
if (derivedKeyResult == null || derivedKeyResult.trim() === '') {
|
|
2008
|
+
throw new Error('Failed to derive recipient\'s public key')
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// Create locking script using recipient's public key
|
|
2012
|
+
const lockingScript = new P2PKH().lock(PublicKey.fromString(derivedKeyResult).toAddress()).toHex()
|
|
2013
|
+
|
|
2014
|
+
// Add to createAction outputs
|
|
2015
|
+
createActionOutputs.push({
|
|
2016
|
+
satoshis: quote.recipientFee,
|
|
2017
|
+
lockingScript,
|
|
2018
|
+
outputDescription: 'Recipient message fee',
|
|
2019
|
+
customInstructions: JSON.stringify({
|
|
2020
|
+
derivationPrefix,
|
|
2021
|
+
derivationSuffix,
|
|
2022
|
+
recipientIdentityKey: recipient
|
|
2023
|
+
})
|
|
2024
|
+
})
|
|
2025
|
+
|
|
2026
|
+
outputs.push({
|
|
2027
|
+
outputIndex: outputIndex++,
|
|
2028
|
+
protocol: 'wallet payment',
|
|
2029
|
+
paymentRemittance: {
|
|
2030
|
+
derivationPrefix,
|
|
2031
|
+
derivationSuffix,
|
|
2032
|
+
senderIdentityKey: (await anyoneWallet.getPublicKey({ identityKey: true })).publicKey
|
|
2033
|
+
}
|
|
2034
|
+
})
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
const { tx } = await this.walletClient.createAction({
|
|
2038
|
+
description,
|
|
2039
|
+
outputs: createActionOutputs,
|
|
2040
|
+
options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
|
|
2041
|
+
}, originator)
|
|
2042
|
+
|
|
2043
|
+
if (tx == null) {
|
|
2044
|
+
throw new Error('Failed to create payment transaction')
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
return {
|
|
2048
|
+
tx,
|
|
2049
|
+
outputs,
|
|
2050
|
+
description
|
|
2051
|
+
// labels
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
1341
2054
|
}
|