@bsv/message-box-client 2.0.2 → 2.0.4

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 (34) hide show
  1. package/dist/cjs/mod.js +1 -0
  2. package/dist/cjs/mod.js.map +1 -1
  3. package/dist/cjs/package.json +1 -1
  4. package/dist/cjs/src/MessageBoxClient.js +119 -30
  5. package/dist/cjs/src/MessageBoxClient.js.map +1 -1
  6. package/dist/cjs/src/RemittanceAdapter.js +137 -0
  7. package/dist/cjs/src/RemittanceAdapter.js.map +1 -0
  8. package/dist/cjs/src/__tests/RemittanceAdapter.test.js +133 -0
  9. package/dist/cjs/src/__tests/RemittanceAdapter.test.js.map +1 -0
  10. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  11. package/dist/esm/mod.js +1 -0
  12. package/dist/esm/mod.js.map +1 -1
  13. package/dist/esm/src/MessageBoxClient.js +119 -30
  14. package/dist/esm/src/MessageBoxClient.js.map +1 -1
  15. package/dist/esm/src/RemittanceAdapter.js +133 -0
  16. package/dist/esm/src/RemittanceAdapter.js.map +1 -0
  17. package/dist/esm/src/__tests/RemittanceAdapter.test.js +131 -0
  18. package/dist/esm/src/__tests/RemittanceAdapter.test.js.map +1 -0
  19. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  20. package/dist/types/mod.d.ts +1 -0
  21. package/dist/types/mod.d.ts.map +1 -1
  22. package/dist/types/src/MessageBoxClient.d.ts +2 -0
  23. package/dist/types/src/MessageBoxClient.d.ts.map +1 -1
  24. package/dist/types/src/RemittanceAdapter.d.ts +103 -0
  25. package/dist/types/src/RemittanceAdapter.d.ts.map +1 -0
  26. package/dist/types/src/__tests/RemittanceAdapter.test.d.ts +2 -0
  27. package/dist/types/src/__tests/RemittanceAdapter.test.d.ts.map +1 -0
  28. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  29. package/dist/umd/bundle.js +1 -1
  30. package/mod.ts +1 -0
  31. package/package.json +1 -1
  32. package/src/MessageBoxClient.ts +148 -52
  33. package/src/RemittanceAdapter.ts +164 -0
  34. package/src/__tests/RemittanceAdapter.test.ts +153 -0
package/mod.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './src/MessageBoxClient.js'
2
2
  export * from './src/PeerPayClient.js'
3
3
  export * from './src/types.js'
4
+ export * from './src/RemittanceAdapter.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/message-box-client",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -96,6 +96,8 @@ export class MessageBoxClient {
96
96
  private readonly lookupResolver: LookupResolver
97
97
  private readonly networkPreset: 'local' | 'mainnet' | 'testnet'
98
98
  private initialized = false
99
+ private socketAuthenticated = false
100
+ private connectionInitPromise?: Promise<void>
99
101
  protected originator?: OriginatorDomainNameStringUnder250Bytes
100
102
  /**
101
103
  * @constructor
@@ -131,7 +133,7 @@ export class MessageBoxClient {
131
133
  } = options
132
134
 
133
135
  const defaultHost =
134
- this.networkPreset === 'testnet'
136
+ networkPreset === 'testnet'
135
137
  ? DEFAULT_TESTNET_HOST
136
138
  : DEFAULT_MAINNET_HOST
137
139
 
@@ -313,6 +315,15 @@ export class MessageBoxClient {
313
315
 
314
316
  Logger.log('[MB CLIENT] Setting up WebSocket connection...')
315
317
 
318
+ if (this.socketAuthenticated && this.socket != null) {
319
+ return
320
+ }
321
+
322
+ if (this.connectionInitPromise != null) {
323
+ await this.connectionInitPromise
324
+ return
325
+ }
326
+
316
327
  if (this.socket == null) {
317
328
  const targetHost = overrideHost ?? this.host
318
329
  if (typeof targetHost !== 'string' || targetHost.trim() === '') {
@@ -320,58 +331,115 @@ export class MessageBoxClient {
320
331
  }
321
332
  this.socket = AuthSocketClient(targetHost, { wallet: this.walletClient, originator: this.originator })
322
333
 
323
- let identitySent = false
324
- let authenticated = false
325
-
326
334
  this.socket.on('connect', () => {
327
335
  Logger.log('[MB CLIENT] Connected to WebSocket.')
328
336
 
329
- if (!identitySent) {
330
- Logger.log('[MB CLIENT] Sending authentication data:', this.myIdentityKey)
331
- if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
332
- Logger.error('[MB CLIENT ERROR] Cannot send authentication: Identity key is missing!')
333
- } else {
334
- this.socket?.emit('authenticated', { identityKey: this.myIdentityKey })
335
- identitySent = true
336
- }
337
+ Logger.log('[MB CLIENT] Sending authentication data:', this.myIdentityKey)
338
+ if (this.myIdentityKey == null || this.myIdentityKey.trim() === '') {
339
+ Logger.error('[MB CLIENT ERROR] Cannot send authentication: Identity key is missing!')
340
+ } else {
341
+ this.socket?.emit('authenticated', { identityKey: this.myIdentityKey })
337
342
  }
338
343
  })
339
344
 
340
345
  // Listen for authentication success from the server
341
346
  this.socket.on('authenticationSuccess', (data) => {
342
347
  Logger.log(`[MB CLIENT] WebSocket authentication successful: ${JSON.stringify(data)}`)
343
- authenticated = true
348
+ this.socketAuthenticated = true
344
349
  })
345
350
 
346
351
  // Handle authentication failures
347
352
  this.socket.on('authenticationFailed', (data) => {
348
353
  Logger.error(`[MB CLIENT ERROR] WebSocket authentication failed: ${JSON.stringify(data)}`)
349
- authenticated = false
354
+ this.socketAuthenticated = false
350
355
  })
351
356
 
352
357
  this.socket.on('disconnect', () => {
353
358
  Logger.log('[MB CLIENT] Disconnected from MessageBox server')
354
359
  this.socket = undefined
355
- identitySent = false
356
- authenticated = false
360
+ this.socketAuthenticated = false
357
361
  })
358
362
 
359
363
  this.socket.on('error', (error) => {
360
364
  Logger.error('[MB CLIENT ERROR] WebSocket error:', error)
361
365
  })
366
+ }
362
367
 
363
- // Wait for authentication confirmation before proceeding
364
- await new Promise<void>((resolve, reject) => {
365
- setTimeout(() => {
366
- if (authenticated) {
367
- Logger.log('[MB CLIENT] WebSocket fully authenticated and ready!')
368
- resolve()
369
- } else {
370
- reject(new Error('[MB CLIENT ERROR] WebSocket authentication timed out!'))
371
- }
372
- }, 5000) // Timeout after 5 seconds
373
- })
368
+ if (this.socket?.connected && !this.socketAuthenticated) {
369
+ this.socket.emit('authenticated', { identityKey: this.myIdentityKey })
374
370
  }
371
+
372
+ this.connectionInitPromise = new Promise<void>((resolve, reject) => {
373
+ const socketAny = this.socket as any
374
+ let settled = false
375
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
376
+
377
+ const finalizeResolve = (): void => {
378
+ if (settled) return
379
+ settled = true
380
+ if (timeoutId != null) {
381
+ clearTimeout(timeoutId)
382
+ timeoutId = undefined
383
+ }
384
+ if (typeof socketAny?.off === 'function') {
385
+ socketAny.off('authenticationSuccess', onSuccess)
386
+ socketAny.off('authenticationFailed', onFailed)
387
+ socketAny.off('disconnect', onDisconnectBeforeAuth)
388
+ }
389
+ this.connectionInitPromise = undefined
390
+ Logger.log('[MB CLIENT] WebSocket fully authenticated and ready!')
391
+ resolve()
392
+ }
393
+
394
+ const finalizeReject = (error: Error): void => {
395
+ if (settled) return
396
+ settled = true
397
+ if (timeoutId != null) {
398
+ clearTimeout(timeoutId)
399
+ timeoutId = undefined
400
+ }
401
+ if (typeof socketAny?.off === 'function') {
402
+ socketAny.off('authenticationSuccess', onSuccess)
403
+ socketAny.off('authenticationFailed', onFailed)
404
+ socketAny.off('disconnect', onDisconnectBeforeAuth)
405
+ }
406
+ this.connectionInitPromise = undefined
407
+ reject(error)
408
+ }
409
+
410
+ const onSuccess = (): void => {
411
+ this.socketAuthenticated = true
412
+ finalizeResolve()
413
+ }
414
+
415
+ const onFailed = (): void => {
416
+ this.socketAuthenticated = false
417
+ finalizeReject(new Error('[MB CLIENT ERROR] WebSocket authentication failed!'))
418
+ }
419
+
420
+ const onDisconnectBeforeAuth = (): void => {
421
+ this.socketAuthenticated = false
422
+ }
423
+
424
+ if (this.socketAuthenticated) {
425
+ finalizeResolve()
426
+ return
427
+ }
428
+
429
+ socketAny?.on('authenticationSuccess', onSuccess)
430
+ socketAny?.on('authenticationFailed', onFailed)
431
+ socketAny?.on('disconnect', onDisconnectBeforeAuth)
432
+
433
+ timeoutId = setTimeout(() => {
434
+ if (this.socketAuthenticated) {
435
+ finalizeResolve()
436
+ } else {
437
+ finalizeReject(new Error('[MB CLIENT ERROR] WebSocket authentication timed out!'))
438
+ }
439
+ }, 5000)
440
+ })
441
+
442
+ await this.connectionInitPromise
375
443
  }
376
444
 
377
445
  /**
@@ -453,7 +521,29 @@ export class MessageBoxClient {
453
521
  } catch (err) {
454
522
  Logger.error('[MB CLIENT ERROR] _queryAdvertisements failed:', err)
455
523
  }
456
- return hosts
524
+ return hosts.filter(item => {
525
+ const h = item.host.trim().toLowerCase()
526
+ if (!h) return false
527
+ try {
528
+ const url = new URL(h)
529
+ if (url.protocol !== 'https:') return false
530
+ const hostname = url.hostname
531
+ if (
532
+ hostname === 'localhost' ||
533
+ hostname === '127.0.0.1' ||
534
+ hostname === '0.0.0.0' ||
535
+ hostname === '::1' ||
536
+ hostname.endsWith('.local') ||
537
+ hostname.endsWith('.example.com') ||
538
+ hostname.endsWith('.test') ||
539
+ hostname.endsWith('.invalid') ||
540
+ hostname.endsWith('.localhost')
541
+ ) return false
542
+ return true
543
+ } catch {
544
+ return false
545
+ }
546
+ })
457
547
  }
458
548
 
459
549
  /**
@@ -700,10 +790,15 @@ export class MessageBoxClient {
700
790
  return await new Promise((resolve, reject) => {
701
791
  const ackEvent = `sendMessageAck-${roomId}`
702
792
  let handled = false
793
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
703
794
 
704
795
  const ackHandler = (response?: SendMessageResponse): void => {
705
796
  if (handled) return
706
797
  handled = true
798
+ if (timeoutId != null) {
799
+ clearTimeout(timeoutId)
800
+ timeoutId = undefined
801
+ }
707
802
 
708
803
  const socketAny = this.socket as any
709
804
  if (typeof socketAny?.off === 'function') {
@@ -749,9 +844,10 @@ export class MessageBoxClient {
749
844
  })
750
845
 
751
846
  // Timeout: Fallback to HTTP if no acknowledgment received
752
- setTimeout(() => {
847
+ timeoutId = setTimeout(() => {
753
848
  if (!handled) {
754
849
  handled = true
850
+ timeoutId = undefined
755
851
  const socketAny = this.socket as any
756
852
  if (typeof socketAny?.off === 'function') {
757
853
  socketAny.off(ackEvent, ackHandler)
@@ -1063,14 +1159,14 @@ export class MessageBoxClient {
1063
1159
  throw new Error('Missing delivery agent identity keys in quote response.')
1064
1160
  }
1065
1161
  if (Object.keys(deliveryAgentIdentityKeyByHost).length > 1 && !overrideHost) {
1066
- // To keep the single-POST invariant, we require all recipients to share a host
1162
+ // To keep the single-POST invariant, we require all recipients to share a host
1067
1163
  throw new Error('Recipients resolve to multiple hosts. Use overrideHost to force a single server or split by host.')
1068
1164
  }
1069
1165
 
1070
1166
  // pick the host to POST to
1071
1167
  const finalHost = (overrideHost ?? await this.resolveHostForRecipient(allowedRecipients[0])).replace(/\/+$/, '')
1072
1168
  const singleDeliveryKey = deliveryAgentIdentityKeyByHost[finalHost] ??
1073
- Object.values(deliveryAgentIdentityKeyByHost)[0]
1169
+ Object.values(deliveryAgentIdentityKeyByHost)[0]
1074
1170
 
1075
1171
  if (!singleDeliveryKey) {
1076
1172
  throw new Error('Could not determine server delivery agent identity key.')
@@ -1100,7 +1196,7 @@ export class MessageBoxClient {
1100
1196
  if (skipEncryption === true) {
1101
1197
  finalBody = typeof body === 'string' ? body : JSON.stringify(body)
1102
1198
  } else {
1103
- // safest for now: send plaintext; the recipients can decrypt payload fields client-side if needed
1199
+ // safest for now: send plaintext; the recipients can decrypt payload fields client-side if needed
1104
1200
  finalBody = typeof body === 'string' ? body : JSON.stringify(body)
1105
1201
  }
1106
1202
 
@@ -1143,18 +1239,18 @@ export class MessageBoxClient {
1143
1239
  const failed: Array<{ recipient: string, error: string }> = [] // handled server-side now
1144
1240
 
1145
1241
  const status: SendListResult['status'] =
1146
- sent.length === allowedRecipients.length
1147
- ? 'success'
1148
- : sent.length > 0
1149
- ? 'partial'
1150
- : 'error'
1242
+ sent.length === allowedRecipients.length
1243
+ ? 'success'
1244
+ : sent.length > 0
1245
+ ? 'partial'
1246
+ : 'error'
1151
1247
 
1152
1248
  const description =
1153
- status === 'success'
1154
- ? `Sent to ${sent.length} recipients.`
1155
- : status === 'partial'
1156
- ? `Sent to ${sent.length} recipients; ${allowedRecipients.length - sent.length} failed; ${blocked.length} blocked.`
1157
- : `Failed to send to ${allowedRecipients.length} allowed recipients. ${blocked.length} blocked.`
1249
+ status === 'success'
1250
+ ? `Sent to ${sent.length} recipients.`
1251
+ : status === 'partial'
1252
+ ? `Sent to ${sent.length} recipients; ${allowedRecipients.length - sent.length} failed; ${blocked.length} blocked.`
1253
+ : `Failed to send to ${allowedRecipients.length} allowed recipients. ${blocked.length} blocked.`
1158
1254
 
1159
1255
  return { status, description, sent, blocked, failed, totals }
1160
1256
  } catch (err) {
@@ -1681,7 +1777,7 @@ export class MessageBoxClient {
1681
1777
  }
1682
1778
  }
1683
1779
 
1684
- private async mapWithConcurrency<T, R> (
1780
+ private async mapWithConcurrency<T, R>(
1685
1781
  items: T[],
1686
1782
  limit: number,
1687
1783
  fn: (item: T, index: number) => Promise<R>
@@ -1997,7 +2093,7 @@ export class MessageBoxClient {
1997
2093
  params: GetQuoteParams,
1998
2094
  overrideHost?: string
1999
2095
  ): Promise<MessageBoxQuote | MessageBoxMultiQuote> {
2000
- // ---------- SINGLE RECIPIENT (back-compat) ----------
2096
+ // ---------- SINGLE RECIPIENT (back-compat) ----------
2001
2097
  if (!Array.isArray(params.recipient)) {
2002
2098
  const finalHost = overrideHost ?? await this.resolveHostForRecipient(params.recipient)
2003
2099
  const queryParams = new URLSearchParams({
@@ -2008,14 +2104,14 @@ export class MessageBoxClient {
2008
2104
  Logger.log('[MB CLIENT] Getting messageBox quote (single)...')
2009
2105
  console.log('HELP IM QUOTING', `${finalHost}/permissions/quote?${queryParams.toString()}`)
2010
2106
  const response = await this.authFetch.fetch(
2011
- `${finalHost}/permissions/quote?${queryParams.toString()}`,
2012
- { method: 'GET' }
2107
+ `${finalHost}/permissions/quote?${queryParams.toString()}`,
2108
+ { method: 'GET' }
2013
2109
  )
2014
2110
  console.log('server response from getquote]', response)
2015
2111
  if (!response.ok) {
2016
2112
  const errorData = await response.json().catch(() => ({}))
2017
2113
  throw new Error(
2018
- `Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`
2114
+ `Failed to get quote: HTTP ${response.status} - ${String(errorData.description) ?? response.statusText}`
2019
2115
  )
2020
2116
  }
2021
2117
 
@@ -2088,7 +2184,7 @@ export class MessageBoxClient {
2088
2184
  if (!resp.ok) {
2089
2185
  const errorData = await resp.json().catch(() => ({}))
2090
2186
  throw new Error(
2091
- `Failed to get quote (host ${host}): HTTP ${resp.status} - ${String(errorData.description) ?? resp.statusText}`
2187
+ `Failed to get quote (host ${host}): HTTP ${resp.status} - ${String(errorData.description) ?? resp.statusText}`
2092
2188
  )
2093
2189
  }
2094
2190
 
@@ -2103,7 +2199,7 @@ export class MessageBoxClient {
2103
2199
  // Server supports both shapes. For multi we expect:
2104
2200
  // { quotesByRecipient, totals, blockedRecipients }
2105
2201
  if (Array.isArray(payload?.quotesByRecipient)) {
2106
- // merge quotes
2202
+ // merge quotes
2107
2203
  for (const q of payload.quotesByRecipient) {
2108
2204
  quotesByRecipient.push({
2109
2205
  recipient: q.recipient,
@@ -2129,12 +2225,12 @@ export class MessageBoxClient {
2129
2225
  }
2130
2226
  }
2131
2227
  } else if (payload?.quote) {
2132
- // Defensive: if an overlay still returns single-quote shape for multi (shouldn’t),
2133
- // we map it to each recipient in the group uniformly.
2228
+ // Defensive: if an overlay still returns single-quote shape for multi (shouldn’t),
2229
+ // we map it to each recipient in the group uniformly.
2134
2230
  for (const r of groupRecipients) {
2135
2231
  const { deliveryFee, recipientFee } = payload.quote
2136
2232
  const status =
2137
- recipientFee === -1 ? 'blocked' : recipientFee === 0 ? 'always_allow' : 'payment_required'
2233
+ recipientFee === -1 ? 'blocked' : recipientFee === 0 ? 'always_allow' : 'payment_required'
2138
2234
  quotesByRecipient.push({
2139
2235
  recipient: r,
2140
2236
  messageBox: params.messageBox,
@@ -0,0 +1,164 @@
1
+ /**
2
+ * RemittanceAdapter - Adapts MessageBoxClient to the CommsLayer interface
3
+ *
4
+ * This adapter bridges MessageBoxClient with the ts-sdk RemittanceManager by implementing
5
+ * the CommsLayer interface. It handles the protocol differences between the two systems,
6
+ * particularly around message body format (MessageBoxClient returns parsed objects,
7
+ * RemittanceManager expects JSON strings).
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { RemittanceAdapter } from '@bsv/message-box-client'
12
+ * import { RemittanceManager } from '@bsv/sdk'
13
+ * import { MessageBoxClient } from '@bsv/message-box-client'
14
+ * import { WalletClient } from '@bsv/sdk'
15
+ *
16
+ * const wallet = new WalletClient()
17
+ * const messageBox = new MessageBoxClient({ walletClient: wallet })
18
+ * const commsLayer = new RemittanceAdapter(messageBox)
19
+ *
20
+ * const manager = new RemittanceManager(
21
+ * {
22
+ * messageBox: 'remittance_inbox',
23
+ * remittanceModules: [new Brc29RemittanceModule()]
24
+ * },
25
+ * wallet,
26
+ * commsLayer
27
+ * )
28
+ * ```
29
+ */
30
+
31
+ import { PubKeyHex } from '@bsv/sdk'
32
+ import type {
33
+ CommsLayer as SdkCommsLayer,
34
+ PeerMessage as SdkRemittancePeerMessage
35
+ } from '@bsv/sdk'
36
+ import type { MessageBoxClient } from './MessageBoxClient.js'
37
+ import type { PeerMessage as MessageBoxPeerMessage } from './types.js'
38
+
39
+ export type CommsLayer = SdkCommsLayer
40
+ export type RemittancePeerMessage = SdkRemittancePeerMessage
41
+
42
+ /**
43
+ * Adapter that implements the CommsLayer interface for MessageBoxClient
44
+ *
45
+ * This class wraps MessageBoxClient to provide compatibility with the RemittanceManager
46
+ * communications interface. It handles format conversions, particularly ensuring message
47
+ * bodies are properly stringified for the RemittanceManager protocol.
48
+ */
49
+ export class RemittanceAdapter implements SdkCommsLayer {
50
+ /**
51
+ * Creates a new RemittanceAdapter
52
+ * @param messageBox - The MessageBoxClient instance to adapt
53
+ */
54
+ constructor(private readonly messageBox: MessageBoxClient) { }
55
+
56
+ /**
57
+ * Sends a message over the store-and-forward channel
58
+ * @param args - Message parameters (recipient, messageBox, body)
59
+ * @param hostOverride - Optional host override
60
+ * @returns The transport message ID
61
+ */
62
+ async sendMessage(
63
+ args: { recipient: PubKeyHex, messageBox: string, body: string },
64
+ hostOverride?: string
65
+ ): Promise<string> {
66
+ const result = await this.messageBox.sendMessage({
67
+ recipient: args.recipient,
68
+ messageBox: args.messageBox,
69
+ body: args.body
70
+ }, hostOverride)
71
+
72
+ return result.messageId
73
+ }
74
+
75
+ /**
76
+ * Sends a message over the live channel.
77
+ * MessageBoxClient handles transport fallback internally (WebSocket -> HTTP).
78
+ * @param args - Message parameters (recipient, messageBox, body)
79
+ * @param hostOverride - Optional host override
80
+ * @returns The transport message ID
81
+ */
82
+ async sendLiveMessage(
83
+ args: { recipient: PubKeyHex, messageBox: string, body: string },
84
+ hostOverride?: string
85
+ ): Promise<string> {
86
+ const result = await this.messageBox.sendLiveMessage({
87
+ recipient: args.recipient,
88
+ messageBox: args.messageBox,
89
+ body: args.body
90
+ }, hostOverride)
91
+
92
+ return result.messageId
93
+ }
94
+
95
+ /**
96
+ * Lists pending messages for a message box
97
+ *
98
+ * Note: MessageBoxClient returns message bodies as parsed objects, but RemittanceManager
99
+ * expects them as JSON strings. This method handles the conversion.
100
+ *
101
+ * @param args - List parameters (messageBox, optional host)
102
+ * @returns Array of peer messages with stringified bodies
103
+ */
104
+ async listMessages(args: { messageBox: string, host?: string }): Promise<RemittancePeerMessage[]> {
105
+ const defaultRecipient = await this.messageBox.getIdentityKey() as PubKeyHex
106
+ const messages = await this.messageBox.listMessages({
107
+ messageBox: args.messageBox,
108
+ host: args.host
109
+ })
110
+
111
+ return messages.map(msg => this.toRemittancePeerMessage(msg, args.messageBox, defaultRecipient))
112
+ }
113
+
114
+ /**
115
+ * Acknowledges messages (deletes them from the server inbox)
116
+ * @param args - Array of message IDs to acknowledge
117
+ */
118
+ async acknowledgeMessage(args: { messageIds: string[] }): Promise<void> {
119
+ // MessageBoxClient's acknowledgeMessage expects the same format
120
+ await this.messageBox.acknowledgeMessage({ messageIds: args.messageIds })
121
+ }
122
+
123
+ /**
124
+ * Starts a live listener and normalizes inbound messages to the remittance PeerMessage shape.
125
+ */
126
+ async listenForLiveMessages(args: {
127
+ messageBox: string
128
+ overrideHost?: string
129
+ onMessage: (msg: RemittancePeerMessage) => void
130
+ }): Promise<void> {
131
+ const defaultRecipient = await this.messageBox.getIdentityKey() as PubKeyHex
132
+
133
+ await this.messageBox.listenForLiveMessages({
134
+ messageBox: args.messageBox,
135
+ overrideHost: args.overrideHost,
136
+ onMessage: (msg: MessageBoxPeerMessage) => {
137
+ args.onMessage(this.toRemittancePeerMessage(msg, args.messageBox, defaultRecipient))
138
+ }
139
+ })
140
+ }
141
+
142
+ private toRemittancePeerMessage (
143
+ msg: MessageBoxPeerMessage & { recipient?: string, messageBox?: string },
144
+ fallbackMessageBox: string,
145
+ fallbackRecipient: PubKeyHex
146
+ ): RemittancePeerMessage {
147
+ return {
148
+ messageId: msg.messageId,
149
+ sender: msg.sender as PubKeyHex,
150
+ recipient: (msg.recipient ?? fallbackRecipient) as PubKeyHex,
151
+ messageBox: msg.messageBox ?? fallbackMessageBox,
152
+ body: this.toBodyString(msg.body)
153
+ }
154
+ }
155
+
156
+ private toBodyString (body: unknown): string {
157
+ if (typeof body === 'string') return body
158
+ try {
159
+ return JSON.stringify(body)
160
+ } catch {
161
+ return String(body)
162
+ }
163
+ }
164
+ }
@@ -0,0 +1,153 @@
1
+ /* eslint-env jest */
2
+ import { jest } from '@jest/globals'
3
+ import { RemittanceAdapter } from '../RemittanceAdapter.js'
4
+ import type { MessageBoxClient } from '../MessageBoxClient.js'
5
+
6
+ describe('RemittanceAdapter', () => {
7
+ const myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
8
+ const senderKey = '03f5d7a10f8ac22f0785a54d7d30fd009a77da27812f4e2f4ac9327dfcb5f65f86'
9
+
10
+ it('delegates sendMessage and returns the transport messageId', async () => {
11
+ const messageBox = {
12
+ sendMessage: jest.fn<() => Promise<{ status: string, messageId: string }>>().mockResolvedValue({
13
+ status: 'success',
14
+ messageId: 'http-mid'
15
+ })
16
+ } as unknown as MessageBoxClient
17
+
18
+ const adapter = new RemittanceAdapter(messageBox)
19
+ const result = await adapter.sendMessage({
20
+ recipient: senderKey,
21
+ messageBox: 'remittance_inbox',
22
+ body: '{"v":1}'
23
+ }, 'https://override-host')
24
+
25
+ expect(result).toBe('http-mid')
26
+ expect(messageBox.sendMessage).toHaveBeenCalledWith({
27
+ recipient: senderKey,
28
+ messageBox: 'remittance_inbox',
29
+ body: '{"v":1}'
30
+ }, 'https://override-host')
31
+ })
32
+
33
+ it('delegates sendLiveMessage (live path) and returns the transport messageId', async () => {
34
+ const messageBox = {
35
+ sendLiveMessage: jest.fn<() => Promise<{ status: string, messageId: string }>>().mockResolvedValue({
36
+ status: 'success',
37
+ messageId: 'ws-mid'
38
+ })
39
+ } as unknown as MessageBoxClient
40
+
41
+ const adapter = new RemittanceAdapter(messageBox)
42
+ const result = await adapter.sendLiveMessage({
43
+ recipient: senderKey,
44
+ messageBox: 'remittance_inbox',
45
+ body: '{"v":1}'
46
+ }, 'https://override-host')
47
+
48
+ expect(result).toBe('ws-mid')
49
+ expect(messageBox.sendLiveMessage).toHaveBeenCalledWith({
50
+ recipient: senderKey,
51
+ messageBox: 'remittance_inbox',
52
+ body: '{"v":1}'
53
+ }, 'https://override-host')
54
+ })
55
+
56
+ it('normalizes listMessages shape for remittance and forwards host', async () => {
57
+ const messageBox = {
58
+ getIdentityKey: jest.fn<() => Promise<string>>().mockResolvedValue(myIdentityKey),
59
+ listMessages: jest.fn<() => Promise<any[]>>().mockResolvedValue([
60
+ {
61
+ messageId: 'm1',
62
+ sender: senderKey,
63
+ created_at: '2026-01-01T00:00:00Z',
64
+ updated_at: '2026-01-01T00:00:00Z',
65
+ body: { kind: 'invoice', amount: 1 }
66
+ },
67
+ {
68
+ messageId: 'm2',
69
+ sender: senderKey,
70
+ recipient: '020202020202020202020202020202020202020202020202020202020202020202',
71
+ messageBox: 'custom_box',
72
+ created_at: '2026-01-01T00:00:00Z',
73
+ updated_at: '2026-01-01T00:00:00Z',
74
+ body: '{"kind":"receipt"}'
75
+ }
76
+ ])
77
+ } as unknown as MessageBoxClient
78
+
79
+ const adapter = new RemittanceAdapter(messageBox)
80
+ const result = await adapter.listMessages({
81
+ messageBox: 'remittance_inbox',
82
+ host: 'https://remote-host'
83
+ })
84
+
85
+ expect(messageBox.listMessages).toHaveBeenCalledWith({
86
+ messageBox: 'remittance_inbox',
87
+ host: 'https://remote-host'
88
+ })
89
+ expect(result).toEqual([
90
+ {
91
+ messageId: 'm1',
92
+ sender: senderKey,
93
+ recipient: myIdentityKey,
94
+ messageBox: 'remittance_inbox',
95
+ body: '{"kind":"invoice","amount":1}'
96
+ },
97
+ {
98
+ messageId: 'm2',
99
+ sender: senderKey,
100
+ recipient: '020202020202020202020202020202020202020202020202020202020202020202',
101
+ messageBox: 'custom_box',
102
+ body: '{"kind":"receipt"}'
103
+ }
104
+ ])
105
+ })
106
+
107
+ it('forwards live listener setup and normalizes inbound message shape', async () => {
108
+ const onPaymentMessage = jest.fn()
109
+ let forwardedListener: ((msg: {
110
+ messageId: string
111
+ sender: string
112
+ body: unknown
113
+ created_at: string
114
+ updated_at: string
115
+ }) => void) | undefined
116
+
117
+ const messageBox = {
118
+ getIdentityKey: jest.fn<() => Promise<string>>().mockResolvedValue(myIdentityKey),
119
+ listenForLiveMessages: jest.fn().mockImplementation(async ({ onMessage }) => {
120
+ forwardedListener = onMessage
121
+ })
122
+ } as unknown as MessageBoxClient
123
+
124
+ const adapter = new RemittanceAdapter(messageBox)
125
+ await adapter.listenForLiveMessages({
126
+ messageBox: 'remittance_inbox',
127
+ overrideHost: 'https://ws-host',
128
+ onMessage: onPaymentMessage
129
+ })
130
+
131
+ expect(messageBox.listenForLiveMessages).toHaveBeenCalledWith({
132
+ messageBox: 'remittance_inbox',
133
+ overrideHost: 'https://ws-host',
134
+ onMessage: expect.any(Function)
135
+ })
136
+
137
+ forwardedListener?.({
138
+ messageId: 'live-1',
139
+ sender: senderKey,
140
+ created_at: '2026-01-01T00:00:00Z',
141
+ updated_at: '2026-01-01T00:00:00Z',
142
+ body: { kind: 'settlement' }
143
+ })
144
+
145
+ expect(onPaymentMessage).toHaveBeenCalledWith({
146
+ messageId: 'live-1',
147
+ sender: senderKey,
148
+ recipient: myIdentityKey,
149
+ messageBox: 'remittance_inbox',
150
+ body: '{"kind":"settlement"}'
151
+ })
152
+ })
153
+ })