@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,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
|
+
)
|