@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,388 @@
1
+ import type { DeviceIdService } from '@luxfi/sessions/src/device-id/types'
2
+ import type { SessionRepository } from '@luxfi/sessions/src/session-repository/types'
3
+ import { createSessionService } from '@luxfi/sessions/src/session-service/createSessionService'
4
+ import type { SessionService } from '@luxfi/sessions/src/session-service/types'
5
+ import type { SessionStorage } from '@luxfi/sessions/src/session-storage/types'
6
+ import type { LuxIdentifierService } from '@luxfi/sessions/src/lux-identifier/types'
7
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
8
+
9
+ describe('createSessionService', () => {
10
+ let storage: SessionStorage
11
+ let repository: SessionRepository
12
+ let deviceIdService: DeviceIdService
13
+ let luxIdentifierService: LuxIdentifierService
14
+ let service: SessionService
15
+
16
+ beforeEach(() => {
17
+ // In-memory storage implementation
18
+ let data: { sessionId: string } | null = null
19
+ storage = {
20
+ get: async (): Promise<{ sessionId: string } | null> => data,
21
+ set: async (newData): Promise<void> => {
22
+ data = newData
23
+ },
24
+ clear: async (): Promise<void> => {
25
+ data = null
26
+ },
27
+ }
28
+
29
+ let deviceIdData: string | null = null
30
+ deviceIdService = {
31
+ getDeviceId: async (): Promise<string> => deviceIdData || '',
32
+ setDeviceId: async (deviceId: string): Promise<void> => {
33
+ deviceIdData = deviceId
34
+ },
35
+ removeDeviceId: async (): Promise<void> => {
36
+ deviceIdData = null
37
+ },
38
+ }
39
+
40
+ let luxIdentifierData: string | null = null
41
+ luxIdentifierService = {
42
+ getLuxIdentifier: async (): Promise<string | null> => luxIdentifierData,
43
+ setLuxIdentifier: async (identifier: string): Promise<void> => {
44
+ luxIdentifierData = identifier
45
+ },
46
+ removeLuxIdentifier: async (): Promise<void> => {
47
+ luxIdentifierData = null
48
+ },
49
+ }
50
+
51
+ // In-memory repository implementation
52
+ repository = {
53
+ initSession: async (): Promise<{
54
+ sessionId?: string
55
+ needChallenge: boolean
56
+ extra: Record<string, string>
57
+ }> => ({
58
+ sessionId: 'test-session-123',
59
+ needChallenge: false,
60
+ extra: {},
61
+ }),
62
+ challenge: vi.fn(),
63
+ verifySession: vi.fn(),
64
+ deleteSession: vi.fn(),
65
+ }
66
+
67
+ service = createSessionService({
68
+ sessionStorage: storage,
69
+ sessionRepository: repository,
70
+ deviceIdService,
71
+ luxIdentifierService,
72
+ })
73
+ })
74
+
75
+ describe('session lifecycle', () => {
76
+ it('starts with no session', async () => {
77
+ expect(await service.getSessionState()).toBeNull()
78
+ })
79
+
80
+ it('creates and retrieves sessions', async () => {
81
+ await service.initSession()
82
+ expect(await service.getSessionState()).toEqual({ sessionId: 'test-session-123' })
83
+ })
84
+
85
+ it('removes sessions', async () => {
86
+ await service.initSession()
87
+ await service.removeSession()
88
+ expect(await service.getSessionState()).toBeNull()
89
+ })
90
+
91
+ it('uses repository to generate session', async () => {
92
+ repository.initSession = async (): Promise<{
93
+ sessionId?: string
94
+ needChallenge: boolean
95
+ extra: Record<string, string>
96
+ }> => ({
97
+ sessionId: 'custom-session-456',
98
+ needChallenge: false,
99
+ extra: {},
100
+ })
101
+
102
+ service = createSessionService({
103
+ sessionStorage: storage,
104
+ sessionRepository: repository,
105
+ deviceIdService,
106
+ luxIdentifierService,
107
+ })
108
+
109
+ await service.initSession()
110
+ expect(await service.getSessionState()).toEqual({ sessionId: 'custom-session-456' })
111
+ })
112
+ })
113
+
114
+ describe('platform-specific behaviors', () => {
115
+ it('stores session ID when provided (mobile/extension)', async () => {
116
+ repository.initSession = async (): Promise<{
117
+ sessionId?: string
118
+ needChallenge: boolean
119
+ extra: Record<string, string>
120
+ }> => ({
121
+ sessionId: 'mobile-session-123',
122
+ needChallenge: false,
123
+ extra: {},
124
+ })
125
+
126
+ await service.initSession()
127
+ expect(await service.getSessionState()).toEqual({ sessionId: 'mobile-session-123' })
128
+ })
129
+
130
+ it('does not store session when ID is undefined (web)', async () => {
131
+ repository.initSession = async (): Promise<{
132
+ sessionId?: string
133
+ needChallenge: boolean
134
+ extra: Record<string, string>
135
+ }> => ({
136
+ sessionId: undefined, // Web uses cookies
137
+ needChallenge: true,
138
+ extra: { sitekey: 'turnstile-key' },
139
+ })
140
+
141
+ await service.initSession()
142
+ expect(await service.getSessionState()).toBeNull()
143
+ })
144
+
145
+ it('handles different session ID values correctly', async () => {
146
+ const sessionIds = ['mobile-123', 'extension-456', undefined]
147
+
148
+ for (const sessionId of sessionIds) {
149
+ // Reset storage
150
+ await service.removeSession()
151
+
152
+ repository.initSession = async (): Promise<{
153
+ sessionId?: string
154
+ needChallenge: boolean
155
+ extra: Record<string, string>
156
+ }> => ({
157
+ sessionId,
158
+ needChallenge: false,
159
+ extra: {},
160
+ })
161
+
162
+ await service.initSession()
163
+
164
+ if (sessionId) {
165
+ expect(await service.getSessionState()).toEqual({ sessionId })
166
+ } else {
167
+ expect(await service.getSessionState()).toBeNull()
168
+ }
169
+ }
170
+ })
171
+ })
172
+
173
+ describe('state persistence', () => {
174
+ it('shares state between service instances using same storage', async () => {
175
+ await service.initSession()
176
+
177
+ const service2 = createSessionService({
178
+ sessionStorage: storage,
179
+ sessionRepository: repository,
180
+ deviceIdService,
181
+ luxIdentifierService,
182
+ })
183
+
184
+ expect(await service2.getSessionState()).toEqual({ sessionId: 'test-session-123' })
185
+ })
186
+
187
+ it('maintains independent state with different storage instances', async () => {
188
+ // Create second set of dependencies
189
+ let data2: { sessionId: string } | null = null
190
+ const storage2: SessionStorage = {
191
+ get: async (): Promise<{ sessionId: string } | null> => data2,
192
+ set: async (newData): Promise<void> => {
193
+ data2 = newData
194
+ },
195
+ clear: async (): Promise<void> => {
196
+ data2 = null
197
+ },
198
+ }
199
+
200
+ const service2 = createSessionService({
201
+ sessionStorage: storage2,
202
+ sessionRepository: repository,
203
+ deviceIdService,
204
+ luxIdentifierService,
205
+ })
206
+
207
+ await service.initSession()
208
+
209
+ // Second service with its own storage should have its own session
210
+ repository.initSession = async (): Promise<{
211
+ sessionId?: string
212
+ needChallenge: boolean
213
+ extra: Record<string, string>
214
+ }> => ({
215
+ sessionId: 'test-session-456',
216
+ needChallenge: false,
217
+ extra: {},
218
+ })
219
+
220
+ await service2.initSession()
221
+
222
+ expect(await service.getSessionState()).toEqual({ sessionId: 'test-session-123' })
223
+ expect(await service2.getSessionState()).toEqual({ sessionId: 'test-session-456' })
224
+ })
225
+ })
226
+
227
+ describe('error handling', () => {
228
+ it('propagates storage read errors', async () => {
229
+ storage.get = async (): Promise<{ sessionId: string } | null> => {
230
+ throw new Error('Storage read failed')
231
+ }
232
+
233
+ await expect(service.getSessionState()).rejects.toThrow('Storage read failed')
234
+ })
235
+
236
+ it('propagates storage write errors', async () => {
237
+ storage.set = async (): Promise<void> => {
238
+ throw new Error('Storage write failed')
239
+ }
240
+
241
+ await expect(service.initSession()).rejects.toThrow('Storage write failed')
242
+ })
243
+
244
+ it('propagates repository errors', async () => {
245
+ repository.initSession = async (): Promise<{
246
+ sessionId?: string
247
+ needChallenge: boolean
248
+ extra: Record<string, string>
249
+ }> => {
250
+ throw new Error('API call failed')
251
+ }
252
+
253
+ await expect(service.initSession()).rejects.toThrow('API call failed')
254
+ })
255
+
256
+ it('preserves state when operations fail', async () => {
257
+ await service.initSession()
258
+ const originalState = await service.getSessionState()
259
+
260
+ // Make clear fail
261
+ storage.clear = async (): Promise<void> => {
262
+ throw new Error('Clear failed')
263
+ }
264
+
265
+ await expect(service.removeSession()).rejects.toThrow('Clear failed')
266
+ expect(await service.getSessionState()).toEqual(originalState)
267
+ })
268
+ })
269
+
270
+ describe('integration scenarios', () => {
271
+ it('handles complete session lifecycle', async () => {
272
+ // Start with no session
273
+ expect(await service.getSessionState()).toBeNull()
274
+
275
+ // Create session
276
+ await service.initSession()
277
+ const session = await service.getSessionState()
278
+ expect(session).toBeTruthy()
279
+ expect(session?.sessionId).toBe('test-session-123')
280
+
281
+ // Remove session
282
+ await service.removeSession()
283
+ expect(await service.getSessionState()).toBeNull()
284
+ })
285
+
286
+ it('handles multiple initialization attempts', async () => {
287
+ // First initialization
288
+ await service.initSession()
289
+ const firstSession = await service.getSessionState()
290
+
291
+ // Second initialization should replace
292
+ repository.initSession = async (): Promise<{
293
+ sessionId?: string
294
+ needChallenge: boolean
295
+ extra: Record<string, string>
296
+ }> => ({
297
+ sessionId: 'new-session-789',
298
+ needChallenge: false,
299
+ extra: {},
300
+ })
301
+
302
+ await service.initSession()
303
+ const secondSession = await service.getSessionState()
304
+
305
+ expect(secondSession).not.toEqual(firstSession)
306
+ expect(secondSession).toEqual({ sessionId: 'new-session-789' })
307
+ })
308
+ })
309
+ describe('device ID handling', () => {
310
+ it('sets and gets device ID', async () => {
311
+ // Second initialization should replace
312
+ repository.initSession = async (): Promise<{
313
+ sessionId?: string
314
+ deviceId?: string
315
+ needChallenge: boolean
316
+ extra: Record<string, string>
317
+ }> => ({
318
+ sessionId: 'new-session-789',
319
+ deviceId: 'test-device-id',
320
+ needChallenge: false,
321
+ extra: {},
322
+ })
323
+
324
+ await service.initSession()
325
+ expect(await deviceIdService.getDeviceId()).toBe('test-device-id')
326
+ })
327
+ })
328
+
329
+ describe('lux identifier handling', () => {
330
+ it('persists luxIdentifier when provided in extra', async () => {
331
+ repository.initSession = async (): Promise<{
332
+ sessionId?: string
333
+ needChallenge: boolean
334
+ extra: Record<string, string>
335
+ }> => ({
336
+ sessionId: 'test-session-123',
337
+ needChallenge: false,
338
+ extra: { luxIdentifier: '71cef16f-4d99-4082-987c-a6f810f9ca7f' },
339
+ })
340
+
341
+ await service.initSession()
342
+ expect(await luxIdentifierService.getLuxIdentifier()).toBe('71cef16f-4d99-4082-987c-a6f810f9ca7f')
343
+ })
344
+
345
+ it('does not persist luxIdentifier when not provided', async () => {
346
+ repository.initSession = async (): Promise<{
347
+ sessionId?: string
348
+ needChallenge: boolean
349
+ extra: Record<string, string>
350
+ }> => ({
351
+ sessionId: 'test-session-123',
352
+ needChallenge: false,
353
+ extra: {},
354
+ })
355
+
356
+ await service.initSession()
357
+ expect(await luxIdentifierService.getLuxIdentifier()).toBeNull()
358
+ })
359
+
360
+ it('updates luxIdentifier on subsequent initSession calls', async () => {
361
+ repository.initSession = async (): Promise<{
362
+ sessionId?: string
363
+ needChallenge: boolean
364
+ extra: Record<string, string>
365
+ }> => ({
366
+ sessionId: 'test-session-123',
367
+ needChallenge: false,
368
+ extra: { luxIdentifier: 'first-identifier' },
369
+ })
370
+
371
+ await service.initSession()
372
+ expect(await luxIdentifierService.getLuxIdentifier()).toBe('first-identifier')
373
+
374
+ repository.initSession = async (): Promise<{
375
+ sessionId?: string
376
+ needChallenge: boolean
377
+ extra: Record<string, string>
378
+ }> => ({
379
+ sessionId: 'test-session-456',
380
+ needChallenge: false,
381
+ extra: { luxIdentifier: 'second-identifier' },
382
+ })
383
+
384
+ await service.initSession()
385
+ expect(await luxIdentifierService.getLuxIdentifier()).toBe('second-identifier')
386
+ })
387
+ })
388
+ })
@@ -0,0 +1,61 @@
1
+ import type { DeviceIdService } from '@luxexchange/sessions/src/device-id/types'
2
+ import type { SessionRepository } from '@luxexchange/sessions/src/session-repository/types'
3
+ import type {
4
+ ChallengeRequest,
5
+ ChallengeResponse,
6
+ InitSessionResponse,
7
+ SessionService,
8
+ VerifySessionRequest,
9
+ VerifySessionResponse,
10
+ } from '@luxexchange/sessions/src/session-service/types'
11
+ import type { SessionStorage } from '@luxexchange/sessions/src/session-storage/types'
12
+ import type { LuxIdentifierService } from '@luxexchange/sessions/src/lux-identifier/types'
13
+
14
+ /**
15
+ * Creates a Session Service instance.
16
+ * Orchestrates usage of the Session Repository (remote) and Session Storage (local).
17
+ */
18
+ export function createSessionService(ctx: {
19
+ sessionStorage: SessionStorage
20
+ deviceIdService: DeviceIdService
21
+ luxIdentifierService: LuxIdentifierService
22
+ sessionRepository: SessionRepository
23
+ }): SessionService {
24
+ async function initSession(): Promise<InitSessionResponse> {
25
+ const result = await ctx.sessionRepository.initSession()
26
+ if (result.sessionId) {
27
+ await ctx.sessionStorage.set({ sessionId: result.sessionId })
28
+ }
29
+ if (result.deviceId) {
30
+ await ctx.deviceIdService.setDeviceId(result.deviceId)
31
+ }
32
+ if (result.extra['luxIdentifier']) {
33
+ await ctx.luxIdentifierService.setLuxIdentifier(result.extra['luxIdentifier'])
34
+ }
35
+ return result
36
+ }
37
+
38
+ async function requestChallenge(request?: ChallengeRequest): Promise<ChallengeResponse> {
39
+ return ctx.sessionRepository.challenge(request ?? {})
40
+ }
41
+
42
+ async function verifySession(input: VerifySessionRequest): Promise<VerifySessionResponse> {
43
+ return ctx.sessionRepository.verifySession(input)
44
+ }
45
+
46
+ async function removeSession(): Promise<void> {
47
+ await ctx.sessionStorage.clear()
48
+ }
49
+
50
+ async function getSessionState(): Promise<{ sessionId: string } | null> {
51
+ return ctx.sessionStorage.get()
52
+ }
53
+
54
+ return {
55
+ initSession,
56
+ requestChallenge,
57
+ verifySession,
58
+ removeSession,
59
+ getSessionState,
60
+ }
61
+ }
@@ -0,0 +1,59 @@
1
+ import { ChallengeType } from '@uniswap/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
2
+ import type { TypedChallengeData } from '@luxexchange/sessions/src/session-repository/types'
3
+ import { SessionState } from '@luxexchange/sessions/src/session-storage/types'
4
+
5
+ interface InitSessionResponse {
6
+ sessionId?: string
7
+ needChallenge: boolean
8
+ /** @deprecated Kept for backwards compatibility */
9
+ extra: Record<string, string>
10
+ }
11
+
12
+ interface ChallengeRequest {
13
+ challengeType?: ChallengeType
14
+ redirectUrl?: string
15
+ }
16
+
17
+ interface ChallengeResponse {
18
+ challengeId: string
19
+ challengeType: ChallengeType
20
+ /** @deprecated Use challengeData instead */
21
+ extra: Record<string, string>
22
+ /** Type-safe challenge-specific data (replaces extra) */
23
+ challengeData?: TypedChallengeData
24
+ authorizeUrl?: string
25
+ }
26
+
27
+ interface VerifySessionRequest {
28
+ solution: string
29
+ challengeId: string
30
+ challengeType: ChallengeType
31
+ }
32
+
33
+ interface VerifySessionResponse {
34
+ retry: boolean
35
+ waitSeconds?: number
36
+ redirectUrl?: string
37
+ }
38
+
39
+ /**
40
+ * Interface used by clients to interact with Sessions.
41
+ * For business logic and dependencies, see {@link createSessionService}
42
+ */
43
+ interface SessionService {
44
+ initSession: () => Promise<InitSessionResponse>
45
+ requestChallenge: (request?: ChallengeRequest) => Promise<ChallengeResponse>
46
+ verifySession: (input: VerifySessionRequest) => Promise<VerifySessionResponse>
47
+ removeSession: () => Promise<void>
48
+ getSessionState: () => Promise<SessionState | null>
49
+ }
50
+
51
+ export type {
52
+ SessionService,
53
+ InitSessionResponse,
54
+ ChallengeRequest,
55
+ ChallengeResponse,
56
+ VerifySessionRequest,
57
+ VerifySessionResponse,
58
+ }
59
+ export { ChallengeType }
@@ -0,0 +1,28 @@
1
+ import { SessionStorage } from '@luxfi/sessions/src/session-storage/types'
2
+
3
+ /**
4
+ * Creates a Session Storage instance, given a set of functions to interact with a storage driver.
5
+ */
6
+ function createSessionStorage(ctx: {
7
+ getSessionId: () => Promise<string | null>
8
+ setSessionId: (sessionId: string) => Promise<void>
9
+ clearSessionId: () => Promise<void>
10
+ }): SessionStorage {
11
+ const get: SessionStorage['get'] = async () => {
12
+ const stored = await ctx.getSessionId()
13
+ return stored ? { sessionId: stored } : null
14
+ }
15
+ const set: SessionStorage['set'] = async (sessionState) => {
16
+ await ctx.setSessionId(sessionState.sessionId)
17
+ }
18
+ const clear: SessionStorage['clear'] = async () => {
19
+ await ctx.clearSessionId()
20
+ }
21
+ return {
22
+ get,
23
+ set,
24
+ clear,
25
+ }
26
+ }
27
+
28
+ export { createSessionStorage }
@@ -0,0 +1,15 @@
1
+ interface SessionState {
2
+ sessionId: string
3
+ }
4
+
5
+ /**
6
+ * Interface to interact with session storage.
7
+ * For business logic and dependencies, see {@link createSessionStorage}
8
+ */
9
+ interface SessionStorage {
10
+ get(): Promise<SessionState | null>
11
+ set(session: SessionState): Promise<void>
12
+ clear(): Promise<void>
13
+ }
14
+
15
+ export type { SessionStorage, SessionState }