@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.
Files changed (70) hide show
  1. package/.depcheckrc +20 -0
  2. package/.eslintrc.js +21 -0
  3. package/README.md +1 -0
  4. package/env.d.ts +12 -0
  5. package/package.json +50 -0
  6. package/project.json +42 -0
  7. package/src/challenge-solvers/createChallengeSolverService.ts +64 -0
  8. package/src/challenge-solvers/createHashcashMockSolver.ts +39 -0
  9. package/src/challenge-solvers/createHashcashSolver.test.ts +385 -0
  10. package/src/challenge-solvers/createHashcashSolver.ts +255 -0
  11. package/src/challenge-solvers/createNoneMockSolver.ts +11 -0
  12. package/src/challenge-solvers/createTurnstileMockSolver.ts +30 -0
  13. package/src/challenge-solvers/createTurnstileSolver.ts +353 -0
  14. package/src/challenge-solvers/hashcash/core.native.ts +34 -0
  15. package/src/challenge-solvers/hashcash/core.test.ts +314 -0
  16. package/src/challenge-solvers/hashcash/core.ts +35 -0
  17. package/src/challenge-solvers/hashcash/core.web.ts +123 -0
  18. package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.test.ts +195 -0
  19. package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.ts +120 -0
  20. package/src/challenge-solvers/hashcash/shared.ts +70 -0
  21. package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.native.ts +22 -0
  22. package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.ts +22 -0
  23. package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.web.ts +212 -0
  24. package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.native.ts +16 -0
  25. package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.ts +16 -0
  26. package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.web.ts +97 -0
  27. package/src/challenge-solvers/hashcash/worker/hashcash.worker.ts +91 -0
  28. package/src/challenge-solvers/hashcash/worker/types.ts +69 -0
  29. package/src/challenge-solvers/turnstileErrors.ts +49 -0
  30. package/src/challenge-solvers/turnstileScriptLoader.ts +325 -0
  31. package/src/challenge-solvers/turnstileSolver.integration.test.ts +331 -0
  32. package/src/challenge-solvers/types.ts +55 -0
  33. package/src/challengeFlow.integration.test.ts +627 -0
  34. package/src/device-id/createDeviceIdService.ts +19 -0
  35. package/src/device-id/types.ts +11 -0
  36. package/src/index.ts +137 -0
  37. package/src/lux-identifier/createLuxIdentifierService.ts +19 -0
  38. package/src/lux-identifier/luxIdentifierQuery.ts +20 -0
  39. package/src/lux-identifier/types.ts +11 -0
  40. package/src/oauth-service/createOAuthService.ts +125 -0
  41. package/src/oauth-service/types.ts +104 -0
  42. package/src/performance/createNoopPerformanceTracker.ts +12 -0
  43. package/src/performance/createPerformanceTracker.ts +43 -0
  44. package/src/performance/index.ts +7 -0
  45. package/src/performance/types.ts +11 -0
  46. package/src/session-initialization/createSessionInitializationService.test.ts +557 -0
  47. package/src/session-initialization/createSessionInitializationService.ts +193 -0
  48. package/src/session-initialization/sessionErrors.ts +32 -0
  49. package/src/session-repository/createSessionClient.ts +10 -0
  50. package/src/session-repository/createSessionRepository.test.ts +313 -0
  51. package/src/session-repository/createSessionRepository.ts +242 -0
  52. package/src/session-repository/errors.ts +22 -0
  53. package/src/session-repository/types.ts +289 -0
  54. package/src/session-service/createNoopSessionService.ts +24 -0
  55. package/src/session-service/createSessionService.test.ts +388 -0
  56. package/src/session-service/createSessionService.ts +61 -0
  57. package/src/session-service/types.ts +59 -0
  58. package/src/session-storage/createSessionStorage.ts +28 -0
  59. package/src/session-storage/types.ts +15 -0
  60. package/src/session.integration.test.ts +480 -0
  61. package/src/sessionLifecycle.integration.test.ts +264 -0
  62. package/src/test-utils/createLocalCookieTransport.ts +52 -0
  63. package/src/test-utils/createLocalHeaderTransport.ts +45 -0
  64. package/src/test-utils/mocks.ts +122 -0
  65. package/src/test-utils.ts +200 -0
  66. package/tsconfig.json +19 -0
  67. package/tsconfig.lint.json +8 -0
  68. package/tsconfig.spec.json +8 -0
  69. package/vitest.config.ts +20 -0
  70. 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 }