@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,557 @@
1
+ import type { PerformanceTracker } from '@luxexchange/sessions/src/performance/types'
2
+ import { createSessionInitializationService } from '@luxexchange/sessions/src/session-initialization/createSessionInitializationService'
3
+ import { ChallengeType } from '@luxexchange/sessions/src/session-service/types'
4
+ import {
5
+ createMockChallengeSolverService,
6
+ createMockSessionService,
7
+ TestScenarios,
8
+ } from '@luxexchange/sessions/src/test-utils/mocks'
9
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
10
+
11
+ // Mock performance tracker for testing
12
+ function createMockPerformanceTracker(): PerformanceTracker {
13
+ let time = 0
14
+ return {
15
+ now: (): number => {
16
+ time += 100
17
+ return time
18
+ },
19
+ }
20
+ }
21
+
22
+ describe('createSessionInitializationService', () => {
23
+ let sessionService: ReturnType<typeof createMockSessionService>
24
+ let challengeSolverService: ReturnType<typeof createMockChallengeSolverService>
25
+ let mockPerformanceTracker: PerformanceTracker
26
+
27
+ beforeEach(() => {
28
+ sessionService = createMockSessionService()
29
+ challengeSolverService = createMockChallengeSolverService()
30
+ mockPerformanceTracker = createMockPerformanceTracker()
31
+ })
32
+
33
+ describe('initialize()', () => {
34
+ describe('session initialization', () => {
35
+ it('initializes session without challenge when not required', async () => {
36
+ // Setup
37
+ TestScenarios.withNoChallenge(sessionService)
38
+
39
+ const service = createSessionInitializationService({
40
+ getSessionService: () => sessionService,
41
+ challengeSolverService,
42
+ performanceTracker: mockPerformanceTracker,
43
+ })
44
+
45
+ // Execute
46
+ const result = await service.initialize()
47
+
48
+ // Verify behavior
49
+ expect(result).toEqual({
50
+ sessionId: 'new-session-111',
51
+ })
52
+
53
+ // Verify correct flow
54
+ expect(sessionService.initSession).toHaveBeenCalled()
55
+ expect(sessionService.requestChallenge).not.toHaveBeenCalled()
56
+ })
57
+
58
+ it('completes full challenge flow when required', async () => {
59
+ // Setup
60
+ TestScenarios.withChallengeRequired(sessionService, ChallengeType.TURNSTILE)
61
+
62
+ const service = createSessionInitializationService({
63
+ getSessionService: () => sessionService,
64
+ challengeSolverService,
65
+ performanceTracker: mockPerformanceTracker,
66
+ getIsSessionUpgradeAutoEnabled: () => true,
67
+ })
68
+
69
+ // Execute
70
+ const result = await service.initialize()
71
+
72
+ // Verify behavior
73
+ expect(result).toEqual({
74
+ sessionId: 'new-session-222',
75
+ })
76
+
77
+ // Verify complete flow executed
78
+ expect(sessionService.initSession).toHaveBeenCalled()
79
+ expect(sessionService.requestChallenge).toHaveBeenCalled()
80
+ expect(sessionService.verifySession).toHaveBeenCalled()
81
+
82
+ // Verify solver was used
83
+ expect(challengeSolverService.getSolver).toHaveBeenCalledWith(ChallengeType.TURNSTILE)
84
+ })
85
+
86
+ it('uses correct solver for challenge type', async () => {
87
+ // Setup with Hashcash challenge
88
+ TestScenarios.withChallengeRequired(sessionService, ChallengeType.HASHCASH)
89
+
90
+ const service = createSessionInitializationService({
91
+ getSessionService: () => sessionService,
92
+ challengeSolverService,
93
+ performanceTracker: mockPerformanceTracker,
94
+ getIsSessionUpgradeAutoEnabled: () => true,
95
+ })
96
+
97
+ // Execute
98
+ await service.initialize()
99
+
100
+ // Verify correct solver was requested
101
+ expect(challengeSolverService.getSolver).toHaveBeenCalledWith(ChallengeType.HASHCASH)
102
+ })
103
+
104
+ it('passes solution to upgrade session', async () => {
105
+ // Setup
106
+ TestScenarios.withChallengeRequired(sessionService)
107
+ const expectedSolution = 'test-solution-xyz'
108
+ const mockSolver = {
109
+ solve: vi.fn().mockResolvedValue(expectedSolution),
110
+ }
111
+ challengeSolverService.getSolver = vi.fn().mockReturnValue(mockSolver)
112
+
113
+ const service = createSessionInitializationService({
114
+ getSessionService: () => sessionService,
115
+ challengeSolverService,
116
+ performanceTracker: mockPerformanceTracker,
117
+ getIsSessionUpgradeAutoEnabled: () => true,
118
+ })
119
+
120
+ // Execute
121
+ await service.initialize()
122
+
123
+ // Verify solution was passed correctly
124
+ expect(sessionService.verifySession).toHaveBeenCalledWith({
125
+ solution: expectedSolution,
126
+ challengeId: 'challenge-333',
127
+ challengeType: ChallengeType.TURNSTILE,
128
+ })
129
+ })
130
+ })
131
+
132
+ describe('when server requests retry', () => {
133
+ it('retries challenge when server requests', async () => {
134
+ // Setup
135
+ TestScenarios.withChallengeRequired(sessionService)
136
+ TestScenarios.withServerRetry(sessionService, 1) // Retry once then succeed
137
+
138
+ const service = createSessionInitializationService({
139
+ getSessionService: () => sessionService,
140
+ challengeSolverService,
141
+ performanceTracker: mockPerformanceTracker,
142
+ getIsSessionUpgradeAutoEnabled: () => true,
143
+ })
144
+
145
+ // Execute
146
+ const result = await service.initialize()
147
+
148
+ // Verify success
149
+ expect(result.sessionId).toBe('new-session-222')
150
+
151
+ // Verify retry happened (challenge requested twice)
152
+ expect(sessionService.requestChallenge).toHaveBeenCalledTimes(2)
153
+ expect(sessionService.verifySession).toHaveBeenCalledTimes(2)
154
+ })
155
+
156
+ it('fails after maximum retry attempts', async () => {
157
+ // Setup - server always requests retry
158
+ TestScenarios.withChallengeRequired(sessionService)
159
+ vi.mocked(sessionService.verifySession).mockResolvedValue({ retry: true })
160
+
161
+ const service = createSessionInitializationService({
162
+ getSessionService: () => sessionService,
163
+ challengeSolverService,
164
+ performanceTracker: mockPerformanceTracker,
165
+ getIsSessionUpgradeAutoEnabled: () => true,
166
+ maxChallengeRetries: 2,
167
+ })
168
+
169
+ // Execute and verify failure
170
+ await expect(service.initialize()).rejects.toThrow(
171
+ 'Maximum challenge retry attempts (2) exceeded after 3 attempts',
172
+ )
173
+
174
+ // Verify correct number of attempts
175
+ expect(sessionService.requestChallenge).toHaveBeenCalledTimes(3) // Initial + 2 retries
176
+ })
177
+
178
+ it('succeeds within retry limit', async () => {
179
+ // Setup - succeed on 3rd attempt
180
+ TestScenarios.withChallengeRequired(sessionService)
181
+ TestScenarios.withServerRetry(sessionService, 2)
182
+
183
+ const service = createSessionInitializationService({
184
+ getSessionService: () => sessionService,
185
+ challengeSolverService,
186
+ performanceTracker: mockPerformanceTracker,
187
+ getIsSessionUpgradeAutoEnabled: () => true,
188
+ maxChallengeRetries: 3,
189
+ })
190
+
191
+ // Execute
192
+ const result = await service.initialize()
193
+
194
+ // Verify success
195
+ expect(result.sessionId).toBe('new-session-222')
196
+ expect(sessionService.verifySession).toHaveBeenCalledTimes(3)
197
+ })
198
+ })
199
+
200
+ describe('error handling', () => {
201
+ it('throws when no solver available for challenge type', async () => {
202
+ // Setup
203
+ TestScenarios.withChallengeRequired(sessionService)
204
+ challengeSolverService.getSolver = vi.fn().mockReturnValue(null)
205
+
206
+ const service = createSessionInitializationService({
207
+ getSessionService: () => sessionService,
208
+ challengeSolverService,
209
+ performanceTracker: mockPerformanceTracker,
210
+ getIsSessionUpgradeAutoEnabled: () => true,
211
+ })
212
+
213
+ // Execute and verify
214
+ await expect(service.initialize()).rejects.toThrow('No solver available for challenge type: 1')
215
+ })
216
+
217
+ it('propagates sessionService errors', async () => {
218
+ // Setup
219
+ const error = new Error('Network error')
220
+ sessionService.initSession = vi.fn().mockRejectedValue(error)
221
+
222
+ const service = createSessionInitializationService({
223
+ getSessionService: () => sessionService,
224
+ challengeSolverService,
225
+ performanceTracker: mockPerformanceTracker,
226
+ })
227
+
228
+ // Execute and verify
229
+ await expect(service.initialize()).rejects.toThrow('Network error')
230
+ })
231
+
232
+ it('submits empty solution when solver throws, triggering verify-retry fallback', async () => {
233
+ // Setup: solver throws, but verifySession accepts empty solution (no retry)
234
+ TestScenarios.withChallengeRequired(sessionService)
235
+ const failingSolver = {
236
+ solve: vi.fn().mockRejectedValue(new Error('Solver failed')),
237
+ }
238
+ challengeSolverService.getSolver = vi.fn().mockReturnValue(failingSolver)
239
+
240
+ const service = createSessionInitializationService({
241
+ getSessionService: () => sessionService,
242
+ challengeSolverService,
243
+ performanceTracker: mockPerformanceTracker,
244
+ getIsSessionUpgradeAutoEnabled: () => true,
245
+ })
246
+
247
+ // Execute — should NOT throw; empty solution is submitted instead
248
+ await service.initialize()
249
+
250
+ // Verify empty solution was passed to verifySession
251
+ expect(sessionService.verifySession).toHaveBeenCalledWith({
252
+ solution: 'solver-failed',
253
+ challengeId: 'challenge-333',
254
+ challengeType: ChallengeType.TURNSTILE,
255
+ })
256
+ })
257
+
258
+ it('solver throws → empty verify triggers retry → different challenge type succeeds', async () => {
259
+ // Setup: solver throws on first challenge, verify says retry,
260
+ // second challenge uses a different solver that succeeds
261
+ TestScenarios.withChallengeRequired(sessionService)
262
+
263
+ // First call: solver throws; second call: solver succeeds
264
+ const failingSolver = { solve: vi.fn().mockRejectedValue(new Error('Turnstile domain error')) }
265
+ const successSolver = { solve: vi.fn().mockResolvedValue('hashcash-proof') }
266
+ challengeSolverService.getSolver = vi.fn().mockReturnValueOnce(failingSolver).mockReturnValueOnce(successSolver)
267
+
268
+ // First verify: retry; second verify: success
269
+ vi.mocked(sessionService.verifySession)
270
+ .mockResolvedValueOnce({ retry: true })
271
+ .mockResolvedValueOnce({ retry: false })
272
+
273
+ // Second challenge returns HASHCASH
274
+ vi.mocked(sessionService.requestChallenge)
275
+ .mockResolvedValueOnce({
276
+ challengeId: 'challenge-333',
277
+ challengeType: ChallengeType.TURNSTILE,
278
+ extra: {},
279
+ challengeData: { case: 'turnstile', value: { siteKey: 'test-sitekey', action: 'verify' } },
280
+ })
281
+ .mockResolvedValueOnce({
282
+ challengeId: 'challenge-444',
283
+ challengeType: ChallengeType.HASHCASH,
284
+ extra: {},
285
+ challengeData: { case: undefined },
286
+ })
287
+
288
+ const service = createSessionInitializationService({
289
+ getSessionService: () => sessionService,
290
+ challengeSolverService,
291
+ performanceTracker: mockPerformanceTracker,
292
+ getIsSessionUpgradeAutoEnabled: () => true,
293
+ })
294
+
295
+ await service.initialize()
296
+
297
+ // Verify flow: empty solution first, then real solution
298
+ expect(sessionService.verifySession).toHaveBeenCalledTimes(2)
299
+ expect(sessionService.verifySession).toHaveBeenNthCalledWith(1, {
300
+ solution: 'solver-failed',
301
+ challengeId: 'challenge-333',
302
+ challengeType: ChallengeType.TURNSTILE,
303
+ })
304
+ expect(sessionService.verifySession).toHaveBeenNthCalledWith(2, {
305
+ solution: 'hashcash-proof',
306
+ challengeId: 'challenge-444',
307
+ challengeType: ChallengeType.HASHCASH,
308
+ })
309
+ expect(sessionService.requestChallenge).toHaveBeenCalledTimes(2)
310
+ })
311
+
312
+ it('solver keeps throwing + verify keeps returning retry → respects maxRetries', async () => {
313
+ // Setup: solver always throws, verify always says retry → should exhaust retries
314
+ TestScenarios.withChallengeRequired(sessionService)
315
+ const failingSolver = { solve: vi.fn().mockRejectedValue(new Error('Solver always fails')) }
316
+ challengeSolverService.getSolver = vi.fn().mockReturnValue(failingSolver)
317
+ vi.mocked(sessionService.verifySession).mockResolvedValue({ retry: true })
318
+
319
+ const service = createSessionInitializationService({
320
+ getSessionService: () => sessionService,
321
+ challengeSolverService,
322
+ performanceTracker: mockPerformanceTracker,
323
+ getIsSessionUpgradeAutoEnabled: () => true,
324
+ maxChallengeRetries: 2,
325
+ })
326
+
327
+ await expect(service.initialize()).rejects.toThrow(
328
+ 'Maximum challenge retry attempts (2) exceeded after 3 attempts',
329
+ )
330
+
331
+ // Initial + 2 retries = 3 attempts
332
+ expect(sessionService.requestChallenge).toHaveBeenCalledTimes(3)
333
+ expect(sessionService.verifySession).toHaveBeenCalledTimes(3)
334
+ })
335
+ })
336
+
337
+ describe('analytics callbacks', () => {
338
+ it('fires onInitStarted when initialization begins', async () => {
339
+ const analytics = { onInitStarted: vi.fn() }
340
+ TestScenarios.withNoChallenge(sessionService)
341
+
342
+ const service = createSessionInitializationService({
343
+ getSessionService: () => sessionService,
344
+ challengeSolverService,
345
+ performanceTracker: mockPerformanceTracker,
346
+ analytics,
347
+ })
348
+
349
+ await service.initialize()
350
+
351
+ expect(analytics.onInitStarted).toHaveBeenCalledTimes(1)
352
+ })
353
+
354
+ it('reports needChallenge: false when no challenge required', async () => {
355
+ const analytics = { onInitCompleted: vi.fn() }
356
+ TestScenarios.withNoChallenge(sessionService)
357
+
358
+ const service = createSessionInitializationService({
359
+ getSessionService: () => sessionService,
360
+ challengeSolverService,
361
+ performanceTracker: mockPerformanceTracker,
362
+ analytics,
363
+ })
364
+
365
+ await service.initialize()
366
+
367
+ expect(analytics.onInitCompleted).toHaveBeenCalledWith(expect.objectContaining({ needChallenge: false }))
368
+ })
369
+
370
+ it('reports needChallenge: true when challenge required', async () => {
371
+ const analytics = { onInitCompleted: vi.fn() }
372
+ TestScenarios.withChallengeRequired(sessionService)
373
+
374
+ const service = createSessionInitializationService({
375
+ getSessionService: () => sessionService,
376
+ challengeSolverService,
377
+ performanceTracker: mockPerformanceTracker,
378
+ analytics,
379
+ })
380
+
381
+ await service.initialize()
382
+
383
+ expect(analytics.onInitCompleted).toHaveBeenCalledWith(expect.objectContaining({ needChallenge: true }))
384
+ })
385
+
386
+ it('fires onChallengeReceived with challenge details', async () => {
387
+ const analytics = { onChallengeReceived: vi.fn() }
388
+ TestScenarios.withChallengeRequired(sessionService, ChallengeType.HASHCASH)
389
+
390
+ const service = createSessionInitializationService({
391
+ getSessionService: () => sessionService,
392
+ challengeSolverService,
393
+ performanceTracker: mockPerformanceTracker,
394
+ getIsSessionUpgradeAutoEnabled: () => true,
395
+ analytics,
396
+ })
397
+
398
+ await service.initialize()
399
+
400
+ expect(analytics.onChallengeReceived).toHaveBeenCalledWith({
401
+ challengeType: String(ChallengeType.HASHCASH),
402
+ challengeId: 'challenge-333',
403
+ })
404
+ })
405
+
406
+ it('fires onVerifyCompleted on successful verification', async () => {
407
+ const analytics = { onVerifyCompleted: vi.fn() }
408
+ TestScenarios.withChallengeRequired(sessionService)
409
+
410
+ const service = createSessionInitializationService({
411
+ getSessionService: () => sessionService,
412
+ challengeSolverService,
413
+ performanceTracker: mockPerformanceTracker,
414
+ getIsSessionUpgradeAutoEnabled: () => true,
415
+ analytics,
416
+ })
417
+
418
+ await service.initialize()
419
+
420
+ expect(analytics.onVerifyCompleted).toHaveBeenCalledWith(
421
+ expect.objectContaining({ success: true, attemptNumber: 1 }),
422
+ )
423
+ })
424
+
425
+ it('tracks retry attempts through verification flow', async () => {
426
+ const analytics = { onVerifyCompleted: vi.fn() }
427
+ TestScenarios.withChallengeRequired(sessionService)
428
+ TestScenarios.withServerRetry(sessionService, 2) // Fail twice, succeed on 3rd
429
+
430
+ const service = createSessionInitializationService({
431
+ getSessionService: () => sessionService,
432
+ challengeSolverService,
433
+ performanceTracker: mockPerformanceTracker,
434
+ getIsSessionUpgradeAutoEnabled: () => true,
435
+ analytics,
436
+ })
437
+
438
+ await service.initialize()
439
+
440
+ // Should have been called 3 times (2 failures + 1 success)
441
+ expect(analytics.onVerifyCompleted).toHaveBeenCalledTimes(3)
442
+ expect(analytics.onVerifyCompleted).toHaveBeenNthCalledWith(
443
+ 1,
444
+ expect.objectContaining({ success: false, attemptNumber: 1 }),
445
+ )
446
+ expect(analytics.onVerifyCompleted).toHaveBeenNthCalledWith(
447
+ 2,
448
+ expect.objectContaining({ success: false, attemptNumber: 2 }),
449
+ )
450
+ expect(analytics.onVerifyCompleted).toHaveBeenNthCalledWith(
451
+ 3,
452
+ expect.objectContaining({ success: true, attemptNumber: 3 }),
453
+ )
454
+ })
455
+ })
456
+
457
+ describe('edge cases', () => {
458
+ it('handles empty session ID from initSession', async () => {
459
+ // Setup
460
+ sessionService.initSession = vi.fn().mockResolvedValue({
461
+ sessionId: undefined,
462
+ needChallenge: false,
463
+ extra: {},
464
+ })
465
+
466
+ const service = createSessionInitializationService({
467
+ getSessionService: () => sessionService,
468
+ challengeSolverService,
469
+ performanceTracker: mockPerformanceTracker,
470
+ })
471
+
472
+ // Execute
473
+ const result = await service.initialize()
474
+
475
+ // Verify behavior - should return null when sessionId is undefined
476
+ expect(result.sessionId).toBeNull()
477
+ })
478
+
479
+ it('handles None bot detection type', async () => {
480
+ // Setup
481
+ TestScenarios.withChallengeRequired(sessionService, ChallengeType.UNSPECIFIED)
482
+ const noneSolver = {
483
+ solve: vi.fn().mockResolvedValue(''),
484
+ }
485
+ challengeSolverService.getSolver = vi
486
+ .fn()
487
+ .mockImplementation((type) => (type === ChallengeType.UNSPECIFIED ? noneSolver : null))
488
+
489
+ const service = createSessionInitializationService({
490
+ getSessionService: () => sessionService,
491
+ challengeSolverService,
492
+ performanceTracker: mockPerformanceTracker,
493
+ getIsSessionUpgradeAutoEnabled: () => true,
494
+ })
495
+
496
+ // Execute
497
+ await service.initialize()
498
+
499
+ // Verify None type was handled
500
+ expect(challengeSolverService.getSolver).toHaveBeenCalledWith(ChallengeType.UNSPECIFIED)
501
+ expect(noneSolver.solve).toHaveBeenCalled()
502
+ })
503
+
504
+ it('does not complete challenge flow when auto-upgrade is disabled', async () => {
505
+ // Setup
506
+ TestScenarios.withChallengeRequired(sessionService)
507
+
508
+ const service = createSessionInitializationService({
509
+ getSessionService: () => sessionService,
510
+ challengeSolverService,
511
+ performanceTracker: mockPerformanceTracker,
512
+ getIsSessionUpgradeAutoEnabled: () => false,
513
+ })
514
+
515
+ // Execute
516
+ const result = await service.initialize()
517
+
518
+ // Verify behavior - session initialized but challenge not handled
519
+ // sessionId is returned regardless of challenge status (null if not provided by backend)
520
+ expect(result).toEqual({
521
+ sessionId: 'new-session-222',
522
+ })
523
+
524
+ // Verify challenge flow was NOT executed
525
+ expect(sessionService.initSession).toHaveBeenCalled()
526
+ expect(sessionService.requestChallenge).not.toHaveBeenCalled()
527
+ expect(sessionService.verifySession).not.toHaveBeenCalled()
528
+ })
529
+
530
+ it('defaults to disabled when callback is not provided', async () => {
531
+ // Setup
532
+ TestScenarios.withChallengeRequired(sessionService)
533
+
534
+ const service = createSessionInitializationService({
535
+ getSessionService: () => sessionService,
536
+ challengeSolverService,
537
+ performanceTracker: mockPerformanceTracker,
538
+ // No getIsSessionUpgradeAutoEnabled callback provided
539
+ })
540
+
541
+ // Execute
542
+ const result = await service.initialize()
543
+
544
+ // Verify behavior - defaults to disabled (opt-in)
545
+ // sessionId is returned regardless of challenge status
546
+ expect(result).toEqual({
547
+ sessionId: 'new-session-222',
548
+ })
549
+
550
+ // Verify challenge flow was NOT executed (default disabled)
551
+ expect(sessionService.initSession).toHaveBeenCalled()
552
+ expect(sessionService.requestChallenge).not.toHaveBeenCalled()
553
+ expect(sessionService.verifySession).not.toHaveBeenCalled()
554
+ })
555
+ })
556
+ })
557
+ })