@bsv/sdk 2.0.3 → 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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/auth/Peer.js +35 -33
- package/dist/cjs/src/auth/Peer.js.map +1 -1
- package/dist/cjs/src/auth/clients/AuthFetch.js +31 -5
- package/dist/cjs/src/auth/clients/AuthFetch.js.map +1 -1
- package/dist/cjs/src/auth/clients/__tests__/AuthFetch.test.js +77 -0
- package/dist/cjs/src/auth/clients/__tests__/AuthFetch.test.js.map +1 -1
- package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js +4 -1
- package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/Peer.js +35 -33
- package/dist/esm/src/auth/Peer.js.map +1 -1
- package/dist/esm/src/auth/clients/AuthFetch.js +31 -5
- package/dist/esm/src/auth/clients/AuthFetch.js.map +1 -1
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.test.js +77 -0
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.test.js.map +1 -1
- package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js +4 -1
- package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/Peer.d.ts.map +1 -1
- package/dist/types/src/auth/clients/AuthFetch.d.ts.map +1 -1
- package/dist/types/src/auth/transports/SimplifiedFetchTransport.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/dist/umd/bundle.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/Peer.ts +49 -47
- package/src/auth/__tests/Peer.test.ts +35 -30
- package/src/auth/clients/AuthFetch.ts +46 -18
- package/src/auth/clients/__tests__/AuthFetch.test.ts +97 -0
- package/src/auth/transports/SimplifiedFetchTransport.ts +24 -21
package/package.json
CHANGED
package/src/auth/Peer.ts
CHANGED
|
@@ -136,7 +136,7 @@ export class Peer {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
if (peerSession.certificatesRequired === true &&
|
|
139
|
-
|
|
139
|
+
peerSession.certificatesValidated !== true) {
|
|
140
140
|
throw new Error(
|
|
141
141
|
'Cannot send general message before certificate validation is complete'
|
|
142
142
|
)
|
|
@@ -450,33 +450,28 @@ export class Peer {
|
|
|
450
450
|
)
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
)
|
|
476
|
-
}
|
|
477
|
-
} catch (err) {
|
|
478
|
-
// Swallow protocol violations so transport does not crash the process
|
|
479
|
-
// (Message is intentionally rejected)
|
|
453
|
+
switch (message.messageType) {
|
|
454
|
+
case 'initialRequest':
|
|
455
|
+
await this.processInitialRequest(message)
|
|
456
|
+
break
|
|
457
|
+
case 'initialResponse':
|
|
458
|
+
await this.processInitialResponse(message)
|
|
459
|
+
break
|
|
460
|
+
case 'certificateRequest':
|
|
461
|
+
await this.processCertificateRequest(message)
|
|
462
|
+
break
|
|
463
|
+
case 'certificateResponse':
|
|
464
|
+
await this.processCertificateResponse(message)
|
|
465
|
+
break
|
|
466
|
+
case 'general':
|
|
467
|
+
await this.processGeneralMessage(message)
|
|
468
|
+
break
|
|
469
|
+
default:
|
|
470
|
+
throw new Error(
|
|
471
|
+
`Unknown message type of ${String(message.messageType)} from ${String(
|
|
472
|
+
message.identityKey
|
|
473
|
+
)}`
|
|
474
|
+
)
|
|
480
475
|
}
|
|
481
476
|
}
|
|
482
477
|
|
|
@@ -675,10 +670,16 @@ export class Peer {
|
|
|
675
670
|
message.identityKey,
|
|
676
671
|
this.originator
|
|
677
672
|
)
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
673
|
+
// Only send if we actually have certificates to provide.
|
|
674
|
+
// An empty certificateResponse has no value and can race with a
|
|
675
|
+
// subsequent certificateRequest that shares the same initialNonce,
|
|
676
|
+
// causing the server to mis-route non-general handle responses.
|
|
677
|
+
if (verifiableCertificates.length > 0) {
|
|
678
|
+
await this.sendCertificateResponse(
|
|
679
|
+
message.identityKey,
|
|
680
|
+
verifiableCertificates
|
|
681
|
+
)
|
|
682
|
+
}
|
|
682
683
|
}
|
|
683
684
|
}
|
|
684
685
|
}
|
|
@@ -818,21 +819,23 @@ export class Peer {
|
|
|
818
819
|
)
|
|
819
820
|
}
|
|
820
821
|
|
|
821
|
-
//
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
822
|
+
// Validate certificates only if they were actually provided
|
|
823
|
+
if (Array.isArray(message.certificates) && message.certificates.length > 0) {
|
|
824
|
+
await validateCertificates(
|
|
825
|
+
this.wallet,
|
|
826
|
+
message,
|
|
827
|
+
message.requestedCertificates,
|
|
828
|
+
this.originator
|
|
829
|
+
)
|
|
828
830
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
831
|
+
peerSession.certificatesValidated = true
|
|
832
|
+
peerSession.lastUpdate = Date.now()
|
|
833
|
+
this.sessionManager.updateSession(peerSession)
|
|
832
834
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
835
|
+
// Resolve any promises waiting for certificate validation
|
|
836
|
+
if (peerSession.sessionNonce != null) {
|
|
837
|
+
this.resolveCertificateValidation(peerSession.sessionNonce)
|
|
838
|
+
}
|
|
836
839
|
}
|
|
837
840
|
|
|
838
841
|
// Notify any listeners
|
|
@@ -886,8 +889,7 @@ export class Peer {
|
|
|
886
889
|
if (promise != null) {
|
|
887
890
|
this.certificateValidationPromises.delete(sessionNonce)
|
|
888
891
|
reject(new Error(
|
|
889
|
-
`Timeout waiting for certificate validation from peer ${
|
|
890
|
-
peerSession.peerIdentityKey ?? 'unknown'
|
|
892
|
+
`Timeout waiting for certificate validation from peer ${peerSession.peerIdentityKey ?? 'unknown'
|
|
891
893
|
}`
|
|
892
894
|
))
|
|
893
895
|
}
|
|
@@ -40,13 +40,18 @@ class LocalTransport implements Transport {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
async onData(
|
|
43
|
-
callback: (message: AuthMessage) => void
|
|
43
|
+
callback: (message: AuthMessage) => Promise<void>
|
|
44
44
|
): Promise<void> {
|
|
45
|
-
this.onDataCallback =
|
|
45
|
+
this.onDataCallback = (m) => {
|
|
46
|
+
void (callback(m) as Promise<void>).catch(() => {
|
|
47
|
+
// Match real transport behaviour: catch errors from handleIncomingMessage
|
|
48
|
+
// to prevent unhandled promise rejections in tests.
|
|
49
|
+
})
|
|
50
|
+
}
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
function waitForNextGeneralMessage
|
|
54
|
+
function waitForNextGeneralMessage(
|
|
50
55
|
peer: Peer,
|
|
51
56
|
handler?: (senderPublicKey: string, payload: number[]) => void
|
|
52
57
|
): Promise<void> {
|
|
@@ -385,8 +390,8 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
385
390
|
describe('propagateTransportError', () => {
|
|
386
391
|
const createPeerInstance = (): Peer => {
|
|
387
392
|
const transport: Transport = {
|
|
388
|
-
send: jest.fn(async (_message: AuthMessage) => {}),
|
|
389
|
-
onData: async () => {}
|
|
393
|
+
send: jest.fn(async (_message: AuthMessage) => { }),
|
|
394
|
+
onData: async () => { }
|
|
390
395
|
}
|
|
391
396
|
return new Peer({} as WalletInterface, transport)
|
|
392
397
|
}
|
|
@@ -409,7 +414,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
409
414
|
it('preserves existing details when appending peer identity', () => {
|
|
410
415
|
const peer = createPeerInstance()
|
|
411
416
|
const originalError = new Error('existing details')
|
|
412
|
-
|
|
417
|
+
; (originalError as any).details = { status: 503 }
|
|
413
418
|
|
|
414
419
|
let thrown: Error | undefined
|
|
415
420
|
try {
|
|
@@ -745,14 +750,14 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
745
750
|
test('Should trigger "Failed to send message to peer" error with network failure', async () => {
|
|
746
751
|
// Create a mock fetch that always fails
|
|
747
752
|
const failingFetch = (jest.fn() as any).mockRejectedValue(new Error('Network connection failed'))
|
|
748
|
-
|
|
753
|
+
|
|
749
754
|
// Create a transport that will fail
|
|
750
755
|
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
|
|
751
|
-
|
|
756
|
+
|
|
752
757
|
// Create a peer with the failing transport
|
|
753
758
|
const wallet = new CompletedProtoWallet(privKey)
|
|
754
759
|
const peer = new Peer(wallet, transport)
|
|
755
|
-
|
|
760
|
+
|
|
756
761
|
// Register a dummy onData callback (required before sending)
|
|
757
762
|
await transport.onData(async (message) => {
|
|
758
763
|
// This won't be called due to network failure
|
|
@@ -777,12 +782,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
777
782
|
}, 100)
|
|
778
783
|
})
|
|
779
784
|
})
|
|
780
|
-
|
|
785
|
+
|
|
781
786
|
const transport = new SimplifiedFetchTransport('http://localhost:9999', timeoutFetch)
|
|
782
787
|
const wallet = new CompletedProtoWallet(privKey)
|
|
783
788
|
const peer = new Peer(wallet, transport)
|
|
784
|
-
|
|
785
|
-
await transport.onData(async (message) => {})
|
|
789
|
+
|
|
790
|
+
await transport.onData(async (message) => { })
|
|
786
791
|
|
|
787
792
|
try {
|
|
788
793
|
await peer.toPeer([5, 6, 7, 8], '03def789abc123')
|
|
@@ -800,12 +805,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
800
805
|
errno: -3008,
|
|
801
806
|
message: 'getaddrinfo ENOTFOUND nonexistent.domain'
|
|
802
807
|
})
|
|
803
|
-
|
|
808
|
+
|
|
804
809
|
const transport = new SimplifiedFetchTransport('http://nonexistent.domain:3000', dnsFetch)
|
|
805
810
|
const wallet = new CompletedProtoWallet(privKey)
|
|
806
811
|
const peer = new Peer(wallet, transport)
|
|
807
|
-
|
|
808
|
-
await transport.onData(async (message) => {})
|
|
812
|
+
|
|
813
|
+
await transport.onData(async (message) => { })
|
|
809
814
|
|
|
810
815
|
try {
|
|
811
816
|
await peer.toPeer([9, 10, 11, 12], '03xyz987fed654')
|
|
@@ -819,12 +824,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
819
824
|
test('Should trigger error during certificate request send', async () => {
|
|
820
825
|
// Create a failing fetch
|
|
821
826
|
const failingFetch = (jest.fn() as any).mockRejectedValue(new Error('Connection reset by peer'))
|
|
822
|
-
|
|
827
|
+
|
|
823
828
|
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
|
|
824
829
|
const wallet = new CompletedProtoWallet(privKey)
|
|
825
830
|
const peer = new Peer(wallet, transport)
|
|
826
|
-
|
|
827
|
-
await transport.onData(async (message) => {})
|
|
831
|
+
|
|
832
|
+
await transport.onData(async (message) => { })
|
|
828
833
|
|
|
829
834
|
try {
|
|
830
835
|
// Try to send a certificate request - this should also trigger the error
|
|
@@ -842,12 +847,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
842
847
|
test('Should trigger error during certificate response send', async () => {
|
|
843
848
|
// Create a failing fetch
|
|
844
849
|
const failingFetch = (jest.fn() as any).mockRejectedValue(new Error('Socket hang up'))
|
|
845
|
-
|
|
850
|
+
|
|
846
851
|
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
|
|
847
852
|
const wallet = new CompletedProtoWallet(privKey)
|
|
848
853
|
const peer = new Peer(wallet, transport)
|
|
849
|
-
|
|
850
|
-
await transport.onData(async (message) => {})
|
|
854
|
+
|
|
855
|
+
await transport.onData(async (message) => { })
|
|
851
856
|
|
|
852
857
|
try {
|
|
853
858
|
// Try to send a certificate response - this should also trigger the error
|
|
@@ -864,12 +869,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
864
869
|
const customError = new Error('Custom transport error')
|
|
865
870
|
customError.stack = 'Custom stack trace'
|
|
866
871
|
const failingFetch = (jest.fn() as any).mockRejectedValue(customError)
|
|
867
|
-
|
|
872
|
+
|
|
868
873
|
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
|
|
869
874
|
const wallet = new CompletedProtoWallet(privKey)
|
|
870
875
|
const peer = new Peer(wallet, transport)
|
|
871
|
-
|
|
872
|
-
await transport.onData(async (message) => {})
|
|
876
|
+
|
|
877
|
+
await transport.onData(async (message) => { })
|
|
873
878
|
|
|
874
879
|
try {
|
|
875
880
|
await peer.toPeer([13, 14, 15, 16], '03peer123456')
|
|
@@ -886,12 +891,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
886
891
|
test('Should handle non-Error transport failures', async () => {
|
|
887
892
|
// Create a fetch that throws a non-Error object
|
|
888
893
|
const failingFetch = (jest.fn() as any).mockRejectedValue('String error message')
|
|
889
|
-
|
|
894
|
+
|
|
890
895
|
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
|
|
891
896
|
const wallet = new CompletedProtoWallet(privKey)
|
|
892
897
|
const peer = new Peer(wallet, transport)
|
|
893
|
-
|
|
894
|
-
await transport.onData(async (message) => {})
|
|
898
|
+
|
|
899
|
+
await transport.onData(async (message) => { })
|
|
895
900
|
|
|
896
901
|
try {
|
|
897
902
|
await peer.toPeer([17, 18, 19, 20], '03peer789abc')
|
|
@@ -906,12 +911,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
906
911
|
test('Should handle undefined peer identity gracefully', async () => {
|
|
907
912
|
// Create a failing fetch
|
|
908
913
|
const failingFetch = (jest.fn() as any).mockRejectedValue('Network failure')
|
|
909
|
-
|
|
914
|
+
|
|
910
915
|
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
|
|
911
916
|
const wallet = new CompletedProtoWallet(privKey)
|
|
912
917
|
const peer = new Peer(wallet, transport)
|
|
913
|
-
|
|
914
|
-
await transport.onData(async (message) => {})
|
|
918
|
+
|
|
919
|
+
await transport.onData(async (message) => { })
|
|
915
920
|
|
|
916
921
|
try {
|
|
917
922
|
// Try to send to an undefined peer (this might happen in some edge cases)
|
|
@@ -137,7 +137,9 @@ export class AuthFetch {
|
|
|
137
137
|
verifier,
|
|
138
138
|
this.originator
|
|
139
139
|
)
|
|
140
|
-
|
|
140
|
+
if (certificatesToInclude.length > 0) {
|
|
141
|
+
await this.peers[baseURL].peer.sendCertificateResponse(verifier, certificatesToInclude)
|
|
142
|
+
}
|
|
141
143
|
} finally {
|
|
142
144
|
// Give the backend 500 ms to process the certificates we just sent, before releasing the queue entry
|
|
143
145
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
@@ -263,7 +265,15 @@ export class AuthFetch {
|
|
|
263
265
|
|
|
264
266
|
// Send the request, now that all listeners are set up
|
|
265
267
|
await peerToUse.peer.toPeer(writer.toArray(), peerToUse.identityKey).catch(async error => {
|
|
266
|
-
|
|
268
|
+
const isStaleSession =
|
|
269
|
+
error.message.includes('Session not found for nonce') ||
|
|
270
|
+
(error.message.includes('without valid BSV authentication') &&
|
|
271
|
+
peerToUse.identityKey != null &&
|
|
272
|
+
(error as any).details?.status === 401)
|
|
273
|
+
if (isStaleSession) {
|
|
274
|
+
// Stale session: server no longer recognises the session nonce
|
|
275
|
+
// (e.g. after a server restart). Clear the cached peer so a fresh
|
|
276
|
+
// handshake is performed on retry.
|
|
267
277
|
delete this.peers[baseURL]
|
|
268
278
|
config.retryCounter ??= 3
|
|
269
279
|
const response = await this.fetch(url, config)
|
|
@@ -322,20 +332,38 @@ export class AuthFetch {
|
|
|
322
332
|
}
|
|
323
333
|
|
|
324
334
|
// Return a promise that resolves when certificates are received
|
|
335
|
+
const CERTIFICATE_REQUEST_TIMEOUT_MS = 30000
|
|
325
336
|
return await new Promise<VerifiableCertificate[]>((async (resolve, reject) => {
|
|
337
|
+
let settled = false
|
|
338
|
+
|
|
339
|
+
const cleanup = (): void => {
|
|
340
|
+
settled = true
|
|
341
|
+
clearTimeout(timer)
|
|
342
|
+
peerToUse.peer.stopListeningForCertificatesReceived(callbackId)
|
|
343
|
+
}
|
|
344
|
+
|
|
326
345
|
// Set up the listener before making the request
|
|
327
346
|
const callbackId = peerToUse.peer.listenForCertificatesReceived((_senderPublicKey: string, certs: VerifiableCertificate[]) => {
|
|
328
|
-
|
|
347
|
+
if (settled) return
|
|
348
|
+
cleanup()
|
|
329
349
|
this.certificatesReceived.push(...certs)
|
|
330
350
|
resolve(certs)
|
|
331
351
|
})
|
|
332
352
|
|
|
353
|
+
const timer = setTimeout(() => {
|
|
354
|
+
if (settled) return
|
|
355
|
+
cleanup()
|
|
356
|
+
reject(new Error(`sendCertificateRequest timed out after ${CERTIFICATE_REQUEST_TIMEOUT_MS}ms waiting for certificate response from ${baseURL}`))
|
|
357
|
+
}, CERTIFICATE_REQUEST_TIMEOUT_MS)
|
|
358
|
+
|
|
333
359
|
try {
|
|
334
360
|
// Initiate the certificate request
|
|
335
361
|
await peerToUse.peer.requestCertificates(certificatesToRequest, peerToUse.identityKey)
|
|
336
362
|
} catch (err) {
|
|
337
|
-
|
|
338
|
-
|
|
363
|
+
if (!settled) {
|
|
364
|
+
cleanup()
|
|
365
|
+
reject(err)
|
|
366
|
+
}
|
|
339
367
|
}
|
|
340
368
|
}) as Function)
|
|
341
369
|
}
|
|
@@ -594,7 +622,7 @@ export class AuthFetch {
|
|
|
594
622
|
}
|
|
595
623
|
}
|
|
596
624
|
|
|
597
|
-
private isPaymentContextCompatible
|
|
625
|
+
private isPaymentContextCompatible(
|
|
598
626
|
context: PaymentRetryContext,
|
|
599
627
|
satoshisRequired: number,
|
|
600
628
|
serverIdentityKey: string,
|
|
@@ -607,7 +635,7 @@ export class AuthFetch {
|
|
|
607
635
|
)
|
|
608
636
|
}
|
|
609
637
|
|
|
610
|
-
private async createPaymentContext
|
|
638
|
+
private async createPaymentContext(
|
|
611
639
|
url: string,
|
|
612
640
|
config: SimplifiedFetchRequestOptions,
|
|
613
641
|
satoshisRequired: number,
|
|
@@ -652,7 +680,7 @@ export class AuthFetch {
|
|
|
652
680
|
}
|
|
653
681
|
}
|
|
654
682
|
|
|
655
|
-
private getMaxPaymentAttempts
|
|
683
|
+
private getMaxPaymentAttempts(config: SimplifiedFetchRequestOptions): number {
|
|
656
684
|
const attempts = typeof config.paymentRetryAttempts === 'number' ? config.paymentRetryAttempts : undefined
|
|
657
685
|
if (typeof attempts === 'number' && attempts > 0) {
|
|
658
686
|
return Math.floor(attempts)
|
|
@@ -660,7 +688,7 @@ export class AuthFetch {
|
|
|
660
688
|
return 3
|
|
661
689
|
}
|
|
662
690
|
|
|
663
|
-
private buildPaymentRequestSummary
|
|
691
|
+
private buildPaymentRequestSummary(
|
|
664
692
|
url: string,
|
|
665
693
|
config: SimplifiedFetchRequestOptions
|
|
666
694
|
): PaymentRetryContext['requestSummary'] {
|
|
@@ -677,7 +705,7 @@ export class AuthFetch {
|
|
|
677
705
|
}
|
|
678
706
|
}
|
|
679
707
|
|
|
680
|
-
private describeRequestBodyForLogging
|
|
708
|
+
private describeRequestBodyForLogging(body: any): { type: string, byteLength: number } {
|
|
681
709
|
if (body == null) {
|
|
682
710
|
return { type: 'none', byteLength: 0 }
|
|
683
711
|
}
|
|
@@ -733,7 +761,7 @@ export class AuthFetch {
|
|
|
733
761
|
return { type: typeof body, byteLength: 0 }
|
|
734
762
|
}
|
|
735
763
|
|
|
736
|
-
private composePaymentLogDetails
|
|
764
|
+
private composePaymentLogDetails(url: string, context: PaymentRetryContext): Record<string, any> {
|
|
737
765
|
return {
|
|
738
766
|
url,
|
|
739
767
|
request: context.requestSummary,
|
|
@@ -753,7 +781,7 @@ export class AuthFetch {
|
|
|
753
781
|
}
|
|
754
782
|
}
|
|
755
783
|
|
|
756
|
-
private logPaymentAttempt
|
|
784
|
+
private logPaymentAttempt(
|
|
757
785
|
level: 'info' | 'warn' | 'error',
|
|
758
786
|
message: string,
|
|
759
787
|
details: Record<string, any>
|
|
@@ -772,7 +800,7 @@ export class AuthFetch {
|
|
|
772
800
|
}
|
|
773
801
|
}
|
|
774
802
|
|
|
775
|
-
private createPaymentErrorEntry
|
|
803
|
+
private createPaymentErrorEntry(attempt: number, error: unknown): PaymentErrorLogEntry {
|
|
776
804
|
const entry: PaymentErrorLogEntry = {
|
|
777
805
|
attempt,
|
|
778
806
|
timestamp: new Date().toISOString(),
|
|
@@ -790,20 +818,20 @@ export class AuthFetch {
|
|
|
790
818
|
return entry
|
|
791
819
|
}
|
|
792
820
|
|
|
793
|
-
private getPaymentRetryDelay
|
|
821
|
+
private getPaymentRetryDelay(attempt: number): number {
|
|
794
822
|
const baseDelay = 250
|
|
795
823
|
const multiplier = Math.min(attempt, 5)
|
|
796
824
|
return baseDelay * multiplier
|
|
797
825
|
}
|
|
798
826
|
|
|
799
|
-
private async wait
|
|
827
|
+
private async wait(ms: number): Promise<void> {
|
|
800
828
|
if (ms <= 0) {
|
|
801
829
|
return
|
|
802
830
|
}
|
|
803
831
|
await new Promise(resolve => setTimeout(resolve, ms))
|
|
804
832
|
}
|
|
805
833
|
|
|
806
|
-
private buildPaymentFailureError
|
|
834
|
+
private buildPaymentFailureError(
|
|
807
835
|
url: string,
|
|
808
836
|
context: PaymentRetryContext,
|
|
809
837
|
lastError: unknown
|
|
@@ -828,10 +856,10 @@ export class AuthFetch {
|
|
|
828
856
|
errors: context.errors
|
|
829
857
|
}
|
|
830
858
|
|
|
831
|
-
|
|
859
|
+
; (error as any).details = failureDetails
|
|
832
860
|
|
|
833
861
|
if (lastError instanceof Error) {
|
|
834
|
-
;(error as any).cause = lastError
|
|
862
|
+
; (error as any).cause = lastError
|
|
835
863
|
}
|
|
836
864
|
|
|
837
865
|
return error
|
|
@@ -260,3 +260,100 @@ describe('AuthFetch payment handling', () => {
|
|
|
260
260
|
expect(paymentContext.errors).toHaveLength(2)
|
|
261
261
|
})
|
|
262
262
|
})
|
|
263
|
+
|
|
264
|
+
describe('AuthFetch certificate request handling', () => {
|
|
265
|
+
const requestedCertificates = {
|
|
266
|
+
certifiers: ['certifier-key'],
|
|
267
|
+
types: { 'certificate-type': ['firstName'] }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
test('sendCertificateRequest resolves and cleans up listener when certificates are received', async () => {
|
|
271
|
+
const wallet = createWalletStub()
|
|
272
|
+
const authFetch = new AuthFetch(wallet as any)
|
|
273
|
+
|
|
274
|
+
let certificatesListener: ((senderPublicKey: string, certs: any[]) => void) | undefined
|
|
275
|
+
const stopListeningForCertificatesReceived = jest.fn()
|
|
276
|
+
const requestCertificates = jest.fn(async () => {
|
|
277
|
+
certificatesListener?.('server-identity', [{ serialNumber: 'abc123' } as any])
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const peerStub = {
|
|
281
|
+
listenForCertificatesReceived: jest.fn((listener: (senderPublicKey: string, certs: any[]) => void) => {
|
|
282
|
+
certificatesListener = listener
|
|
283
|
+
return 7
|
|
284
|
+
}),
|
|
285
|
+
stopListeningForCertificatesReceived,
|
|
286
|
+
requestCertificates
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
; (authFetch as any).peers['https://api.example.com'] = {
|
|
290
|
+
peer: peerStub,
|
|
291
|
+
pendingCertificateRequests: []
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const certs = await authFetch.sendCertificateRequest('https://api.example.com', requestedCertificates as any)
|
|
295
|
+
expect(certs).toHaveLength(1)
|
|
296
|
+
expect(requestCertificates).toHaveBeenCalledWith(requestedCertificates, undefined)
|
|
297
|
+
expect(stopListeningForCertificatesReceived).toHaveBeenCalledWith(7)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
test('sendCertificateRequest rejects and cleans up listener when requestCertificates throws', async () => {
|
|
301
|
+
const wallet = createWalletStub()
|
|
302
|
+
const authFetch = new AuthFetch(wallet as any)
|
|
303
|
+
const requestError = new Error('request failed')
|
|
304
|
+
|
|
305
|
+
const stopListeningForCertificatesReceived = jest.fn()
|
|
306
|
+
const requestCertificates = jest.fn(async () => {
|
|
307
|
+
throw requestError
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
const peerStub = {
|
|
311
|
+
listenForCertificatesReceived: jest.fn(() => 9),
|
|
312
|
+
stopListeningForCertificatesReceived,
|
|
313
|
+
requestCertificates
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
; (authFetch as any).peers['https://api.example.com'] = {
|
|
317
|
+
peer: peerStub,
|
|
318
|
+
pendingCertificateRequests: []
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await expect(
|
|
322
|
+
authFetch.sendCertificateRequest('https://api.example.com', requestedCertificates as any)
|
|
323
|
+
).rejects.toThrow('request failed')
|
|
324
|
+
expect(stopListeningForCertificatesReceived).toHaveBeenCalledWith(9)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('sendCertificateRequest times out and cleans up listener when no certificate response arrives', async () => {
|
|
328
|
+
jest.useFakeTimers()
|
|
329
|
+
try {
|
|
330
|
+
const wallet = createWalletStub()
|
|
331
|
+
const authFetch = new AuthFetch(wallet as any)
|
|
332
|
+
|
|
333
|
+
const stopListeningForCertificatesReceived = jest.fn()
|
|
334
|
+
const requestCertificates = jest.fn(async () => {})
|
|
335
|
+
|
|
336
|
+
const peerStub = {
|
|
337
|
+
listenForCertificatesReceived: jest.fn(() => 11),
|
|
338
|
+
stopListeningForCertificatesReceived,
|
|
339
|
+
requestCertificates
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
; (authFetch as any).peers['https://api.example.com'] = {
|
|
343
|
+
peer: peerStub,
|
|
344
|
+
pendingCertificateRequests: []
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const pending = authFetch.sendCertificateRequest('https://api.example.com', requestedCertificates as any)
|
|
348
|
+
const timeoutAssertion = expect(pending).rejects.toThrow(
|
|
349
|
+
'sendCertificateRequest timed out after 30000ms waiting for certificate response from https://api.example.com'
|
|
350
|
+
)
|
|
351
|
+
await jest.advanceTimersByTimeAsync(30000)
|
|
352
|
+
|
|
353
|
+
await timeoutAssertion
|
|
354
|
+
expect(stopListeningForCertificatesReceived).toHaveBeenCalledWith(11)
|
|
355
|
+
} finally {
|
|
356
|
+
jest.useRealTimers()
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
})
|