@luxexchange/sessions 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.depcheckrc +20 -0
- package/.eslintrc.js +21 -0
- package/README.md +1 -0
- package/env.d.ts +12 -0
- package/package.json +50 -0
- package/project.json +42 -0
- package/src/challenge-solvers/createChallengeSolverService.ts +64 -0
- package/src/challenge-solvers/createHashcashMockSolver.ts +39 -0
- package/src/challenge-solvers/createHashcashSolver.test.ts +385 -0
- package/src/challenge-solvers/createHashcashSolver.ts +255 -0
- package/src/challenge-solvers/createNoneMockSolver.ts +11 -0
- package/src/challenge-solvers/createTurnstileMockSolver.ts +30 -0
- package/src/challenge-solvers/createTurnstileSolver.ts +353 -0
- package/src/challenge-solvers/hashcash/core.native.ts +34 -0
- package/src/challenge-solvers/hashcash/core.test.ts +314 -0
- package/src/challenge-solvers/hashcash/core.ts +35 -0
- package/src/challenge-solvers/hashcash/core.web.ts +123 -0
- package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.test.ts +195 -0
- package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.ts +120 -0
- package/src/challenge-solvers/hashcash/shared.ts +70 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.native.ts +22 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.ts +22 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.web.ts +212 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.native.ts +16 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.ts +16 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.web.ts +97 -0
- package/src/challenge-solvers/hashcash/worker/hashcash.worker.ts +91 -0
- package/src/challenge-solvers/hashcash/worker/types.ts +69 -0
- package/src/challenge-solvers/turnstileErrors.ts +49 -0
- package/src/challenge-solvers/turnstileScriptLoader.ts +325 -0
- package/src/challenge-solvers/turnstileSolver.integration.test.ts +331 -0
- package/src/challenge-solvers/types.ts +55 -0
- package/src/challengeFlow.integration.test.ts +627 -0
- package/src/device-id/createDeviceIdService.ts +19 -0
- package/src/device-id/types.ts +11 -0
- package/src/index.ts +137 -0
- package/src/lux-identifier/createLuxIdentifierService.ts +19 -0
- package/src/lux-identifier/luxIdentifierQuery.ts +20 -0
- package/src/lux-identifier/types.ts +11 -0
- package/src/oauth-service/createOAuthService.ts +125 -0
- package/src/oauth-service/types.ts +104 -0
- package/src/performance/createNoopPerformanceTracker.ts +12 -0
- package/src/performance/createPerformanceTracker.ts +43 -0
- package/src/performance/index.ts +7 -0
- package/src/performance/types.ts +11 -0
- package/src/session-initialization/createSessionInitializationService.test.ts +557 -0
- package/src/session-initialization/createSessionInitializationService.ts +193 -0
- package/src/session-initialization/sessionErrors.ts +32 -0
- package/src/session-repository/createSessionClient.ts +10 -0
- package/src/session-repository/createSessionRepository.test.ts +313 -0
- package/src/session-repository/createSessionRepository.ts +242 -0
- package/src/session-repository/errors.ts +22 -0
- package/src/session-repository/types.ts +289 -0
- package/src/session-service/createNoopSessionService.ts +24 -0
- package/src/session-service/createSessionService.test.ts +388 -0
- package/src/session-service/createSessionService.ts +61 -0
- package/src/session-service/types.ts +59 -0
- package/src/session-storage/createSessionStorage.ts +28 -0
- package/src/session-storage/types.ts +15 -0
- package/src/session.integration.test.ts +480 -0
- package/src/sessionLifecycle.integration.test.ts +264 -0
- package/src/test-utils/createLocalCookieTransport.ts +52 -0
- package/src/test-utils/createLocalHeaderTransport.ts +45 -0
- package/src/test-utils/mocks.ts +122 -0
- package/src/test-utils.ts +200 -0
- package/tsconfig.json +19 -0
- package/tsconfig.lint.json +8 -0
- package/tsconfig.spec.json +8 -0
- package/vitest.config.ts +20 -0
- package/vitest.integration.config.ts +14 -0
|
@@ -0,0 +1,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 }
|