@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,627 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChallengeResponse,
|
|
3
|
+
ChallengeType,
|
|
4
|
+
DeleteSessionResponse,
|
|
5
|
+
GetChallengeTypesResponse,
|
|
6
|
+
InitSessionResponse,
|
|
7
|
+
SignoutResponse,
|
|
8
|
+
VerifyResponse,
|
|
9
|
+
VerifySuccess,
|
|
10
|
+
} from '@uniswap/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
|
|
11
|
+
import { createChallengeSolverService } from '@luxexchange/sessions/src/challenge-solvers/createChallengeSolverService'
|
|
12
|
+
import type { ChallengeSolver } from '@luxexchange/sessions/src/challenge-solvers/types'
|
|
13
|
+
import type { PerformanceTracker } from '@luxexchange/sessions/src/performance/types'
|
|
14
|
+
import {
|
|
15
|
+
createSessionInitializationService,
|
|
16
|
+
type SessionInitializationService,
|
|
17
|
+
} from '@luxexchange/sessions/src/session-initialization/createSessionInitializationService'
|
|
18
|
+
import { createSessionRepository } from '@luxexchange/sessions/src/session-repository/createSessionRepository'
|
|
19
|
+
import { createSessionService } from '@luxexchange/sessions/src/session-service/createSessionService'
|
|
20
|
+
import type { SessionService } from '@luxexchange/sessions/src/session-service/types'
|
|
21
|
+
import {
|
|
22
|
+
createMockSessionClient,
|
|
23
|
+
createTestTransport,
|
|
24
|
+
InMemoryDeviceIdService,
|
|
25
|
+
InMemorySessionStorage,
|
|
26
|
+
InMemoryLuxIdentifierService,
|
|
27
|
+
type MockEndpoints,
|
|
28
|
+
} from '@luxexchange/sessions/src/test-utils'
|
|
29
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
30
|
+
|
|
31
|
+
// Helper: create a VerifyResponse with a success outcome (proto3 validation requires outcome.case)
|
|
32
|
+
function createSuccessVerifyResponse(): VerifyResponse {
|
|
33
|
+
const response = new VerifyResponse({ retry: false })
|
|
34
|
+
response.outcome = { case: 'success', value: new VerifySuccess({}) }
|
|
35
|
+
return response
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Mock performance tracker for testing
|
|
39
|
+
function createMockPerformanceTracker(): PerformanceTracker {
|
|
40
|
+
let time = 0
|
|
41
|
+
return {
|
|
42
|
+
now: (): number => {
|
|
43
|
+
time += 100
|
|
44
|
+
return time
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Mock Turnstile solver for integration tests
|
|
50
|
+
const mockTurnstileSolve = vi.fn()
|
|
51
|
+
|
|
52
|
+
describe('Challenge Flow Integration Tests', () => {
|
|
53
|
+
let sessionStorage: InMemorySessionStorage
|
|
54
|
+
let deviceIdService: InMemoryDeviceIdService
|
|
55
|
+
let luxIdentifierService: InMemoryLuxIdentifierService
|
|
56
|
+
let sessionService: SessionService
|
|
57
|
+
let sessionInitializationService: SessionInitializationService
|
|
58
|
+
let mockEndpoints: MockEndpoints
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
// Initialize in-memory storage
|
|
62
|
+
sessionStorage = new InMemorySessionStorage()
|
|
63
|
+
deviceIdService = new InMemoryDeviceIdService()
|
|
64
|
+
luxIdentifierService = new InMemoryLuxIdentifierService()
|
|
65
|
+
|
|
66
|
+
// Set up mock endpoints with default responses
|
|
67
|
+
mockEndpoints = {
|
|
68
|
+
'/lux.platformservice.v1.SessionService/InitSession': async (): Promise<InitSessionResponse> => {
|
|
69
|
+
return new InitSessionResponse({
|
|
70
|
+
sessionId: 'test-session-123',
|
|
71
|
+
needChallenge: true,
|
|
72
|
+
extra: {},
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
'/lux.platformservice.v1.SessionService/Challenge': async (): Promise<ChallengeResponse> => {
|
|
76
|
+
return new ChallengeResponse({
|
|
77
|
+
challengeId: '02c241f3-8d45-4a88-842a-d364c30a6c44',
|
|
78
|
+
challengeType: ChallengeType.TURNSTILE,
|
|
79
|
+
extra: {
|
|
80
|
+
challengeData: '{"siteKey":"0x4AAAAAABiAHneWOWZHzZtO","action":"session_verification"}',
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
'/lux.platformservice.v1.SessionService/Verify': async (): Promise<VerifyResponse> => {
|
|
85
|
+
return createSuccessVerifyResponse()
|
|
86
|
+
},
|
|
87
|
+
'/lux.platformservice.v1.SessionService/DeleteSession': async (): Promise<DeleteSessionResponse> => {
|
|
88
|
+
return new DeleteSessionResponse({})
|
|
89
|
+
},
|
|
90
|
+
'/lux.platformservice.v1.SessionService/GetChallengeTypes': async (): Promise<GetChallengeTypesResponse> => {
|
|
91
|
+
return new GetChallengeTypesResponse({ challengeTypes: [] })
|
|
92
|
+
},
|
|
93
|
+
'/lux.platformservice.v1.SessionService/Signout': async (): Promise<SignoutResponse> => {
|
|
94
|
+
return new SignoutResponse({})
|
|
95
|
+
},
|
|
96
|
+
} as unknown as MockEndpoints
|
|
97
|
+
|
|
98
|
+
// Create test transport
|
|
99
|
+
createTestTransport(mockEndpoints)
|
|
100
|
+
|
|
101
|
+
// Create session client
|
|
102
|
+
const sessionClient = createMockSessionClient(mockEndpoints, sessionStorage, deviceIdService)
|
|
103
|
+
|
|
104
|
+
// Create repository
|
|
105
|
+
const sessionRepository = createSessionRepository({
|
|
106
|
+
client: sessionClient as any,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Create session service
|
|
110
|
+
sessionService = createSessionService({
|
|
111
|
+
sessionStorage,
|
|
112
|
+
deviceIdService,
|
|
113
|
+
luxIdentifierService,
|
|
114
|
+
sessionRepository,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Create challenge solver service with mock Turnstile solver
|
|
118
|
+
const challengeSolverService = createChallengeSolverService()
|
|
119
|
+
|
|
120
|
+
// Mock the Turnstile solver
|
|
121
|
+
mockTurnstileSolve.mockResolvedValue('test-turnstile-solution-token')
|
|
122
|
+
challengeSolverService.getSolver = (type: ChallengeType): ChallengeSolver | null => {
|
|
123
|
+
if (type === ChallengeType.TURNSTILE) {
|
|
124
|
+
return {
|
|
125
|
+
solve: mockTurnstileSolve,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Create session initialization service
|
|
132
|
+
sessionInitializationService = createSessionInitializationService({
|
|
133
|
+
getSessionService: () => sessionService,
|
|
134
|
+
challengeSolverService,
|
|
135
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
136
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
afterEach(async () => {
|
|
141
|
+
// Clean up any stored data
|
|
142
|
+
await sessionStorage.clear()
|
|
143
|
+
await deviceIdService.removeDeviceId()
|
|
144
|
+
|
|
145
|
+
// Reset mocks
|
|
146
|
+
vi.clearAllMocks()
|
|
147
|
+
mockTurnstileSolve.mockResolvedValue('test-turnstile-solution-token')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('initializes a session with needChallenge: true and completes challenge flow', async () => {
|
|
151
|
+
// Update mock to return needChallenge: true
|
|
152
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
153
|
+
async (): Promise<InitSessionResponse> => {
|
|
154
|
+
return new InitSessionResponse({
|
|
155
|
+
sessionId: '776973bd-bbc2-452b-9c35-1b72c475afbd',
|
|
156
|
+
needChallenge: true,
|
|
157
|
+
extra: {},
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Track calls to verify flow
|
|
162
|
+
const initCalls: Array<{ request: any; headers: Record<string, string> }> = []
|
|
163
|
+
const challengeCalls: Array<{ request: any; headers: Record<string, string> }> = []
|
|
164
|
+
const verifyCalls: Array<{ request: any; headers: Record<string, string> }> = []
|
|
165
|
+
|
|
166
|
+
// Wrap handlers to track calls
|
|
167
|
+
const originalInit = mockEndpoints['/lux.platformservice.v1.SessionService/InitSession']
|
|
168
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] = async (
|
|
169
|
+
request,
|
|
170
|
+
headers,
|
|
171
|
+
): Promise<InitSessionResponse> => {
|
|
172
|
+
initCalls.push({ request, headers })
|
|
173
|
+
return originalInit(request, headers)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const originalChallenge = mockEndpoints['/lux.platformservice.v1.SessionService/Challenge']
|
|
177
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Challenge'] = async (
|
|
178
|
+
request,
|
|
179
|
+
headers,
|
|
180
|
+
): Promise<ChallengeResponse> => {
|
|
181
|
+
challengeCalls.push({ request, headers })
|
|
182
|
+
return originalChallenge(request, headers)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const originalVerify = mockEndpoints['/lux.platformservice.v1.SessionService/Verify']
|
|
186
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Verify'] = async (
|
|
187
|
+
request,
|
|
188
|
+
headers,
|
|
189
|
+
): Promise<VerifyResponse> => {
|
|
190
|
+
verifyCalls.push({ request, headers })
|
|
191
|
+
return originalVerify(request, headers)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Execute the full initialization flow
|
|
195
|
+
await sessionInitializationService.initialize()
|
|
196
|
+
|
|
197
|
+
// Verify all API calls were made in the correct order
|
|
198
|
+
expect(initCalls).toHaveLength(1)
|
|
199
|
+
expect(challengeCalls).toHaveLength(1)
|
|
200
|
+
expect(verifyCalls).toHaveLength(1)
|
|
201
|
+
|
|
202
|
+
// Verify session was stored
|
|
203
|
+
const storedSession = await sessionStorage.get()
|
|
204
|
+
expect(storedSession?.sessionId).toBe('776973bd-bbc2-452b-9c35-1b72c475afbd')
|
|
205
|
+
|
|
206
|
+
// Verify challenge request had session headers
|
|
207
|
+
expect(challengeCalls[0].headers['X-Session-ID']).toBe('776973bd-bbc2-452b-9c35-1b72c475afbd')
|
|
208
|
+
|
|
209
|
+
// Verify upgrade request had correct challenge ID and solution
|
|
210
|
+
expect(verifyCalls[0].request).toMatchObject({
|
|
211
|
+
challengeId: '02c241f3-8d45-4a88-842a-d364c30a6c44',
|
|
212
|
+
solution: 'test-turnstile-solution-token',
|
|
213
|
+
})
|
|
214
|
+
expect(verifyCalls[0].headers['X-Session-ID']).toBe('776973bd-bbc2-452b-9c35-1b72c475afbd')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('handles challenge flow with proper request/response data', async () => {
|
|
218
|
+
// Set device ID
|
|
219
|
+
await deviceIdService.setDeviceId('66629fec-ff9d-430b-8a31-d256b4128527')
|
|
220
|
+
|
|
221
|
+
// Initialize session first
|
|
222
|
+
await sessionService.initSession()
|
|
223
|
+
|
|
224
|
+
// Request challenge
|
|
225
|
+
const challengeResponse = await sessionService.requestChallenge()
|
|
226
|
+
|
|
227
|
+
expect(challengeResponse).toEqual({
|
|
228
|
+
challengeId: '02c241f3-8d45-4a88-842a-d364c30a6c44',
|
|
229
|
+
challengeType: ChallengeType.TURNSTILE,
|
|
230
|
+
extra: {
|
|
231
|
+
challengeData: '{"siteKey":"0x4AAAAAABiAHneWOWZHzZtO","action":"session_verification"}',
|
|
232
|
+
},
|
|
233
|
+
challengeData: { case: undefined },
|
|
234
|
+
authorizeUrl: undefined,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// Verify mock solver is configured
|
|
238
|
+
expect(mockTurnstileSolve).toHaveBeenCalledTimes(0)
|
|
239
|
+
|
|
240
|
+
// Simulate solving the challenge and upgrading session
|
|
241
|
+
const solution = 'test-turnstile-solution-token'
|
|
242
|
+
const upgradeResponse = await sessionService.verifySession({
|
|
243
|
+
solution,
|
|
244
|
+
challengeId: challengeResponse.challengeId,
|
|
245
|
+
challengeType: challengeResponse.challengeType,
|
|
246
|
+
})
|
|
247
|
+
expect(upgradeResponse.retry).toBe(false)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('always calls initSession even with existing session - backend handles reuse', async () => {
|
|
251
|
+
// Pre-populate storage with existing session
|
|
252
|
+
await sessionStorage.set({ sessionId: 'existing-session-123' })
|
|
253
|
+
await deviceIdService.setDeviceId('existing-device-123')
|
|
254
|
+
|
|
255
|
+
// Track calls
|
|
256
|
+
let initCallCount = 0
|
|
257
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
258
|
+
async (): Promise<InitSessionResponse> => {
|
|
259
|
+
initCallCount++
|
|
260
|
+
// Backend returns the same session ID (simulating session reuse)
|
|
261
|
+
return new InitSessionResponse({
|
|
262
|
+
sessionId: 'existing-session-123',
|
|
263
|
+
needChallenge: false,
|
|
264
|
+
extra: {},
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Initialize should call API - backend decides whether to reuse session
|
|
269
|
+
// (in production, existing session ID is sent via X-Session-ID header)
|
|
270
|
+
await sessionInitializationService.initialize()
|
|
271
|
+
|
|
272
|
+
// Verify initialization call was made (previously this was skipped)
|
|
273
|
+
expect(initCallCount).toBe(1)
|
|
274
|
+
|
|
275
|
+
// Verify session in storage matches what backend returned
|
|
276
|
+
const storedSession = await sessionStorage.get()
|
|
277
|
+
expect(storedSession?.sessionId).toBe('existing-session-123')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('handles challenge retry when upgrade fails', async () => {
|
|
281
|
+
// Set up to require challenge
|
|
282
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
283
|
+
async (): Promise<InitSessionResponse> => {
|
|
284
|
+
return new InitSessionResponse({
|
|
285
|
+
sessionId: 'retry-session-123',
|
|
286
|
+
needChallenge: true,
|
|
287
|
+
extra: {},
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Make first verify attempt fail with retry
|
|
292
|
+
let verifyAttempts = 0
|
|
293
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Verify'] = async (): Promise<VerifyResponse> => {
|
|
294
|
+
verifyAttempts++
|
|
295
|
+
if (verifyAttempts === 1) {
|
|
296
|
+
return new VerifyResponse({ retry: true })
|
|
297
|
+
}
|
|
298
|
+
return createSuccessVerifyResponse()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Track challenge calls
|
|
302
|
+
const challengeCalls: any[] = []
|
|
303
|
+
const originalChallenge = mockEndpoints['/lux.platformservice.v1.SessionService/Challenge']
|
|
304
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Challenge'] = async (
|
|
305
|
+
request,
|
|
306
|
+
headers,
|
|
307
|
+
): Promise<ChallengeResponse> => {
|
|
308
|
+
challengeCalls.push({ request, headers })
|
|
309
|
+
return originalChallenge(request, headers)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Execute flow
|
|
313
|
+
await sessionInitializationService.initialize()
|
|
314
|
+
|
|
315
|
+
// Should have made 2 challenge requests (initial + retry)
|
|
316
|
+
expect(challengeCalls).toHaveLength(2)
|
|
317
|
+
expect(verifyAttempts).toBe(2)
|
|
318
|
+
|
|
319
|
+
// Session should be stored after successful retry
|
|
320
|
+
const storedSession = await sessionStorage.get()
|
|
321
|
+
expect(storedSession?.sessionId).toBe('retry-session-123')
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('respects maximum retry limit for challenges', async () => {
|
|
325
|
+
// Set up to require challenge
|
|
326
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
327
|
+
async (): Promise<InitSessionResponse> => {
|
|
328
|
+
return new InitSessionResponse({
|
|
329
|
+
sessionId: 'max-retry-session',
|
|
330
|
+
needChallenge: true,
|
|
331
|
+
extra: {},
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Always return retry: true
|
|
336
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Verify'] = async (): Promise<VerifyResponse> => {
|
|
337
|
+
return new VerifyResponse({ retry: true })
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Track attempts
|
|
341
|
+
const challengeCalls: any[] = []
|
|
342
|
+
const verifyCalls: any[] = []
|
|
343
|
+
|
|
344
|
+
const originalChallenge = mockEndpoints['/lux.platformservice.v1.SessionService/Challenge']
|
|
345
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Challenge'] = async (
|
|
346
|
+
request,
|
|
347
|
+
headers,
|
|
348
|
+
): Promise<ChallengeResponse> => {
|
|
349
|
+
challengeCalls.push({ request, headers })
|
|
350
|
+
return originalChallenge(request, headers)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const originalVerify = mockEndpoints['/lux.platformservice.v1.SessionService/Verify']
|
|
354
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Verify'] = async (
|
|
355
|
+
request,
|
|
356
|
+
headers,
|
|
357
|
+
): Promise<VerifyResponse> => {
|
|
358
|
+
verifyCalls.push({ request, headers })
|
|
359
|
+
return originalVerify(request, headers)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Should throw after max retries
|
|
363
|
+
await expect(sessionInitializationService.initialize()).rejects.toThrow()
|
|
364
|
+
|
|
365
|
+
// Should have attempted 4 times (1 initial + 3 retries)
|
|
366
|
+
expect(challengeCalls).toHaveLength(4)
|
|
367
|
+
expect(verifyCalls).toHaveLength(4)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('handles device ID in headers for challenge requests', async () => {
|
|
371
|
+
// Set device ID
|
|
372
|
+
await deviceIdService.setDeviceId('test-device-456')
|
|
373
|
+
await sessionService.initSession()
|
|
374
|
+
|
|
375
|
+
// Track challenge request to verify headers
|
|
376
|
+
let capturedHeaders: Record<string, string> = {}
|
|
377
|
+
const originalChallenge = mockEndpoints['/lux.platformservice.v1.SessionService/Challenge']
|
|
378
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Challenge'] = async (
|
|
379
|
+
request,
|
|
380
|
+
headers,
|
|
381
|
+
): Promise<ChallengeResponse> => {
|
|
382
|
+
capturedHeaders = headers
|
|
383
|
+
return originalChallenge(request, headers)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Request challenge
|
|
387
|
+
await sessionService.requestChallenge()
|
|
388
|
+
|
|
389
|
+
// Verify device ID was included in headers
|
|
390
|
+
expect(capturedHeaders['X-Device-ID']).toBe('test-device-456')
|
|
391
|
+
expect(capturedHeaders['X-Session-ID']).toBe('test-session-123')
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('handles missing device ID gracefully', async () => {
|
|
395
|
+
// Don't set device ID
|
|
396
|
+
await sessionService.initSession()
|
|
397
|
+
|
|
398
|
+
// Track challenge request to verify headers
|
|
399
|
+
let capturedHeaders: Record<string, string> = {}
|
|
400
|
+
const originalChallenge = mockEndpoints['/lux.platformservice.v1.SessionService/Challenge']
|
|
401
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Challenge'] = async (
|
|
402
|
+
request,
|
|
403
|
+
headers,
|
|
404
|
+
): Promise<ChallengeResponse> => {
|
|
405
|
+
capturedHeaders = headers
|
|
406
|
+
return originalChallenge(request, headers)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Request challenge
|
|
410
|
+
await sessionService.requestChallenge()
|
|
411
|
+
|
|
412
|
+
// Verify only session ID was included in headers
|
|
413
|
+
expect(capturedHeaders['X-Device-ID']).toBeUndefined()
|
|
414
|
+
expect(capturedHeaders['X-Session-ID']).toBe('test-session-123')
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('submits empty solution when solver throws, allowing verify-retry fallback', async () => {
|
|
418
|
+
// Set up to require challenge
|
|
419
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
420
|
+
async (): Promise<InitSessionResponse> => {
|
|
421
|
+
return new InitSessionResponse({
|
|
422
|
+
sessionId: 'error-session-123',
|
|
423
|
+
needChallenge: true,
|
|
424
|
+
extra: {},
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Make Turnstile solver throw (e.g. domain not approved on Vercel preview)
|
|
429
|
+
mockTurnstileSolve.mockRejectedValue(new Error('Turnstile error: domain not allowed'))
|
|
430
|
+
|
|
431
|
+
// Verify endpoint accepts empty solution (no retry needed for this test)
|
|
432
|
+
const verifyCalls: Array<{ request: any }> = []
|
|
433
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Verify'] = async (request): Promise<VerifyResponse> => {
|
|
434
|
+
verifyCalls.push({ request })
|
|
435
|
+
return createSuccessVerifyResponse()
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const challengeSolverService = createChallengeSolverService()
|
|
439
|
+
challengeSolverService.getSolver = (type: ChallengeType): ChallengeSolver | null => {
|
|
440
|
+
if (type === ChallengeType.TURNSTILE) {
|
|
441
|
+
return { solve: mockTurnstileSolve }
|
|
442
|
+
}
|
|
443
|
+
return null
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const initService = createSessionInitializationService({
|
|
447
|
+
getSessionService: () => sessionService,
|
|
448
|
+
challengeSolverService,
|
|
449
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
450
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
// Should NOT throw — empty solution is submitted instead
|
|
454
|
+
await initService.initialize()
|
|
455
|
+
|
|
456
|
+
// Solver was called once and threw
|
|
457
|
+
expect(mockTurnstileSolve).toHaveBeenCalledTimes(1)
|
|
458
|
+
// Verify was called with empty solution
|
|
459
|
+
expect(verifyCalls).toHaveLength(1)
|
|
460
|
+
expect(verifyCalls[0].request.solution).toBe('solver-failed')
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
it('Turnstile solver fails → empty verify → retry → Hashcash succeeds end-to-end', async () => {
|
|
464
|
+
// Set up to require challenge
|
|
465
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
466
|
+
async (): Promise<InitSessionResponse> => {
|
|
467
|
+
return new InitSessionResponse({
|
|
468
|
+
sessionId: 'fallback-e2e-session',
|
|
469
|
+
needChallenge: true,
|
|
470
|
+
extra: {},
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// First challenge returns Turnstile, second returns Hashcash
|
|
475
|
+
let challengeRequestCount = 0
|
|
476
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Challenge'] = async (): Promise<ChallengeResponse> => {
|
|
477
|
+
challengeRequestCount++
|
|
478
|
+
if (challengeRequestCount === 1) {
|
|
479
|
+
return new ChallengeResponse({
|
|
480
|
+
challengeId: 'turnstile-challenge-id',
|
|
481
|
+
challengeType: ChallengeType.TURNSTILE,
|
|
482
|
+
extra: {
|
|
483
|
+
challengeData: '{"siteKey":"0x4AAAAAABiAHneWOWZHzZtO","action":"session_verification"}',
|
|
484
|
+
},
|
|
485
|
+
})
|
|
486
|
+
}
|
|
487
|
+
return new ChallengeResponse({
|
|
488
|
+
challengeId: 'hashcash-challenge-id',
|
|
489
|
+
challengeType: ChallengeType.HASHCASH,
|
|
490
|
+
extra: {
|
|
491
|
+
challengeData: '{"difficulty":10}',
|
|
492
|
+
},
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// First verify rejects empty solution, second succeeds
|
|
497
|
+
let verifyCount = 0
|
|
498
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Verify'] = async (): Promise<VerifyResponse> => {
|
|
499
|
+
verifyCount++
|
|
500
|
+
if (verifyCount === 1) {
|
|
501
|
+
return new VerifyResponse({ retry: true })
|
|
502
|
+
}
|
|
503
|
+
return createSuccessVerifyResponse()
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Turnstile solver throws (domain mismatch), Hashcash solver succeeds
|
|
507
|
+
mockTurnstileSolve.mockRejectedValue(new Error('Turnstile error 110200: domain not allowed'))
|
|
508
|
+
const mockHashcashSolve = vi.fn().mockResolvedValue('hashcash-solution-token')
|
|
509
|
+
|
|
510
|
+
const challengeSolverService = createChallengeSolverService()
|
|
511
|
+
challengeSolverService.getSolver = (type: ChallengeType): ChallengeSolver | null => {
|
|
512
|
+
if (type === ChallengeType.TURNSTILE) {
|
|
513
|
+
return { solve: mockTurnstileSolve }
|
|
514
|
+
}
|
|
515
|
+
if (type === ChallengeType.HASHCASH) {
|
|
516
|
+
return { solve: mockHashcashSolve }
|
|
517
|
+
}
|
|
518
|
+
return null
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const initService = createSessionInitializationService({
|
|
522
|
+
getSessionService: () => sessionService,
|
|
523
|
+
challengeSolverService,
|
|
524
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
525
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
// Full flow: Turnstile throws → empty verify → retry → Hashcash succeeds
|
|
529
|
+
await initService.initialize()
|
|
530
|
+
|
|
531
|
+
// Turnstile solver was called once (and threw)
|
|
532
|
+
expect(mockTurnstileSolve).toHaveBeenCalledTimes(1)
|
|
533
|
+
// Hashcash solver was called once (and succeeded)
|
|
534
|
+
expect(mockHashcashSolve).toHaveBeenCalledTimes(1)
|
|
535
|
+
// Two challenge requests: Turnstile then Hashcash
|
|
536
|
+
expect(challengeRequestCount).toBe(2)
|
|
537
|
+
// Two verify calls: first rejected empty solution, second accepted Hashcash
|
|
538
|
+
expect(verifyCount).toBe(2)
|
|
539
|
+
|
|
540
|
+
// Session should be stored
|
|
541
|
+
const storedSession = await sessionStorage.get()
|
|
542
|
+
expect(storedSession?.sessionId).toBe('fallback-e2e-session')
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('falls back to Hashcash via verify-retry when mock Turnstile token is rejected', async () => {
|
|
546
|
+
// Set up to require challenge
|
|
547
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
548
|
+
async (): Promise<InitSessionResponse> => {
|
|
549
|
+
return new InitSessionResponse({
|
|
550
|
+
sessionId: 'fallback-session-123',
|
|
551
|
+
needChallenge: true,
|
|
552
|
+
extra: {},
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// First challenge returns Turnstile, second returns Hashcash
|
|
557
|
+
// (backend switches after failed verification)
|
|
558
|
+
let challengeRequestCount = 0
|
|
559
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Challenge'] = async (): Promise<ChallengeResponse> => {
|
|
560
|
+
challengeRequestCount++
|
|
561
|
+
if (challengeRequestCount === 1) {
|
|
562
|
+
return new ChallengeResponse({
|
|
563
|
+
challengeId: 'turnstile-challenge-id',
|
|
564
|
+
challengeType: ChallengeType.TURNSTILE,
|
|
565
|
+
extra: {
|
|
566
|
+
challengeData: '{"siteKey":"0x4AAAAAABiAHneWOWZHzZtO","action":"session_verification"}',
|
|
567
|
+
},
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
return new ChallengeResponse({
|
|
571
|
+
challengeId: 'hashcash-challenge-id',
|
|
572
|
+
challengeType: ChallengeType.HASHCASH,
|
|
573
|
+
extra: {
|
|
574
|
+
challengeData: '{"difficulty":10}',
|
|
575
|
+
},
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// First verify rejects mock token, second succeeds
|
|
580
|
+
let verifyCount = 0
|
|
581
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/Verify'] = async (): Promise<VerifyResponse> => {
|
|
582
|
+
verifyCount++
|
|
583
|
+
if (verifyCount === 1) {
|
|
584
|
+
return new VerifyResponse({ retry: true })
|
|
585
|
+
}
|
|
586
|
+
return createSuccessVerifyResponse()
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Mock Turnstile returns a fake token (doesn't throw), Hashcash succeeds
|
|
590
|
+
const mockHashcashSolve = vi.fn().mockResolvedValue('hashcash-solution-token')
|
|
591
|
+
mockTurnstileSolve.mockResolvedValue('mock-turnstile-token')
|
|
592
|
+
|
|
593
|
+
const challengeSolverService = createChallengeSolverService()
|
|
594
|
+
challengeSolverService.getSolver = (type: ChallengeType): ChallengeSolver | null => {
|
|
595
|
+
if (type === ChallengeType.TURNSTILE) {
|
|
596
|
+
return { solve: mockTurnstileSolve }
|
|
597
|
+
}
|
|
598
|
+
if (type === ChallengeType.HASHCASH) {
|
|
599
|
+
return { solve: mockHashcashSolve }
|
|
600
|
+
}
|
|
601
|
+
return null
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const initService = createSessionInitializationService({
|
|
605
|
+
getSessionService: () => sessionService,
|
|
606
|
+
challengeSolverService,
|
|
607
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
608
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
// Flow: mock Turnstile token → verify rejects → retry → backend sends Hashcash → succeeds
|
|
612
|
+
await initService.initialize()
|
|
613
|
+
|
|
614
|
+
// Mock Turnstile was called (returned fake token)
|
|
615
|
+
expect(mockTurnstileSolve).toHaveBeenCalledTimes(1)
|
|
616
|
+
// Hashcash was used as fallback after verify rejected mock token
|
|
617
|
+
expect(mockHashcashSolve).toHaveBeenCalledTimes(1)
|
|
618
|
+
// Two challenge requests (Turnstile, then Hashcash after failed verify)
|
|
619
|
+
expect(challengeRequestCount).toBe(2)
|
|
620
|
+
// Two verify calls (first rejected mock token, second accepted Hashcash)
|
|
621
|
+
expect(verifyCount).toBe(2)
|
|
622
|
+
|
|
623
|
+
// Session should be stored
|
|
624
|
+
const storedSession = await sessionStorage.get()
|
|
625
|
+
expect(storedSession?.sessionId).toBe('fallback-session-123')
|
|
626
|
+
})
|
|
627
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DeviceIdService } from '@luxfi/sessions/src/device-id/types'
|
|
2
|
+
|
|
3
|
+
function createDeviceIdService(ctx: {
|
|
4
|
+
getDeviceId: () => Promise<string>
|
|
5
|
+
setDeviceId: (deviceId: string) => Promise<void>
|
|
6
|
+
removeDeviceId: () => Promise<void>
|
|
7
|
+
}): DeviceIdService {
|
|
8
|
+
const getDeviceId = ctx.getDeviceId
|
|
9
|
+
const setDeviceId = ctx.setDeviceId
|
|
10
|
+
const removeDeviceId = ctx.removeDeviceId
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
getDeviceId,
|
|
14
|
+
setDeviceId,
|
|
15
|
+
removeDeviceId,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { createDeviceIdService }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device ID provider interface
|
|
3
|
+
* Platform-specific implementations handle device identification
|
|
4
|
+
*/
|
|
5
|
+
interface DeviceIdService {
|
|
6
|
+
getDeviceId(): Promise<string | null>
|
|
7
|
+
setDeviceId(deviceId: string): Promise<void>
|
|
8
|
+
removeDeviceId(): Promise<void>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type { DeviceIdService }
|