@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,242 @@
1
+ import {
2
+ ChallengeFailure_Reason,
3
+ VerifyFailure_Reason,
4
+ } from '@uniswap/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
5
+ import type { SessionServiceClient } from '@luxexchange/sessions/src/session-repository/createSessionClient'
6
+ import { ChallengeRejectedError } from '@luxexchange/sessions/src/session-repository/errors'
7
+ import type { SessionRepository, TypedChallengeData } from '@luxexchange/sessions/src/session-repository/types'
8
+ import { ChallengeFailureReason, VerifyFailureReason } from '@luxexchange/sessions/src/session-repository/types'
9
+ import type { Logger } from '@luxfi/utilities/src/logger/logger'
10
+
11
+ /**
12
+ * Creates a session repository that handles communication with the session service.
13
+ * This is the layer that makes actual API calls to the backend.
14
+ *
15
+ * TODO(proto): `VerifyResponse` in `@uniswap/client-platform-service` still has a proto3
16
+ * issue where an empty `VerifySuccess` message gets silently dropped, leaving
17
+ * `outcome.case === undefined`. The `verifySession()` method works around this by using
18
+ * the `retry` flag as a discriminator.
19
+ *
20
+ * The `ChallengeResponse` failure case was fixed in v0.0.14 — the proto now includes a
21
+ * proper `failure?: ChallengeFailure` field with typed `ChallengeFailure_Reason`.
22
+ */
23
+ function createSessionRepository(ctx: { client: SessionServiceClient; getLogger?: () => Logger }): SessionRepository {
24
+ const initSession: SessionRepository['initSession'] = async () => {
25
+ try {
26
+ const response = await ctx.client['initSession']({})
27
+
28
+ return {
29
+ sessionId: response.sessionId,
30
+ deviceId: response.deviceId,
31
+ needChallenge: response.needChallenge || false,
32
+ extra: response.extra,
33
+ }
34
+ } catch (error) {
35
+ const errorMessage = error instanceof Error ? error.message : String(error)
36
+ throw new Error(`Failed to initialize session: ${errorMessage}`, { cause: error })
37
+ }
38
+ }
39
+
40
+ const challenge: SessionRepository['challenge'] = async (request) => {
41
+ try {
42
+ const response = await ctx.client['challenge']({
43
+ challengeType: request.challengeType,
44
+ identifier: request.identifier,
45
+ })
46
+
47
+ const logger = ctx.getLogger?.()
48
+
49
+ logger?.debug('createSessionRepository', 'challenge', 'Raw challenge response', {
50
+ challengeId: response.challengeId,
51
+ challengeType: response.challengeType,
52
+ extraKeys: Object.keys(response.extra),
53
+ extra: response.extra,
54
+ challengeDataCase: (response as unknown as { challengeData?: TypedChallengeData }).challengeData?.case,
55
+ challengeDataValue: (response as unknown as { challengeData?: TypedChallengeData }).challengeData?.value,
56
+ })
57
+
58
+ // Check for explicit challenge failure from the backend (e.g., bot detection required)
59
+ if (response.failure) {
60
+ const reasonEnum = response.failure.reason
61
+ const reason = `REASON_${ChallengeFailure_Reason[reasonEnum]}` as ChallengeFailureReason
62
+ throw new ChallengeRejectedError(reason, { failure: response.failure, extra: response.extra })
63
+ }
64
+
65
+ // Map proto oneof challengeData to our typed interface
66
+ let challengeData: TypedChallengeData = { case: undefined }
67
+ let authorizeUrl: string | undefined
68
+ const protoChallengeData = (response as unknown as { challengeData?: TypedChallengeData }).challengeData
69
+
70
+ if (protoChallengeData && protoChallengeData.case === 'turnstile') {
71
+ challengeData = {
72
+ case: 'turnstile',
73
+ value: {
74
+ siteKey: protoChallengeData.value.siteKey,
75
+ action: protoChallengeData.value.action,
76
+ },
77
+ }
78
+ } else if (protoChallengeData && protoChallengeData.case === 'hashcash') {
79
+ challengeData = {
80
+ case: 'hashcash',
81
+ value: {
82
+ difficulty: protoChallengeData.value.difficulty,
83
+ subject: protoChallengeData.value.subject,
84
+ algorithm: protoChallengeData.value.algorithm,
85
+ nonce: protoChallengeData.value.nonce,
86
+ maxProofLength: protoChallengeData.value.maxProofLength,
87
+ verifier: protoChallengeData.value.verifier,
88
+ },
89
+ }
90
+ } else if (protoChallengeData && protoChallengeData.case === 'github') {
91
+ challengeData = {
92
+ case: 'github',
93
+ value: {
94
+ authorizeUrl: protoChallengeData.value.authorizeUrl,
95
+ },
96
+ }
97
+ authorizeUrl = protoChallengeData.value.authorizeUrl
98
+ } else {
99
+ // Fallback to legacy extra field for authorize URL
100
+ const legacyChallengeData = response.extra['challengeData']
101
+ // Legacy format is JSON: {"authorizeUrl":"https://..."} or a raw URL
102
+ if (legacyChallengeData?.startsWith('http')) {
103
+ authorizeUrl = legacyChallengeData
104
+ } else if (legacyChallengeData) {
105
+ try {
106
+ const parsed = JSON.parse(legacyChallengeData) as { authorizeUrl?: string }
107
+ authorizeUrl = parsed.authorizeUrl
108
+ } catch {
109
+ // Not JSON, not a URL — skip
110
+ }
111
+ }
112
+
113
+ logger?.debug('createSessionRepository', 'challenge', 'No typed challengeData, falling back to extra', {
114
+ legacyChallengeData,
115
+ resolvedAuthorizeUrl: authorizeUrl,
116
+ })
117
+ }
118
+
119
+ logger?.debug('createSessionRepository', 'challenge', 'Mapped challenge response', {
120
+ challengeDataCase: challengeData.case,
121
+ authorizeUrl,
122
+ })
123
+
124
+ return {
125
+ challengeId: response.challengeId || '',
126
+ challengeType: response.challengeType || 0,
127
+ extra: response.extra,
128
+ challengeData,
129
+ authorizeUrl,
130
+ }
131
+ } catch (error) {
132
+ // Don't wrap typed session errors — they carry structured data for callers
133
+ if (error instanceof ChallengeRejectedError) {
134
+ throw error
135
+ }
136
+ const errorMessage = error instanceof Error ? error.message : String(error)
137
+ throw new Error(`Failed to get challenge: ${errorMessage}`, { cause: error })
138
+ }
139
+ }
140
+
141
+ const verifySession: SessionRepository['verifySession'] = async (request) => {
142
+ try {
143
+ const response = await ctx.client['verify']({
144
+ solution: request.solution,
145
+ challengeId: request.challengeId,
146
+ type: request.challengeType,
147
+ })
148
+
149
+ const logger = ctx.getLogger?.()
150
+ logger?.debug('createSessionRepository', 'verifySession', 'Raw verify response', {
151
+ retry: response.retry,
152
+ retryType: typeof response.retry,
153
+ outcomeCase: response.outcome.case,
154
+ outcomeValue: JSON.stringify(response.outcome.value),
155
+ newSessionId: response.newSessionId,
156
+ responseKeys: Object.keys(response),
157
+ })
158
+
159
+ // Proto3 validation: When outcome.case is undefined, proto3 silently dropped the oneof.
160
+ // This happens for BOTH success and failure outcomes with empty/default values.
161
+ // Use `retry` as the discriminator: `retry: false` means the backend accepted the
162
+ // solution (proto3 dropped an empty VerifySuccess); `retry: true` means try again.
163
+ if (response.outcome.case === undefined) {
164
+ if (response.retry) {
165
+ // Backend wants a retry — outcome was likely a dropped failure. This is fine,
166
+ // the caller handles retries via the `retry` flag.
167
+ logger?.debug('createSessionRepository', 'verifySession', 'Empty outcome with retry=true, treating as retry')
168
+ } else {
169
+ // Backend accepted the solution — proto3 dropped the empty VerifySuccess message.
170
+ logger?.debug(
171
+ 'createSessionRepository',
172
+ 'verifySession',
173
+ 'Empty outcome with retry=false, treating as success (proto3 likely dropped empty VerifySuccess)',
174
+ )
175
+ }
176
+ }
177
+
178
+ // Extract userInfo from success outcome, waitSeconds from failure outcome
179
+ const userInfo =
180
+ response.outcome.case === 'success'
181
+ ? response.outcome.value.userInfo
182
+ ? { name: response.outcome.value.userInfo.name, email: response.outcome.value.userInfo.email }
183
+ : undefined
184
+ : undefined
185
+
186
+ const waitSeconds = response.outcome.case === 'failure' ? response.outcome.value.waitSeconds : undefined
187
+
188
+ // Extract failure info — cast the constructed string to the typed VerifyFailureReason union
189
+ const failureReasonEnum = response.outcome.case === 'failure' ? response.outcome.value.reason : undefined
190
+ const failureReason =
191
+ failureReasonEnum !== undefined
192
+ ? (`REASON_${VerifyFailure_Reason[failureReasonEnum]}` as VerifyFailureReason)
193
+ : undefined
194
+ const failureMessage = response.outcome.case === 'failure' ? response.outcome.value.message : undefined
195
+
196
+ return {
197
+ retry: response.retry,
198
+ waitSeconds,
199
+ userInfo,
200
+ failureReason,
201
+ failureMessage,
202
+ }
203
+ } catch (error) {
204
+ const errorMessage = error instanceof Error ? error.message : String(error)
205
+ throw new Error(`Failed to verify session: ${errorMessage}`, { cause: error })
206
+ }
207
+ }
208
+
209
+ const deleteSession: SessionRepository['deleteSession'] = async () => {
210
+ try {
211
+ // Proto renamed deleteSession to signout
212
+ await ctx.client['signout']({})
213
+ return {}
214
+ } catch (error) {
215
+ const errorMessage = error instanceof Error ? error.message : String(error)
216
+ throw new Error(`Failed to delete session: ${errorMessage}`, { cause: error })
217
+ }
218
+ }
219
+
220
+ const getChallengeTypes: SessionRepository['getChallengeTypes'] = async () => {
221
+ try {
222
+ const response = await ctx.client['getChallengeTypes']({})
223
+ return response.challengeTypeConfig.map((cfg: { type: number; config: Record<string, string> }) => ({
224
+ type: cfg.type,
225
+ config: cfg.config,
226
+ }))
227
+ } catch (error) {
228
+ const errorMessage = error instanceof Error ? error.message : String(error)
229
+ throw new Error(`Failed to get challenge types: ${errorMessage}`, { cause: error })
230
+ }
231
+ }
232
+
233
+ return {
234
+ initSession,
235
+ challenge,
236
+ verifySession,
237
+ deleteSession,
238
+ getChallengeTypes,
239
+ }
240
+ }
241
+
242
+ export { createSessionRepository }
@@ -0,0 +1,22 @@
1
+ import { SessionError } from '@luxexchange/sessions/src/session-initialization/sessionErrors'
2
+
3
+ /**
4
+ * Error thrown when the Entry Gateway rejects a challenge request.
5
+ *
6
+ * Since v0.0.14, the proto `ChallengeResponse` includes a typed `failure?: ChallengeFailure`
7
+ * field with `ChallengeFailure_Reason` (e.g., `BOT_DETECTION_REQUIRED`). This error is
8
+ * thrown when that field is present, carrying the typed reason for callers to handle.
9
+ */
10
+ export class ChallengeRejectedError extends SessionError {
11
+ /** The failure reason string (may not be in our proto types yet) */
12
+ readonly reason: string
13
+
14
+ /** The full raw failure object for debugging */
15
+ readonly rawFailure: unknown
16
+
17
+ constructor(reason: string, rawFailure?: unknown) {
18
+ super(`Challenge rejected by Entry Gateway: ${reason}`, 'ChallengeRejectedError')
19
+ this.reason = reason
20
+ this.rawFailure = rawFailure
21
+ }
22
+ }
@@ -0,0 +1,289 @@
1
+ import { ChallengeType } from '@luxdex/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
2
+
3
+ /**
4
+ * Typed challenge data for Turnstile bot detection
5
+ */
6
+ interface TurnstileChallengeData {
7
+ siteKey: string
8
+ action: string
9
+ }
10
+
11
+ /**
12
+ * Typed challenge data for HashCash proof-of-work
13
+ */
14
+ interface HashCashChallengeData {
15
+ difficulty: number
16
+ subject: string
17
+ algorithm: string
18
+ nonce: string
19
+ maxProofLength: number
20
+ verifier: string
21
+ }
22
+
23
+ /**
24
+ * Typed challenge data for GitHub OAuth
25
+ */
26
+ interface GitHubChallengeData {
27
+ authorizeUrl: string
28
+ }
29
+
30
+ /**
31
+ * Type-safe challenge data union (mirrors proto oneof ChallengeResponse.challenge_data)
32
+ */
33
+ type TypedChallengeData =
34
+ | { case: 'turnstile'; value: TurnstileChallengeData }
35
+ | { case: 'hashcash'; value: HashCashChallengeData }
36
+ | { case: 'github'; value: GitHubChallengeData }
37
+ | { case: undefined; value?: undefined }
38
+
39
+ /**
40
+ * Response from session initialization
41
+ */
42
+ interface InitSessionResponse {
43
+ /**
44
+ * Session ID
45
+ * - Web: undefined (in Set-Cookie header)
46
+ * - Mobile/Extension: actual session ID string
47
+ */
48
+ sessionId?: string // optional string session_id
49
+ deviceId?: string // string device_id
50
+
51
+ /** Whether bot detection challenge is required */
52
+ needChallenge: boolean // bool need_challenge
53
+
54
+ /** @deprecated Extra information for bot detection (JSON data) — kept for backwards compatibility */
55
+ extra: Record<string, string> // map<string, string> extra
56
+ }
57
+
58
+ /**
59
+ * Request for a challenge
60
+ * For bot detection: empty (server decides challenge type)
61
+ * For OAuth: specify challengeType
62
+ */
63
+ interface ChallengeRequest {
64
+ /** Challenge type to request (optional - server decides if not specified) */
65
+ challengeType?: ChallengeType
66
+ /** Email or other identifier (required for email OTP challenges) */
67
+ identifier?: string
68
+ }
69
+
70
+ /**
71
+ * Challenge response
72
+ * For bot detection: typed challenge data in challengeData
73
+ * For OAuth: authorizeUrl in challengeData (GitHub) or extra (legacy)
74
+ */
75
+ interface ChallengeResponse {
76
+ /** Unique challenge identifier (used as OAuth state parameter) */
77
+ challengeId: string // string challenge_id = 1
78
+
79
+ /** Type of challenge */
80
+ challengeType: ChallengeType // ChallengeType challenge_type = 2
81
+
82
+ /** @deprecated Use challengeData instead. Kept for backwards compatibility. */
83
+ extra: Record<string, string> // map<string, string> extra = 3 [deprecated]
84
+
85
+ /** Type-safe challenge-specific data (replaces extra) */
86
+ challengeData?: TypedChallengeData
87
+
88
+ /** OAuth authorization URL extracted from challengeData or extra */
89
+ authorizeUrl?: string
90
+ }
91
+
92
+ /**
93
+ * Request to verify session with bot detection solution
94
+ */
95
+ interface VerifySessionRequest {
96
+ /** Solution token (Turnstile token or HashCash proof) */
97
+ solution: string
98
+
99
+ /** Challenge ID being solved */
100
+ challengeId: string
101
+
102
+ /** Type of challenge being solved */
103
+ challengeType: ChallengeType
104
+ }
105
+
106
+ /**
107
+ * User information from OAuth provider
108
+ */
109
+ interface UserInfo {
110
+ name?: string
111
+ email?: string
112
+ }
113
+
114
+ /**
115
+ * Typed failure reasons from the SessionService/Verify proto.
116
+ * Values match the wire format: `REASON_${VerifyFailure_Reason[enum]}`.
117
+ *
118
+ * Use for exhaustive case matching in strategies and services:
119
+ * ```ts
120
+ * switch (result.failureReason) {
121
+ * case VerifyFailureReason.INVALID_CHALLENGE:
122
+ * return { action: 'error', message: result.failureMessage }
123
+ * }
124
+ * ```
125
+ */
126
+ const VerifyFailureReason = {
127
+ /** Default/unknown failure */
128
+ UNSPECIFIED: 'REASON_UNSPECIFIED',
129
+ /** Bad OTP code or bot detection solution */
130
+ INVALID_SOLUTION: 'REASON_INVALID_SOLUTION',
131
+ /** OAuth provider email needs verification */
132
+ EMAIL_NOT_VERIFIED: 'REASON_EMAIL_NOT_VERIFIED',
133
+ /** Challenge ID is invalid or expired — re-initiate a challenge */
134
+ INVALID_CHALLENGE: 'REASON_IVALID_CHALLENGE', // backend proto typo preserved
135
+ /** Email is linked to a different auth provider */
136
+ PROVIDER_MISMATCH: 'REASON_PROVIDER_MISMATCH',
137
+ } as const
138
+
139
+ type VerifyFailureReason = (typeof VerifyFailureReason)[keyof typeof VerifyFailureReason]
140
+
141
+ /**
142
+ * Response from session verification
143
+ */
144
+ interface VerifySessionResponse {
145
+ /** Whether to retry the challenge */
146
+ retry: boolean // bool retry = 1
147
+
148
+ /** Seconds to wait before retry (for rate limiting, e.g., email OTP) */
149
+ waitSeconds?: number // from VerifyFailure.wait_seconds
150
+
151
+ /** User information from successful OAuth verification */
152
+ userInfo?: UserInfo // from VerifySuccess.user_info
153
+
154
+ /** Typed failure reason code — use VerifyFailureReason for matching */
155
+ failureReason?: VerifyFailureReason
156
+
157
+ /** Human-readable failure message from the backend */
158
+ failureMessage?: string
159
+ }
160
+
161
+ /**
162
+ * Request to delete a session
163
+ * Empty - session identified via cookie or header
164
+ */
165
+
166
+ // biome-ignore lint/complexity/noBannedTypes: Empty per proto
167
+ type DeleteSessionRequest = {}
168
+
169
+ /**
170
+ * Response from session deletion
171
+ */
172
+
173
+ // biome-ignore lint/complexity/noBannedTypes: Empty per proto
174
+ type DeleteSessionResponse = {}
175
+
176
+ /**
177
+ * Introspect request - Entry Gateway only
178
+ * Frontend doesn't use this
179
+ */
180
+ interface IntrospectRequest {
181
+ /** Session ID to introspect */
182
+ sessionId: string // string session_id = 1
183
+ }
184
+
185
+ /**
186
+ * Introspect response - Entry Gateway only
187
+ * Frontend doesn't use this
188
+ */
189
+ interface IntrospectResponse {
190
+ /** Wrapped/hashed session ID */
191
+ wrappedId: string // string wrapped_id = 1
192
+
193
+ /** Validation result */
194
+ result: boolean // bool result = 2
195
+
196
+ /** Trust score */
197
+ score: number // int32 score = 4
198
+
199
+ /** Persona identifier */
200
+ personaId: string // string persona_id = 5
201
+ }
202
+
203
+ /**
204
+ * Configuration for a challenge type (OAuth provider or bot detection)
205
+ * Returned by getChallengeTypes to provide client-side SDK configuration
206
+ */
207
+ interface ChallengeTypeConfig {
208
+ /** Challenge type (e.g., GOOGLE, GITHUB, EMAIL, TURNSTILE) */
209
+ type: ChallengeType
210
+
211
+ /** Provider-specific configuration (e.g., clientId, scope for OAuth) */
212
+ config: Record<string, string>
213
+ }
214
+
215
+ /**
216
+ * Session Service API client interface
217
+ * Wraps the protobuf-generated client
218
+ */
219
+ interface SessionRepository {
220
+ /**
221
+ * Initialize a new session
222
+ * - Headers: X-Device-ID (mobile/extension only)
223
+ * - Response: session_id in body (mobile/ext) or Set-Cookie (web)
224
+ * TODO: this is pretty implicit: when on web, we exclude the device ID header,
225
+ * so then it implicitly returns a session ID via the Set-Cookie header
226
+ *
227
+ */
228
+ initSession(): Promise<InitSessionResponse>
229
+
230
+ /**
231
+ * Request a bot detection challenge
232
+ * - Headers: X-Session-ID (mobile/ext) or Cookie (web)
233
+ */
234
+ challenge(request: ChallengeRequest): Promise<ChallengeResponse>
235
+
236
+ /**
237
+ * Submit bot detection solution to verify session
238
+ * - Headers: X-Session-ID (mobile/ext) or Cookie (web)
239
+ */
240
+ verifySession(request: VerifySessionRequest): Promise<VerifySessionResponse>
241
+
242
+ /**
243
+ * Delete the current session
244
+ * - Headers: X-Session-ID (mobile/ext) or Cookie (web)
245
+ */
246
+ deleteSession(request: DeleteSessionRequest): Promise<DeleteSessionResponse>
247
+
248
+ /**
249
+ * Introspect session validity (Entry Gateway only)
250
+ * Frontend should NOT use this - EGW internal only
251
+ */
252
+ introspect?(request: IntrospectRequest): Promise<IntrospectResponse>
253
+
254
+ /**
255
+ * Get available challenge types and their configuration
256
+ * Returns OAuth provider configs (e.g., Google client ID) and bot detection configs
257
+ */
258
+ getChallengeTypes(): Promise<ChallengeTypeConfig[]>
259
+ }
260
+
261
+ /**
262
+ * Typed failure reasons from the SessionService/Challenge proto.
263
+ * Values match the wire format: `REASON_${ChallengeFailure_Reason[enum]}`.
264
+ */
265
+ const ChallengeFailureReason = {
266
+ /** Default/unknown failure */
267
+ UNSPECIFIED: 'REASON_UNSPECIFIED',
268
+ /** Session must pass bot detection first (score < 60) */
269
+ BOT_DETECTION_REQUIRED: 'REASON_BOT_DETECTION_REQUIRED',
270
+ } as const
271
+
272
+ type ChallengeFailureReason = (typeof ChallengeFailureReason)[keyof typeof ChallengeFailureReason]
273
+
274
+ export { ChallengeFailureReason, VerifyFailureReason }
275
+
276
+ export type {
277
+ SessionRepository,
278
+ ChallengeRequest,
279
+ ChallengeResponse,
280
+ ChallengeTypeConfig,
281
+ VerifySessionRequest,
282
+ VerifySessionResponse,
283
+ InitSessionResponse,
284
+ UserInfo,
285
+ TypedChallengeData,
286
+ TurnstileChallengeData,
287
+ HashCashChallengeData,
288
+ GitHubChallengeData,
289
+ }
@@ -0,0 +1,24 @@
1
+ import { ChallengeType } from '@luxdex/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
2
+ import type { SessionService } from '@luxfi/sessions/src/session-service/types'
3
+
4
+ function createNoopSessionService(): SessionService {
5
+ const initSession: SessionService['initSession'] = async () => ({ needChallenge: false, extra: {} })
6
+ const removeSession: SessionService['removeSession'] = async () => {}
7
+ const getSessionState: SessionService['getSessionState'] = async () => null
8
+ const requestChallenge: SessionService['requestChallenge'] = async () => ({
9
+ challengeId: 'noop-challenge-123',
10
+ challengeType: ChallengeType.UNSPECIFIED,
11
+ extra: {},
12
+ })
13
+ const verifySession: SessionService['verifySession'] = async () => ({ retry: false })
14
+
15
+ return {
16
+ initSession,
17
+ requestChallenge,
18
+ verifySession,
19
+ removeSession,
20
+ getSessionState,
21
+ }
22
+ }
23
+
24
+ export { createNoopSessionService }