@bsv/sdk 1.3.28 → 1.3.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/auth/Peer.js +142 -91
- package/dist/cjs/src/auth/Peer.js.map +1 -1
- package/dist/cjs/src/auth/SessionManager.js +82 -21
- package/dist/cjs/src/auth/SessionManager.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/Peer.js +138 -88
- package/dist/esm/src/auth/Peer.js.map +1 -1
- package/dist/esm/src/auth/SessionManager.js +88 -22
- package/dist/esm/src/auth/SessionManager.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/Peer.d.ts +21 -23
- package/dist/types/src/auth/Peer.d.ts.map +1 -1
- package/dist/types/src/auth/SessionManager.d.ts +25 -7
- package/dist/types/src/auth/SessionManager.d.ts.map +1 -1
- package/dist/types/src/auth/types.d.ts +1 -0
- package/dist/types/src/auth/types.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/docs/auth.md +33 -38
- package/package.json +1 -1
- package/src/auth/Peer.ts +186 -130
- package/src/auth/SessionManager.ts +89 -22
- package/src/auth/__tests/Peer.test.ts +66 -0
- package/src/auth/__tests/SessionManager.test.ts +3 -2
- package/src/auth/types.ts +1 -0
|
@@ -1,55 +1,112 @@
|
|
|
1
1
|
import { PeerSession } from './types.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Manages sessions for peers, allowing
|
|
5
|
-
*
|
|
4
|
+
* Manages sessions for peers, allowing multiple concurrent sessions
|
|
5
|
+
* per identity key. Primary lookup is always by `sessionNonce`.
|
|
6
6
|
*/
|
|
7
7
|
export class SessionManager {
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Maps sessionNonce -> PeerSession
|
|
10
|
+
*/
|
|
11
|
+
private readonly sessionNonceToSession: Map<string, PeerSession>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maps identityKey -> Set of sessionNonces
|
|
15
|
+
*/
|
|
16
|
+
private readonly identityKeyToNonces: Map<string, Set<string>>
|
|
9
17
|
|
|
10
18
|
constructor () {
|
|
11
|
-
this.
|
|
19
|
+
this.sessionNonceToSession = new Map<string, PeerSession>()
|
|
20
|
+
this.identityKeyToNonces = new Map<string, Set<string>>()
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
/**
|
|
15
|
-
* Adds a session to the manager, associating it with
|
|
24
|
+
* Adds a session to the manager, associating it with its sessionNonce,
|
|
25
|
+
* and also with its peerIdentityKey (if any).
|
|
26
|
+
*
|
|
27
|
+
* This does NOT overwrite existing sessions for the same peerIdentityKey,
|
|
28
|
+
* allowing multiple concurrent sessions for the same peer.
|
|
16
29
|
*
|
|
17
30
|
* @param {PeerSession} session - The peer session to add.
|
|
18
31
|
*/
|
|
19
32
|
addSession (session: PeerSession): void {
|
|
20
|
-
if (
|
|
21
|
-
(session.peerIdentityKey === null || session.peerIdentityKey === undefined || session.peerIdentityKey === '')) {
|
|
33
|
+
if (typeof session.sessionNonce !== 'string') {
|
|
22
34
|
throw new Error(
|
|
23
|
-
'Invalid session:
|
|
35
|
+
'Invalid session: sessionNonce is required to add a session.'
|
|
24
36
|
)
|
|
25
37
|
}
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
// Use the sessionNonce as the primary key
|
|
40
|
+
this.sessionNonceToSession.set(session.sessionNonce, session)
|
|
41
|
+
|
|
42
|
+
// Also track it by identity key if present
|
|
43
|
+
if (typeof session.peerIdentityKey === 'string') {
|
|
44
|
+
let nonces = this.identityKeyToNonces.get(session.peerIdentityKey)
|
|
45
|
+
if (nonces == null) {
|
|
46
|
+
nonces = new Set<string>()
|
|
47
|
+
this.identityKeyToNonces.set(session.peerIdentityKey, nonces)
|
|
48
|
+
}
|
|
49
|
+
nonces.add(session.sessionNonce)
|
|
32
50
|
}
|
|
33
51
|
}
|
|
34
52
|
|
|
35
53
|
/**
|
|
36
|
-
* Updates a session in the manager
|
|
54
|
+
* Updates a session in the manager (primarily by re-adding it),
|
|
55
|
+
* ensuring we record the latest data (e.g., isAuthenticated, lastUpdate, etc.).
|
|
37
56
|
*
|
|
38
57
|
* @param {PeerSession} session - The peer session to update.
|
|
39
58
|
*/
|
|
40
59
|
updateSession (session: PeerSession): void {
|
|
60
|
+
// Remove the old references (if any) and re-add
|
|
41
61
|
this.removeSession(session)
|
|
42
62
|
this.addSession(session)
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
/**
|
|
46
|
-
* Retrieves a session based on a given identifier
|
|
66
|
+
* Retrieves a session based on a given identifier, which can be:
|
|
67
|
+
* - A sessionNonce, or
|
|
68
|
+
* - A peerIdentityKey.
|
|
69
|
+
*
|
|
70
|
+
* If it is a `sessionNonce`, returns that exact session.
|
|
71
|
+
* If it is a `peerIdentityKey`, returns the "best" (e.g. most recently updated,
|
|
72
|
+
* authenticated) session associated with that peer, if any.
|
|
47
73
|
*
|
|
48
74
|
* @param {string} identifier - The identifier for the session (sessionNonce or peerIdentityKey).
|
|
49
75
|
* @returns {PeerSession | undefined} - The matching peer session, or undefined if not found.
|
|
50
76
|
*/
|
|
51
77
|
getSession (identifier: string): PeerSession | undefined {
|
|
52
|
-
|
|
78
|
+
// Check if this identifier is directly a sessionNonce
|
|
79
|
+
const direct = this.sessionNonceToSession.get(identifier)
|
|
80
|
+
if (direct != null) {
|
|
81
|
+
return direct
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Otherwise, interpret the identifier as an identity key
|
|
85
|
+
const nonces = this.identityKeyToNonces.get(identifier)
|
|
86
|
+
if ((nonces == null) || nonces.size === 0) {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Pick the "best" session. One sensible approach:
|
|
91
|
+
// - Choose an authenticated session if available
|
|
92
|
+
// - Among them, pick the most recently updated
|
|
93
|
+
let best: PeerSession | undefined
|
|
94
|
+
for (const nonce of nonces) {
|
|
95
|
+
const s = this.sessionNonceToSession.get(nonce)
|
|
96
|
+
if (s == null) continue
|
|
97
|
+
// We can prefer authenticated sessions
|
|
98
|
+
if (best == null) {
|
|
99
|
+
best = s
|
|
100
|
+
} else {
|
|
101
|
+
// If we want the "most recently updated" AND isAuthenticated
|
|
102
|
+
if ((s.lastUpdate ?? 0) > (best.lastUpdate ?? 0)) {
|
|
103
|
+
best = s
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Optionally, you could also filter out isAuthenticated===false if you only want
|
|
108
|
+
// an authenticated session. But for our usage, let's return the latest any session.
|
|
109
|
+
return best
|
|
53
110
|
}
|
|
54
111
|
|
|
55
112
|
/**
|
|
@@ -58,21 +115,31 @@ export class SessionManager {
|
|
|
58
115
|
* @param {PeerSession} session - The peer session to remove.
|
|
59
116
|
*/
|
|
60
117
|
removeSession (session: PeerSession): void {
|
|
61
|
-
if (
|
|
62
|
-
this.
|
|
118
|
+
if (typeof session.sessionNonce === 'string') {
|
|
119
|
+
this.sessionNonceToSession.delete(session.sessionNonce)
|
|
63
120
|
}
|
|
64
|
-
if (
|
|
65
|
-
this.
|
|
121
|
+
if (typeof session.peerIdentityKey === 'string') {
|
|
122
|
+
const nonces = this.identityKeyToNonces.get(session.peerIdentityKey)
|
|
123
|
+
if (nonces != null) {
|
|
124
|
+
nonces.delete(session.sessionNonce ?? '')
|
|
125
|
+
if (nonces.size === 0) {
|
|
126
|
+
this.identityKeyToNonces.delete(session.peerIdentityKey)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
66
129
|
}
|
|
67
130
|
}
|
|
68
131
|
|
|
69
132
|
/**
|
|
70
|
-
* Checks if a session exists
|
|
133
|
+
* Checks if a session exists for a given identifier (either sessionNonce or identityKey).
|
|
71
134
|
*
|
|
72
135
|
* @param {string} identifier - The identifier to check.
|
|
73
136
|
* @returns {boolean} - True if the session exists, false otherwise.
|
|
74
137
|
*/
|
|
75
138
|
hasSession (identifier: string): boolean {
|
|
76
|
-
|
|
139
|
+
const direct = this.sessionNonceToSession.has(identifier)
|
|
140
|
+
if (direct) return true
|
|
141
|
+
// if not directly a nonce, interpret as identityKey
|
|
142
|
+
const nonces = this.identityKeyToNonces.get(identifier)
|
|
143
|
+
return !(nonces == null) && nonces.size > 0
|
|
77
144
|
}
|
|
78
145
|
}
|
|
@@ -232,6 +232,72 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
232
232
|
expect(certificatesReceivedByBob).toEqual([])
|
|
233
233
|
}, 15000)
|
|
234
234
|
|
|
235
|
+
it('Alice talks to Bob across two devices, Bob can respond across both sessions', async () => {
|
|
236
|
+
const transportA1 = new LocalTransport()
|
|
237
|
+
const transportA2 = new LocalTransport()
|
|
238
|
+
const transportB = new LocalTransport()
|
|
239
|
+
transportA1.connect(transportB)
|
|
240
|
+
const aliceKey = PrivateKey.fromRandom()
|
|
241
|
+
const walletA1 = new CompletedProtoWallet(aliceKey)
|
|
242
|
+
const walletA2 = new CompletedProtoWallet(aliceKey)
|
|
243
|
+
const walletB = new CompletedProtoWallet(PrivateKey.fromRandom())
|
|
244
|
+
const aliceFirstDevice = new Peer(
|
|
245
|
+
walletA1,
|
|
246
|
+
transportA1
|
|
247
|
+
)
|
|
248
|
+
const aliceOtherDevice = new Peer(
|
|
249
|
+
walletA2,
|
|
250
|
+
transportA2
|
|
251
|
+
)
|
|
252
|
+
const bob = new Peer(
|
|
253
|
+
walletB,
|
|
254
|
+
transportB
|
|
255
|
+
)
|
|
256
|
+
const alice1MessageHandler = jest.fn()
|
|
257
|
+
const alice2MessageHandler = jest.fn()
|
|
258
|
+
const bobMessageHandler = jest.fn()
|
|
259
|
+
|
|
260
|
+
const bobReceivedGeneralMessage = new Promise<void>((resolve) => {
|
|
261
|
+
bob.listenForGeneralMessages((senderPublicKey, payload) => {
|
|
262
|
+
(async () => {
|
|
263
|
+
console.log('Bob 1 received message:', Utils.toUTF8(payload))
|
|
264
|
+
await bob.toPeer(Utils.toArray('Hello Alice!'), senderPublicKey)
|
|
265
|
+
resolve()
|
|
266
|
+
bobMessageHandler(senderPublicKey, payload)
|
|
267
|
+
})().catch(e => console.log(e))
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
let aliceReceivedGeneralMessageOnFirstDevice = new Promise<void>((resolve) => {
|
|
271
|
+
aliceFirstDevice.listenForGeneralMessages((senderPublicKey, payload) => {
|
|
272
|
+
(async () => {
|
|
273
|
+
console.log('Alice 1 received message:', Utils.toUTF8(payload))
|
|
274
|
+
resolve()
|
|
275
|
+
alice1MessageHandler(senderPublicKey, payload)
|
|
276
|
+
})().catch(e => console.log(e))
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
const aliceReceivedGeneralMessageOnOtherDevice = new Promise<void>((resolve) => {
|
|
280
|
+
aliceOtherDevice.listenForGeneralMessages((senderPublicKey, payload) => {
|
|
281
|
+
(async () => {
|
|
282
|
+
console.log('Alice 2 received message:', Utils.toUTF8(payload))
|
|
283
|
+
resolve()
|
|
284
|
+
alice2MessageHandler(senderPublicKey, payload)
|
|
285
|
+
})().catch(e => console.log(e))
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
await aliceFirstDevice.toPeer(Utils.toArray('Hello Bob!'))
|
|
290
|
+
await bobReceivedGeneralMessage
|
|
291
|
+
await aliceReceivedGeneralMessageOnFirstDevice
|
|
292
|
+
transportA2.connect(transportB)
|
|
293
|
+
await aliceOtherDevice.toPeer(Utils.toArray('Hello Bob from my other device!'))
|
|
294
|
+
await aliceReceivedGeneralMessageOnOtherDevice
|
|
295
|
+
transportA1.connect(transportB)
|
|
296
|
+
await aliceFirstDevice.toPeer(Utils.toArray('Back on my first device now, Bob! Can you still hear me?'))
|
|
297
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
298
|
+
expect(alice1MessageHandler.mock.calls.length).toEqual(2)
|
|
299
|
+
}, 30000)
|
|
300
|
+
|
|
235
301
|
it('Bob requests certificates from Alice, Alice does not request any from Bob', async () => {
|
|
236
302
|
const alicePubKey = (await walletA.getPublicKey({ identityKey: true }))
|
|
237
303
|
.publicKey
|
|
@@ -10,7 +10,8 @@ describe('SessionManager', () => {
|
|
|
10
10
|
validSession = {
|
|
11
11
|
isAuthenticated: false,
|
|
12
12
|
sessionNonce: 'testSessionNonce',
|
|
13
|
-
peerIdentityKey: 'testPeerIdentityKey'
|
|
13
|
+
peerIdentityKey: 'testPeerIdentityKey',
|
|
14
|
+
lastUpdate: 1
|
|
14
15
|
}
|
|
15
16
|
})
|
|
16
17
|
|
|
@@ -39,7 +40,7 @@ describe('SessionManager', () => {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
expect(() => sessionManager.addSession(invalidSession)).toThrow(
|
|
42
|
-
'Invalid session:
|
|
43
|
+
'Invalid session: sessionNonce is required to add a session.'
|
|
43
44
|
)
|
|
44
45
|
})
|
|
45
46
|
|