1
+ import { getCaptchaToken , retrieveCaptchaInfo } from '../utils/captcha' ;
2
+ import type { Clerk } from './resources/internal' ;
3
+
1
4
/**
2
5
* TODO: @nikos Move captcha and fraud detection logic to this class
3
6
*/
4
7
class FraudProtectionService {
5
8
private inflightRequest : Promise < unknown > | null = null ;
9
+ private ticks = 0 ;
10
+ private readonly interval = 6 ;
6
11
7
12
public async execute < T extends ( ) => Promise < any > > ( cb : T ) : Promise < Awaited < ReturnType < T > > > {
8
13
if ( this . inflightRequest ) {
@@ -20,6 +25,63 @@ class FraudProtectionService {
20
25
public blockUntilReady ( ) {
21
26
return this . inflightRequest ? this . inflightRequest . then ( ( ) => null ) : Promise . resolve ( ) ;
22
27
}
28
+
29
+ public async challengeHeartbeat ( clerk : Clerk ) {
30
+ if ( ! clerk . __unstable__environment ?. displayConfig . captchaHeartbeat || this . ticks ++ % ( this . interval - 1 ) ) {
31
+ return undefined ;
32
+ }
33
+ return this . invisibleChallenge ( clerk ) ;
34
+ }
35
+
36
+ /**
37
+ * Triggers an invisible challenge.
38
+ * This will always use the non-interactive variant of the CAPTCHA challenge and will
39
+ * always use the fallback key.
40
+ */
41
+ public async invisibleChallenge ( clerk : Clerk ) {
42
+ const { captchaSiteKey, canUseCaptcha, captchaURL, captchaPublicKeyInvisible } = retrieveCaptchaInfo ( clerk ) ;
43
+
44
+ if ( canUseCaptcha && captchaSiteKey && captchaURL && captchaPublicKeyInvisible ) {
45
+ return getCaptchaToken ( {
46
+ siteKey : captchaPublicKeyInvisible ,
47
+ invisibleSiteKey : captchaPublicKeyInvisible ,
48
+ widgetType : 'invisible' ,
49
+ scriptUrl : captchaURL ,
50
+ captchaProvider : 'turnstile' ,
51
+ } ) ;
52
+ }
53
+
54
+ return undefined ;
55
+ }
56
+
57
+ /**
58
+ * Triggers a smart challenge if the user is required to solve a CAPTCHA.
59
+ * Depending on the environment settings, this will either trigger an
60
+ * invisible or smart (managed) CAPTCHA challenge.
61
+ * Managed challenged start as non-interactive and escalate to interactive if necessary.
62
+ * Important: For this to work at the moment, the instance needs to be using SMART protection
63
+ * as we need both keys (visible and invisible) to be present.
64
+ */
65
+ public async managedChallenge ( clerk : Clerk ) {
66
+ const { captchaSiteKey, canUseCaptcha, captchaURL, captchaWidgetType, captchaProvider, captchaPublicKeyInvisible } =
67
+ retrieveCaptchaInfo ( clerk ) ;
68
+
69
+ if ( canUseCaptcha && captchaSiteKey && captchaURL && captchaPublicKeyInvisible ) {
70
+ return getCaptchaToken ( {
71
+ siteKey : captchaSiteKey ,
72
+ widgetType : captchaWidgetType ,
73
+ invisibleSiteKey : captchaPublicKeyInvisible ,
74
+ scriptUrl : captchaURL ,
75
+ captchaProvider,
76
+ modalWrapperQuerySelector : '#cl-modal-captcha-wrapper' ,
77
+ modalContainerQuerySelector : '#cl-modal-captcha-container' ,
78
+ openModal : ( ) => clerk . __internal_openBlankCaptchaModal ( ) ,
79
+ closeModal : ( ) => clerk . __internal_closeBlankCaptchaModal ( ) ,
80
+ } ) ;
81
+ }
82
+
83
+ return { } ;
84
+ }
23
85
}
24
86
25
87
export const fraudProtection = new FraudProtectionService ( ) ;
0 commit comments