@bsv/sdk 2.1.2 → 2.1.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.
- package/dist/cjs/package.json +13 -13
- package/dist/cjs/src/auth/Peer.js +21 -18
- package/dist/cjs/src/auth/Peer.js.map +1 -1
- package/dist/cjs/src/auth/SessionManager.js.map +1 -1
- package/dist/cjs/src/auth/clients/AuthFetch.js +4 -1
- package/dist/cjs/src/auth/clients/AuthFetch.js.map +1 -1
- package/dist/cjs/src/compat/Mnemonic.js +12 -0
- package/dist/cjs/src/compat/Mnemonic.js.map +1 -1
- package/dist/cjs/src/overlay-tools/LookupResolver.js +99 -28
- package/dist/cjs/src/overlay-tools/LookupResolver.js.map +1 -1
- package/dist/cjs/src/transaction/MerklePath.js +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/Peer.js +28 -18
- package/dist/esm/src/auth/Peer.js.map +1 -1
- package/dist/esm/src/auth/SessionManager.js.map +1 -1
- package/dist/esm/src/auth/clients/AuthFetch.js +4 -1
- package/dist/esm/src/auth/clients/AuthFetch.js.map +1 -1
- package/dist/esm/src/compat/Mnemonic.js +12 -0
- package/dist/esm/src/compat/Mnemonic.js.map +1 -1
- package/dist/esm/src/overlay-tools/LookupResolver.js +99 -28
- package/dist/esm/src/overlay-tools/LookupResolver.js.map +1 -1
- package/dist/esm/src/transaction/MerklePath.js +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/Peer.d.ts +3 -3
- package/dist/types/src/auth/Peer.d.ts.map +1 -1
- package/dist/types/src/auth/SessionManager.d.ts +21 -0
- package/dist/types/src/auth/SessionManager.d.ts.map +1 -1
- package/dist/types/src/auth/clients/AuthFetch.d.ts +2 -2
- package/dist/types/src/auth/clients/AuthFetch.d.ts.map +1 -1
- package/dist/types/src/compat/Mnemonic.d.ts +2 -0
- package/dist/types/src/compat/Mnemonic.d.ts.map +1 -1
- package/dist/types/src/overlay-tools/LookupResolver.d.ts +1 -0
- package/dist/types/src/overlay-tools/LookupResolver.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/package.json +13 -13
- package/src/auth/Peer.ts +30 -20
- package/src/auth/SessionManager.ts +22 -0
- package/src/auth/__tests/Peer.test.ts +47 -1
- package/src/auth/clients/AuthFetch.ts +6 -3
- package/src/compat/Mnemonic.ts +13 -0
- package/src/compat/__tests/Mnemonic.test.ts +49 -5
- package/src/overlay-tools/LookupResolver.ts +102 -25
- package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +90 -0
- package/src/script/__tests/Spend.test.ts +45 -4
- package/src/transaction/MerklePath.ts +1 -1
- package/src/transaction/__tests/Transaction.test.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsv/sdk",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "BSV Blockchain Software Development Kit",
|
|
6
6
|
"main": "dist/cjs/mod.js",
|
|
@@ -237,23 +237,23 @@
|
|
|
237
237
|
},
|
|
238
238
|
"homepage": "https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk#readme",
|
|
239
239
|
"devDependencies": {
|
|
240
|
-
"@eslint/js": "^
|
|
241
|
-
"@jest/globals": "^30.
|
|
242
|
-
"@rspack/cli": "^2.0.
|
|
243
|
-
"@rspack/core": "^
|
|
240
|
+
"@eslint/js": "^10.0.1",
|
|
241
|
+
"@jest/globals": "^30.4.1",
|
|
242
|
+
"@rspack/cli": "^2.0.4",
|
|
243
|
+
"@rspack/core": "^2.0.4",
|
|
244
244
|
"@types/jest": "^30.0.0",
|
|
245
|
-
"@types/node": "^
|
|
246
|
-
"eslint": "^
|
|
247
|
-
"globals": "^
|
|
248
|
-
"jest": "^30.
|
|
249
|
-
"jest-environment-jsdom": "^30.
|
|
250
|
-
"ts-jest": "^29.4.
|
|
245
|
+
"@types/node": "^25.9.1",
|
|
246
|
+
"eslint": "^10.4.0",
|
|
247
|
+
"globals": "^17.6.0",
|
|
248
|
+
"jest": "^30.4.2",
|
|
249
|
+
"jest-environment-jsdom": "^30.4.1",
|
|
250
|
+
"ts-jest": "^29.4.11",
|
|
251
251
|
"ts-loader": "^9.5.4",
|
|
252
252
|
"ts-standard": "^12.0.2",
|
|
253
253
|
"ts2md": "^0.2.8",
|
|
254
254
|
"tsconfig-to-dual-package": "^1.2.0",
|
|
255
|
-
"typescript": "^
|
|
256
|
-
"typescript-eslint": "^8.
|
|
255
|
+
"typescript": "^6.0.3",
|
|
256
|
+
"typescript-eslint": "^8.60.0"
|
|
257
257
|
},
|
|
258
258
|
"ts-standard": {
|
|
259
259
|
"project": "tsconfig.eslint.json",
|
package/src/auth/Peer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SessionManager } from './SessionManager.js'
|
|
1
|
+
import { SessionManager, AsyncSessionManager } from './SessionManager.js'
|
|
2
2
|
import {
|
|
3
3
|
createNonce,
|
|
4
4
|
verifyNonce,
|
|
@@ -28,6 +28,13 @@ const BufferCtor =
|
|
|
28
28
|
* This version supports multiple concurrent sessions per peer identityKey.
|
|
29
29
|
*/
|
|
30
30
|
export class Peer {
|
|
31
|
+
// Declared as the synchronous {@link SessionManager} for back-compat with
|
|
32
|
+
// pre-existing consumers that read `peer.sessionManager.getSession(...)`
|
|
33
|
+
// and friends as synchronous calls. The constructor also accepts an
|
|
34
|
+
// {@link AsyncSessionManager}; in that case the runtime value's methods
|
|
35
|
+
// return Promises. Peer always awaits internal calls so both work, but
|
|
36
|
+
// external code that reaches in directly should match the implementation
|
|
37
|
+
// it injected. See `AsyncSessionManager` for the opt-in async contract.
|
|
31
38
|
public sessionManager: SessionManager
|
|
32
39
|
private readonly transport: Transport
|
|
33
40
|
private readonly wallet: WalletInterface
|
|
@@ -97,7 +104,7 @@ export class Peer {
|
|
|
97
104
|
* @param {WalletInterface} wallet - The wallet instance used for cryptographic operations.
|
|
98
105
|
* @param {Transport} transport - The transport mechanism used for sending and receiving messages.
|
|
99
106
|
* @param {RequestedCertificateSet} [certificatesToRequest] - Optional set of certificates to request from a peer during the initial handshake.
|
|
100
|
-
* @param {SessionManager} [sessionManager] - Optional
|
|
107
|
+
* @param {SessionManager | AsyncSessionManager} [sessionManager] - Optional session store. Pass an {@link AsyncSessionManager} for shared/durable storage in load-balanced deployments; otherwise the default in-process {@link SessionManager} is used.
|
|
101
108
|
* @param {boolean} [autoPersistLastSession] - Whether to auto-persist the session with the last-interacted-with peer. Defaults to true.
|
|
102
109
|
* @param {OriginatorDomainNameStringUnder250Bytes} [originator] - Optional originator domain name.
|
|
103
110
|
*/
|
|
@@ -105,7 +112,7 @@ export class Peer {
|
|
|
105
112
|
wallet: WalletInterface,
|
|
106
113
|
transport: Transport,
|
|
107
114
|
certificatesToRequest?: RequestedCertificateSet,
|
|
108
|
-
sessionManager?: SessionManager,
|
|
115
|
+
sessionManager?: SessionManager | AsyncSessionManager,
|
|
109
116
|
autoPersistLastSession?: boolean,
|
|
110
117
|
originator?: OriginatorDomainNameStringUnder250Bytes
|
|
111
118
|
) {
|
|
@@ -117,8 +124,11 @@ export class Peer {
|
|
|
117
124
|
types: {}
|
|
118
125
|
}
|
|
119
126
|
this.ready = this.transport.onData(this.handleIncomingMessage.bind(this)) // NOSONAR(typescript:S7059): listener must register synchronously — see ready field comment
|
|
127
|
+
// Cast keeps the public field typed as the synchronous `SessionManager`
|
|
128
|
+
// for back-compat. When an `AsyncSessionManager` is injected, the actual
|
|
129
|
+
// runtime methods return Promises — Peer awaits them internally below.
|
|
120
130
|
this.sessionManager =
|
|
121
|
-
sessionManager ?? new SessionManager()
|
|
131
|
+
(sessionManager ?? new SessionManager()) as SessionManager
|
|
122
132
|
if (autoPersistLastSession === false) {
|
|
123
133
|
this.autoPersistLastSession = false
|
|
124
134
|
} else {
|
|
@@ -178,7 +188,7 @@ export class Peer {
|
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
peerSession.lastUpdate = Date.now()
|
|
181
|
-
this.sessionManager.updateSession(peerSession)
|
|
191
|
+
await this.sessionManager.updateSession(peerSession)
|
|
182
192
|
|
|
183
193
|
try {
|
|
184
194
|
await this.transport.send(generalMessage)
|
|
@@ -233,7 +243,7 @@ export class Peer {
|
|
|
233
243
|
|
|
234
244
|
// Update last-used timestamp
|
|
235
245
|
peerSession.lastUpdate = Date.now()
|
|
236
|
-
this.sessionManager.updateSession(peerSession)
|
|
246
|
+
await this.sessionManager.updateSession(peerSession)
|
|
237
247
|
|
|
238
248
|
try {
|
|
239
249
|
await this.transport.send(certRequestMessage)
|
|
@@ -262,7 +272,7 @@ export class Peer {
|
|
|
262
272
|
|
|
263
273
|
let peerSession: PeerSession | undefined
|
|
264
274
|
if (typeof identityKey === 'string') {
|
|
265
|
-
peerSession = this.sessionManager.getSession(identityKey)
|
|
275
|
+
peerSession = await this.sessionManager.getSession(identityKey)
|
|
266
276
|
}
|
|
267
277
|
|
|
268
278
|
// If that session doesn't exist or isn't authenticated, initiate handshake
|
|
@@ -270,7 +280,7 @@ export class Peer {
|
|
|
270
280
|
// This will create a brand-new session
|
|
271
281
|
const sessionNonce = await this.initiateHandshake(identityKey)
|
|
272
282
|
// Now retrieve it by the sessionNonce
|
|
273
|
-
peerSession = this.sessionManager.getSession(sessionNonce)
|
|
283
|
+
peerSession = await this.sessionManager.getSession(sessionNonce)
|
|
274
284
|
if (peerSession?.isAuthenticated !== true) {
|
|
275
285
|
throw new Error('Unable to establish mutual authentication with peer!')
|
|
276
286
|
}
|
|
@@ -367,7 +377,7 @@ export class Peer {
|
|
|
367
377
|
const certificatesRequired =
|
|
368
378
|
this.certificatesToRequest.certifiers.length > 0
|
|
369
379
|
|
|
370
|
-
this.sessionManager.addSession({
|
|
380
|
+
await this.sessionManager.addSession({
|
|
371
381
|
isAuthenticated: false,
|
|
372
382
|
sessionNonce,
|
|
373
383
|
peerIdentityKey: identityKey,
|
|
@@ -511,7 +521,7 @@ export class Peer {
|
|
|
511
521
|
Array.isArray(this.certificatesToRequest?.certifiers) &&
|
|
512
522
|
this.certificatesToRequest.certifiers.length > 0
|
|
513
523
|
|
|
514
|
-
this.sessionManager.addSession({
|
|
524
|
+
await this.sessionManager.addSession({
|
|
515
525
|
isAuthenticated: true,
|
|
516
526
|
sessionNonce,
|
|
517
527
|
peerNonce: message.initialNonce,
|
|
@@ -588,7 +598,7 @@ export class Peer {
|
|
|
588
598
|
)
|
|
589
599
|
}
|
|
590
600
|
|
|
591
|
-
const peerSession = this.sessionManager.getSession(message.yourNonce as string)
|
|
601
|
+
const peerSession = await this.sessionManager.getSession(message.yourNonce as string)
|
|
592
602
|
if (peerSession == null) {
|
|
593
603
|
throw new Error(`Peer session not found for peer: ${message.identityKey}`)
|
|
594
604
|
}
|
|
@@ -624,7 +634,7 @@ export class Peer {
|
|
|
624
634
|
peerSession.certificatesValidated = !peerSession.certificatesRequired
|
|
625
635
|
|
|
626
636
|
peerSession.lastUpdate = Date.now()
|
|
627
|
-
this.sessionManager.updateSession(peerSession)
|
|
637
|
+
await this.sessionManager.updateSession(peerSession)
|
|
628
638
|
|
|
629
639
|
// --- Validate certificates if provided ---
|
|
630
640
|
if (
|
|
@@ -641,7 +651,7 @@ export class Peer {
|
|
|
641
651
|
|
|
642
652
|
peerSession.certificatesValidated = true
|
|
643
653
|
peerSession.lastUpdate = Date.now()
|
|
644
|
-
this.sessionManager.updateSession(peerSession)
|
|
654
|
+
await this.sessionManager.updateSession(peerSession)
|
|
645
655
|
|
|
646
656
|
// Resolve any promises waiting for certificate validation
|
|
647
657
|
if (peerSession.sessionNonce != null) {
|
|
@@ -711,7 +721,7 @@ export class Peer {
|
|
|
711
721
|
`Unable to verify nonce for certificate request message from: ${message.identityKey}`
|
|
712
722
|
)
|
|
713
723
|
}
|
|
714
|
-
const peerSession = this.sessionManager.getSession(message.yourNonce as string)
|
|
724
|
+
const peerSession = await this.sessionManager.getSession(message.yourNonce as string)
|
|
715
725
|
if (peerSession == null) {
|
|
716
726
|
throw new Error(`Session not found for nonce: ${message.yourNonce as string}`)
|
|
717
727
|
}
|
|
@@ -731,7 +741,7 @@ export class Peer {
|
|
|
731
741
|
|
|
732
742
|
// Update usage
|
|
733
743
|
peerSession.lastUpdate = Date.now()
|
|
734
|
-
this.sessionManager.updateSession(peerSession)
|
|
744
|
+
await this.sessionManager.updateSession(peerSession)
|
|
735
745
|
|
|
736
746
|
if (
|
|
737
747
|
(message.requestedCertificates != null) &&
|
|
@@ -789,7 +799,7 @@ export class Peer {
|
|
|
789
799
|
|
|
790
800
|
// Update usage
|
|
791
801
|
peerSession.lastUpdate = Date.now()
|
|
792
|
-
this.sessionManager.updateSession(peerSession)
|
|
802
|
+
await this.sessionManager.updateSession(peerSession)
|
|
793
803
|
|
|
794
804
|
try {
|
|
795
805
|
await this.transport.send(certificateResponse)
|
|
@@ -813,7 +823,7 @@ export class Peer {
|
|
|
813
823
|
)
|
|
814
824
|
}
|
|
815
825
|
|
|
816
|
-
const peerSession = this.sessionManager.getSession(message.yourNonce as string)
|
|
826
|
+
const peerSession = await this.sessionManager.getSession(message.yourNonce as string)
|
|
817
827
|
if (peerSession == null) {
|
|
818
828
|
throw new Error(`Session not found for nonce: ${message.yourNonce as string}`)
|
|
819
829
|
}
|
|
@@ -843,7 +853,7 @@ export class Peer {
|
|
|
843
853
|
|
|
844
854
|
peerSession.certificatesValidated = true
|
|
845
855
|
peerSession.lastUpdate = Date.now()
|
|
846
|
-
this.sessionManager.updateSession(peerSession)
|
|
856
|
+
await this.sessionManager.updateSession(peerSession)
|
|
847
857
|
|
|
848
858
|
// Resolve any promises waiting for certificate validation
|
|
849
859
|
if (peerSession.sessionNonce != null) {
|
|
@@ -878,7 +888,7 @@ export class Peer {
|
|
|
878
888
|
)
|
|
879
889
|
}
|
|
880
890
|
|
|
881
|
-
const peerSession = this.sessionManager.getSession(message.yourNonce as string)
|
|
891
|
+
const peerSession = await this.sessionManager.getSession(message.yourNonce as string)
|
|
882
892
|
if (peerSession == null) {
|
|
883
893
|
throw new Error(`Session not found for nonce: ${message.yourNonce as string}`)
|
|
884
894
|
}
|
|
@@ -942,7 +952,7 @@ export class Peer {
|
|
|
942
952
|
|
|
943
953
|
// Mark last usage
|
|
944
954
|
peerSession.lastUpdate = Date.now()
|
|
945
|
-
this.sessionManager.updateSession(peerSession)
|
|
955
|
+
await this.sessionManager.updateSession(peerSession)
|
|
946
956
|
|
|
947
957
|
// Update lastInteractedWithPeer
|
|
948
958
|
this.lastInteractedWithPeer = message.identityKey
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { PeerSession } from './types.js'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Opt-in async session-manager contract for horizontally scaled deployments.
|
|
5
|
+
*
|
|
6
|
+
* The default in-process {@link SessionManager} stores BRC-103 nonce/session
|
|
7
|
+
* state in memory and is synchronous. Multi-instance HTTP servers that need
|
|
8
|
+
* every instance to resolve the same handshake state — e.g. behind a load
|
|
9
|
+
* balancer without sticky routing — can implement `AsyncSessionManager`
|
|
10
|
+
* against a shared store such as Redis or SQL and pass it to {@link Peer}
|
|
11
|
+
* or `createAuthMiddleware` instead.
|
|
12
|
+
*
|
|
13
|
+
* {@link Peer} accepts `SessionManager | AsyncSessionManager` and awaits every
|
|
14
|
+
* call internally, so sync stores incur no extra latency while async stores
|
|
15
|
+
* work transparently.
|
|
16
|
+
*/
|
|
17
|
+
export interface AsyncSessionManager {
|
|
18
|
+
addSession: (session: PeerSession) => Promise<void>
|
|
19
|
+
updateSession: (session: PeerSession) => Promise<void>
|
|
20
|
+
getSession: (identifier: string) => Promise<PeerSession | undefined>
|
|
21
|
+
removeSession: (session: PeerSession) => Promise<void>
|
|
22
|
+
hasSession: (identifier: string) => Promise<boolean>
|
|
23
|
+
}
|
|
24
|
+
|
|
3
25
|
/**
|
|
4
26
|
* Manages sessions for peers, allowing multiple concurrent sessions
|
|
5
27
|
* per identity key. Primary lookup is always by `sessionNonce`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Peer } from '../../auth/Peer.js'
|
|
2
|
-
import { AuthMessage, Transport } from '../../auth/types.js'
|
|
2
|
+
import { AuthMessage, PeerSession, Transport } from '../../auth/types.js'
|
|
3
3
|
import { jest } from '@jest/globals'
|
|
4
4
|
import { WalletInterface } from '../../wallet/Wallet.interfaces.js'
|
|
5
5
|
import { Utils, PrivateKey } from '../../primitives/index.js'
|
|
@@ -8,6 +8,7 @@ import { MasterCertificate } from '../../auth/certificates/MasterCertificate.js'
|
|
|
8
8
|
import { getVerifiableCertificates } from '../../auth/utils/getVerifiableCertificates.js'
|
|
9
9
|
import { CompletedProtoWallet } from '../certificates/__tests/CompletedProtoWallet.js'
|
|
10
10
|
import { SimplifiedFetchTransport } from '../../auth/transports/SimplifiedFetchTransport.js'
|
|
11
|
+
import { SessionManager, AsyncSessionManager } from '../../auth/SessionManager.js'
|
|
11
12
|
|
|
12
13
|
const certifierPrivKey = new PrivateKey(21)
|
|
13
14
|
const alicePrivKey = new PrivateKey(22)
|
|
@@ -51,6 +52,35 @@ class LocalTransport implements Transport {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
class AsyncSessionStore implements AsyncSessionManager {
|
|
56
|
+
private readonly sessions = new SessionManager()
|
|
57
|
+
|
|
58
|
+
async addSession(session: PeerSession): Promise<void> {
|
|
59
|
+
await Promise.resolve()
|
|
60
|
+
this.sessions.addSession(session)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async updateSession(session: PeerSession): Promise<void> {
|
|
64
|
+
await Promise.resolve()
|
|
65
|
+
this.sessions.updateSession(session)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async getSession(identifier: string): Promise<PeerSession | undefined> {
|
|
69
|
+
await Promise.resolve()
|
|
70
|
+
return this.sessions.getSession(identifier)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async removeSession(session: PeerSession): Promise<void> {
|
|
74
|
+
await Promise.resolve()
|
|
75
|
+
this.sessions.removeSession(session)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async hasSession(identifier: string): Promise<boolean> {
|
|
79
|
+
await Promise.resolve()
|
|
80
|
+
return this.sessions.hasSession(identifier)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
54
84
|
function waitForNextGeneralMessage(
|
|
55
85
|
peer: Peer,
|
|
56
86
|
handler?: (senderPublicKey: string, payload: number[]) => void
|
|
@@ -290,6 +320,22 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
290
320
|
expect(certificatesReceivedByBob).toEqual([])
|
|
291
321
|
}, 15000)
|
|
292
322
|
|
|
323
|
+
it('supports asynchronous shared session managers', async () => {
|
|
324
|
+
alice = new Peer(walletA, transportA, undefined, new AsyncSessionStore())
|
|
325
|
+
bob = new Peer(walletB, transportB, undefined, new AsyncSessionStore())
|
|
326
|
+
|
|
327
|
+
const bobReceivedGeneralMessage = waitForNextGeneralMessage(bob)
|
|
328
|
+
const aliceReceivedGeneralMessage = waitForNextGeneralMessage(alice)
|
|
329
|
+
|
|
330
|
+
bob.listenForGeneralMessages((senderPublicKey) => {
|
|
331
|
+
void bob.toPeer(Utils.toArray('Hello Alice!'), senderPublicKey)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
await alice.toPeer(Utils.toArray('Hello Bob!'))
|
|
335
|
+
await bobReceivedGeneralMessage
|
|
336
|
+
await aliceReceivedGeneralMessage
|
|
337
|
+
}, 15000)
|
|
338
|
+
|
|
293
339
|
it('Alice talks to Bob across two devices, Bob can respond across both sessions', async () => {
|
|
294
340
|
const transportA1 = new LocalTransport()
|
|
295
341
|
const transportA2 = new LocalTransport()
|
|
@@ -7,7 +7,7 @@ import { OriginatorDomainNameStringUnder250Bytes, WalletInterface } from '../../
|
|
|
7
7
|
import { createNonce } from '../utils/createNonce.js'
|
|
8
8
|
import { Peer } from '../Peer.js'
|
|
9
9
|
import { SimplifiedFetchTransport } from '../transports/SimplifiedFetchTransport.js'
|
|
10
|
-
import { SessionManager } from '../SessionManager.js'
|
|
10
|
+
import { SessionManager, AsyncSessionManager } from '../SessionManager.js'
|
|
11
11
|
import { RequestedCertificateSet } from '../types.js'
|
|
12
12
|
import { VerifiableCertificate } from '../certificates/VerifiableCertificate.js'
|
|
13
13
|
import { Writer } from '../../primitives/utils.js'
|
|
@@ -79,10 +79,13 @@ export class AuthFetch {
|
|
|
79
79
|
* @param wallet - The wallet instance for signing and authentication.
|
|
80
80
|
* @param requestedCertificates - Optional set of certificates to request from peers.
|
|
81
81
|
*/
|
|
82
|
-
constructor(wallet: WalletInterface, requestedCertificates?: RequestedCertificateSet, sessionManager?: SessionManager, originator?: OriginatorDomainNameStringUnder250Bytes) {
|
|
82
|
+
constructor(wallet: WalletInterface, requestedCertificates?: RequestedCertificateSet, sessionManager?: SessionManager | AsyncSessionManager, originator?: OriginatorDomainNameStringUnder250Bytes) {
|
|
83
83
|
this.wallet = wallet
|
|
84
84
|
this.requestedCertificates = requestedCertificates
|
|
85
|
-
|
|
85
|
+
// See `Peer.sessionManager`: field stays typed as the synchronous
|
|
86
|
+
// `SessionManager` for back-compat; if an `AsyncSessionManager` is
|
|
87
|
+
// injected, the underlying Peer awaits all calls internally.
|
|
88
|
+
this.sessionManager = (sessionManager ?? new SessionManager()) as SessionManager
|
|
86
89
|
this.originator = originator
|
|
87
90
|
}
|
|
88
91
|
|
package/src/compat/Mnemonic.ts
CHANGED
|
@@ -125,9 +125,22 @@ export default class Mnemonic {
|
|
|
125
125
|
* Sets the mnemonic for the instance from a string.
|
|
126
126
|
* @param {string} mnemonic - The mnemonic phrase as a string.
|
|
127
127
|
* @returns {this} The Mnemonic instance with the set mnemonic.
|
|
128
|
+
* @throws {Error} If the mnemonic does not pass BIP-39 validation
|
|
129
|
+
* (unknown words, invalid length, or bad checksum).
|
|
128
130
|
*/
|
|
129
131
|
public fromString (mnemonic: string): this {
|
|
130
132
|
this.mnemonic = mnemonic
|
|
133
|
+
let valid = false
|
|
134
|
+
try {
|
|
135
|
+
valid = this.check()
|
|
136
|
+
} catch {
|
|
137
|
+
valid = false
|
|
138
|
+
}
|
|
139
|
+
if (!valid) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
'Mnemonic does not pass the check - was the mnemonic typed incorrectly? Are there extra spaces?'
|
|
142
|
+
)
|
|
143
|
+
}
|
|
131
144
|
return this
|
|
132
145
|
}
|
|
133
146
|
|
|
@@ -49,15 +49,21 @@ describe('Mnemonic', function () {
|
|
|
49
49
|
mnemonic = m.mnemonic
|
|
50
50
|
|
|
51
51
|
// mnemonics with extra whitespace do not pass the check
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const trailingSpace = mnemonic + ' '
|
|
53
|
+
const trailing = new Mnemonic()
|
|
54
|
+
trailing.mnemonic = trailingSpace
|
|
55
|
+
expect(trailing.check()).toEqual(false)
|
|
56
|
+
expect(() => new Mnemonic().fromString(trailingSpace)).toThrow()
|
|
54
57
|
|
|
55
58
|
// mnemonics with a word replaced do not pass the check
|
|
56
59
|
const words = mnemonic.split(' ')
|
|
57
60
|
expect(words[words.length - 1]).not.toEqual('zoo')
|
|
58
61
|
words[words.length - 1] = 'zoo'
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
const badWord = words.join(' ')
|
|
63
|
+
const replaced = new Mnemonic()
|
|
64
|
+
replaced.mnemonic = badWord
|
|
65
|
+
expect(replaced.check()).toEqual(false)
|
|
66
|
+
expect(() => new Mnemonic().fromString(badWord)).toThrow()
|
|
61
67
|
})
|
|
62
68
|
|
|
63
69
|
describe('#toBinary', () => {
|
|
@@ -132,13 +138,51 @@ describe('Mnemonic', function () {
|
|
|
132
138
|
})
|
|
133
139
|
|
|
134
140
|
describe('#fromString', () => {
|
|
135
|
-
it('should throw an error
|
|
141
|
+
it('should throw an error on invalid mnemonic when toSeed is called', () => {
|
|
136
142
|
expect(() => {
|
|
137
143
|
new Mnemonic().fromString('invalid mnemonic').toSeed()
|
|
138
144
|
}).toThrow(
|
|
139
145
|
'Mnemonic does not pass the check - was the mnemonic typed incorrectly? Are there extra spaces?'
|
|
140
146
|
)
|
|
141
147
|
})
|
|
148
|
+
|
|
149
|
+
it('should throw immediately on nonsense input', () => {
|
|
150
|
+
expect(() => {
|
|
151
|
+
Mnemonic.fromString('this is not a real bip39 mnemonic phrase at all')
|
|
152
|
+
}).toThrow(
|
|
153
|
+
'Mnemonic does not pass the check - was the mnemonic typed incorrectly? Are there extra spaces?'
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should throw on garbage string', () => {
|
|
158
|
+
expect(() => {
|
|
159
|
+
Mnemonic.fromString('asdfghjkl qwertyuiop zxcvbnm')
|
|
160
|
+
}).toThrow()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should throw on empty string', () => {
|
|
164
|
+
expect(() => {
|
|
165
|
+
Mnemonic.fromString('')
|
|
166
|
+
}).toThrow()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should throw on valid words but wrong checksum', () => {
|
|
170
|
+
// 12 valid wordlist entries but checksum will not match
|
|
171
|
+
expect(() => {
|
|
172
|
+
Mnemonic.fromString(
|
|
173
|
+
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon'
|
|
174
|
+
)
|
|
175
|
+
}).toThrow()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should accept a known-valid BIP-39 phrase', () => {
|
|
179
|
+
const m = Mnemonic.fromString(
|
|
180
|
+
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
|
|
181
|
+
)
|
|
182
|
+
expect(m.mnemonic).toEqual(
|
|
183
|
+
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
|
|
184
|
+
)
|
|
185
|
+
})
|
|
142
186
|
})
|
|
143
187
|
|
|
144
188
|
describe('@isValid', () => {
|
|
@@ -105,6 +105,73 @@ export const DEFAULT_TESTNET_SLAP_TRACKERS: string[] = [
|
|
|
105
105
|
|
|
106
106
|
const MAX_TRACKER_WAIT_TIME = 5000
|
|
107
107
|
|
|
108
|
+
/** A wall-clock deadline that rejects after `timeoutMs`, optionally aborting a controller. */
|
|
109
|
+
interface Deadline {
|
|
110
|
+
/** Rejects with `Error('Request timed out')` once the timer fires. */
|
|
111
|
+
promise: Promise<never>
|
|
112
|
+
/** Clears the underlying timer. Safe to call after the timer has already fired. */
|
|
113
|
+
cancel: () => void
|
|
114
|
+
/** Returns true once the timer has fired. */
|
|
115
|
+
didTimeOut: () => boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function createDeadline (timeoutMs: number, controller?: AbortController): Deadline {
|
|
119
|
+
let expired = false
|
|
120
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
121
|
+
const promise = new Promise<never>((_, reject) => {
|
|
122
|
+
timer = setTimeout(() => {
|
|
123
|
+
expired = true
|
|
124
|
+
try { controller?.abort() } catch { /* noop */ }
|
|
125
|
+
reject(new Error('Request timed out'))
|
|
126
|
+
}, timeoutMs)
|
|
127
|
+
})
|
|
128
|
+
return {
|
|
129
|
+
promise,
|
|
130
|
+
cancel: () => {
|
|
131
|
+
if (timer !== null) clearTimeout(timer)
|
|
132
|
+
},
|
|
133
|
+
didTimeOut: () => expired
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeLookupError (err: unknown, timedOut: boolean): Error {
|
|
138
|
+
if (timedOut) return new Error('Request timed out')
|
|
139
|
+
if ((err as { name?: string })?.name === 'AbortError') return new Error('Request timed out')
|
|
140
|
+
if (err instanceof Error) return err
|
|
141
|
+
return new Error(stringifyErrorValue(err))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Coerce a non-Error thrown value to a human-readable string without falling
|
|
146
|
+
* back to the default `'[object Object]'` for plain objects.
|
|
147
|
+
*/
|
|
148
|
+
function stringifyErrorValue (value: unknown): string {
|
|
149
|
+
if (value === null) return 'null'
|
|
150
|
+
if (value === undefined) return 'undefined'
|
|
151
|
+
if (typeof value === 'string') return value
|
|
152
|
+
if (typeof value === 'number') return value.toString()
|
|
153
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
|
154
|
+
if (typeof value === 'bigint') return value.toString()
|
|
155
|
+
const message = (value as { message?: unknown }).message
|
|
156
|
+
if (typeof message === 'string' && message.length > 0) return message
|
|
157
|
+
try {
|
|
158
|
+
return JSON.stringify(value) ?? 'Unknown error'
|
|
159
|
+
} catch {
|
|
160
|
+
return 'Unknown error'
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Returns true when the given Content-Type header value represents
|
|
166
|
+
* `application/octet-stream`, ignoring case and any media-type parameters
|
|
167
|
+
* (e.g. `; charset=utf-8`).
|
|
168
|
+
*/
|
|
169
|
+
function isOctetStream (contentType: string | null): boolean {
|
|
170
|
+
if (typeof contentType !== 'string') return false
|
|
171
|
+
const baseType = contentType.split(';', 1)[0].trim().toLowerCase()
|
|
172
|
+
return baseType === 'application/octet-stream'
|
|
173
|
+
}
|
|
174
|
+
|
|
108
175
|
/** Internal cache options. Kept optional to preserve drop-in compatibility. */
|
|
109
176
|
interface CacheOptions {
|
|
110
177
|
/** How long (ms) a hosts entry is considered fresh. Default 5 minutes. */
|
|
@@ -181,36 +248,46 @@ export class HTTPSOverlayLookupFacilitator implements OverlayLookupFacilitator {
|
|
|
181
248
|
}
|
|
182
249
|
|
|
183
250
|
const controller = typeof AbortController === 'undefined' ? undefined : new AbortController()
|
|
184
|
-
const
|
|
185
|
-
try { controller?.abort() } catch { /* noop */ }
|
|
186
|
-
}, timeout)
|
|
251
|
+
const deadline = createDeadline(timeout, controller)
|
|
187
252
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
signal: controller?.signal
|
|
197
|
-
}
|
|
198
|
-
const response: Response = await this.fetchClient(`${url}/lookup`, fco)
|
|
253
|
+
// Hard wall-clock deadline: in some environments (e.g. browser/Electron CORS
|
|
254
|
+
// failures) the underlying fetch can stall without ever settling, and the
|
|
255
|
+
// AbortController signal alone is insufficient to make the returned promise
|
|
256
|
+
// resolve or reject. Race the fetch against a setTimeout-backed reject so
|
|
257
|
+
// the consumer-facing promise always settles within `timeout` ms.
|
|
258
|
+
const fetchPromise = this.performLookupRequest(url, question, controller?.signal)
|
|
259
|
+
// Swallow background rejection if the deadline wins first.
|
|
260
|
+
fetchPromise.catch(() => { /* noop */ })
|
|
199
261
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return await this.parseOctetStreamLookup(response)
|
|
203
|
-
}
|
|
204
|
-
return await response.json()
|
|
262
|
+
try {
|
|
263
|
+
return await Promise.race([fetchPromise, deadline.promise])
|
|
205
264
|
} catch (e) {
|
|
206
|
-
|
|
207
|
-
if ((e as { name?: string })?.name === 'AbortError') {
|
|
208
|
-
throw new Error('Request timed out')
|
|
209
|
-
}
|
|
210
|
-
throw e
|
|
265
|
+
throw normalizeLookupError(e, deadline.didTimeOut())
|
|
211
266
|
} finally {
|
|
212
|
-
|
|
267
|
+
deadline.cancel()
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async performLookupRequest (
|
|
272
|
+
url: string,
|
|
273
|
+
question: LookupQuestion,
|
|
274
|
+
signal: AbortSignal | undefined
|
|
275
|
+
): Promise<LookupAnswer> {
|
|
276
|
+
const fco: RequestInit = {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
headers: {
|
|
279
|
+
'Content-Type': 'application/json',
|
|
280
|
+
'X-Aggregation': 'yes'
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify({ service: question.service, query: question.query }),
|
|
283
|
+
signal
|
|
284
|
+
}
|
|
285
|
+
const response: Response = await this.fetchClient(`${url}/lookup`, fco)
|
|
286
|
+
if (!response.ok) throw new Error(`Failed to facilitate lookup (HTTP ${response.status})`)
|
|
287
|
+
if (isOctetStream(response.headers.get('content-type'))) {
|
|
288
|
+
return await this.parseOctetStreamLookup(response)
|
|
213
289
|
}
|
|
290
|
+
return await response.json()
|
|
214
291
|
}
|
|
215
292
|
|
|
216
293
|
/** Parse the aggregated octet-stream lookup response into an output-list LookupAnswer. */
|