@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
package/src/index.ts ADDED
@@ -0,0 +1,137 @@
1
+ /** biome-ignore-all assist/source/organizeImports: we want to manually group exports by category */
2
+
3
+ /**
4
+ * @luxexchange/sessions
5
+ *
6
+ * This is the ONLY public entry point for the Sessions package.
7
+ * All exports must be explicitly listed here.
8
+ * Deep imports are forbidden and will be blocked by ESLint.
9
+ */
10
+
11
+ // Device ID
12
+ export { createDeviceIdService } from '@luxexchange/sessions/src/device-id/createDeviceIdService'
13
+ export type { DeviceIdService } from '@luxexchange/sessions/src/device-id/types'
14
+ // Lux Identifier
15
+ export { createLuxIdentifierService } from '@luxexchange/sessions/src/lux-identifier/createLuxIdentifierService'
16
+ export { luxIdentifierQuery } from '@luxexchange/sessions/src/lux-identifier/luxIdentifierQuery'
17
+ export type { LuxIdentifierService } from '@luxexchange/sessions/src/lux-identifier/types'
18
+ // Session Repository
19
+ export { createSessionRepository } from '@luxexchange/sessions/src/session-repository/createSessionRepository'
20
+ export { ChallengeRejectedError } from '@luxexchange/sessions/src/session-repository/errors'
21
+ export { ChallengeFailureReason, VerifyFailureReason } from '@luxexchange/sessions/src/session-repository/types'
22
+ export type {
23
+ SessionRepository,
24
+ ChallengeTypeConfig,
25
+ TypedChallengeData,
26
+ TurnstileChallengeData,
27
+ HashCashChallengeData,
28
+ GitHubChallengeData,
29
+ } from '@luxexchange/sessions/src/session-repository/types'
30
+
31
+ // Session Service
32
+ export { createNoopSessionService } from '@luxexchange/sessions/src/session-service/createNoopSessionService'
33
+ export { createSessionService } from '@luxexchange/sessions/src/session-service/createSessionService'
34
+ export type {
35
+ SessionService,
36
+ InitSessionResponse,
37
+ ChallengeRequest,
38
+ ChallengeResponse,
39
+ VerifySessionRequest,
40
+ VerifySessionResponse,
41
+ } from '@luxexchange/sessions/src/session-service/types'
42
+
43
+ // Session Storage
44
+ export { createSessionStorage } from '@luxexchange/sessions/src/session-storage/createSessionStorage'
45
+ export type { SessionStorage, SessionState } from '@luxexchange/sessions/src/session-storage/types'
46
+
47
+ // Session Client
48
+ export { createSessionClient } from '@luxexchange/sessions/src/session-repository/createSessionClient'
49
+ export type { SessionServiceClient } from '@luxexchange/sessions/src/session-repository/createSessionClient'
50
+
51
+ // Session Initialization
52
+ export { createSessionInitializationService } from '@luxexchange/sessions/src/session-initialization/createSessionInitializationService'
53
+ export {
54
+ SessionError,
55
+ MaxChallengeRetriesError,
56
+ NoSolverAvailableError,
57
+ } from '@luxexchange/sessions/src/session-initialization/sessionErrors'
58
+ export type {
59
+ SessionInitializationService,
60
+ SessionInitOptions,
61
+ SessionInitResult,
62
+ SessionInitAnalytics,
63
+ } from '@luxexchange/sessions/src/session-initialization/createSessionInitializationService'
64
+
65
+ // Challenge Solvers
66
+ export { createChallengeSolverService } from '@luxexchange/sessions/src/challenge-solvers/createChallengeSolverService'
67
+ export { createTurnstileMockSolver } from '@luxexchange/sessions/src/challenge-solvers/createTurnstileMockSolver'
68
+ export { createHashcashMockSolver } from '@luxexchange/sessions/src/challenge-solvers/createHashcashMockSolver'
69
+ export { createNoneMockSolver } from '@luxexchange/sessions/src/challenge-solvers/createNoneMockSolver'
70
+ export { createTurnstileSolver } from '@luxexchange/sessions/src/challenge-solvers/createTurnstileSolver'
71
+ export { createHashcashSolver } from '@luxexchange/sessions/src/challenge-solvers/createHashcashSolver'
72
+ export { createWorkerHashcashSolver } from '@luxexchange/sessions/src/challenge-solvers/hashcash/createWorkerHashcashSolver'
73
+ export {
74
+ TurnstileScriptLoadError,
75
+ TurnstileApiNotAvailableError,
76
+ TurnstileTimeoutError,
77
+ TurnstileError,
78
+ TurnstileTokenExpiredError,
79
+ } from '@luxexchange/sessions/src/challenge-solvers/turnstileErrors'
80
+ export type {
81
+ ChallengeSolver,
82
+ ChallengeSolverService,
83
+ ChallengeData,
84
+ TurnstileScriptOptions,
85
+ } from '@luxexchange/sessions/src/challenge-solvers/types'
86
+ export type {
87
+ CreateTurnstileSolverContext,
88
+ TurnstileSolveAnalytics,
89
+ } from '@luxexchange/sessions/src/challenge-solvers/createTurnstileSolver'
90
+ export type {
91
+ CreateHashcashWorkerChannelContext,
92
+ HashcashWorkerChannel,
93
+ HashcashWorkerChannelFactory,
94
+ } from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/types'
95
+ export { createHashcashWorkerChannel } from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel'
96
+ export { createHashcashMultiWorkerChannel } from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel'
97
+ export type { MultiWorkerConfig } from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel'
98
+ export type { CreateWorkerHashcashSolverContext } from '@luxexchange/sessions/src/challenge-solvers/hashcash/createWorkerHashcashSolver'
99
+ export type {
100
+ CreateHashcashSolverContext,
101
+ HashcashSolveAnalytics,
102
+ } from '@luxexchange/sessions/src/challenge-solvers/createHashcashSolver'
103
+
104
+ export { ChallengeType } from '@luxexchange/sessions/src/session-service/types'
105
+
106
+ // OAuth Service
107
+ export { createOAuthService } from '@luxexchange/sessions/src/oauth-service/createOAuthService'
108
+ export type { CreateOAuthServiceContext } from '@luxexchange/sessions/src/oauth-service/createOAuthService'
109
+ export type {
110
+ OAuthService,
111
+ OAuthInitiationResult,
112
+ OAuthCallbackParams,
113
+ OAuthVerificationResult,
114
+ OAuthInitiateParams,
115
+ OAuthVerifyParams,
116
+ OAuthUserInfo,
117
+ } from '@luxexchange/sessions/src/oauth-service/types'
118
+
119
+ // Performance Tracking
120
+ export type { PerformanceTracker } from '@luxexchange/sessions/src/performance/types'
121
+ export {
122
+ createPerformanceTracker,
123
+ PERFORMANCE_TRACKING_DISABLED,
124
+ } from '@luxexchange/sessions/src/performance/createPerformanceTracker'
125
+ export type { CreatePerformanceTrackerContext } from '@luxexchange/sessions/src/performance/createPerformanceTracker'
126
+ export { createNoopPerformanceTracker } from '@luxexchange/sessions/src/performance/createNoopPerformanceTracker'
127
+
128
+ // Test utilities (for integration testing)
129
+ export {
130
+ InMemorySessionStorage,
131
+ InMemoryDeviceIdService,
132
+ InMemoryLuxIdentifierService,
133
+ } from '@luxexchange/sessions/src/test-utils'
134
+ export {
135
+ createCookieJar,
136
+ createLocalCookieTransport,
137
+ } from '@luxexchange/sessions/src/test-utils/createLocalCookieTransport'
@@ -0,0 +1,19 @@
1
+ import type { LuxIdentifierService } from '@luxfi/sessions/src/lux-identifier/types'
2
+
3
+ function createLuxIdentifierService(ctx: {
4
+ getLuxIdentifier: () => Promise<string | null>
5
+ setLuxIdentifier: (identifier: string) => Promise<void>
6
+ removeLuxIdentifier: () => Promise<void>
7
+ }): LuxIdentifierService {
8
+ const getLuxIdentifier = ctx.getLuxIdentifier
9
+ const setLuxIdentifier = ctx.setLuxIdentifier
10
+ const removeLuxIdentifier = ctx.removeLuxIdentifier
11
+
12
+ return {
13
+ getLuxIdentifier,
14
+ setLuxIdentifier,
15
+ removeLuxIdentifier,
16
+ }
17
+ }
18
+
19
+ export { createLuxIdentifierService }
@@ -0,0 +1,20 @@
1
+ import { queryOptions } from '@tanstack/react-query'
2
+ import type { LuxIdentifierService } from '@luxexchange/sessions/src/lux-identifier/types'
3
+ import { ReactQueryCacheKey } from '@luxfi/utilities/src/reactQuery/cache'
4
+ import type { QueryOptionsResult } from '@luxfi/utilities/src/reactQuery/queryOptions'
5
+
6
+ type LuxIdentifierQueryOptions = QueryOptionsResult<
7
+ string | null,
8
+ Error,
9
+ string | null,
10
+ [ReactQueryCacheKey.LuxIdentifier]
11
+ >
12
+
13
+ export function luxIdentifierQuery(getService: () => LuxIdentifierService): LuxIdentifierQueryOptions {
14
+ return queryOptions({
15
+ queryKey: [ReactQueryCacheKey.LuxIdentifier],
16
+ queryFn: async () => getService().getLuxIdentifier(),
17
+ staleTime: Infinity,
18
+ gcTime: Infinity,
19
+ })
20
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Lux Identifier provider interface
3
+ * Platform-specific implementations handle lux identifier persistence
4
+ */
5
+ interface LuxIdentifierService {
6
+ getLuxIdentifier(): Promise<string | null>
7
+ setLuxIdentifier(identifier: string): Promise<void>
8
+ removeLuxIdentifier(): Promise<void>
9
+ }
10
+
11
+ export type { LuxIdentifierService }
@@ -0,0 +1,125 @@
1
+ import type {
2
+ OAuthCallbackParams,
3
+ OAuthInitiateParams,
4
+ OAuthInitiationResult,
5
+ OAuthService,
6
+ OAuthVerificationResult,
7
+ OAuthVerifyParams,
8
+ } from '@luxexchange/sessions/src/oauth-service/types'
9
+ import type { SessionRepository } from '@luxexchange/sessions/src/session-repository/types'
10
+
11
+ /**
12
+ * Context (dependencies) for creating an OAuthService
13
+ */
14
+ export interface CreateOAuthServiceContext {
15
+ /** Session repository for backend communication */
16
+ sessionRepository: SessionRepository
17
+ }
18
+
19
+ /**
20
+ * Creates an OAuth Service instance.
21
+ *
22
+ * Handles OAuth redirect-based authentication flows by wrapping
23
+ * the session repository's challenge/verify methods.
24
+ *
25
+ * Unlike ChallengeSolver (synchronous solve), OAuth requires:
26
+ * - External redirect to OAuth provider
27
+ * - User action (authorization)
28
+ * - Callback with authorization code
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const oauthService = createOAuthService({ sessionRepository })
33
+ *
34
+ * // 1. Initiate - get URL and redirect user
35
+ * const { authorizeUrl } = await oauthService.initiate({
36
+ * challengeType: ChallengeType.GITHUB,
37
+ * callbackUrl: 'https://app.com/auth/callback/github',
38
+ * })
39
+ * redirect(authorizeUrl)
40
+ *
41
+ * // 2. In callback route - parse and verify
42
+ * const params = oauthService.parseCallback(new URL(request.url))
43
+ * const result = await oauthService.verify({
44
+ * ...params,
45
+ * challengeType: ChallengeType.GITHUB,
46
+ * })
47
+ * // result.userInfo contains { name?, email? } from OAuth provider
48
+ * redirect('/')
49
+ * ```
50
+ */
51
+ export function createOAuthService(ctx: CreateOAuthServiceContext): OAuthService {
52
+ /**
53
+ * Initiate OAuth flow by requesting authorization URL
54
+ * Note: Callback URL is now configured server-side per OAuth provider
55
+ */
56
+ async function initiate(params: OAuthInitiateParams): Promise<OAuthInitiationResult> {
57
+ const response = await ctx.sessionRepository.challenge({
58
+ challengeType: params.challengeType,
59
+ // Note: callbackUrl is no longer sent to backend - configured server-side
60
+ })
61
+
62
+ if (!response.authorizeUrl) {
63
+ throw new Error('No authorization URL returned from challenge')
64
+ }
65
+
66
+ return {
67
+ authorizeUrl: response.authorizeUrl,
68
+ state: response.challengeId,
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Parse OAuth callback URL to extract parameters
74
+ */
75
+ function parseCallback(url: URL): OAuthCallbackParams {
76
+ return {
77
+ code: url.searchParams.get('code'),
78
+ state: url.searchParams.get('state'),
79
+ error: url.searchParams.get('error'),
80
+ errorDescription: url.searchParams.get('error_description'),
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Verify OAuth callback by submitting authorization code
86
+ */
87
+ async function verify(params: OAuthVerifyParams): Promise<OAuthVerificationResult> {
88
+ // Handle OAuth error from provider
89
+ if (params.error) {
90
+ return {
91
+ success: false,
92
+ retry: false,
93
+ }
94
+ }
95
+
96
+ // Validate required params
97
+ if (!params.code || !params.state) {
98
+ return {
99
+ success: false,
100
+ retry: false,
101
+ }
102
+ }
103
+
104
+ const result = await ctx.sessionRepository.verifySession({
105
+ solution: params.code,
106
+ challengeId: params.state,
107
+ challengeType: params.challengeType,
108
+ })
109
+
110
+ return {
111
+ success: !result.retry && !result.failureReason,
112
+ retry: result.retry,
113
+ waitSeconds: result.waitSeconds,
114
+ userInfo: result.userInfo,
115
+ failureReason: result.failureReason,
116
+ failureMessage: result.failureMessage,
117
+ }
118
+ }
119
+
120
+ return {
121
+ initiate,
122
+ parseCallback,
123
+ verify,
124
+ }
125
+ }
@@ -0,0 +1,104 @@
1
+ import { ChallengeType } from '@uniswap/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
2
+
3
+ /**
4
+ * Result from initiating an OAuth flow
5
+ */
6
+ export interface OAuthInitiationResult {
7
+ /** URL to redirect user to for OAuth provider authorization */
8
+ authorizeUrl: string
9
+ /** Challenge ID used as OAuth state parameter for CSRF protection */
10
+ state: string
11
+ }
12
+
13
+ /**
14
+ * Parameters extracted from OAuth callback URL
15
+ */
16
+ export interface OAuthCallbackParams {
17
+ /** Authorization code from OAuth provider */
18
+ code: string | null
19
+ /** State parameter (challenge ID) for CSRF validation */
20
+ state: string | null
21
+ /** OAuth error code if authorization failed */
22
+ error: string | null
23
+ /** Human-readable error description */
24
+ errorDescription: string | null
25
+ }
26
+
27
+ /**
28
+ * User information from OAuth provider
29
+ */
30
+ export interface OAuthUserInfo {
31
+ name?: string
32
+ email?: string
33
+ }
34
+
35
+ /**
36
+ * Result from verifying OAuth callback
37
+ */
38
+ export interface OAuthVerificationResult {
39
+ /** Whether verification succeeded */
40
+ success: boolean
41
+ /** Whether to retry the flow */
42
+ retry: boolean
43
+ /** Seconds to wait before retry (for rate limiting) */
44
+ waitSeconds?: number
45
+ /** User information from OAuth provider */
46
+ userInfo?: OAuthUserInfo
47
+ /** Failure reason from backend (e.g., 'REASON_PROVIDER_MISMATCH') */
48
+ failureReason?: string
49
+ /** Failure message with details (e.g., contains 'CHALLENGE_TYPE_GOOGLE') */
50
+ failureMessage?: string
51
+ }
52
+
53
+ /**
54
+ * OAuth initiation parameters
55
+ */
56
+ export interface OAuthInitiateParams {
57
+ /** Type of OAuth challenge (GITHUB, GOOGLE, SLACK, etc.) */
58
+ challengeType: ChallengeType
59
+ /** URL where OAuth provider should redirect after authorization */
60
+ callbackUrl: string
61
+ }
62
+
63
+ /**
64
+ * OAuth verification parameters
65
+ */
66
+ export interface OAuthVerifyParams extends OAuthCallbackParams {
67
+ /** Type of OAuth challenge being verified */
68
+ challengeType: ChallengeType
69
+ }
70
+
71
+ /**
72
+ * OAuth Service interface
73
+ *
74
+ * Handles OAuth redirect-based authentication flows.
75
+ * Unlike ChallengeSolver (which solves challenges synchronously),
76
+ * OAuth requires external user action and callback handling.
77
+ *
78
+ * Flow:
79
+ * 1. initiate() - Get authorization URL and redirect user
80
+ * 2. parseCallback() - Extract params from callback URL
81
+ * 3. verify() - Submit authorization code to backend
82
+ */
83
+ export interface OAuthService {
84
+ /**
85
+ * Initiate OAuth flow by requesting authorization URL from backend
86
+ * @param params - Challenge type and callback URL
87
+ * @returns Authorization URL to redirect user to
88
+ */
89
+ initiate(params: OAuthInitiateParams): Promise<OAuthInitiationResult>
90
+
91
+ /**
92
+ * Parse OAuth callback URL to extract parameters
93
+ * @param url - Callback URL with query params from OAuth provider
94
+ * @returns Extracted callback parameters
95
+ */
96
+ parseCallback(url: URL): OAuthCallbackParams
97
+
98
+ /**
99
+ * Verify OAuth callback by submitting authorization code to backend
100
+ * @param params - Callback params plus challenge type
101
+ * @returns Verification result
102
+ */
103
+ verify(params: OAuthVerifyParams): Promise<OAuthVerificationResult>
104
+ }
@@ -0,0 +1,12 @@
1
+ import { PERFORMANCE_TRACKING_DISABLED } from '@luxexchange/sessions/src/performance/createPerformanceTracker'
2
+ import type { PerformanceTracker } from '@luxexchange/sessions/src/performance/types'
3
+
4
+ /**
5
+ * Creates a noop performance tracker that always returns the disabled sentinel.
6
+ * Use this when performance tracking is not needed (e.g., extension, mobile, tests).
7
+ */
8
+ function createNoopPerformanceTracker(): PerformanceTracker {
9
+ return { now: () => PERFORMANCE_TRACKING_DISABLED }
10
+ }
11
+
12
+ export { createNoopPerformanceTracker }
@@ -0,0 +1,43 @@
1
+ import type { PerformanceTracker } from '@luxexchange/sessions/src/performance/types'
2
+
3
+ /** Sentinel value indicating performance tracking is disabled */
4
+ export const PERFORMANCE_TRACKING_DISABLED = -1
5
+
6
+ interface CreatePerformanceTrackerContext {
7
+ /** Feature flag to enable/disable performance tracking. Required. */
8
+ getIsPerformanceTrackingEnabled: () => boolean
9
+ /**
10
+ * Injected timing function. This is the actual performance API.
11
+ * Allows the caller to pass performance.now, Date.now, or a mock.
12
+ * Required - no implicit dependency on globalThis.performance.
13
+ */
14
+ getNow: () => number
15
+ }
16
+
17
+ /**
18
+ * Creates a performance tracker with feature flag control.
19
+ *
20
+ * Behavior:
21
+ * - If tracking is disabled (feature flag returns false), returns -1 (sentinel)
22
+ * - Otherwise calls the injected getNow() function
23
+ *
24
+ * The sentinel value (-1) allows analytics consumers to distinguish
25
+ * between "0ms duration" and "tracking was disabled".
26
+ *
27
+ * Note: All dependencies are explicitly passed in - no implicit globals.
28
+ */
29
+ function createPerformanceTracker(ctx: CreatePerformanceTrackerContext): PerformanceTracker {
30
+ function now(): number {
31
+ // If disabled via feature flag, return sentinel (-1)
32
+ if (!ctx.getIsPerformanceTrackingEnabled()) {
33
+ return PERFORMANCE_TRACKING_DISABLED
34
+ }
35
+
36
+ return ctx.getNow()
37
+ }
38
+
39
+ return { now }
40
+ }
41
+
42
+ export { createPerformanceTracker }
43
+ export type { CreatePerformanceTrackerContext }
@@ -0,0 +1,7 @@
1
+ /* eslint-disable check-file/no-index */
2
+ export type { CreatePerformanceTrackerContext } from '@luxexchange/sessions/src/performance/createPerformanceTracker'
3
+ export {
4
+ createPerformanceTracker,
5
+ PERFORMANCE_TRACKING_DISABLED,
6
+ } from '@luxexchange/sessions/src/performance/createPerformanceTracker'
7
+ export type { PerformanceTracker } from '@luxexchange/sessions/src/performance/types'
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Contract for performance timing.
3
+ * Abstraction over performance.now() that can be:
4
+ * - Injected for testing
5
+ * - Disabled via feature flag
6
+ * - Platform-specific (browser vs React Native vs Node)
7
+ */
8
+ export interface PerformanceTracker {
9
+ /** Returns current high-resolution timestamp in milliseconds */
10
+ now(): number
11
+ }