@bsv/sdk 1.2.20 → 1.2.22

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 (160) hide show
  1. package/dist/cjs/package.json +3 -3
  2. package/dist/cjs/src/auth/Peer.js +536 -0
  3. package/dist/cjs/src/auth/Peer.js.map +1 -0
  4. package/dist/cjs/src/auth/SessionManager.js +66 -0
  5. package/dist/cjs/src/auth/SessionManager.js.map +1 -0
  6. package/dist/cjs/src/auth/{Certificate.js → certificates/Certificate.js} +22 -26
  7. package/dist/cjs/src/auth/certificates/Certificate.js.map +1 -0
  8. package/dist/cjs/src/auth/certificates/MasterCertificate.js +79 -0
  9. package/dist/cjs/src/auth/certificates/MasterCertificate.js.map +1 -0
  10. package/dist/cjs/src/auth/certificates/VerifiableCertificate.js +49 -0
  11. package/dist/cjs/src/auth/certificates/VerifiableCertificate.js.map +1 -0
  12. package/dist/cjs/src/auth/certificates/index.js +25 -0
  13. package/dist/cjs/src/auth/certificates/index.js.map +1 -0
  14. package/dist/cjs/src/auth/clients/AuthFetch.js +411 -0
  15. package/dist/cjs/src/auth/clients/AuthFetch.js.map +1 -0
  16. package/dist/cjs/src/auth/clients/index.js +18 -0
  17. package/dist/cjs/src/auth/clients/index.js.map +1 -0
  18. package/dist/cjs/src/auth/index.js +20 -5
  19. package/dist/cjs/src/auth/index.js.map +1 -1
  20. package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js +259 -0
  21. package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js.map +1 -0
  22. package/dist/cjs/src/auth/transports/index.js +18 -0
  23. package/dist/cjs/src/auth/transports/index.js.map +1 -0
  24. package/dist/cjs/src/auth/types.js +3 -0
  25. package/dist/cjs/src/auth/types.js.map +1 -0
  26. package/dist/cjs/src/auth/utils/certificateHelpers.js +51 -0
  27. package/dist/cjs/src/auth/utils/certificateHelpers.js.map +1 -0
  28. package/dist/cjs/src/auth/utils/createNonce.js +19 -0
  29. package/dist/cjs/src/auth/utils/createNonce.js.map +1 -0
  30. package/dist/cjs/src/auth/utils/getVerifiableCertificates.js +31 -0
  31. package/dist/cjs/src/auth/utils/getVerifiableCertificates.js.map +1 -0
  32. package/dist/cjs/src/auth/utils/index.js +22 -0
  33. package/dist/cjs/src/auth/utils/index.js.map +1 -0
  34. package/dist/cjs/src/auth/utils/validateCertificates.js +42 -0
  35. package/dist/cjs/src/auth/utils/validateCertificates.js.map +1 -0
  36. package/dist/cjs/src/auth/utils/verifyNonce.js +27 -0
  37. package/dist/cjs/src/auth/utils/verifyNonce.js.map +1 -0
  38. package/dist/cjs/src/primitives/Point.js +1 -1
  39. package/dist/cjs/src/primitives/Point.js.map +1 -1
  40. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js +1 -1
  41. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  42. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js +148 -148
  43. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  44. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  45. package/dist/esm/src/auth/Peer.js +533 -0
  46. package/dist/esm/src/auth/Peer.js.map +1 -0
  47. package/dist/esm/src/auth/SessionManager.js +63 -0
  48. package/dist/esm/src/auth/SessionManager.js.map +1 -0
  49. package/dist/esm/src/auth/{Certificate.js → certificates/Certificate.js} +1 -2
  50. package/dist/esm/src/auth/certificates/Certificate.js.map +1 -0
  51. package/dist/esm/src/auth/certificates/MasterCertificate.js +73 -0
  52. package/dist/esm/src/auth/certificates/MasterCertificate.js.map +1 -0
  53. package/dist/esm/src/auth/certificates/VerifiableCertificate.js +44 -0
  54. package/dist/esm/src/auth/certificates/VerifiableCertificate.js.map +1 -0
  55. package/dist/esm/src/auth/certificates/index.js +4 -0
  56. package/dist/esm/src/auth/certificates/index.js.map +1 -0
  57. package/dist/esm/src/auth/clients/AuthFetch.js +409 -0
  58. package/dist/esm/src/auth/clients/AuthFetch.js.map +1 -0
  59. package/dist/esm/src/auth/clients/index.js +2 -0
  60. package/dist/esm/src/auth/clients/index.js.map +1 -0
  61. package/dist/esm/src/auth/index.js +7 -1
  62. package/dist/esm/src/auth/index.js.map +1 -1
  63. package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js +258 -0
  64. package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js.map +1 -0
  65. package/dist/esm/src/auth/transports/index.js +2 -0
  66. package/dist/esm/src/auth/transports/index.js.map +1 -0
  67. package/dist/esm/src/auth/types.js +2 -0
  68. package/dist/esm/src/auth/types.js.map +1 -0
  69. package/dist/esm/src/auth/utils/certificateHelpers.js +47 -0
  70. package/dist/esm/src/auth/utils/certificateHelpers.js.map +1 -0
  71. package/dist/esm/src/auth/utils/createNonce.js +16 -0
  72. package/dist/esm/src/auth/utils/createNonce.js.map +1 -0
  73. package/dist/esm/src/auth/utils/getVerifiableCertificates.js +27 -0
  74. package/dist/esm/src/auth/utils/getVerifiableCertificates.js.map +1 -0
  75. package/dist/esm/src/auth/utils/index.js +6 -0
  76. package/dist/esm/src/auth/utils/index.js.map +1 -0
  77. package/dist/esm/src/auth/utils/validateCertificates.js +38 -0
  78. package/dist/esm/src/auth/utils/validateCertificates.js.map +1 -0
  79. package/dist/esm/src/auth/utils/verifyNonce.js +24 -0
  80. package/dist/esm/src/auth/utils/verifyNonce.js.map +1 -0
  81. package/dist/esm/src/primitives/Point.js +1 -1
  82. package/dist/esm/src/primitives/Point.js.map +1 -1
  83. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js +1 -1
  84. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  85. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js +1 -1
  86. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  87. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  88. package/dist/types/src/auth/Peer.d.ts +193 -0
  89. package/dist/types/src/auth/Peer.d.ts.map +1 -0
  90. package/dist/types/src/auth/SessionManager.d.ts +42 -0
  91. package/dist/types/src/auth/SessionManager.d.ts.map +1 -0
  92. package/dist/types/src/auth/{Certificate.d.ts → certificates/Certificate.d.ts} +1 -1
  93. package/dist/types/src/auth/certificates/Certificate.d.ts.map +1 -0
  94. package/dist/types/src/auth/certificates/MasterCertificate.d.ts +38 -0
  95. package/dist/types/src/auth/certificates/MasterCertificate.d.ts.map +1 -0
  96. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts +26 -0
  97. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts.map +1 -0
  98. package/dist/types/src/auth/certificates/index.d.ts +4 -0
  99. package/dist/types/src/auth/certificates/index.d.ts.map +1 -0
  100. package/dist/types/src/auth/clients/AuthFetch.d.ts +87 -0
  101. package/dist/types/src/auth/clients/AuthFetch.d.ts.map +1 -0
  102. package/dist/types/src/auth/clients/index.d.ts +2 -0
  103. package/dist/types/src/auth/clients/index.d.ts.map +1 -0
  104. package/dist/types/src/auth/index.d.ts +7 -1
  105. package/dist/types/src/auth/index.d.ts.map +1 -1
  106. package/dist/types/src/auth/transports/SimplifiedFetchTransport.d.ts +51 -0
  107. package/dist/types/src/auth/transports/SimplifiedFetchTransport.d.ts.map +1 -0
  108. package/dist/types/src/auth/transports/index.d.ts +2 -0
  109. package/dist/types/src/auth/transports/index.d.ts.map +1 -0
  110. package/dist/types/src/auth/types.d.ts +31 -0
  111. package/dist/types/src/auth/types.d.ts.map +1 -0
  112. package/dist/types/src/auth/utils/certificateHelpers.d.ts +26 -0
  113. package/dist/types/src/auth/utils/certificateHelpers.d.ts.map +1 -0
  114. package/dist/types/src/auth/utils/createNonce.d.ts +8 -0
  115. package/dist/types/src/auth/utils/createNonce.d.ts.map +1 -0
  116. package/dist/types/src/auth/utils/getVerifiableCertificates.d.ts +13 -0
  117. package/dist/types/src/auth/utils/getVerifiableCertificates.d.ts.map +1 -0
  118. package/dist/types/src/auth/utils/index.d.ts +6 -0
  119. package/dist/types/src/auth/utils/index.d.ts.map +1 -0
  120. package/dist/types/src/auth/utils/validateCertificates.d.ts +12 -0
  121. package/dist/types/src/auth/utils/validateCertificates.d.ts.map +1 -0
  122. package/dist/types/src/auth/utils/verifyNonce.d.ts +9 -0
  123. package/dist/types/src/auth/utils/verifyNonce.d.ts.map +1 -0
  124. package/dist/types/src/primitives/Point.d.ts.map +1 -1
  125. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  126. package/dist/umd/bundle.js +1 -1
  127. package/docs/README.md +1 -0
  128. package/docs/auth.md +1193 -0
  129. package/package.json +13 -3
  130. package/src/auth/Peer.ts +600 -0
  131. package/src/auth/SessionManager.ts +71 -0
  132. package/src/auth/__tests/Peer.test.ts +599 -0
  133. package/src/auth/__tests/SessionManager.test.ts +87 -0
  134. package/src/auth/{Certificate.ts → certificates/Certificate.ts} +15 -8
  135. package/src/auth/certificates/MasterCertificate.ts +106 -0
  136. package/src/auth/certificates/VerifiableCertificate.ts +73 -0
  137. package/src/auth/certificates/__tests/Certificate.test.ts +282 -0
  138. package/src/auth/certificates/index.ts +3 -0
  139. package/src/auth/clients/AuthFetch.ts +482 -0
  140. package/src/auth/clients/index.ts +1 -0
  141. package/src/auth/index.ts +7 -1
  142. package/src/auth/transports/SimplifiedFetchTransport.ts +288 -0
  143. package/src/auth/transports/index.ts +1 -0
  144. package/src/auth/types.ts +41 -0
  145. package/src/auth/utils/__tests/cryptononce.test.ts +84 -0
  146. package/src/auth/utils/__tests/getVerifiableCertificates.test.ts +126 -0
  147. package/src/auth/utils/__tests/validateCertificates.test.ts +142 -0
  148. package/src/auth/utils/certificateHelpers.ts +86 -0
  149. package/src/auth/utils/createNonce.ts +16 -0
  150. package/src/auth/utils/getVerifiableCertificates.ts +40 -0
  151. package/src/auth/utils/index.ts +5 -0
  152. package/src/auth/utils/validateCertificates.ts +54 -0
  153. package/src/auth/utils/verifyNonce.ts +27 -0
  154. package/src/primitives/Point.ts +59 -59
  155. package/src/wallet/substrates/WalletWireProcessor.ts +1 -1
  156. package/src/wallet/substrates/WalletWireTransceiver.ts +1 -1
  157. package/dist/cjs/src/auth/Certificate.js.map +0 -1
  158. package/dist/esm/src/auth/Certificate.js.map +0 -1
  159. package/dist/types/src/auth/Certificate.d.ts.map +0 -1
  160. package/src/auth/__tests/Certificate.test.ts +0 -282
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.2.20",
3
+ "version": "1.2.22",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -164,6 +164,16 @@
164
164
  "require": "./dist/cjs/src/auth/*.js",
165
165
  "types": "./dist/types/src/auth/*.d.ts"
166
166
  },
167
+ "./auth/certificate": {
168
+ "import": "./dist/esm/src/auth/certificate/index.js",
169
+ "require": "./dist/cjs/src/auth/certificate/index.js",
170
+ "types": "./dist/types/src/auth/certificate/index.d.ts"
171
+ },
172
+ "./auth/certificate/*": {
173
+ "import": "./dist/esm/src/auth/certificate/*.js",
174
+ "require": "./dist/cjs/src/auth/certificate/*.js",
175
+ "types": "./dist/types/src/auth/certificate/*.d.ts"
176
+ },
167
177
  "./overlay-tools": {
168
178
  "import": "./dist/esm/src/overlay-tools/index.js",
169
179
  "require": "./dist/cjs/src/overlay-tools/index.js",
@@ -188,7 +198,7 @@
188
198
  "build:umd": "webpack --config webpack.config.js",
189
199
  "dev": "tsc -b -w",
190
200
  "prepublish": "npm run build",
191
- "doc": "ts2md --inputFilename=src/script/index.ts --outputFilename=docs/script.md --filenameSubString=script --firstHeadingLevel=1 && ts2md --inputFilename=src/primitives/index.ts --outputFilename=docs/primitives.md --filenameSubString=primitives --firstHeadingLevel=1 && ts2md --inputFilename=src/transaction/index.ts --outputFilename=docs/transaction.md --filenameSubString=transaction --firstHeadingLevel=1 && ts2md --inputFilename=src/messages/index.ts --outputFilename=docs/messages.md --filenameSubString=messages --firstHeadingLevel=1 && ts2md --inputFilename=src/compat/index.ts --outputFilename=docs/compat.md --filenameSubString=compat --firstHeadingLevel=1 && ts2md --inputFilename=src/wallet/index.ts --outputFilename=docs/wallet.md --filenameSubString=wallet --firstHeadingLevel=1 && ts2md --inputFilename=src/wallet/substrates/index.ts --outputFilename=docs/wallet-substrates.md --filenameSubString=wallet/substrates --firstHeadingLevel=1 && ts2md --inputFilename=src/totp/index.ts --outputFilename=docs/totp.md --filenameSubString=totp --firstHeadingLevel=1 && ts2md --inputFilename=src/overlay-tools/index.ts --outputFilename=docs/overlay-tools.md --filenameSubString=overlay-tools --firstHeadingLevel=1"
201
+ "doc": "ts2md"
192
202
  },
193
203
  "repository": {
194
204
  "type": "git",
@@ -214,7 +224,7 @@
214
224
  "ts-jest": "^29.1.1",
215
225
  "ts-loader": "^9.5.1",
216
226
  "ts-standard": "^12.0.2",
217
- "ts2md": "^0.2.4",
227
+ "ts2md": "^0.2.5",
218
228
  "tsconfig-to-dual-package": "^1.2.0",
219
229
  "typescript": "^5.2.2",
220
230
  "webpack": "^5.95.0",
@@ -0,0 +1,600 @@
1
+ import { SessionManager } from './SessionManager.js'
2
+ import { createNonce, verifyNonce, getVerifiableCertificates, validateCertificates } from './utils/index.js'
3
+ import { AuthMessage, PeerSession, RequestedCertificateSet, Transport } from './types.js'
4
+ import { VerifiableCertificate } from './certificates/VerifiableCertificate.js'
5
+ import { Random, Utils, Wallet } from '../../mod.js'
6
+
7
+ const AUTH_VERSION = '0.1'
8
+
9
+ /**
10
+ * Represents a peer capable of performing mutual authentication.
11
+ * Manages sessions, handles authentication handshakes, certificate requests and responses,
12
+ * and sending and receiving general messages over a transport layer.
13
+ */
14
+ export class Peer {
15
+ public sessionManager: SessionManager
16
+ private readonly transport: Transport
17
+ private readonly wallet: Wallet
18
+ certificatesToRequest: RequestedCertificateSet
19
+ private readonly onGeneralMessageReceivedCallbacks: Map<number, (senderPublicKey: string, payload: number[]) => void> = new Map()
20
+ private readonly onCertificatesReceivedCallbacks: Map<number, (senderPublicKey: string, certs: VerifiableCertificate[]) => void> = new Map()
21
+ private readonly onCertificateRequestReceivedCallbacks: Map<number, (senderPublicKey: string, requestedCertificates: RequestedCertificateSet) => void> = new Map()
22
+ private readonly onInitialResponseReceivedCallbacks: Map<number, ({ callback: (sessionNonce: string) => void, sessionNonce: string })> = new Map()
23
+
24
+ // Single shared counter for all callback types
25
+ private callbackIdCounter: number = 0
26
+
27
+ /**
28
+ * Creates a new Peer instance
29
+ *
30
+ * @param {Wallet} wallet - The wallet instance used for cryptographic operations.
31
+ * @param {Transport} transport - The transport mechanism used for sending and receiving messages.
32
+ * @param {RequestedCertificateSet} [certificatesToRequest] - Optional set of certificates to request from a peer during the initial handshake.
33
+ * @param {SessionManager} [sessionManager] - Optional SessionManager to be used for managing peer sessions.
34
+ */
35
+ constructor (
36
+ wallet: Wallet,
37
+ transport: Transport,
38
+ certificatesToRequest?: RequestedCertificateSet,
39
+ sessionManager?: SessionManager
40
+ ) {
41
+ this.wallet = wallet
42
+ this.transport = transport
43
+ this.certificatesToRequest = certificatesToRequest ?? { certifiers: [], types: {} }
44
+ this.transport.onData(this.handleIncomingMessage.bind(this))
45
+ this.sessionManager = sessionManager || new SessionManager()
46
+ }
47
+
48
+ /**
49
+ * Sends a general message to a peer, and initiates a handshake if necessary.
50
+ *
51
+ * @param {number[]} message - The message payload to send.
52
+ * @param {string} [identityKey] - The identity public key of the peer. If not provided, a handshake will be initiated.
53
+ * @returns {Promise<void>}
54
+ * @throws Will throw an error if the message fails to send.
55
+ */
56
+ async toPeer (message: number[], identityKey?: string, maxWaitTime?: number): Promise<void> {
57
+ const peerSession = await this.getAuthenticatedSession(identityKey, maxWaitTime)
58
+
59
+ // Prepare the general message
60
+ const requestNonce = Utils.toBase64(Random(32))
61
+ const { signature } = await this.wallet.createSignature({
62
+ data: message,
63
+ protocolID: [2, 'auth message signature'],
64
+ keyID: `${requestNonce} ${peerSession.peerNonce}`,
65
+ counterparty: peerSession.peerIdentityKey
66
+ })
67
+
68
+ const generalMessage: AuthMessage = {
69
+ version: AUTH_VERSION,
70
+ messageType: 'general',
71
+ identityKey: (await this.wallet.getPublicKey({ identityKey: true })).publicKey,
72
+ nonce: requestNonce,
73
+ yourNonce: peerSession.peerNonce,
74
+ payload: message,
75
+ signature
76
+ }
77
+
78
+ try {
79
+ await this.transport.send(generalMessage)
80
+ } catch (error) {
81
+ const e = new Error(`Failed to send message to peer ${peerSession.peerIdentityKey}: ${error.message}`)
82
+ e.stack = error.stack
83
+ throw e
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Sends a request for certificates to a peer.
89
+ * This method allows a peer to dynamically request specific certificates after
90
+ * an initial handshake or message has been exchanged.
91
+ *
92
+ * @param {RequestedCertificateSet} certificatesToRequest - Specifies the certifiers and types of certificates required from the peer.
93
+ * @param {string} [identityKey] - The identity public key of the peer. If not provided, the current session identity is used.
94
+ * @param {number} [maxWaitTime=10000] - Maximum time in milliseconds to wait for the peer session to be authenticated.
95
+ * @returns {Promise<void>} Resolves if the certificate request message is successfully sent.
96
+ * @throws Will throw an error if the peer session is not authenticated or if sending the request fails.
97
+ */
98
+ async requestCertificates (certificatesToRequest: RequestedCertificateSet, identityKey?: string, maxWaitTime = 10000): Promise<void> {
99
+ const peerSession = await this.getAuthenticatedSession(identityKey, maxWaitTime)
100
+
101
+ // Prepare the general message
102
+ const requestNonce = Utils.toBase64(Random(32))
103
+ const { signature } = await this.wallet.createSignature({
104
+ data: Utils.toArray(JSON.stringify(certificatesToRequest), 'utf8'),
105
+ protocolID: [2, 'auth message signature'],
106
+ keyID: `${requestNonce} ${peerSession.peerNonce}`,
107
+ counterparty: peerSession.peerIdentityKey
108
+ })
109
+
110
+ const certRequestMessage: AuthMessage = {
111
+ version: AUTH_VERSION,
112
+ messageType: 'certificateRequest',
113
+ identityKey: (await this.wallet.getPublicKey({ identityKey: true })).publicKey,
114
+ nonce: requestNonce,
115
+ initialNonce: peerSession.sessionNonce,
116
+ yourNonce: peerSession.peerNonce,
117
+ requestedCertificates: certificatesToRequest,
118
+ signature
119
+ }
120
+
121
+ try {
122
+ await this.transport.send(certRequestMessage)
123
+ } catch (error) {
124
+ throw new Error(`Failed to send certificate request message to peer ${peerSession.peerIdentityKey}: ${error.message}`)
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Retrieves an authenticated session for a given peer identity. If no session exists
130
+ * or the session is not authenticated, initiates a handshake to create or authenticate the session.
131
+ *
132
+ * @param {string} [identityKey] - The identity public key of the peer. If provided, it attempts
133
+ * to retrieve an existing session associated with this identity.
134
+ * @param {number} [maxWaitTime] - The maximum time in milliseconds to wait for the handshake
135
+ * to complete if a new session is required. Defaults to a pre-defined timeout if not specified.
136
+ * @returns {Promise<PeerSession>} - A promise that resolves with an authenticated `PeerSession`.
137
+ * @throws {Error} - Throws an error if the transport is not connected or if the handshake fails.
138
+ */
139
+ async getAuthenticatedSession (identityKey?: string, maxWaitTime?: number): Promise<PeerSession> {
140
+ if (!this.transport) {
141
+ throw new Error('Peer transport is not connected!')
142
+ }
143
+
144
+ let peerSession = identityKey ? this.sessionManager.getSession(identityKey) : undefined
145
+ if (!peerSession || !peerSession.isAuthenticated) {
146
+ const sessionNonce = await this.initiateHandshake(identityKey, maxWaitTime)
147
+ peerSession = this.sessionManager.getSession(identityKey || sessionNonce)
148
+ if (!peerSession.isAuthenticated) {
149
+ throw new Error('Unable to establish mutual authentication with peer!')
150
+ }
151
+ }
152
+
153
+ return peerSession
154
+ }
155
+
156
+ /**
157
+ * Registers a callback to listen for general messages from peers.
158
+ *
159
+ * @param {(senderPublicKey: string, payload: number[]) => void} callback - The function to call when a general message is received.
160
+ * @returns {number} The ID of the callback listener.
161
+ */
162
+ listenForGeneralMessages (callback: (senderPublicKey: string, payload: number[]) => void): number {
163
+ const callbackID = this.callbackIdCounter++
164
+ this.onGeneralMessageReceivedCallbacks.set(callbackID, callback)
165
+ return callbackID
166
+ }
167
+
168
+ /**
169
+ * Removes a general message listener.
170
+ *
171
+ * @param {number} callbackID - The ID of the callback to remove.
172
+ */
173
+ stopListeningForGeneralMessages (callbackID: number): void {
174
+ this.onGeneralMessageReceivedCallbacks.delete(callbackID)
175
+ }
176
+
177
+ /**
178
+ * Registers a callback to listen for certificates received from peers.
179
+ *
180
+ * @param {(certs: VerifiableCertificate[]) => void} callback - The function to call when certificates are received.
181
+ * @returns {number} The ID of the callback listener.
182
+ */
183
+ listenForCertificatesReceived (callback: (senderPublicKey: string, certs: VerifiableCertificate[]) => void): number {
184
+ const callbackID = this.callbackIdCounter++
185
+ this.onCertificatesReceivedCallbacks.set(callbackID, callback)
186
+ return callbackID
187
+ }
188
+
189
+ /**
190
+ * Cancels and unsubscribes a certificatesReceived listener.
191
+ *
192
+ * @param {number} callbackID - The ID of the certificates received callback to cancel.
193
+ */
194
+ stopListeningForCertificatesReceived (callbackID: number): void {
195
+ this.onCertificatesReceivedCallbacks.delete(callbackID)
196
+ }
197
+
198
+ /**
199
+ * Registers a callback to listen for certificates requested from peers.
200
+ *
201
+ * @param {(requestedCertificates: RequestedCertificateSet) => void} callback - The function to call when a certificate request is received
202
+ * @returns {number} The ID of the callback listener.
203
+ */
204
+ listenForCertificatesRequested (callback: (senderPublicKey: string, requestedCertificates: RequestedCertificateSet) => void): number {
205
+ const callbackID = this.callbackIdCounter++
206
+ this.onCertificateRequestReceivedCallbacks.set(callbackID, callback)
207
+ return callbackID
208
+ }
209
+
210
+ /**
211
+ * Cancels and unsubscribes a certificatesRequested listener.
212
+ *
213
+ * @param {number} callbackID - The ID of the requested certificates callback to cancel.
214
+ */
215
+ stopListeningForCertificatesRequested (callbackID: number): void {
216
+ this.onCertificateRequestReceivedCallbacks.delete(callbackID)
217
+ }
218
+
219
+ /**
220
+ * Initiates the mutual authentication handshake with a peer.
221
+ *
222
+ * @private
223
+ * @param {string} [identityKey] - The identity public key of the peer.
224
+ * @returns {Promise<string>} A promise that resolves to the session nonce.
225
+ */
226
+ private async initiateHandshake (identityKey?: string, maxWaitTime = 10000): Promise<string> {
227
+ const sessionNonce = await createNonce(this.wallet) // Initial request nonce
228
+ this.sessionManager.addSession({
229
+ isAuthenticated: false,
230
+ sessionNonce,
231
+ peerIdentityKey: identityKey
232
+ })
233
+
234
+ const initialRequest: AuthMessage = {
235
+ version: AUTH_VERSION,
236
+ messageType: 'initialRequest',
237
+ identityKey: (await this.wallet.getPublicKey({ identityKey: true })).publicKey,
238
+ initialNonce: sessionNonce,
239
+ requestedCertificates: this.certificatesToRequest
240
+ }
241
+
242
+ await this.transport.send(initialRequest)
243
+ return await this.waitForInitialResponse(sessionNonce, maxWaitTime)
244
+ }
245
+
246
+ /**
247
+ * Waits for the initial response from the peer after sending an initial handshake request message.
248
+ *
249
+ * @param {string} sessionNonce - The session nonce created in the initial request.
250
+ * @returns {Promise<string>} A promise that resolves with the session nonce when the initial response is received.
251
+ */
252
+ private async waitForInitialResponse (sessionNonce: string, maxWaitTime = 10000): Promise<string> {
253
+ return await new Promise((resolve, reject) => {
254
+ const callbackID = this.listenForInitialResponse(sessionNonce, (sessionNonce) => {
255
+ clearTimeout(timeoutHandle)
256
+ this.stopListeningForInitialResponses(callbackID)
257
+ resolve(sessionNonce)
258
+ })
259
+
260
+ const timeoutHandle = setTimeout(() => {
261
+ this.stopListeningForInitialResponses(callbackID)
262
+ reject(new Error('Initial response timed out.'))
263
+ }, maxWaitTime)
264
+ })
265
+ }
266
+
267
+ /**
268
+ * Adds a listener for an initial response message matching a specific initial nonce.
269
+ *
270
+ * @private
271
+ * @param {string} sessionNonce - The session nonce to match.
272
+ * @param {(sessionNonce: string) => void} callback - The callback to invoke when the initial response is received.
273
+ * @returns {number} The ID of the callback listener.
274
+ */
275
+ private listenForInitialResponse (sessionNonce: string, callback: (sessionNonce: string) => void) {
276
+ const callbackID = this.callbackIdCounter++
277
+ this.onInitialResponseReceivedCallbacks.set(callbackID, { callback, sessionNonce })
278
+ return callbackID
279
+ }
280
+
281
+ /**
282
+ * Removes a listener for initial responses.
283
+ *
284
+ * @private
285
+ * @param {number} callbackID - The ID of the callback to remove.
286
+ */
287
+ private stopListeningForInitialResponses (callbackID: number) {
288
+ this.onInitialResponseReceivedCallbacks.delete(callbackID)
289
+ }
290
+
291
+ /**
292
+ * Handles incoming messages from the transport.
293
+ *
294
+ * @param {AuthMessage} message - The incoming message to process.
295
+ * @returns {Promise<void>}
296
+ */
297
+ private async handleIncomingMessage (message: AuthMessage): Promise<void> {
298
+ if (!message.version || message.version !== AUTH_VERSION) {
299
+ console.error(`Invalid message auth version! Received: ${message.version}, expected: ${AUTH_VERSION}`)
300
+ return
301
+ }
302
+
303
+ switch (message.messageType) {
304
+ case 'initialRequest':
305
+ await this.processInitialRequest(message)
306
+ break
307
+ case 'initialResponse':
308
+ await this.processInitialResponse(message)
309
+ break
310
+ case 'certificateRequest':
311
+ await this.processCertificateRequest(message)
312
+ break
313
+ case 'certificateResponse':
314
+ await this.processCertificateResponse(message)
315
+ break
316
+ case 'general':
317
+ await this.processGeneralMessage(message)
318
+ break
319
+ default:
320
+ console.error(`Unknown message type of ${message.messageType} from ${message.identityKey}`)
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Processes an initial request message from a peer.
326
+ *
327
+ * @param {AuthMessage} message - The incoming initial request message.
328
+ * @returns {Promise<void>}
329
+ */
330
+ async processInitialRequest (message: AuthMessage) {
331
+ if (!message.identityKey || !message.initialNonce) {
332
+ throw new Error('Missing required fields in initialResponse message.')
333
+ }
334
+
335
+ // Create an initial session nonce
336
+ const sessionNonce = await createNonce(this.wallet)
337
+ this.sessionManager.addSession({
338
+ isAuthenticated: true,
339
+ sessionNonce,
340
+ peerNonce: message.initialNonce,
341
+ peerIdentityKey: message.identityKey
342
+ })
343
+
344
+ // Handle initial certificate requests
345
+ let certificatesToInclude
346
+ if (message.requestedCertificates?.certifiers?.length > 0) {
347
+ if (this.onCertificateRequestReceivedCallbacks.size > 0) {
348
+ // The application wants to handle certificate requests
349
+ this.onCertificateRequestReceivedCallbacks.forEach(callback => {
350
+ callback(message.identityKey, message.requestedCertificates)
351
+ })
352
+ } else {
353
+ // Attempt to find exact matching certificates to return automatically to save round trips
354
+ certificatesToInclude = await getVerifiableCertificates(this.wallet, message.requestedCertificates, message.identityKey)
355
+ }
356
+ }
357
+
358
+ // Create the signature for the message
359
+ const { signature } = await this.wallet.createSignature({
360
+ data: Utils.toArray(message.initialNonce + sessionNonce, 'base64'),
361
+ protocolID: [2, 'auth message signature'],
362
+ keyID: `${message.initialNonce} ${sessionNonce}`,
363
+ counterparty: message.identityKey
364
+ })
365
+
366
+ const initialResponseMessage: AuthMessage = {
367
+ version: AUTH_VERSION,
368
+ messageType: 'initialResponse',
369
+ identityKey: (await this.wallet.getPublicKey({ identityKey: true })).publicKey,
370
+ initialNonce: sessionNonce,
371
+ yourNonce: message.initialNonce,
372
+ certificates: certificatesToInclude,
373
+ requestedCertificates: this.certificatesToRequest,
374
+ signature
375
+ }
376
+
377
+ await this.transport.send(initialResponseMessage)
378
+ }
379
+
380
+ /**
381
+ * Processes an initial response message from a peer.
382
+ *
383
+ * @private
384
+ * @param {AuthMessage} message - The incoming initial response message.
385
+ * @returns {Promise<void>}
386
+ * @throws Will throw an error if nonce verification or signature verification fails.
387
+ */
388
+ private async processInitialResponse (message: AuthMessage) {
389
+ const validNonce = await verifyNonce(message.yourNonce, this.wallet)
390
+ if (!validNonce) {
391
+ throw new Error(`Initial response nonce verification failed from peer: ${message.identityKey}`)
392
+ }
393
+
394
+ const peerSession = this.sessionManager.getSession(message.yourNonce)
395
+ if (!peerSession) {
396
+ throw new Error(`Peer session not found for peer: ${message.identityKey}`)
397
+ }
398
+
399
+ // Validate message signature
400
+ const { valid } = await this.wallet.verifySignature({
401
+ data: Utils.toArray(peerSession.sessionNonce + message.initialNonce, 'base64'),
402
+ signature: message.signature,
403
+ protocolID: [2, 'auth message signature'],
404
+ keyID: `${peerSession.sessionNonce} ${message.initialNonce}`,
405
+ counterparty: message.identityKey
406
+ })
407
+ if (!valid) {
408
+ throw new Error(`Unable to verify initial response signature for peer: ${message.identityKey}`)
409
+ }
410
+
411
+ // After signature and nonce verification is complete, the peer is considered authenticated
412
+ // Save the peer's identity key and initial nonce
413
+ // This allows future requests to be linked to the same session
414
+ peerSession.peerNonce = message.initialNonce
415
+ peerSession.peerIdentityKey = message.identityKey
416
+ peerSession.isAuthenticated = true
417
+ // Run update to ensure lookup is available by both peerIdentityKey and sessionNonce
418
+ this.sessionManager.updateSession(peerSession)
419
+
420
+ // Process certificates received
421
+ if (this.certificatesToRequest?.certifiers?.length && message.certificates?.length) {
422
+ await validateCertificates(this.wallet, message, this.certificatesToRequest)
423
+
424
+ this.onCertificatesReceivedCallbacks.forEach(callback =>
425
+ callback(message.identityKey, message.certificates)
426
+ )
427
+ }
428
+
429
+ this.onInitialResponseReceivedCallbacks.forEach((entry) => {
430
+ if (entry && entry.sessionNonce === peerSession.sessionNonce) {
431
+ entry.callback(peerSession.sessionNonce)
432
+ }
433
+ })
434
+
435
+ // Check if the peer requested certificates from us
436
+ if (message.requestedCertificates?.certifiers?.length > 0) {
437
+ if (this.onCertificateRequestReceivedCallbacks.size > 0) {
438
+ // Application wants to handle certificate requests
439
+ this.onCertificateRequestReceivedCallbacks.forEach(callback => {
440
+ callback(message.identityKey, message.requestedCertificates)
441
+ })
442
+ } else {
443
+ // Attempt to find exact matching certificates to respond automatically and save round trips
444
+ const verifiableCertificates = await getVerifiableCertificates(this.wallet, message.requestedCertificates, message.identityKey)
445
+ await this.sendCertificateResponse(message.identityKey, verifiableCertificates)
446
+ }
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Processes an incoming certificate request message from a peer.
452
+ * Verifies the nonce and signature to ensure the authenticity of the request,
453
+ * then initiates a response with any requested certificates that are available.
454
+ *
455
+ * @param {AuthMessage} message - The certificate request message received from the peer.
456
+ * @throws {Error} Throws an error if nonce verification fails, or the message signature is invalid.
457
+ */
458
+ private async processCertificateRequest (message: AuthMessage) {
459
+ const validNonce = await verifyNonce(message.yourNonce, this.wallet)
460
+ if (!validNonce) {
461
+ throw new Error(`Unable to verify nonce for certificate request message from: ${message.identityKey}`)
462
+ }
463
+ const peerSession = this.sessionManager.getSession(message.yourNonce)
464
+
465
+ const { valid } = await this.wallet.verifySignature({
466
+ data: Utils.toArray(JSON.stringify(message.requestedCertificates), 'utf8'),
467
+ signature: message.signature,
468
+ protocolID: [2, 'auth message signature'],
469
+ keyID: `${message.nonce} ${peerSession.sessionNonce}`,
470
+ counterparty: peerSession.peerIdentityKey
471
+ })
472
+
473
+ if (!valid) {
474
+ throw new Error(`Invalid signature in certificate request message from ${peerSession.peerIdentityKey}`)
475
+ }
476
+
477
+ if (message.requestedCertificates?.certifiers?.length > 0) {
478
+ if (this.onCertificateRequestReceivedCallbacks.size > 0) {
479
+ // Application wants to handle certificate requests
480
+ this.onCertificateRequestReceivedCallbacks.forEach(callback => {
481
+ callback(message.identityKey, message.requestedCertificates)
482
+ })
483
+ } else {
484
+ // Attempt to find exact matching certificates to respond automatically and save round trips
485
+ const verifiableCertificates = await getVerifiableCertificates(this.wallet, message.requestedCertificates, message.identityKey)
486
+ await this.sendCertificateResponse(message.identityKey, verifiableCertificates)
487
+ }
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Sends a certificate response message containing the specified certificates to a peer.
493
+ *
494
+ * @param {string} verifierIdentityKey - The identity key of the peer requesting the certificates.
495
+ * @param {VerifiableCertificate[]} certificates - The list of certificates to be included in the response.
496
+ * @returns {Promise<void>} - A promise that resolves once the certificate response has been sent successfully.
497
+ *
498
+ * @throws {Error} Throws an error if the peer session could not be authenticated or if message signing fails.
499
+ */
500
+ async sendCertificateResponse (
501
+ verifierIdentityKey: string,
502
+ certificates: VerifiableCertificate[]
503
+ ) {
504
+ const peerSession = await this.getAuthenticatedSession(verifierIdentityKey)
505
+ const requestNonce = Utils.toBase64(Random(32))
506
+ const { signature } = await this.wallet.createSignature({
507
+ data: Utils.toArray(JSON.stringify(certificates), 'utf8'),
508
+ protocolID: [2, 'auth message signature'],
509
+ keyID: `${requestNonce} ${peerSession.peerNonce}`,
510
+ counterparty: peerSession.peerIdentityKey
511
+ })
512
+
513
+ const certificateResponse: AuthMessage = {
514
+ version: AUTH_VERSION,
515
+ messageType: 'certificateResponse',
516
+ identityKey: (await this.wallet.getPublicKey({ identityKey: true })).publicKey,
517
+ nonce: requestNonce,
518
+ initialNonce: peerSession.sessionNonce,
519
+ yourNonce: peerSession.peerNonce,
520
+ certificates,
521
+ signature
522
+ }
523
+
524
+ try {
525
+ await this.transport.send(certificateResponse)
526
+ } catch (error) {
527
+ throw new Error(`Failed to send certificate response message to peer ${peerSession.peerIdentityKey}: ${error.message}`)
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Processes a certificate response message from a peer.
533
+ *
534
+ * @private
535
+ * @param {AuthMessage} message - The incoming certificate response message.
536
+ * @returns {Promise<void>}
537
+ * @throws Will throw an error if nonce verification or signature verification fails.
538
+ */
539
+ private async processCertificateResponse (
540
+ message: AuthMessage
541
+ ) {
542
+ const validNonce = await verifyNonce(message.yourNonce, this.wallet)
543
+ if (!validNonce) {
544
+ throw new Error(`Unable to verify nonce for certificate response from: ${message.identityKey}!`)
545
+ }
546
+ const peerSession = this.sessionManager.getSession(message.yourNonce)
547
+
548
+ // Validate message signature
549
+ const { valid } = await this.wallet.verifySignature({
550
+ data: Utils.toArray(JSON.stringify(message.certificates), 'utf8'),
551
+ signature: message.signature,
552
+ protocolID: [2, 'auth message signature'],
553
+ keyID: `${message.nonce} ${peerSession.sessionNonce}`,
554
+ counterparty: message.identityKey
555
+ })
556
+
557
+ if (!valid) {
558
+ throw new Error(`Unable to verify certificate response signature for peer: ${message.identityKey}`)
559
+ }
560
+
561
+ // Process and verify any certificates received
562
+ await validateCertificates(this.wallet, message, message.requestedCertificates)
563
+
564
+ this.onCertificatesReceivedCallbacks.forEach(callback => {
565
+ callback(message.identityKey, message.certificates)
566
+ })
567
+ }
568
+
569
+ /**
570
+ * Processes a general message from a peer.
571
+ *
572
+ * @private
573
+ * @param {AuthMessage} message - The incoming general message.
574
+ * @returns {Promise<void>}
575
+ * @throws Will throw an error if nonce verification or signature verification fails.
576
+ */
577
+ private async processGeneralMessage (message: AuthMessage) {
578
+ const validNonce = await verifyNonce(message.yourNonce, this.wallet)
579
+ if (!validNonce) {
580
+ throw new Error(`Unable to verify nonce for general message from: ${message.identityKey}`)
581
+ }
582
+ const peerSession = this.sessionManager.getSession(message.yourNonce)
583
+
584
+ const { valid } = await this.wallet.verifySignature({
585
+ data: message.payload,
586
+ signature: message.signature,
587
+ protocolID: [2, 'auth message signature'],
588
+ keyID: `${message.nonce} ${peerSession.sessionNonce}`,
589
+ counterparty: peerSession.peerIdentityKey
590
+ })
591
+
592
+ if (!valid) {
593
+ throw new Error(`Invalid signature in generalMessage from ${peerSession.peerIdentityKey}`)
594
+ }
595
+
596
+ this.onGeneralMessageReceivedCallbacks.forEach(callback => {
597
+ callback(message.identityKey, message.payload)
598
+ })
599
+ }
600
+ }