@luxexchange/sessions 1.0.0
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/.depcheckrc +20 -0
- package/.eslintrc.js +21 -0
- package/README.md +1 -0
- package/env.d.ts +12 -0
- package/package.json +50 -0
- package/project.json +42 -0
- package/src/challenge-solvers/createChallengeSolverService.ts +64 -0
- package/src/challenge-solvers/createHashcashMockSolver.ts +39 -0
- package/src/challenge-solvers/createHashcashSolver.test.ts +385 -0
- package/src/challenge-solvers/createHashcashSolver.ts +255 -0
- package/src/challenge-solvers/createNoneMockSolver.ts +11 -0
- package/src/challenge-solvers/createTurnstileMockSolver.ts +30 -0
- package/src/challenge-solvers/createTurnstileSolver.ts +353 -0
- package/src/challenge-solvers/hashcash/core.native.ts +34 -0
- package/src/challenge-solvers/hashcash/core.test.ts +314 -0
- package/src/challenge-solvers/hashcash/core.ts +35 -0
- package/src/challenge-solvers/hashcash/core.web.ts +123 -0
- package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.test.ts +195 -0
- package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.ts +120 -0
- package/src/challenge-solvers/hashcash/shared.ts +70 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.native.ts +22 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.ts +22 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.web.ts +212 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.native.ts +16 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.ts +16 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.web.ts +97 -0
- package/src/challenge-solvers/hashcash/worker/hashcash.worker.ts +91 -0
- package/src/challenge-solvers/hashcash/worker/types.ts +69 -0
- package/src/challenge-solvers/turnstileErrors.ts +49 -0
- package/src/challenge-solvers/turnstileScriptLoader.ts +325 -0
- package/src/challenge-solvers/turnstileSolver.integration.test.ts +331 -0
- package/src/challenge-solvers/types.ts +55 -0
- package/src/challengeFlow.integration.test.ts +627 -0
- package/src/device-id/createDeviceIdService.ts +19 -0
- package/src/device-id/types.ts +11 -0
- package/src/index.ts +137 -0
- package/src/lux-identifier/createLuxIdentifierService.ts +19 -0
- package/src/lux-identifier/luxIdentifierQuery.ts +20 -0
- package/src/lux-identifier/types.ts +11 -0
- package/src/oauth-service/createOAuthService.ts +125 -0
- package/src/oauth-service/types.ts +104 -0
- package/src/performance/createNoopPerformanceTracker.ts +12 -0
- package/src/performance/createPerformanceTracker.ts +43 -0
- package/src/performance/index.ts +7 -0
- package/src/performance/types.ts +11 -0
- package/src/session-initialization/createSessionInitializationService.test.ts +557 -0
- package/src/session-initialization/createSessionInitializationService.ts +193 -0
- package/src/session-initialization/sessionErrors.ts +32 -0
- package/src/session-repository/createSessionClient.ts +10 -0
- package/src/session-repository/createSessionRepository.test.ts +313 -0
- package/src/session-repository/createSessionRepository.ts +242 -0
- package/src/session-repository/errors.ts +22 -0
- package/src/session-repository/types.ts +289 -0
- package/src/session-service/createNoopSessionService.ts +24 -0
- package/src/session-service/createSessionService.test.ts +388 -0
- package/src/session-service/createSessionService.ts +61 -0
- package/src/session-service/types.ts +59 -0
- package/src/session-storage/createSessionStorage.ts +28 -0
- package/src/session-storage/types.ts +15 -0
- package/src/session.integration.test.ts +480 -0
- package/src/sessionLifecycle.integration.test.ts +264 -0
- package/src/test-utils/createLocalCookieTransport.ts +52 -0
- package/src/test-utils/createLocalHeaderTransport.ts +45 -0
- package/src/test-utils/mocks.ts +122 -0
- package/src/test-utils.ts +200 -0
- package/tsconfig.json +19 -0
- package/tsconfig.lint.json +8 -0
- package/tsconfig.spec.json +8 -0
- package/vitest.config.ts +20 -0
- package/vitest.integration.config.ts +14 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { HashcashChallenge, ProofResult } from '@luxexchange/sessions/src/challenge-solvers/hashcash/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parameters for finding a hashcash proof
|
|
5
|
+
*/
|
|
6
|
+
interface FindProofParams {
|
|
7
|
+
challenge: HashcashChallenge
|
|
8
|
+
rangeStart?: number
|
|
9
|
+
rangeSize?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Context for creating a hashcash worker channel.
|
|
14
|
+
* Apps inject the Worker instance getter to control Worker instantiation.
|
|
15
|
+
*/
|
|
16
|
+
interface CreateHashcashWorkerChannelContext {
|
|
17
|
+
/**
|
|
18
|
+
* Returns a Worker instance for hashcash proof-of-work.
|
|
19
|
+
* Called once on first channel creation (singleton pattern).
|
|
20
|
+
*/
|
|
21
|
+
getWorker: () => Worker
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The API exposed by the hashcash worker.
|
|
26
|
+
* This is the contract between main thread and worker.
|
|
27
|
+
*/
|
|
28
|
+
interface HashcashWorkerAPI {
|
|
29
|
+
/**
|
|
30
|
+
* Find a proof-of-work solution for the given challenge.
|
|
31
|
+
* Returns null if no solution found within range or if cancelled.
|
|
32
|
+
*/
|
|
33
|
+
findProof(params: FindProofParams): Promise<ProofResult | null>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Cancel any in-progress proof search.
|
|
37
|
+
*/
|
|
38
|
+
cancel(): Promise<void>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A channel to communicate with a hashcash worker.
|
|
43
|
+
* Platform-specific implementations create these.
|
|
44
|
+
*/
|
|
45
|
+
interface HashcashWorkerChannel {
|
|
46
|
+
/**
|
|
47
|
+
* The worker API - call methods to execute on worker thread
|
|
48
|
+
*/
|
|
49
|
+
api: HashcashWorkerAPI
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Terminate the worker and clean up resources
|
|
53
|
+
*/
|
|
54
|
+
terminate(): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Factory function that creates a HashcashWorkerChannel.
|
|
59
|
+
* Injected into the solver to enable platform-specific implementations.
|
|
60
|
+
*/
|
|
61
|
+
type HashcashWorkerChannelFactory = () => HashcashWorkerChannel
|
|
62
|
+
|
|
63
|
+
export type {
|
|
64
|
+
CreateHashcashWorkerChannelContext,
|
|
65
|
+
FindProofParams,
|
|
66
|
+
HashcashWorkerAPI,
|
|
67
|
+
HashcashWorkerChannel,
|
|
68
|
+
HashcashWorkerChannelFactory,
|
|
69
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SessionError } from '@luxexchange/sessions/src/session-initialization/sessionErrors'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when Turnstile script fails to load
|
|
5
|
+
*/
|
|
6
|
+
export class TurnstileScriptLoadError extends SessionError {
|
|
7
|
+
constructor(message: string, cause?: unknown) {
|
|
8
|
+
super(`Turnstile script load error: ${message}`, 'TurnstileScriptLoadError')
|
|
9
|
+
if (cause) {
|
|
10
|
+
this.cause = cause
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Error thrown when Turnstile API is not available
|
|
17
|
+
*/
|
|
18
|
+
export class TurnstileApiNotAvailableError extends SessionError {
|
|
19
|
+
constructor() {
|
|
20
|
+
super('Turnstile API not available', 'TurnstileApiNotAvailableError')
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Error thrown when Turnstile challenge times out
|
|
26
|
+
*/
|
|
27
|
+
export class TurnstileTimeoutError extends SessionError {
|
|
28
|
+
constructor(timeoutMs: number) {
|
|
29
|
+
super(`Turnstile challenge timed out after ${timeoutMs}ms`, 'TurnstileTimeoutError')
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Error thrown when Turnstile returns an error
|
|
35
|
+
*/
|
|
36
|
+
export class TurnstileError extends SessionError {
|
|
37
|
+
constructor(errorCode: string) {
|
|
38
|
+
super(`Turnstile error: ${errorCode}`, 'TurnstileError')
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Error thrown when Turnstile token expires
|
|
44
|
+
*/
|
|
45
|
+
export class TurnstileTokenExpiredError extends SessionError {
|
|
46
|
+
constructor() {
|
|
47
|
+
super('Turnstile token expired', 'TurnstileTokenExpiredError')
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { TurnstileScriptLoadError } from '@luxexchange/sessions/src/challenge-solvers/turnstileErrors'
|
|
2
|
+
import type { TurnstileScriptOptions } from '@luxexchange/sessions/src/challenge-solvers/types'
|
|
3
|
+
|
|
4
|
+
type TurnstileState = 'unloaded' | 'loading' | 'ready'
|
|
5
|
+
|
|
6
|
+
interface TurnstileLoadPromise {
|
|
7
|
+
resolve: (value?: void | PromiseLike<void>) => void
|
|
8
|
+
reject: (reason?: unknown) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Global state machine for Turnstile script loading
|
|
13
|
+
* Singleton pattern ensures shared state across solver instances
|
|
14
|
+
*/
|
|
15
|
+
class TurnstileScriptState {
|
|
16
|
+
private state: TurnstileState = 'unloaded'
|
|
17
|
+
private loadPromise: Promise<void>
|
|
18
|
+
private loadResolvers: TurnstileLoadPromise | null = null
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
this.loadPromise = new Promise((resolve, reject) => {
|
|
22
|
+
this.loadResolvers = { resolve, reject }
|
|
23
|
+
// If already ready, resolve immediately
|
|
24
|
+
if (this.state === 'ready' && window.turnstile) {
|
|
25
|
+
resolve(undefined)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getState(): TurnstileState {
|
|
31
|
+
return this.state
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setState(newState: TurnstileState): void {
|
|
35
|
+
this.state = newState
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getLoadPromise(): Promise<void> {
|
|
39
|
+
return this.loadPromise
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resolveLoad(): void {
|
|
43
|
+
if (this.loadResolvers) {
|
|
44
|
+
this.loadResolvers.resolve(undefined)
|
|
45
|
+
this.loadResolvers = null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
rejectLoad(reason: unknown): void {
|
|
50
|
+
if (this.loadResolvers) {
|
|
51
|
+
this.loadResolvers.reject(reason)
|
|
52
|
+
this.loadResolvers = null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reset state for testing purposes
|
|
58
|
+
*/
|
|
59
|
+
reset(): void {
|
|
60
|
+
this.state = 'unloaded'
|
|
61
|
+
this.loadPromise = new Promise((resolve, reject) => {
|
|
62
|
+
this.loadResolvers = { resolve, reject }
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Singleton instance
|
|
68
|
+
const turnstileState = new TurnstileScriptState()
|
|
69
|
+
|
|
70
|
+
const DEFAULT_SCRIPT_ID = 'cf-turnstile-script'
|
|
71
|
+
const DEFAULT_ONLOAD_NAME = 'onloadTurnstileCallback'
|
|
72
|
+
const TURNSTILE_SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Observes script loading using MutationObserver
|
|
76
|
+
* More reliable than polling for detecting when script is loaded
|
|
77
|
+
*/
|
|
78
|
+
function observeScriptLoad(scriptId: string, timeoutMs = 10000): Promise<void> {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
// Check if script already exists and is loaded
|
|
81
|
+
const existingScript = document.getElementById(scriptId) as HTMLScriptElement | null
|
|
82
|
+
if (existingScript && window.turnstile) {
|
|
83
|
+
resolve(undefined)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If script exists but not loaded yet, set up load listener
|
|
88
|
+
if (existingScript && !window.turnstile) {
|
|
89
|
+
// Track intervals so we can clean them up from any code path
|
|
90
|
+
let loadCheckIntervalId: ReturnType<typeof setInterval> | null = null
|
|
91
|
+
let raceCheckIntervalId: ReturnType<typeof setInterval> | null = null
|
|
92
|
+
|
|
93
|
+
const cleanup = (): void => {
|
|
94
|
+
if (loadCheckIntervalId) {
|
|
95
|
+
clearInterval(loadCheckIntervalId)
|
|
96
|
+
}
|
|
97
|
+
if (raceCheckIntervalId) {
|
|
98
|
+
clearInterval(raceCheckIntervalId)
|
|
99
|
+
}
|
|
100
|
+
existingScript.removeEventListener('load', onScriptLoad)
|
|
101
|
+
existingScript.removeEventListener('error', onScriptError)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const onScriptLoad = (): void => {
|
|
105
|
+
// Wait a bit for turnstile to initialize
|
|
106
|
+
loadCheckIntervalId = setInterval(() => {
|
|
107
|
+
if (window.turnstile) {
|
|
108
|
+
cleanup()
|
|
109
|
+
resolve(undefined)
|
|
110
|
+
}
|
|
111
|
+
}, 50)
|
|
112
|
+
|
|
113
|
+
// Timeout if turnstile doesn't initialize
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
cleanup()
|
|
116
|
+
if (!window.turnstile) {
|
|
117
|
+
reject(new TurnstileScriptLoadError('Turnstile did not initialize after script load'))
|
|
118
|
+
} else {
|
|
119
|
+
resolve(undefined)
|
|
120
|
+
}
|
|
121
|
+
}, timeoutMs)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const onScriptError = (): void => {
|
|
125
|
+
cleanup()
|
|
126
|
+
reject(new TurnstileScriptLoadError('Failed to load Turnstile script'))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Script exists but not loaded yet, listen for load event
|
|
130
|
+
// For scripts with defer/async, the load event will fire when ready
|
|
131
|
+
existingScript.addEventListener('load', onScriptLoad)
|
|
132
|
+
existingScript.addEventListener('error', onScriptError)
|
|
133
|
+
|
|
134
|
+
// Also check periodically in case the script loaded before we attached listeners
|
|
135
|
+
// (race condition with defer/async scripts)
|
|
136
|
+
raceCheckIntervalId = setInterval(() => {
|
|
137
|
+
if (window.turnstile) {
|
|
138
|
+
cleanup()
|
|
139
|
+
resolve(undefined)
|
|
140
|
+
}
|
|
141
|
+
}, 50)
|
|
142
|
+
|
|
143
|
+
// Timeout fallback
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
cleanup()
|
|
146
|
+
if (!window.turnstile) {
|
|
147
|
+
reject(new TurnstileScriptLoadError('Turnstile script loaded but API not available'))
|
|
148
|
+
} else {
|
|
149
|
+
resolve(undefined)
|
|
150
|
+
}
|
|
151
|
+
}, timeoutMs)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let resolved = false
|
|
156
|
+
const timeoutId = setTimeout(() => {
|
|
157
|
+
if (!resolved) {
|
|
158
|
+
resolved = true
|
|
159
|
+
reject(new TurnstileScriptLoadError(`Script observation timeout after ${timeoutMs}ms`))
|
|
160
|
+
}
|
|
161
|
+
}, timeoutMs)
|
|
162
|
+
|
|
163
|
+
const observer = new MutationObserver(() => {
|
|
164
|
+
const script = document.getElementById(scriptId) as HTMLScriptElement | null
|
|
165
|
+
if (script && window.turnstile) {
|
|
166
|
+
if (!resolved) {
|
|
167
|
+
resolved = true
|
|
168
|
+
clearTimeout(timeoutId)
|
|
169
|
+
observer.disconnect()
|
|
170
|
+
resolve(undefined)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Start observing
|
|
176
|
+
observer.observe(document.head, {
|
|
177
|
+
childList: true,
|
|
178
|
+
subtree: true,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Also check immediately in case script was added synchronously
|
|
182
|
+
const scriptCheck = document.getElementById(scriptId) as HTMLScriptElement | null
|
|
183
|
+
if (scriptCheck && window.turnstile) {
|
|
184
|
+
resolved = true
|
|
185
|
+
clearTimeout(timeoutId)
|
|
186
|
+
observer.disconnect()
|
|
187
|
+
resolve(undefined)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Injects Turnstile script with CSP-compliant options
|
|
195
|
+
*/
|
|
196
|
+
function injectTurnstileScript(options: TurnstileScriptOptions = {}): void {
|
|
197
|
+
const scriptId = options.id || DEFAULT_SCRIPT_ID
|
|
198
|
+
const onLoadCallbackName = options.onLoadCallbackName || DEFAULT_ONLOAD_NAME
|
|
199
|
+
|
|
200
|
+
// Check if script already exists
|
|
201
|
+
if (document.getElementById(scriptId)) {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Set up global onload callback
|
|
206
|
+
// Use unknown first to avoid type errors
|
|
207
|
+
const windowWithCallback = window as unknown as Record<string, unknown>
|
|
208
|
+
windowWithCallback[onLoadCallbackName] = (): void => {
|
|
209
|
+
turnstileState.setState('ready')
|
|
210
|
+
turnstileState.resolveLoad()
|
|
211
|
+
// Clean up callback
|
|
212
|
+
delete windowWithCallback[onLoadCallbackName]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Create script element
|
|
216
|
+
const script = document.createElement('script')
|
|
217
|
+
script.id = scriptId
|
|
218
|
+
script.src = TURNSTILE_SCRIPT_URL
|
|
219
|
+
|
|
220
|
+
// Apply CSP-compliant options
|
|
221
|
+
if (options.nonce) {
|
|
222
|
+
script.nonce = options.nonce
|
|
223
|
+
}
|
|
224
|
+
if (options.defer !== undefined) {
|
|
225
|
+
script.defer = options.defer
|
|
226
|
+
}
|
|
227
|
+
if (options.async !== undefined) {
|
|
228
|
+
script.async = options.async
|
|
229
|
+
}
|
|
230
|
+
if (options.crossOrigin) {
|
|
231
|
+
script.crossOrigin = options.crossOrigin
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Track interval so it can be cleaned up from timeout
|
|
235
|
+
let checkIntervalId: ReturnType<typeof setInterval> | null = null
|
|
236
|
+
|
|
237
|
+
// Set onload callback via script attribute for CSP compliance
|
|
238
|
+
script.onload = (): void => {
|
|
239
|
+
// Wait for turnstile to actually be available
|
|
240
|
+
// The global callback will handle state transition
|
|
241
|
+
checkIntervalId = setInterval(() => {
|
|
242
|
+
if (window.turnstile) {
|
|
243
|
+
if (checkIntervalId) {
|
|
244
|
+
clearInterval(checkIntervalId)
|
|
245
|
+
}
|
|
246
|
+
// If global callback hasn't fired, resolve manually
|
|
247
|
+
if (turnstileState.getState() !== 'ready') {
|
|
248
|
+
turnstileState.setState('ready')
|
|
249
|
+
turnstileState.resolveLoad()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}, 50)
|
|
253
|
+
|
|
254
|
+
// Fallback timeout
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
if (checkIntervalId) {
|
|
257
|
+
clearInterval(checkIntervalId)
|
|
258
|
+
}
|
|
259
|
+
// Only reject if BOTH conditions are true: state isn't ready AND turnstile doesn't exist.
|
|
260
|
+
// If turnstile exists but state isn't ready, the interval will handle it.
|
|
261
|
+
// If state is ready, the global callback already resolved.
|
|
262
|
+
if (turnstileState.getState() !== 'ready' && !window.turnstile) {
|
|
263
|
+
turnstileState.rejectLoad(new TurnstileScriptLoadError('Turnstile did not initialize after script load'))
|
|
264
|
+
}
|
|
265
|
+
}, 5000)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
script.onerror = (): void => {
|
|
269
|
+
turnstileState.setState('unloaded')
|
|
270
|
+
turnstileState.rejectLoad(new TurnstileScriptLoadError('Failed to load Turnstile script'))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Inject script
|
|
274
|
+
document.head.appendChild(script)
|
|
275
|
+
turnstileState.setState('loading')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Ensures Turnstile script is loaded, using state machine for coordination
|
|
280
|
+
*/
|
|
281
|
+
async function ensureTurnstileScript(scriptOptions: TurnstileScriptOptions = {}): Promise<void> {
|
|
282
|
+
// If already ready, return immediately
|
|
283
|
+
if (turnstileState.getState() === 'ready' && window.turnstile) {
|
|
284
|
+
return Promise.resolve()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check if script exists in DOM (including preloaded scripts)
|
|
288
|
+
const scriptId = scriptOptions.id || DEFAULT_SCRIPT_ID
|
|
289
|
+
const existingScript = document.getElementById(scriptId) as HTMLScriptElement | null
|
|
290
|
+
|
|
291
|
+
if (existingScript) {
|
|
292
|
+
// Script exists in DOM - check if it's already loaded
|
|
293
|
+
if (window.turnstile) {
|
|
294
|
+
turnstileState.setState('ready')
|
|
295
|
+
turnstileState.resolveLoad()
|
|
296
|
+
return Promise.resolve()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Script exists but not loaded yet - observe for loading
|
|
300
|
+
// This handles preloaded scripts with defer/async
|
|
301
|
+
turnstileState.setState('loading')
|
|
302
|
+
return observeScriptLoad(scriptId).then(() => {
|
|
303
|
+
turnstileState.setState('ready')
|
|
304
|
+
turnstileState.resolveLoad()
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// If already loading (from a previous call), wait for existing promise
|
|
309
|
+
if (turnstileState.getState() === 'loading') {
|
|
310
|
+
return turnstileState.getLoadPromise()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Script doesn't exist - inject it
|
|
314
|
+
injectTurnstileScript(scriptOptions)
|
|
315
|
+
return turnstileState.getLoadPromise()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Reset turnstile state for testing purposes
|
|
320
|
+
*/
|
|
321
|
+
function resetTurnstileState(): void {
|
|
322
|
+
turnstileState.reset()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export { ensureTurnstileScript, observeScriptLoad, turnstileState, resetTurnstileState }
|