@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,480 @@
1
+ import { createChallengeSolverService } from '@luxexchange/sessions/src/challenge-solvers/createChallengeSolverService'
2
+ import { createHashcashSolver } from '@luxexchange/sessions/src/challenge-solvers/createHashcashSolver'
3
+ import { createNoneMockSolver } from '@luxexchange/sessions/src/challenge-solvers/createNoneMockSolver'
4
+ import { createTurnstileMockSolver } from '@luxexchange/sessions/src/challenge-solvers/createTurnstileMockSolver'
5
+ import type { PerformanceTracker } from '@luxexchange/sessions/src/performance/types'
6
+ import { createSessionInitializationService } from '@luxexchange/sessions/src/session-initialization/createSessionInitializationService'
7
+ import { createSessionClient } from '@luxexchange/sessions/src/session-repository/createSessionClient'
8
+ import { createSessionRepository } from '@luxexchange/sessions/src/session-repository/createSessionRepository'
9
+ import { createSessionService } from '@luxexchange/sessions/src/session-service/createSessionService'
10
+ import type { SessionService } from '@luxexchange/sessions/src/session-service/types'
11
+ import { ChallengeType } from '@luxexchange/sessions/src/session-service/types'
12
+ import {
13
+ InMemoryDeviceIdService,
14
+ InMemorySessionStorage,
15
+ InMemoryLuxIdentifierService,
16
+ } from '@luxexchange/sessions/src/test-utils'
17
+ import {
18
+ createCookieJar,
19
+ createLocalCookieTransport,
20
+ } from '@luxexchange/sessions/src/test-utils/createLocalCookieTransport'
21
+ import { createLocalHeaderTransport } from '@luxexchange/sessions/src/test-utils/createLocalHeaderTransport'
22
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
23
+
24
+ // Mock performance tracker for testing
25
+ function createMockPerformanceTracker(): PerformanceTracker {
26
+ let time = 0
27
+ return {
28
+ now: (): number => {
29
+ time += 100
30
+ return time
31
+ },
32
+ }
33
+ }
34
+
35
+ const BACKEND_URL = 'https://entry-gateway.backend-staging.api.lux.org'
36
+ // const BACKEND_URL = 'http://localhost:3000'
37
+
38
+ // =============================================================================
39
+ // Web Platform Tests (Turnstile + Hashcash)
40
+ // =============================================================================
41
+ // Web uses Turnstile (browser CAPTCHA) first, then falls back to Hashcash
42
+ describe('Real Backend Integration - Web (Turnstile + Hashcash)', () => {
43
+ let sessionService: SessionService
44
+ let sessionStorage: InMemorySessionStorage
45
+ let cookieJar: Map<string, string>
46
+ let challengeSolverService: ReturnType<typeof createChallengeSolverService>
47
+
48
+ beforeAll(() => {
49
+ sessionStorage = new InMemorySessionStorage()
50
+ cookieJar = createCookieJar()
51
+
52
+ // Web uses Turnstile first (which will fail with mock), then falls back to hashcash
53
+ const solvers = new Map([
54
+ [ChallengeType.UNSPECIFIED, createNoneMockSolver()],
55
+ [ChallengeType.TURNSTILE, createTurnstileMockSolver()],
56
+ [ChallengeType.HASHCASH, createHashcashSolver({ performanceTracker: createMockPerformanceTracker() })],
57
+ ])
58
+
59
+ const transport = createLocalCookieTransport({ baseUrl: BACKEND_URL, cookieJar })
60
+ const sessionClient = createSessionClient({ transport })
61
+ const sessionRepository = createSessionRepository({ client: sessionClient })
62
+
63
+ sessionService = createSessionService({
64
+ sessionStorage,
65
+ deviceIdService: new InMemoryDeviceIdService(),
66
+ luxIdentifierService: new InMemoryLuxIdentifierService(),
67
+ sessionRepository,
68
+ })
69
+
70
+ challengeSolverService = createChallengeSolverService({ solvers })
71
+ })
72
+
73
+ beforeEach(async () => {
74
+ await sessionService.removeSession()
75
+ cookieJar.clear()
76
+ await sessionStorage.clear()
77
+ })
78
+
79
+ it('initializes session with cookie, empty response sessionId', async () => {
80
+ const manualInitService = createSessionInitializationService({
81
+ getSessionService: () => sessionService,
82
+ challengeSolverService,
83
+ performanceTracker: createMockPerformanceTracker(),
84
+ getIsSessionUpgradeAutoEnabled: () => false,
85
+ })
86
+
87
+ const result = await manualInitService.initialize()
88
+
89
+ // Web: Session ID is in the cookie, not in response body
90
+ expect(cookieJar.has('x-session-id')).toBe(true)
91
+ expect(cookieJar.get('x-session-id')).toBeTruthy()
92
+
93
+ // Web platform: sessionId is null (stored in cookie, not response body)
94
+ expect(result.sessionId).toBeNull()
95
+
96
+ // Session is NOT stored locally yet because challenge is needed
97
+ const sessionState = await sessionService.getSessionState()
98
+ expect(sessionState).toBeNull()
99
+ }, 30000)
100
+
101
+ it('receives Turnstile challenge first', async () => {
102
+ const manualInitService = createSessionInitializationService({
103
+ getSessionService: () => sessionService,
104
+ challengeSolverService,
105
+ performanceTracker: createMockPerformanceTracker(),
106
+ getIsSessionUpgradeAutoEnabled: () => false,
107
+ })
108
+
109
+ await manualInitService.initialize()
110
+
111
+ const challenge = await sessionService.requestChallenge()
112
+
113
+ // Web gets Turnstile first (browser-based CAPTCHA)
114
+ expect(challenge.challengeType).toBe(ChallengeType.TURNSTILE)
115
+ expect(challenge.challengeId).toBeTruthy()
116
+ }, 30000)
117
+
118
+ it('falls back to Hashcash after Turnstile fails', async () => {
119
+ const manualInitService = createSessionInitializationService({
120
+ getSessionService: () => sessionService,
121
+ challengeSolverService,
122
+ performanceTracker: createMockPerformanceTracker(),
123
+ getIsSessionUpgradeAutoEnabled: () => false,
124
+ })
125
+
126
+ await manualInitService.initialize()
127
+
128
+ // Get Turnstile challenge
129
+ const turnstileChallenge = await sessionService.requestChallenge()
130
+ expect(turnstileChallenge.challengeType).toBe(ChallengeType.TURNSTILE)
131
+
132
+ // Solve with mock Turnstile (will fail)
133
+ const turnstileSolver = challengeSolverService.getSolver(ChallengeType.TURNSTILE)
134
+ const turnstileSolution = await turnstileSolver?.solve({
135
+ challengeId: turnstileChallenge.challengeId,
136
+ challengeType: turnstileChallenge.challengeType,
137
+ extra: turnstileChallenge.extra,
138
+ })
139
+
140
+ // Submit Turnstile mock solution
141
+ const turnstileResult = await sessionService.verifySession({
142
+ solution: turnstileSolution || '',
143
+ challengeId: turnstileChallenge.challengeId,
144
+ challengeType: turnstileChallenge.challengeType,
145
+ })
146
+
147
+ // Turnstile failed, retry requested
148
+ expect(turnstileResult.retry).toBe(true)
149
+
150
+ // Request challenge again - now we get Hashcash
151
+ const hashcashChallenge = await sessionService.requestChallenge()
152
+ expect(hashcashChallenge.challengeType).toBe(ChallengeType.HASHCASH)
153
+ expect(hashcashChallenge.challengeId).toBeTruthy()
154
+ }, 30000)
155
+
156
+ it('successfully upgrades session with Hashcash after Turnstile fails', { timeout: 60000, retry: 2 }, async () => {
157
+ const manualInitService = createSessionInitializationService({
158
+ getSessionService: () => sessionService,
159
+ challengeSolverService,
160
+ performanceTracker: createMockPerformanceTracker(),
161
+ getIsSessionUpgradeAutoEnabled: () => false,
162
+ })
163
+
164
+ await manualInitService.initialize()
165
+
166
+ // Turnstile attempt (fails)
167
+ const turnstileChallenge = await sessionService.requestChallenge()
168
+ const turnstileSolver = challengeSolverService.getSolver(ChallengeType.TURNSTILE)
169
+ const turnstileSolution = await turnstileSolver?.solve({
170
+ challengeId: turnstileChallenge.challengeId,
171
+ challengeType: turnstileChallenge.challengeType,
172
+ extra: turnstileChallenge.extra,
173
+ })
174
+
175
+ const turnstileResult = await sessionService.verifySession({
176
+ solution: turnstileSolution || '',
177
+ challengeId: turnstileChallenge.challengeId,
178
+ challengeType: turnstileChallenge.challengeType,
179
+ })
180
+ expect(turnstileResult.retry).toBe(true)
181
+
182
+ // Hashcash attempt
183
+ const hashcashChallenge = await sessionService.requestChallenge()
184
+ expect(hashcashChallenge.challengeType).toBe(ChallengeType.HASHCASH)
185
+
186
+ const hashcashSolver = challengeSolverService.getSolver(ChallengeType.HASHCASH)
187
+ const hashcashSolution = await hashcashSolver?.solve({
188
+ challengeId: hashcashChallenge.challengeId,
189
+ challengeType: hashcashChallenge.challengeType,
190
+ extra: hashcashChallenge.extra,
191
+ })
192
+
193
+ const hashcashResult = await sessionService.verifySession({
194
+ solution: hashcashSolution || '',
195
+ challengeId: hashcashChallenge.challengeId,
196
+ challengeType: hashcashChallenge.challengeType,
197
+ })
198
+
199
+ // Success!
200
+ expect(hashcashResult.retry).toBe(false)
201
+ })
202
+
203
+ it('completes auto-upgrade flow (Turnstile fail → Hashcash success)', { timeout: 60000, retry: 2 }, async () => {
204
+ const autoInitService = createSessionInitializationService({
205
+ getSessionService: () => sessionService,
206
+ challengeSolverService,
207
+ performanceTracker: createMockPerformanceTracker(),
208
+ getIsSessionUpgradeAutoEnabled: () => true,
209
+ })
210
+
211
+ const result = await autoInitService.initialize()
212
+
213
+ // Web: Session ID is in cookie, not returned by initSession when challenge is needed
214
+ // After challenge completion, the session is valid but sessionId in result may be empty
215
+ // because initSession originally returned empty sessionId
216
+ expect(cookieJar.has('x-session-id')).toBe(true)
217
+ expect(cookieJar.get('x-session-id')).toBeTruthy()
218
+ })
219
+
220
+ it(
221
+ 'calls initSession on reinit - backend handles session reuse via cookie',
222
+ { timeout: 60000, retry: 2 },
223
+ async () => {
224
+ const autoInitService = createSessionInitializationService({
225
+ getSessionService: () => sessionService,
226
+ challengeSolverService,
227
+ performanceTracker: createMockPerformanceTracker(),
228
+ getIsSessionUpgradeAutoEnabled: () => true,
229
+ })
230
+
231
+ await autoInitService.initialize()
232
+
233
+ // Cookie should be set from first init
234
+ expect(cookieJar.has('x-session-id')).toBe(true)
235
+ const originalSessionId = cookieJar.get('x-session-id')
236
+
237
+ // Simulate page refresh - call initialize again
238
+ // Backend receives cookie and decides to reuse session
239
+ const reinitService = createSessionInitializationService({
240
+ getSessionService: () => sessionService,
241
+ challengeSolverService,
242
+ performanceTracker: createMockPerformanceTracker(),
243
+ getIsSessionUpgradeAutoEnabled: () => true,
244
+ })
245
+
246
+ await reinitService.initialize()
247
+
248
+ // Backend should reuse session - cookie remains the same
249
+ expect(cookieJar.get('x-session-id')).toBe(originalSessionId)
250
+ },
251
+ )
252
+
253
+ it('fires analytics callbacks during auto-upgrade flow', { timeout: 60000, retry: 2 }, async () => {
254
+ const analytics = {
255
+ onInitStarted: vi.fn(),
256
+ onInitCompleted: vi.fn(),
257
+ onChallengeReceived: vi.fn(),
258
+ onVerifyCompleted: vi.fn(),
259
+ }
260
+
261
+ const autoInitService = createSessionInitializationService({
262
+ getSessionService: () => sessionService,
263
+ challengeSolverService,
264
+ performanceTracker: createMockPerformanceTracker(),
265
+ getIsSessionUpgradeAutoEnabled: () => true,
266
+ analytics,
267
+ })
268
+
269
+ await autoInitService.initialize()
270
+
271
+ // Verify analytics flow
272
+ expect(analytics.onInitStarted).toHaveBeenCalledTimes(1)
273
+ expect(analytics.onInitCompleted).toHaveBeenCalledWith(expect.objectContaining({ needChallenge: true }))
274
+ expect(analytics.onChallengeReceived).toHaveBeenCalled()
275
+ // Verification may be called multiple times due to retries
276
+ expect(analytics.onVerifyCompleted).toHaveBeenCalled()
277
+ // Last call should be success
278
+ const lastVerificationCall = analytics.onVerifyCompleted.mock.calls.at(-1)?.[0]
279
+ expect(lastVerificationCall?.success).toBe(true)
280
+ })
281
+
282
+ afterAll(async () => {
283
+ await sessionService.removeSession()
284
+ cookieJar.clear()
285
+ await sessionStorage.clear()
286
+ })
287
+ })
288
+
289
+ // =============================================================================
290
+ // Non-Web Platform Tests (Hashcash only)
291
+ // =============================================================================
292
+ // iOS, Android, and Extension skip Turnstile and go straight to Hashcash
293
+ type NonWebPlatform = 'ios' | 'android' | 'extension'
294
+ type NonWebRequestSource = 'lux-ios' | 'lux-android' | 'lux-extension'
295
+
296
+ interface NonWebPlatformConfig {
297
+ platform: NonWebPlatform
298
+ requestSource: NonWebRequestSource
299
+ }
300
+
301
+ const NON_WEB_PLATFORMS: NonWebPlatformConfig[] = [
302
+ { platform: 'ios', requestSource: 'lux-ios' },
303
+ { platform: 'android', requestSource: 'lux-android' },
304
+ { platform: 'extension', requestSource: 'lux-extension' },
305
+ ]
306
+
307
+ describe.each(NON_WEB_PLATFORMS)(
308
+ 'Real Backend Integration - $platform (Hashcash only)',
309
+ ({ platform: _platform, requestSource }) => {
310
+ let sessionService: SessionService
311
+ let sessionStorage: InMemorySessionStorage
312
+ let deviceIdService: InMemoryDeviceIdService
313
+ let luxIdentifierService: InMemoryLuxIdentifierService
314
+ let challengeSolverService: ReturnType<typeof createChallengeSolverService>
315
+
316
+ beforeAll(() => {
317
+ sessionStorage = new InMemorySessionStorage()
318
+ deviceIdService = new InMemoryDeviceIdService()
319
+ luxIdentifierService = new InMemoryLuxIdentifierService()
320
+
321
+ // Non-web platforms only use Hashcash (no Turnstile)
322
+ const solvers = new Map([
323
+ [ChallengeType.UNSPECIFIED, createNoneMockSolver()],
324
+ [ChallengeType.HASHCASH, createHashcashSolver({ performanceTracker: createMockPerformanceTracker() })],
325
+ ])
326
+
327
+ const transport = createLocalHeaderTransport({
328
+ baseUrl: BACKEND_URL,
329
+ requestSource,
330
+ getSessionId: async () => (await sessionStorage.get())?.sessionId ?? null,
331
+ getDeviceId: async () => deviceIdService.getDeviceId(),
332
+ })
333
+
334
+ const sessionClient = createSessionClient({ transport })
335
+ const sessionRepository = createSessionRepository({ client: sessionClient })
336
+
337
+ sessionService = createSessionService({
338
+ sessionStorage,
339
+ deviceIdService,
340
+ luxIdentifierService,
341
+ sessionRepository,
342
+ })
343
+
344
+ challengeSolverService = createChallengeSolverService({ solvers })
345
+ })
346
+
347
+ beforeEach(async () => {
348
+ await sessionService.removeSession()
349
+ await sessionStorage.clear()
350
+ await deviceIdService.removeDeviceId()
351
+ await luxIdentifierService.removeLuxIdentifier()
352
+ })
353
+
354
+ it('initializes session with session ID and device ID stored locally', async () => {
355
+ const manualInitService = createSessionInitializationService({
356
+ getSessionService: () => sessionService,
357
+ challengeSolverService,
358
+ performanceTracker: createMockPerformanceTracker(),
359
+ getIsSessionUpgradeAutoEnabled: () => false,
360
+ })
361
+
362
+ const result = await manualInitService.initialize()
363
+
364
+ // Non-web: Backend returns session ID in response, which gets stored locally
365
+ // sessionId is returned regardless of challenge status
366
+ expect(result.sessionId).toBeTruthy()
367
+
368
+ // Session IS stored locally (unlike web which uses cookies)
369
+ const sessionState = await sessionService.getSessionState()
370
+ expect(sessionState?.sessionId).toBe(result.sessionId)
371
+
372
+ // Device ID should also be returned and stored
373
+ const storedDeviceId = await deviceIdService.getDeviceId()
374
+ expect(storedDeviceId).toBeTruthy()
375
+ }, 30000)
376
+
377
+ it('receives Hashcash challenge directly (no Turnstile)', async () => {
378
+ const manualInitService = createSessionInitializationService({
379
+ getSessionService: () => sessionService,
380
+ challengeSolverService,
381
+ performanceTracker: createMockPerformanceTracker(),
382
+ getIsSessionUpgradeAutoEnabled: () => false,
383
+ })
384
+
385
+ await manualInitService.initialize()
386
+
387
+ const challenge = await sessionService.requestChallenge()
388
+
389
+ // Non-web gets Hashcash directly (Turnstile is browser-only)
390
+ expect(challenge.challengeType).toBe(ChallengeType.HASHCASH)
391
+ expect(challenge.challengeId).toBeTruthy()
392
+ }, 30000)
393
+
394
+ it('successfully upgrades session with Hashcash', { timeout: 60000, retry: 2 }, async () => {
395
+ const manualInitService = createSessionInitializationService({
396
+ getSessionService: () => sessionService,
397
+ challengeSolverService,
398
+ performanceTracker: createMockPerformanceTracker(),
399
+ getIsSessionUpgradeAutoEnabled: () => false,
400
+ })
401
+
402
+ await manualInitService.initialize()
403
+
404
+ // Get Hashcash challenge directly
405
+ const hashcashChallenge = await sessionService.requestChallenge()
406
+ expect(hashcashChallenge.challengeType).toBe(ChallengeType.HASHCASH)
407
+
408
+ const hashcashSolver = challengeSolverService.getSolver(ChallengeType.HASHCASH)
409
+ const hashcashSolution = await hashcashSolver?.solve({
410
+ challengeId: hashcashChallenge.challengeId,
411
+ challengeType: hashcashChallenge.challengeType,
412
+ extra: hashcashChallenge.extra,
413
+ })
414
+
415
+ const hashcashResult = await sessionService.verifySession({
416
+ solution: hashcashSolution || '',
417
+ challengeId: hashcashChallenge.challengeId,
418
+ challengeType: hashcashChallenge.challengeType,
419
+ })
420
+
421
+ // Success!
422
+ expect(hashcashResult.retry).toBe(false)
423
+ })
424
+
425
+ it('completes auto-upgrade flow', { timeout: 60000, retry: 2 }, async () => {
426
+ const autoInitService = createSessionInitializationService({
427
+ getSessionService: () => sessionService,
428
+ challengeSolverService,
429
+ performanceTracker: createMockPerformanceTracker(),
430
+ getIsSessionUpgradeAutoEnabled: () => true,
431
+ })
432
+
433
+ const result = await autoInitService.initialize()
434
+
435
+ expect(result.sessionId).toBeTruthy()
436
+
437
+ const sessionState = await sessionService.getSessionState()
438
+ expect(sessionState?.sessionId).toBe(result.sessionId)
439
+ })
440
+
441
+ it(
442
+ 'calls initSession on reinit - backend handles session reuse via X-Session-ID header',
443
+ { timeout: 60000, retry: 2 },
444
+ async () => {
445
+ // First: Complete auto-upgrade flow
446
+ const autoInitService = createSessionInitializationService({
447
+ getSessionService: () => sessionService,
448
+ challengeSolverService,
449
+ performanceTracker: createMockPerformanceTracker(),
450
+ getIsSessionUpgradeAutoEnabled: () => true,
451
+ })
452
+
453
+ const firstResult = await autoInitService.initialize()
454
+ expect(firstResult.sessionId).toBeTruthy()
455
+ const originalSessionId = firstResult.sessionId
456
+
457
+ // Simulate app refresh - call initialize again
458
+ // Backend receives X-Session-ID header and decides to reuse session
459
+ const reinitService = createSessionInitializationService({
460
+ getSessionService: () => sessionService,
461
+ challengeSolverService,
462
+ performanceTracker: createMockPerformanceTracker(),
463
+ getIsSessionUpgradeAutoEnabled: () => true,
464
+ })
465
+
466
+ const secondResult = await reinitService.initialize()
467
+
468
+ // Backend should reuse session - session ID remains the same
469
+ expect(secondResult.sessionId).toBe(originalSessionId)
470
+ },
471
+ )
472
+
473
+ afterAll(async () => {
474
+ await sessionService.removeSession()
475
+ await sessionStorage.clear()
476
+ await deviceIdService.removeDeviceId()
477
+ await luxIdentifierService.removeLuxIdentifier()
478
+ })
479
+ },
480
+ )