@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,264 @@
1
+ import {
2
+ ChallengeResponse,
3
+ ChallengeType,
4
+ DeleteSessionResponse,
5
+ GetChallengeTypesResponse,
6
+ InitSessionResponse,
7
+ IntrospectSessionResponse,
8
+ SignoutResponse,
9
+ UpdateSessionResponse,
10
+ VerifyResponse,
11
+ } from '@luxdex/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
12
+ import { createSessionRepository } from '@luxfi/sessions/src/session-repository/createSessionRepository'
13
+ import { createSessionService } from '@luxfi/sessions/src/session-service/createSessionService'
14
+ import type { SessionService } from '@luxfi/sessions/src/session-service/types'
15
+ import {
16
+ createMockSessionClient,
17
+ InMemoryDeviceIdService,
18
+ InMemorySessionStorage,
19
+ InMemoryLuxIdentifierService,
20
+ type MockEndpoints,
21
+ } from '@luxfi/sessions/src/test-utils'
22
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
23
+
24
+ describe('Session Lifecycle Integration Tests', () => {
25
+ let sessionStorage: InMemorySessionStorage
26
+ let deviceIdService: InMemoryDeviceIdService
27
+ let luxIdentifierService: InMemoryLuxIdentifierService
28
+ let sessionService: SessionService
29
+ let mockEndpoints: MockEndpoints
30
+
31
+ beforeEach(() => {
32
+ // Initialize in-memory storage
33
+ sessionStorage = new InMemorySessionStorage()
34
+ deviceIdService = new InMemoryDeviceIdService()
35
+ luxIdentifierService = new InMemoryLuxIdentifierService()
36
+
37
+ // Set up mock endpoints with default responses
38
+ mockEndpoints = {
39
+ '/lux.platformservice.v1.SessionService/InitSession': async (): Promise<InitSessionResponse> => {
40
+ return new InitSessionResponse({
41
+ sessionId: 'test-session-123',
42
+ needChallenge: false,
43
+ extra: {},
44
+ })
45
+ },
46
+ '/lux.platformservice.v1.SessionService/Challenge': async (): Promise<ChallengeResponse> => {
47
+ return new ChallengeResponse({
48
+ challengeId: 'challenge-123',
49
+ challengeType: ChallengeType.TURNSTILE,
50
+ extra: { sitekey: 'test-key' },
51
+ })
52
+ },
53
+ '/lux.platformservice.v1.SessionService/Verify': async (): Promise<VerifyResponse> => {
54
+ return new VerifyResponse({
55
+ retry: false,
56
+ })
57
+ },
58
+ '/lux.platformservice.v1.SessionService/DeleteSession': async (): Promise<DeleteSessionResponse> => {
59
+ return new DeleteSessionResponse({})
60
+ },
61
+ '/lux.platformservice.v1.SessionService/IntrospectSession': async (): Promise<IntrospectSessionResponse> => {
62
+ return new IntrospectSessionResponse({})
63
+ },
64
+ '/lux.platformservice.v1.SessionService/UpdateSession': async (): Promise<UpdateSessionResponse> => {
65
+ return new UpdateSessionResponse({})
66
+ },
67
+ '/lux.platformservice.v1.SessionService/GetChallengeTypes': async (): Promise<GetChallengeTypesResponse> => {
68
+ return new GetChallengeTypesResponse({ challengeTypes: [] })
69
+ },
70
+ '/lux.platformservice.v1.SessionService/Signout': async (): Promise<SignoutResponse> => {
71
+ return new SignoutResponse({})
72
+ },
73
+ }
74
+
75
+ // Create session client with test transport
76
+ const sessionClient = createMockSessionClient(mockEndpoints, sessionStorage, deviceIdService)
77
+
78
+ // Create repository
79
+ const sessionRepository = createSessionRepository({
80
+ client: sessionClient as any,
81
+ })
82
+
83
+ // Create session service
84
+ sessionService = createSessionService({
85
+ sessionStorage,
86
+ deviceIdService,
87
+ luxIdentifierService,
88
+ sessionRepository,
89
+ })
90
+ })
91
+
92
+ afterEach(async () => {
93
+ // Clean up any stored data
94
+ await sessionStorage.clear()
95
+ await deviceIdService.removeDeviceId()
96
+ })
97
+
98
+ it('initializes and stores a session', async () => {
99
+ const response = await sessionService.initSession()
100
+
101
+ expect(response).toEqual({
102
+ sessionId: 'test-session-123',
103
+ needChallenge: false,
104
+ extra: {},
105
+ })
106
+
107
+ // Verify session was stored
108
+ const storedSession = await sessionStorage.get()
109
+ expect(storedSession?.sessionId).toBe('test-session-123')
110
+ })
111
+
112
+ it('handles session initialization without challenge requirement', async () => {
113
+ // Override default to return needChallenge: false
114
+ mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
115
+ async (): Promise<InitSessionResponse> => {
116
+ return new InitSessionResponse({
117
+ sessionId: 'simple-session-123',
118
+ needChallenge: false,
119
+ extra: {},
120
+ })
121
+ }
122
+
123
+ await deviceIdService.setDeviceId('simple-device-123')
124
+
125
+ // Initialize
126
+ const response = await sessionService.initSession()
127
+
128
+ // Verify response
129
+ expect(response.sessionId).toBe('simple-session-123')
130
+ expect(response.needChallenge).toBe(false)
131
+
132
+ // Session should be stored
133
+ const storedSession = await sessionStorage.get()
134
+ expect(storedSession?.sessionId).toBe('simple-session-123')
135
+ })
136
+
137
+ it('properly clears session on deletion', async () => {
138
+ // Initialize a session first
139
+ await sessionService.initSession()
140
+
141
+ // Verify session exists
142
+ let storedSession = await sessionStorage.get()
143
+ expect(storedSession?.sessionId).toBe('test-session-123')
144
+
145
+ // Delete session
146
+ await sessionService.removeSession()
147
+
148
+ // Verify session is cleared
149
+ storedSession = await sessionStorage.get()
150
+ expect(storedSession).toBeNull()
151
+ })
152
+
153
+ it('stores session ID when provided (mobile/extension behavior)', async () => {
154
+ mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
155
+ async (): Promise<InitSessionResponse> => {
156
+ return new InitSessionResponse({
157
+ sessionId: 'mobile-session-123',
158
+ needChallenge: false,
159
+ extra: {},
160
+ })
161
+ }
162
+
163
+ const response = await sessionService.initSession()
164
+
165
+ expect(response.sessionId).toBe('mobile-session-123')
166
+
167
+ // Session should be stored
168
+ const storedSession = await sessionStorage.get()
169
+ expect(storedSession?.sessionId).toBe('mobile-session-123')
170
+ })
171
+
172
+ it('handles web sessions without storing session ID when undefined', async () => {
173
+ mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
174
+ async (): Promise<InitSessionResponse> => {
175
+ return new InitSessionResponse({
176
+ sessionId: undefined,
177
+ needChallenge: true,
178
+ extra: { sitekey: 'turnstile-key' },
179
+ })
180
+ }
181
+
182
+ const response = await sessionService.initSession()
183
+
184
+ expect(response.sessionId).toBeUndefined()
185
+ expect(response.needChallenge).toBe(true)
186
+
187
+ // Session should not be stored
188
+ const storedSession = await sessionStorage.get()
189
+ expect(storedSession).toBeNull()
190
+ })
191
+
192
+ it('retrieves existing session state', async () => {
193
+ // Initialize a session
194
+ await sessionService.initSession()
195
+
196
+ // Get session state
197
+ const sessionState = await sessionService.getSessionState()
198
+
199
+ expect(sessionState).toEqual({
200
+ sessionId: 'test-session-123',
201
+ })
202
+ })
203
+
204
+ it('returns null when no session exists', async () => {
205
+ // Don't initialize any session
206
+ const sessionState = await sessionService.getSessionState()
207
+
208
+ expect(sessionState).toBeNull()
209
+ })
210
+
211
+ it('handles multiple initialization attempts', async () => {
212
+ // First initialization
213
+ await sessionService.initSession()
214
+
215
+ const firstSession = await sessionStorage.get()
216
+ expect(firstSession?.sessionId).toBe('test-session-123')
217
+
218
+ // Update mock to return different session
219
+ mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
220
+ async (): Promise<InitSessionResponse> => {
221
+ return new InitSessionResponse({
222
+ sessionId: 'new-session-789',
223
+ needChallenge: false,
224
+ extra: {},
225
+ })
226
+ }
227
+
228
+ // Second initialization should replace the first
229
+ await sessionService.initSession()
230
+
231
+ const secondSession = await sessionStorage.get()
232
+ expect(secondSession?.sessionId).toBe('new-session-789')
233
+ })
234
+
235
+ it('sets and retrieves device ID through init response', async () => {
236
+ mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
237
+ async (): Promise<InitSessionResponse> => {
238
+ return new InitSessionResponse({
239
+ sessionId: 'device-test-session',
240
+ deviceId: 'new-device-123',
241
+ needChallenge: false,
242
+ extra: {},
243
+ })
244
+ }
245
+
246
+ await sessionService.initSession()
247
+
248
+ // Device ID should be stored
249
+ const deviceId = await deviceIdService.getDeviceId()
250
+ expect(deviceId).toBe('new-device-123')
251
+ })
252
+
253
+ it('preserves existing device ID when not provided in response', async () => {
254
+ // Set device ID first
255
+ await deviceIdService.setDeviceId('existing-device-456')
256
+
257
+ // Init without device_id in response
258
+ await sessionService.initSession()
259
+
260
+ // Device ID should remain unchanged
261
+ const deviceId = await deviceIdService.getDeviceId()
262
+ expect(deviceId).toBe('existing-device-456')
263
+ })
264
+ })
@@ -0,0 +1,52 @@
1
+ import type { Transport } from '@connectrpc/connect'
2
+ import { createConnectTransport } from '@connectrpc/connect-web'
3
+
4
+ /**
5
+ * Creates a test transport that simulates browser cookie behavior
6
+ * for integration testing with real backends.
7
+ */
8
+ export function createLocalCookieTransport(options: { baseUrl: string; cookieJar: Map<string, string> }): Transport {
9
+ const { baseUrl, cookieJar } = options
10
+
11
+ return createConnectTransport({
12
+ baseUrl,
13
+ credentials: 'include', // Tell backend we want cookies
14
+ interceptors: [
15
+ (next) => async (request) => {
16
+ // Add required headers that backend expects
17
+ request.header.set('x-request-source', 'lux-web')
18
+
19
+ // Simulate browser cookie behavior: add stored cookies to request
20
+ if (cookieJar.size > 0) {
21
+ const cookieHeader = Array.from(cookieJar.entries())
22
+ .map(([name, value]) => `${name}=${value}`)
23
+ .join('; ')
24
+ request.header.set('cookie', cookieHeader)
25
+ }
26
+
27
+ // Make the request
28
+ const response = await next(request)
29
+
30
+ // Simulate browser cookie behavior: store cookies from response
31
+ for (const cookie of response.header.getSetCookie()) {
32
+ const [nameValue] = cookie.split(';')
33
+ if (nameValue) {
34
+ const [name, value] = nameValue.split('=')
35
+ if (name && value) {
36
+ cookieJar.set(name.trim(), value.trim())
37
+ }
38
+ }
39
+ }
40
+
41
+ return response
42
+ },
43
+ ],
44
+ })
45
+ }
46
+
47
+ /**
48
+ * Creates a cookie jar for managing cookies in tests
49
+ */
50
+ export function createCookieJar(): Map<string, string> {
51
+ return new Map<string, string>()
52
+ }
@@ -0,0 +1,45 @@
1
+ import type { Transport } from '@connectrpc/connect'
2
+ import { createConnectTransport } from '@connectrpc/connect-web'
3
+
4
+ interface CreateLocalHeaderTransportOptions {
5
+ baseUrl: string
6
+ requestSource: 'lux-ios' | 'lux-android' | 'lux-extension'
7
+ getSessionId: () => Promise<string | null>
8
+ getDeviceId: () => Promise<string | null>
9
+ }
10
+
11
+ /**
12
+ * Creates a test transport that simulates native app behavior
13
+ * for integration testing with real backends.
14
+ *
15
+ * Unlike web which uses cookies, native platforms send session
16
+ * and device IDs via headers on every request.
17
+ */
18
+ export function createLocalHeaderTransport(options: CreateLocalHeaderTransportOptions): Transport {
19
+ const { baseUrl, requestSource, getSessionId, getDeviceId } = options
20
+
21
+ return createConnectTransport({
22
+ baseUrl,
23
+ // NO credentials: 'include' - native platforms don't use cookies
24
+ interceptors: [
25
+ (next) => async (request) => {
26
+ // Add platform-specific request source header
27
+ request.header.set('x-request-source', requestSource)
28
+
29
+ // Add session ID header if available
30
+ const sessionId = await getSessionId()
31
+ if (sessionId) {
32
+ request.header.set('X-Session-ID', sessionId)
33
+ }
34
+
35
+ // Add device ID header if available
36
+ const deviceId = await getDeviceId()
37
+ if (deviceId) {
38
+ request.header.set('X-Device-ID', deviceId)
39
+ }
40
+
41
+ return next(request)
42
+ },
43
+ ],
44
+ })
45
+ }
@@ -0,0 +1,122 @@
1
+ import type { ChallengeSolver, ChallengeSolverService } from '@luxfi/sessions/src/challenge-solvers/types'
2
+ import type { SessionService } from '@luxfi/sessions/src/session-service/types'
3
+ import { ChallengeType } from '@luxfi/sessions/src/session-service/types'
4
+ import { vi } from 'vitest'
5
+
6
+ /**
7
+ * Creates a mock SessionService with sensible defaults
8
+ * All methods are vi.fn() mocks that can be overridden
9
+ */
10
+ export function createMockSessionService(overrides: Partial<SessionService> = {}): SessionService {
11
+ return {
12
+ getSessionState: vi.fn().mockResolvedValue(null),
13
+ initSession: vi.fn().mockResolvedValue({
14
+ sessionId: 'mock-session-123',
15
+ needChallenge: false,
16
+ extra: {},
17
+ }),
18
+ requestChallenge: vi.fn().mockResolvedValue({
19
+ challengeId: 'mock-challenge-456',
20
+ challengeType: ChallengeType.TURNSTILE,
21
+ extra: {},
22
+ challengeData: { case: 'turnstile', value: { siteKey: 'mock-sitekey', action: 'verify' } },
23
+ }),
24
+ verifySession: vi.fn().mockResolvedValue({
25
+ retry: false,
26
+ }),
27
+ removeSession: vi.fn().mockResolvedValue(undefined),
28
+ ...overrides,
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Creates a mock ChallengeSolver
34
+ */
35
+ export function createMockChallengeSolver(
36
+ solveFn: () => Promise<string> = async (): Promise<string> => 'mock-solution',
37
+ ): ChallengeSolver {
38
+ return {
39
+ solve: vi.fn().mockImplementation(solveFn),
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Creates a mock ChallengeSolverService
45
+ */
46
+ export function createMockChallengeSolverService(
47
+ solvers: Map<ChallengeType, ChallengeSolver> = new Map(),
48
+ ): ChallengeSolverService {
49
+ // Default solvers if not provided
50
+ if (solvers.size === 0) {
51
+ solvers.set(
52
+ ChallengeType.TURNSTILE,
53
+ createMockChallengeSolver(async () => 'mock-turnstile-token'),
54
+ )
55
+ solvers.set(
56
+ ChallengeType.HASHCASH,
57
+ createMockChallengeSolver(async () => 'mock-hashcash-proof'),
58
+ )
59
+ }
60
+
61
+ return {
62
+ getSolver: vi.fn().mockImplementation((type: ChallengeType) => solvers.get(type) || null),
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Test scenario helpers for common setups
68
+ */
69
+ export const TestScenarios = {
70
+ /**
71
+ * Setup for when a session already exists
72
+ */
73
+ withExistingSession(service: SessionService, sessionId = 'existing-session-789'): void {
74
+ vi.mocked(service.getSessionState).mockResolvedValue({ sessionId })
75
+ },
76
+
77
+ /**
78
+ * Setup for when initialization requires no challenge
79
+ */
80
+ withNoChallenge(service: SessionService): void {
81
+ vi.mocked(service.getSessionState).mockResolvedValue(null)
82
+ vi.mocked(service.initSession).mockResolvedValue({
83
+ sessionId: 'new-session-111',
84
+ needChallenge: false,
85
+ extra: {},
86
+ })
87
+ },
88
+
89
+ /**
90
+ * Setup for when initialization requires a challenge
91
+ */
92
+ withChallengeRequired(service: SessionService, challengeType = ChallengeType.TURNSTILE): void {
93
+ vi.mocked(service.getSessionState).mockResolvedValue(null)
94
+ vi.mocked(service.initSession).mockResolvedValue({
95
+ sessionId: 'new-session-222',
96
+ needChallenge: true,
97
+ extra: {},
98
+ })
99
+ vi.mocked(service.requestChallenge).mockResolvedValue({
100
+ challengeId: 'challenge-333',
101
+ challengeType,
102
+ extra: {},
103
+ challengeData: { case: 'turnstile', value: { siteKey: 'test-sitekey', action: 'verify' } },
104
+ })
105
+ vi.mocked(service.verifySession).mockResolvedValue({
106
+ retry: false,
107
+ })
108
+ },
109
+
110
+ /**
111
+ * Setup for when server requests challenge retry
112
+ */
113
+ withServerRetry(service: SessionService, retriesBeforeSuccess = 1): void {
114
+ let attemptCount = 0
115
+ vi.mocked(service.verifySession).mockImplementation(async () => {
116
+ attemptCount++
117
+ return {
118
+ retry: attemptCount <= retriesBeforeSuccess,
119
+ }
120
+ })
121
+ },
122
+ }
@@ -0,0 +1,200 @@
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: mock handlers */
2
+ import type { PartialMessage } from '@bufbuild/protobuf'
3
+ import type { CallOptions } from '@connectrpc/connect'
4
+ import { createConnectTransport } from '@connectrpc/connect-web'
5
+ import {
6
+ type ChallengeRequest,
7
+ type ChallengeResponse,
8
+ type GetChallengeTypesRequest,
9
+ type GetChallengeTypesResponse,
10
+ type InitSessionRequest,
11
+ type InitSessionResponse,
12
+ type IntrospectSessionRequest,
13
+ type IntrospectSessionResponse,
14
+ type SignoutRequest,
15
+ type SignoutResponse,
16
+ type UpdateSessionRequest,
17
+ type UpdateSessionResponse,
18
+ type VerifyRequest,
19
+ type VerifyResponse,
20
+ } from '@luxdex/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
21
+ import type { DeviceIdService } from '@luxfi/sessions/src/device-id/types'
22
+ import type { SessionServiceClient } from '@luxfi/sessions/src/session-repository/createSessionClient'
23
+ import type { SessionState, SessionStorage } from '@luxfi/sessions/src/session-storage/types'
24
+ import type { LuxIdentifierService } from '@luxfi/sessions/src/lux-identifier/types'
25
+ // Types for our test transport
26
+ export interface MockEndpointHandler {
27
+ (request: any, headers: Record<string, string>): Promise<any>
28
+ }
29
+
30
+ export interface MockEndpoints {
31
+ '/lux.platformservice.v1.SessionService/InitSession': MockEndpointHandler
32
+ '/lux.platformservice.v1.SessionService/Challenge': MockEndpointHandler
33
+ '/lux.platformservice.v1.SessionService/Verify': MockEndpointHandler
34
+ '/lux.platformservice.v1.SessionService/IntrospectSession': MockEndpointHandler
35
+ '/lux.platformservice.v1.SessionService/UpdateSession': MockEndpointHandler
36
+ '/lux.platformservice.v1.SessionService/GetChallengeTypes': MockEndpointHandler
37
+ '/lux.platformservice.v1.SessionService/Signout': MockEndpointHandler
38
+ }
39
+
40
+ // Test transport that intercepts requests and returns mock responses
41
+ export function createTestTransport(mockEndpoints: MockEndpoints): ReturnType<typeof createConnectTransport> {
42
+ return createConnectTransport({
43
+ baseUrl: 'https://test.api.lux.org',
44
+ interceptors: [
45
+ (_next) => async (request) => {
46
+ const url = request.url
47
+ const path = new URL(url).pathname
48
+ const handler = mockEndpoints[path as keyof MockEndpoints]
49
+
50
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
51
+ if (!handler) {
52
+ throw new Error(`No mock handler for ${path}`)
53
+ }
54
+
55
+ // Extract headers
56
+ const headers: Record<string, string> = {}
57
+ request.header.forEach((value, key) => {
58
+ headers[key] = value
59
+ })
60
+
61
+ const requestData = request.message
62
+
63
+ const responseData = await handler(requestData, headers)
64
+
65
+ // Return properly typed response
66
+ return {
67
+ stream: false as const,
68
+ service: request.service,
69
+ method: request.method,
70
+ header: new Headers(),
71
+ message: responseData,
72
+ trailer: new Headers(),
73
+ }
74
+ },
75
+ ],
76
+ })
77
+ }
78
+
79
+ // In-memory storage implementations
80
+ export class InMemorySessionStorage implements SessionStorage {
81
+ private state: SessionState | null = null
82
+
83
+ async get(): Promise<SessionState | null> {
84
+ return this.state
85
+ }
86
+
87
+ async set(session: SessionState): Promise<void> {
88
+ this.state = session
89
+ }
90
+
91
+ async clear(): Promise<void> {
92
+ this.state = null
93
+ }
94
+ }
95
+
96
+ export class InMemoryDeviceIdService implements DeviceIdService {
97
+ private deviceId: string | null = null
98
+
99
+ async getDeviceId(): Promise<string | null> {
100
+ return this.deviceId
101
+ }
102
+
103
+ async setDeviceId(id: string): Promise<void> {
104
+ this.deviceId = id
105
+ }
106
+
107
+ async removeDeviceId(): Promise<void> {
108
+ this.deviceId = null
109
+ }
110
+ }
111
+
112
+ export class InMemoryLuxIdentifierService implements LuxIdentifierService {
113
+ private identifier: string | null = null
114
+
115
+ async getLuxIdentifier(): Promise<string | null> {
116
+ return this.identifier
117
+ }
118
+
119
+ async setLuxIdentifier(id: string): Promise<void> {
120
+ this.identifier = id
121
+ }
122
+
123
+ async removeLuxIdentifier(): Promise<void> {
124
+ this.identifier = null
125
+ }
126
+ }
127
+
128
+ // Create a mock session client for testing
129
+ // eslint-disable-next-line max-params
130
+ export function createMockSessionClient(
131
+ mockEndpoints: MockEndpoints,
132
+ sessionStorage: SessionStorage,
133
+ deviceIdService: DeviceIdService,
134
+ ): SessionServiceClient {
135
+ return {
136
+ initSession: async (
137
+ request: PartialMessage<InitSessionRequest>,
138
+ _options?: CallOptions,
139
+ ): Promise<InitSessionResponse> => {
140
+ const response = await mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'](request, {})
141
+ return response as InitSessionResponse
142
+ },
143
+ challenge: async (
144
+ request: PartialMessage<ChallengeRequest>,
145
+ _options?: CallOptions,
146
+ ): Promise<ChallengeResponse> => {
147
+ const sessionId = await sessionStorage.get()
148
+ const deviceId = await deviceIdService.getDeviceId()
149
+ const headers: Record<string, string> = {}
150
+ if (sessionId?.sessionId) {
151
+ headers['X-Session-ID'] = sessionId.sessionId
152
+ }
153
+ if (deviceId) {
154
+ headers['X-Device-ID'] = deviceId
155
+ }
156
+
157
+ const response = await mockEndpoints['/lux.platformservice.v1.SessionService/Challenge'](request, headers)
158
+ return response as ChallengeResponse
159
+ },
160
+ verify: async (request: PartialMessage<VerifyRequest>, _options?: CallOptions): Promise<VerifyResponse> => {
161
+ const sessionId = await sessionStorage.get()
162
+ const deviceId = await deviceIdService.getDeviceId()
163
+ const headers: Record<string, string> = {}
164
+ if (sessionId?.sessionId) {
165
+ headers['X-Session-ID'] = sessionId.sessionId
166
+ }
167
+ if (deviceId) {
168
+ headers['X-Device-ID'] = deviceId
169
+ }
170
+
171
+ const response = await mockEndpoints['/lux.platformservice.v1.SessionService/Verify'](request, headers)
172
+ return response as VerifyResponse
173
+ },
174
+ introspectSession: async (
175
+ request: PartialMessage<IntrospectSessionRequest>,
176
+ _options?: CallOptions,
177
+ ): Promise<IntrospectSessionResponse> => {
178
+ const response = await mockEndpoints['/lux.platformservice.v1.SessionService/IntrospectSession'](request, {})
179
+ return response as IntrospectSessionResponse
180
+ },
181
+ updateSession: async (
182
+ request: PartialMessage<UpdateSessionRequest>,
183
+ _options?: CallOptions,
184
+ ): Promise<UpdateSessionResponse> => {
185
+ const response = await mockEndpoints['/lux.platformservice.v1.SessionService/UpdateSession'](request, {})
186
+ return response as UpdateSessionResponse
187
+ },
188
+ getChallengeTypes: async (
189
+ request: PartialMessage<GetChallengeTypesRequest>,
190
+ _options?: CallOptions,
191
+ ): Promise<GetChallengeTypesResponse> => {
192
+ const response = await mockEndpoints['/lux.platformservice.v1.SessionService/GetChallengeTypes'](request, {})
193
+ return response as GetChallengeTypesResponse
194
+ },
195
+ signout: async (request: PartialMessage<SignoutRequest>, _options?: CallOptions): Promise<SignoutResponse> => {
196
+ const response = await mockEndpoints['/lux.platformservice.v1.SessionService/Signout'](request, {})
197
+ return response as SignoutResponse
198
+ },
199
+ }
200
+ }