@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.
Files changed (36) hide show
  1. package/dist/cjs/package.json +4 -4
  2. package/dist/cjs/src/MessageBoxClient.js +747 -129
  3. package/dist/cjs/src/MessageBoxClient.js.map +1 -1
  4. package/dist/cjs/src/PeerPayClient.js +61 -28
  5. package/dist/cjs/src/PeerPayClient.js.map +1 -1
  6. package/dist/cjs/src/Utils/logger.js +22 -21
  7. package/dist/cjs/src/Utils/logger.js.map +1 -1
  8. package/dist/cjs/src/types/permissions.js +6 -0
  9. package/dist/cjs/src/types/permissions.js.map +1 -0
  10. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  11. package/dist/esm/src/MessageBoxClient.js +636 -57
  12. package/dist/esm/src/MessageBoxClient.js.map +1 -1
  13. package/dist/esm/src/PeerPayClient.js +1 -1
  14. package/dist/esm/src/PeerPayClient.js.map +1 -1
  15. package/dist/esm/src/Utils/logger.js +17 -19
  16. package/dist/esm/src/Utils/logger.js.map +1 -1
  17. package/dist/esm/src/types/permissions.js +5 -0
  18. package/dist/esm/src/types/permissions.js.map +1 -0
  19. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  20. package/dist/types/src/MessageBoxClient.d.ts +235 -24
  21. package/dist/types/src/MessageBoxClient.d.ts.map +1 -1
  22. package/dist/types/src/PeerPayClient.d.ts.map +1 -1
  23. package/dist/types/src/Utils/logger.d.ts +5 -8
  24. package/dist/types/src/Utils/logger.d.ts.map +1 -1
  25. package/dist/types/src/types/permissions.d.ts +75 -0
  26. package/dist/types/src/types/permissions.d.ts.map +1 -0
  27. package/dist/types/src/types.d.ts +80 -2
  28. package/dist/types/src/types.d.ts.map +1 -1
  29. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  30. package/dist/umd/bundle.js +1 -1
  31. package/package.json +4 -4
  32. package/src/MessageBoxClient.ts +781 -68
  33. package/src/PeerPayClient.ts +11 -11
  34. package/src/Utils/logger.ts +17 -19
  35. package/src/types/permissions.ts +81 -0
  36. package/src/types.ts +87 -2
@@ -30,25 +30,30 @@ import {
30
30
  Utils,
31
31
  Transaction,
32
32
  PushDrop,
33
- BEEF,
34
- LockingScript,
35
- HexString
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 { Logger } from './Utils/logger.js'
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: 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 {WalletClient} options.walletClient - Wallet instance used for authentication, signing, and encryption.
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
- Logger.log('[MB CLIENT] Fetching identity key...')
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
- * Decryption automatically derives a shared secret using the sender’s identity key and the receiver’s child private key.
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
- typeof (parsedBody as any).encryptedMessage === 'string'
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
- (parsedBody as any).encryptedMessage,
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
- message.body = parsedBody as string | Record<string, any>
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
  }