@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,331 @@
1
+ /// @vitest-environment happy-dom
2
+ import { ChallengeType } from '@uniswap/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
3
+ import { createTurnstileSolver } from '@luxexchange/sessions/src/challenge-solvers/createTurnstileSolver'
4
+ import { resetTurnstileState } from '@luxexchange/sessions/src/challenge-solvers/turnstileScriptLoader'
5
+ import type { PerformanceTracker } from '@luxexchange/sessions/src/performance/types'
6
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
7
+
8
+ // Mock performance tracker for testing
9
+ function createMockPerformanceTracker(): PerformanceTracker {
10
+ let time = 0
11
+ return {
12
+ now: (): number => {
13
+ time += 100
14
+ return time
15
+ },
16
+ }
17
+ }
18
+
19
+ // Mock window.turnstile API
20
+ const mockTurnstileAPI = {
21
+ render: vi.fn(),
22
+ remove: vi.fn(),
23
+ reset: vi.fn(),
24
+ getResponse: vi.fn(),
25
+ ready: vi.fn(),
26
+ }
27
+
28
+ // Setup DOM mocks
29
+ beforeAll(() => {
30
+ const originalCreateElement = document.createElement.bind(document)
31
+ vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
32
+ const element = originalCreateElement(tagName)
33
+ if (tagName === 'div') {
34
+ // Track created divs for assertions
35
+ // eslint-disable-next-line no-extra-semi
36
+ ;(element as any)._testCreated = true
37
+ }
38
+ return element
39
+ })
40
+
41
+ vi.spyOn(document.head, 'appendChild').mockImplementation((node) => {
42
+ if (node instanceof HTMLScriptElement && node.src.includes('challenges.cloudflare.com')) {
43
+ // Simulate script load immediately
44
+ setTimeout(() => {
45
+ // Set up the mock turnstile API
46
+ // eslint-disable-next-line no-extra-semi
47
+ ;(window as any).turnstile = mockTurnstileAPI
48
+ if (node.onload) {
49
+ node.onload({} as Event)
50
+ }
51
+ }, 0)
52
+ }
53
+ return node
54
+ })
55
+
56
+ const originalBodyAppendChild = document.body.appendChild.bind(document.body)
57
+ vi.spyOn(document.body, 'appendChild').mockImplementation((node) => {
58
+ // eslint-disable-next-line no-extra-semi
59
+ ;(node as any)._testAppended = true
60
+ // Actually append to the DOM so we can query it later
61
+ return originalBodyAppendChild(node)
62
+ })
63
+
64
+ vi.spyOn(Element.prototype, 'removeChild').mockImplementation(function (this: Element, child: Node) {
65
+ // eslint-disable-next-line no-extra-semi
66
+ ;(child as any)._testRemoved = true
67
+ return child
68
+ })
69
+ })
70
+
71
+ afterAll(() => {
72
+ vi.restoreAllMocks()
73
+ })
74
+
75
+ describe('Turnstile Solver Integration Tests', () => {
76
+ beforeEach(() => {
77
+ // Configure mock Turnstile API behavior
78
+ mockTurnstileAPI.ready.mockImplementation((callback: () => void) => {
79
+ // Call the callback immediately
80
+ callback()
81
+ })
82
+
83
+ mockTurnstileAPI.render.mockImplementation((container: string | HTMLElement, options: any) => {
84
+ // Simulate successful render and call the callback with a test token
85
+ if (options.callback) {
86
+ setTimeout(() => {
87
+ options.callback('test-turnstile-solution-token')
88
+ }, 10) // Small delay to simulate async behavior
89
+ }
90
+ return 'widget-123' // Return a mock widget ID
91
+ })
92
+ })
93
+
94
+ afterEach(() => {
95
+ // Clean up DOM
96
+ document.querySelectorAll('div[id^="turnstile-"]').forEach((el) => el.remove())
97
+
98
+ // Reset mocks to default successful behavior
99
+ vi.clearAllMocks()
100
+ ;(window as any).turnstile = undefined
101
+
102
+ // Reset turnstile script loader state for next test
103
+ resetTurnstileState()
104
+ })
105
+
106
+ it('verifies Turnstile solver basic functionality', async () => {
107
+ // Create a challenge solver directly to test
108
+ const turnstileSolver = createTurnstileSolver({ performanceTracker: createMockPerformanceTracker() })
109
+
110
+ // Create challenge data with proper structure
111
+ const challengeData = {
112
+ challengeId: 'dom-test-challenge-123',
113
+ challengeType: ChallengeType.TURNSTILE,
114
+ extra: {
115
+ challengeData: JSON.stringify({
116
+ siteKey: '0x4AAAAAABiAHneWOWZHzZtO',
117
+ action: 'session_verification',
118
+ }),
119
+ },
120
+ }
121
+
122
+ // Execute the solver and wait for solution
123
+ const solution = await turnstileSolver.solve(challengeData)
124
+
125
+ // Verify solution was returned
126
+ expect(solution).toBe('test-turnstile-solution-token')
127
+
128
+ // Verify script injection was attempted
129
+ expect(document.head.appendChild).toHaveBeenCalledWith(
130
+ expect.objectContaining({
131
+ src: 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit',
132
+ }),
133
+ )
134
+ })
135
+
136
+ it('handles Turnstile solver errors properly', async () => {
137
+ // Configure mock to simulate an error with proper timing
138
+ mockTurnstileAPI.render.mockImplementation(async (container: string | HTMLElement, options: any) => {
139
+ if (options['error-callback']) {
140
+ // Use microtask to ensure promise handlers are set up
141
+ await Promise.resolve().then(() => {
142
+ options['error-callback']('NETWORK_ERROR')
143
+ })
144
+ }
145
+ return 'widget-error-123'
146
+ })
147
+
148
+ const turnstileSolver = createTurnstileSolver({ performanceTracker: createMockPerformanceTracker() })
149
+ const challengeData = {
150
+ challengeId: 'error-test-123',
151
+ challengeType: ChallengeType.TURNSTILE,
152
+ extra: {
153
+ challengeData: JSON.stringify({
154
+ siteKey: 'test-site-key',
155
+ action: 'test-action',
156
+ }),
157
+ },
158
+ }
159
+
160
+ // Should reject with Turnstile error
161
+ await expect(turnstileSolver.solve(challengeData)).rejects.toThrow('Turnstile error: NETWORK_ERROR')
162
+ })
163
+
164
+ it('handles expired tokens', async () => {
165
+ // Configure mock to simulate token expiration
166
+ mockTurnstileAPI.render.mockImplementation(async (container: string | HTMLElement, options: any) => {
167
+ if (options['expired-callback']) {
168
+ // Use microtask to ensure promise handlers are set up
169
+ await Promise.resolve().then(() => {
170
+ options['expired-callback']()
171
+ })
172
+ }
173
+ return 'widget-expired-123'
174
+ })
175
+
176
+ const turnstileSolver = createTurnstileSolver({ performanceTracker: createMockPerformanceTracker() })
177
+ const challengeData = {
178
+ challengeId: 'expired-test-123',
179
+ challengeType: ChallengeType.TURNSTILE,
180
+ extra: {
181
+ challengeData: JSON.stringify({
182
+ siteKey: 'test-site-key',
183
+ action: 'test-action',
184
+ }),
185
+ },
186
+ }
187
+
188
+ // Should reject with expiration error
189
+ await expect(turnstileSolver.solve(challengeData)).rejects.toThrow('Turnstile token expired')
190
+ })
191
+
192
+ it('handles timeout scenarios', async () => {
193
+ // Configure mock to never call any callbacks
194
+ mockTurnstileAPI.render.mockImplementation(() => {
195
+ // Don't call any callbacks - simulate timeout
196
+ return 'widget-timeout-123'
197
+ })
198
+
199
+ // Use a short timeout for testing (100ms instead of 30s)
200
+ const turnstileSolver = createTurnstileSolver({
201
+ performanceTracker: createMockPerformanceTracker(),
202
+ timeoutMs: 100,
203
+ })
204
+ const challengeData = {
205
+ challengeId: 'timeout-test-123',
206
+ challengeType: ChallengeType.TURNSTILE,
207
+ extra: {
208
+ challengeData: JSON.stringify({
209
+ siteKey: 'test-site-key',
210
+ action: 'test-action',
211
+ }),
212
+ },
213
+ }
214
+
215
+ // Should reject with timeout error after 100ms
216
+ await expect(turnstileSolver.solve(challengeData)).rejects.toThrow('Turnstile challenge timed out after')
217
+ })
218
+
219
+ it('handles missing challenge data', async () => {
220
+ const turnstileSolver = createTurnstileSolver({ performanceTracker: createMockPerformanceTracker() })
221
+ const challengeData = {
222
+ challengeId: 'missing-data-123',
223
+ challengeType: ChallengeType.TURNSTILE,
224
+ extra: {}, // Missing challengeData
225
+ }
226
+
227
+ // Should reject with missing data error
228
+ await expect(turnstileSolver.solve(challengeData)).rejects.toThrow('Missing challengeData in challenge extra')
229
+ })
230
+
231
+ it('handles invalid challenge data JSON', async () => {
232
+ const turnstileSolver = createTurnstileSolver({ performanceTracker: createMockPerformanceTracker() })
233
+ const challengeData = {
234
+ challengeId: 'invalid-json-123',
235
+ challengeType: ChallengeType.TURNSTILE,
236
+ extra: {
237
+ challengeData: 'invalid-json-{',
238
+ },
239
+ }
240
+
241
+ // Should reject with JSON parse error
242
+ await expect(turnstileSolver.solve(challengeData)).rejects.toThrow('Failed to parse challengeData')
243
+ })
244
+
245
+ it('handles missing siteKey in challenge data', async () => {
246
+ const turnstileSolver = createTurnstileSolver({ performanceTracker: createMockPerformanceTracker() })
247
+ const challengeData = {
248
+ challengeId: 'missing-sitekey-123',
249
+ challengeType: ChallengeType.TURNSTILE,
250
+ extra: {
251
+ challengeData: JSON.stringify({
252
+ action: 'test-action',
253
+ // Missing siteKey
254
+ }),
255
+ },
256
+ }
257
+
258
+ // Should reject with missing siteKey error
259
+ await expect(turnstileSolver.solve(challengeData)).rejects.toThrow('Missing siteKey in challengeData')
260
+ })
261
+
262
+ it('handles script loading failures', async () => {
263
+ // Mock script loading failure
264
+ vi.spyOn(document.head, 'appendChild').mockImplementationOnce((node) => {
265
+ if (node instanceof HTMLScriptElement && node.src.includes('challenges.cloudflare.com')) {
266
+ setTimeout(() => {
267
+ if (node.onerror) {
268
+ node.onerror({} as Event)
269
+ }
270
+ }, 0)
271
+ }
272
+ return node
273
+ })
274
+
275
+ const turnstileSolver = createTurnstileSolver({ performanceTracker: createMockPerformanceTracker() })
276
+ const challengeData = {
277
+ challengeId: 'script-fail-123',
278
+ challengeType: ChallengeType.TURNSTILE,
279
+ extra: {
280
+ challengeData: JSON.stringify({
281
+ siteKey: 'test-site-key',
282
+ action: 'test-action',
283
+ }),
284
+ },
285
+ }
286
+
287
+ // Should reject with script loading error
288
+ await expect(turnstileSolver.solve(challengeData)).rejects.toThrow('Failed to load Turnstile script')
289
+ })
290
+
291
+ it('handles multiple concurrent solve requests', async () => {
292
+ const turnstileSolver = createTurnstileSolver({ performanceTracker: createMockPerformanceTracker() })
293
+
294
+ // Create multiple challenge data objects
295
+ const challenges = Array.from({ length: 3 }, (_, i) => ({
296
+ challengeId: `concurrent-test-${i}`,
297
+ challengeType: ChallengeType.TURNSTILE,
298
+ extra: {
299
+ challengeData: JSON.stringify({
300
+ siteKey: `test-site-key-${i}`,
301
+ action: 'test-action',
302
+ }),
303
+ },
304
+ }))
305
+
306
+ // Configure mock to return different tokens for each widget
307
+ let widgetCounter = 0
308
+ mockTurnstileAPI.render.mockImplementation((container: string | HTMLElement, options: any) => {
309
+ const widgetId = `widget-${widgetCounter++}`
310
+ if (options.callback) {
311
+ setTimeout(() => {
312
+ options.callback(`solution-for-${widgetId}`)
313
+ }, 10)
314
+ }
315
+ return widgetId
316
+ })
317
+
318
+ // Execute all solvers concurrently
319
+ const solutionPromises = challenges.map((challenge) => turnstileSolver.solve(challenge))
320
+
321
+ // Wait for all solutions
322
+ const solutions = await Promise.all(solutionPromises)
323
+
324
+ // Verify all solutions are unique
325
+ expect(solutions).toHaveLength(3)
326
+ expect(new Set(solutions).size).toBe(3)
327
+ solutions.forEach((solution) => {
328
+ expect(solution).toMatch(/^solution-for-widget-\d+$/)
329
+ })
330
+ })
331
+ })
@@ -0,0 +1,55 @@
1
+ import type { TypedChallengeData } from '@luxexchange/sessions/src/session-repository/types'
2
+ import type { ChallengeType } from '@luxexchange/sessions/src/session-service/types'
3
+
4
+ interface ChallengeData {
5
+ challengeId: string
6
+ challengeType: ChallengeType
7
+ /** @deprecated Use challengeData instead */
8
+ extra?: Record<string, string>
9
+ /** Type-safe challenge-specific data (replaces extra) */
10
+ challengeData?: TypedChallengeData
11
+ }
12
+
13
+ interface ChallengeSolver {
14
+ solve(challengeData: ChallengeData): Promise<string>
15
+ }
16
+
17
+ interface ChallengeSolverService {
18
+ getSolver(type: ChallengeType): ChallengeSolver | null
19
+ }
20
+
21
+ /**
22
+ * Script injection options for CSP compliance and customization
23
+ */
24
+ interface TurnstileScriptOptions {
25
+ /**
26
+ * Custom nonce for the injected script (for CSP compliance)
27
+ */
28
+ nonce?: string
29
+ /**
30
+ * Whether to set the script as defer
31
+ * @default false (Turnstile requires synchronous loading for ready())
32
+ */
33
+ defer?: boolean
34
+ /**
35
+ * Whether to set the script as async
36
+ * @default false (Turnstile requires synchronous loading for ready())
37
+ */
38
+ async?: boolean
39
+ /**
40
+ * Custom crossOrigin for the injected script
41
+ */
42
+ crossOrigin?: string
43
+ /**
44
+ * Custom ID for the injected script
45
+ * @default "cf-turnstile-script"
46
+ */
47
+ id?: string
48
+ /**
49
+ * Custom name for the onload callback
50
+ * @default "onloadTurnstileCallback"
51
+ */
52
+ onLoadCallbackName?: string
53
+ }
54
+
55
+ export type { ChallengeData, ChallengeSolver, ChallengeSolverService, TurnstileScriptOptions }