@oxyhq/services 5.16.22 → 5.16.24

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 (73) hide show
  1. package/lib/commonjs/core/HttpService.js +15 -79
  2. package/lib/commonjs/core/HttpService.js.map +1 -1
  3. package/lib/commonjs/core/OxyServices.base.js +3 -11
  4. package/lib/commonjs/core/OxyServices.base.js.map +1 -1
  5. package/lib/commonjs/core/services/SessionService.js +163 -0
  6. package/lib/commonjs/core/services/SessionService.js.map +1 -0
  7. package/lib/commonjs/core/services/TokenService.js +206 -0
  8. package/lib/commonjs/core/services/TokenService.js.map +1 -0
  9. package/lib/commonjs/models/interfaces.js +15 -0
  10. package/lib/commonjs/models/interfaces.js.map +1 -1
  11. package/lib/commonjs/ui/context/OxyContext.js +100 -22
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +11 -3
  14. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  15. package/lib/commonjs/ui/hooks/useSessionSocket.js +228 -57
  16. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  17. package/lib/module/core/HttpService.js +15 -79
  18. package/lib/module/core/HttpService.js.map +1 -1
  19. package/lib/module/core/OxyServices.base.js +4 -11
  20. package/lib/module/core/OxyServices.base.js.map +1 -1
  21. package/lib/module/core/services/SessionService.js +159 -0
  22. package/lib/module/core/services/SessionService.js.map +1 -0
  23. package/lib/module/core/services/TokenService.js +203 -0
  24. package/lib/module/core/services/TokenService.js.map +1 -0
  25. package/lib/module/models/interfaces.js +15 -0
  26. package/lib/module/models/interfaces.js.map +1 -1
  27. package/lib/module/ui/context/OxyContext.js +100 -22
  28. package/lib/module/ui/context/OxyContext.js.map +1 -1
  29. package/lib/module/ui/context/hooks/useAuthOperations.js +11 -3
  30. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  31. package/lib/module/ui/hooks/useSessionSocket.js +228 -57
  32. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  33. package/lib/typescript/core/HttpService.d.ts +1 -1
  34. package/lib/typescript/core/HttpService.d.ts.map +1 -1
  35. package/lib/typescript/core/OxyServices.base.d.ts +6 -0
  36. package/lib/typescript/core/OxyServices.base.d.ts.map +1 -1
  37. package/lib/typescript/core/mixins/OxyServices.analytics.d.ts.map +1 -1
  38. package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
  39. package/lib/typescript/core/mixins/OxyServices.auth.d.ts.map +1 -1
  40. package/lib/typescript/core/mixins/OxyServices.developer.d.ts.map +1 -1
  41. package/lib/typescript/core/mixins/OxyServices.devices.d.ts.map +1 -1
  42. package/lib/typescript/core/mixins/OxyServices.karma.d.ts.map +1 -1
  43. package/lib/typescript/core/mixins/OxyServices.language.d.ts.map +1 -1
  44. package/lib/typescript/core/mixins/OxyServices.location.d.ts.map +1 -1
  45. package/lib/typescript/core/mixins/OxyServices.payment.d.ts.map +1 -1
  46. package/lib/typescript/core/mixins/OxyServices.privacy.d.ts.map +1 -1
  47. package/lib/typescript/core/mixins/OxyServices.security.d.ts.map +1 -1
  48. package/lib/typescript/core/mixins/OxyServices.user.d.ts +1 -3
  49. package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -1
  50. package/lib/typescript/core/mixins/OxyServices.utility.d.ts.map +1 -1
  51. package/lib/typescript/core/mixins/index.d.ts.map +1 -1
  52. package/lib/typescript/core/services/SessionService.d.ts +78 -0
  53. package/lib/typescript/core/services/SessionService.d.ts.map +1 -0
  54. package/lib/typescript/core/services/TokenService.d.ts +72 -0
  55. package/lib/typescript/core/services/TokenService.d.ts.map +1 -0
  56. package/lib/typescript/models/interfaces.d.ts +14 -0
  57. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  58. package/lib/typescript/models/session.d.ts +7 -0
  59. package/lib/typescript/models/session.d.ts.map +1 -1
  60. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  61. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  62. package/lib/typescript/ui/hooks/useSessionSocket.d.ts +8 -1
  63. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/core/HttpService.ts +15 -95
  66. package/src/core/OxyServices.base.ts +3 -20
  67. package/src/core/services/SessionService.ts +173 -0
  68. package/src/core/services/TokenService.ts +226 -0
  69. package/src/models/interfaces.ts +16 -2
  70. package/src/models/session.ts +8 -1
  71. package/src/ui/context/OxyContext.tsx +105 -23
  72. package/src/ui/context/hooks/useAuthOperations.ts +11 -3
  73. package/src/ui/hooks/useSessionSocket.ts +229 -55
@@ -17,17 +17,8 @@ import { TTLCache, registerCacheForCleanup } from '../utils/cache';
17
17
  import { RequestDeduplicator, RequestQueue, SimpleLogger } from '../utils/requestUtils';
18
18
  import { retryAsync } from '../utils/asyncUtils';
19
19
  import { handleHttpError } from '../utils/errorUtils';
20
- import { jwtDecode } from 'jwt-decode';
21
20
  import type { OxyConfig } from '../models/interfaces';
22
21
 
23
- interface JwtPayload {
24
- exp?: number;
25
- userId?: string;
26
- id?: string;
27
- sessionId?: string;
28
- [key: string]: any;
29
- }
30
-
31
22
  export interface RequestOptions {
32
23
  cache?: boolean;
33
24
  cacheTTL?: number;
@@ -46,45 +37,8 @@ interface RequestConfig extends RequestOptions {
46
37
  params?: Record<string, unknown>;
47
38
  }
48
39
 
49
- /**
50
- * Token store for authentication (singleton)
51
- */
52
- class TokenStore {
53
- private static instance: TokenStore;
54
- private accessToken: string | null = null;
55
- private refreshToken: string | null = null;
56
-
57
- private constructor() {}
58
-
59
- static getInstance(): TokenStore {
60
- if (!TokenStore.instance) {
61
- TokenStore.instance = new TokenStore();
62
- }
63
- return TokenStore.instance;
64
- }
65
-
66
- setTokens(accessToken: string, refreshToken = ''): void {
67
- this.accessToken = accessToken;
68
- this.refreshToken = refreshToken;
69
- }
70
-
71
- getAccessToken(): string | null {
72
- return this.accessToken;
73
- }
74
-
75
- getRefreshToken(): string | null {
76
- return this.refreshToken;
77
- }
78
-
79
- clearTokens(): void {
80
- this.accessToken = null;
81
- this.refreshToken = null;
82
- }
83
-
84
- hasAccessToken(): boolean {
85
- return !!this.accessToken;
86
- }
87
- }
40
+ // Token management moved to TokenService - import it instead
41
+ import { tokenService } from './services/TokenService';
88
42
 
89
43
  /**
90
44
  * Unified HTTP Service
@@ -94,7 +48,6 @@ class TokenStore {
94
48
  */
95
49
  export class HttpService {
96
50
  private baseURL: string;
97
- private tokenStore: TokenStore;
98
51
  private cache: TTLCache<any>;
99
52
  private deduplicator: RequestDeduplicator;
100
53
  private requestQueue: RequestQueue;
@@ -114,7 +67,9 @@ export class HttpService {
114
67
  constructor(config: OxyConfig) {
115
68
  this.config = config;
116
69
  this.baseURL = config.baseURL;
117
- this.tokenStore = TokenStore.getInstance();
70
+
71
+ // Initialize TokenService with baseURL
72
+ tokenService.initialize(this.baseURL);
118
73
 
119
74
  this.logger = new SimpleLogger(
120
75
  config.enableLogging || false,
@@ -261,7 +216,7 @@ export class HttpService {
261
216
  // Handle response
262
217
  if (!response.ok) {
263
218
  if (response.status === 401) {
264
- this.tokenStore.clearTokens();
219
+ tokenService.clearTokens();
265
220
  }
266
221
 
267
222
  // Try to parse error response (handle empty/malformed JSON)
@@ -415,45 +370,10 @@ export class HttpService {
415
370
 
416
371
  /**
417
372
  * Get auth header with automatic token refresh
373
+ * Uses TokenService for all token operations
418
374
  */
419
375
  private async getAuthHeader(): Promise<string | null> {
420
- const accessToken = this.tokenStore.getAccessToken();
421
- if (!accessToken) {
422
- return null;
423
- }
424
-
425
- try {
426
- const decoded = jwtDecode<JwtPayload>(accessToken);
427
- const currentTime = Math.floor(Date.now() / 1000);
428
-
429
- // If token expires in less than 60 seconds, refresh it
430
- if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
431
- try {
432
- const refreshUrl = `${this.baseURL}/api/session/token/${decoded.sessionId}`;
433
-
434
- // Use AbortSignal.timeout for consistent timeout handling
435
- const response = await fetch(refreshUrl, {
436
- method: 'GET',
437
- headers: { 'Accept': 'application/json' },
438
- signal: AbortSignal.timeout(5000),
439
- });
440
-
441
- if (response.ok) {
442
- const { accessToken: newToken } = await response.json();
443
- this.tokenStore.setTokens(newToken);
444
- this.logger.debug('Token refreshed');
445
- return `Bearer ${newToken}`;
446
- }
447
- } catch (refreshError) {
448
- this.logger.warn('Token refresh failed, using current token');
449
- }
450
- }
451
-
452
- return `Bearer ${accessToken}`;
453
- } catch (error) {
454
- this.logger.error('Error processing token:', error);
455
- return `Bearer ${accessToken}`;
456
- }
376
+ return await tokenService.getAuthHeader();
457
377
  }
458
378
 
459
379
  /**
@@ -558,21 +478,21 @@ export class HttpService {
558
478
  return { data: result as T };
559
479
  }
560
480
 
561
- // Token management
481
+ // Token management - delegates to TokenService
562
482
  setTokens(accessToken: string, refreshToken = ''): void {
563
- this.tokenStore.setTokens(accessToken, refreshToken);
483
+ tokenService.setTokens(accessToken, refreshToken);
564
484
  }
565
485
 
566
486
  clearTokens(): void {
567
- this.tokenStore.clearTokens();
487
+ tokenService.clearTokens();
568
488
  }
569
489
 
570
490
  getAccessToken(): string | null {
571
- return this.tokenStore.getAccessToken();
491
+ return tokenService.getAccessToken();
572
492
  }
573
493
 
574
494
  hasAccessToken(): boolean {
575
- return this.tokenStore.hasAccessToken();
495
+ return tokenService.hasAccessToken();
576
496
  }
577
497
 
578
498
  getBaseURL(): string {
@@ -606,10 +526,10 @@ export class HttpService {
606
526
  // Test-only utility
607
527
  static __resetTokensForTests(): void {
608
528
  try {
609
- TokenStore.getInstance().clearTokens();
529
+ tokenService.clearTokens();
610
530
  } catch (error) {
611
531
  // Silently fail in test cleanup - this is expected behavior
612
- // TokenStore might not be initialized in some test scenarios
532
+ // TokenService might not be initialized in some test scenarios
613
533
  }
614
534
  }
615
535
  }
@@ -3,24 +3,16 @@
3
3
  *
4
4
  * Contains core infrastructure, HTTP client, request management, and error handling
5
5
  */
6
- import { jwtDecode } from 'jwt-decode';
7
6
  import type { OxyConfig as OxyConfigBase, ApiError, User } from '../models/interfaces';
8
7
  import { handleHttpError } from '../utils/errorUtils';
9
8
  import { HttpService, type RequestOptions } from './HttpService';
10
9
  import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
10
+ import { tokenService } from './services/TokenService';
11
11
 
12
12
  export interface OxyConfig extends OxyConfigBase {
13
13
  cloudURL?: string;
14
14
  }
15
15
 
16
- interface JwtPayload {
17
- exp?: number;
18
- userId?: string;
19
- id?: string;
20
- sessionId?: string;
21
- [key: string]: any;
22
- }
23
-
24
16
  /**
25
17
  * Base class for OxyServices with core infrastructure
26
18
  */
@@ -135,19 +127,10 @@ export class OxyServicesBase {
135
127
 
136
128
  /**
137
129
  * Get the current user ID from the access token
130
+ * Returns MongoDB ObjectId (never publicKey)
138
131
  */
139
132
  public getCurrentUserId(): string | null {
140
- const accessToken = this.httpService.getAccessToken();
141
- if (!accessToken) {
142
- return null;
143
- }
144
-
145
- try {
146
- const decoded = jwtDecode<JwtPayload>(accessToken);
147
- return decoded.userId || decoded.id || null;
148
- } catch (error) {
149
- return null;
150
- }
133
+ return tokenService.getUserIdFromToken();
151
134
  }
152
135
 
153
136
  /**
@@ -0,0 +1,173 @@
1
+ /**
2
+ * SessionService - Single Source of Truth for Session Management
3
+ *
4
+ * Handles all session operations: creation, validation, refresh, invalidation.
5
+ * Manages active session state and provides session data to other services.
6
+ *
7
+ * Architecture:
8
+ * - Single source of truth for session operations
9
+ * - Handles both online and offline sessions
10
+ * - Integrates with TokenService for token management
11
+ * - userId is always MongoDB ObjectId, never publicKey
12
+ */
13
+
14
+ import type { OxyServices } from '../OxyServices';
15
+ import { tokenService } from './TokenService';
16
+ import type { ClientSession, SessionLoginResponse } from '../../models/session';
17
+ import type { User } from '../../models/interfaces';
18
+
19
+ export interface Session {
20
+ sessionId: string;
21
+ deviceId: string;
22
+ userId: string; // MongoDB ObjectId - PRIMARY IDENTIFIER
23
+ expiresAt: string;
24
+ lastActive: string;
25
+ isCurrent: boolean;
26
+ isOffline?: boolean;
27
+ }
28
+
29
+ /**
30
+ * SessionService - Singleton pattern for global session management
31
+ */
32
+ class SessionService {
33
+ private static instance: SessionService;
34
+ private oxyServices: OxyServices | null = null;
35
+ private activeSession: Session | null = null;
36
+ private sessions: Session[] = [];
37
+
38
+ private constructor() {}
39
+
40
+ static getInstance(): SessionService {
41
+ if (!SessionService.instance) {
42
+ SessionService.instance = new SessionService();
43
+ }
44
+ return SessionService.instance;
45
+ }
46
+
47
+ /**
48
+ * Initialize SessionService with OxyServices instance
49
+ */
50
+ initialize(oxyServices: OxyServices): void {
51
+ this.oxyServices = oxyServices;
52
+ }
53
+
54
+ /**
55
+ * Get active session
56
+ */
57
+ getActiveSession(): Session | null {
58
+ return this.activeSession;
59
+ }
60
+
61
+ /**
62
+ * Get all sessions
63
+ */
64
+ getAllSessions(): Session[] {
65
+ return [...this.sessions];
66
+ }
67
+
68
+ /**
69
+ * Create a new session (sign in)
70
+ * @param publicKey - User's public key for authentication
71
+ * @returns User object and session data
72
+ */
73
+ async createSession(publicKey: string): Promise<{ user: User; session: Session }> {
74
+ if (!this.oxyServices) {
75
+ throw new Error('SessionService not initialized with OxyServices');
76
+ }
77
+
78
+ // This will be implemented by delegating to existing sign-in logic
79
+ // For now, this is a placeholder that shows the interface
80
+ throw new Error('SessionService.createSession not yet implemented - use existing signIn flow');
81
+ }
82
+
83
+ /**
84
+ * Refresh current session
85
+ */
86
+ async refreshSession(): Promise<Session> {
87
+ if (!this.activeSession) {
88
+ throw new Error('No active session to refresh');
89
+ }
90
+
91
+ // Refresh token first
92
+ await tokenService.refreshTokenIfNeeded();
93
+
94
+ // Then refresh session data from server
95
+ // Implementation will be added
96
+ return this.activeSession;
97
+ }
98
+
99
+ /**
100
+ * Validate current session
101
+ */
102
+ async validateSession(): Promise<boolean> {
103
+ if (!this.activeSession) {
104
+ return false;
105
+ }
106
+
107
+ // Check if session expired
108
+ if (new Date(this.activeSession.expiresAt) < new Date()) {
109
+ return false;
110
+ }
111
+
112
+ // Check if token is valid
113
+ const token = tokenService.getAccessToken();
114
+ if (!token) {
115
+ return false;
116
+ }
117
+
118
+ // Additional validation can be added here
119
+ return true;
120
+ }
121
+
122
+ /**
123
+ * Invalidate current session (sign out)
124
+ */
125
+ async invalidateSession(): Promise<void> {
126
+ if (!this.activeSession || !this.oxyServices) {
127
+ return;
128
+ }
129
+
130
+ try {
131
+ // Call API to invalidate session on server
132
+ await this.oxyServices.makeRequest('POST', `/api/session/${this.activeSession.sessionId}/logout`, undefined, { cache: false });
133
+ } catch (error) {
134
+ // Continue with local cleanup even if API call fails
135
+ console.warn('Failed to invalidate session on server:', error);
136
+ }
137
+
138
+ // Clear tokens
139
+ tokenService.clearTokens();
140
+
141
+ // Clear active session
142
+ this.activeSession = null;
143
+ this.sessions = this.sessions.filter(s => s.sessionId !== this.activeSession?.sessionId);
144
+ }
145
+
146
+ /**
147
+ * Set active session (internal use)
148
+ */
149
+ setActiveSession(session: Session): void {
150
+ this.activeSession = session;
151
+
152
+ // Update sessions list
153
+ const existingIndex = this.sessions.findIndex(s => s.sessionId === session.sessionId);
154
+ if (existingIndex >= 0) {
155
+ this.sessions[existingIndex] = session;
156
+ } else {
157
+ this.sessions.push(session);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Clear all sessions (logout all)
163
+ */
164
+ clearAllSessions(): void {
165
+ this.activeSession = null;
166
+ this.sessions = [];
167
+ tokenService.clearTokens();
168
+ }
169
+ }
170
+
171
+ // Export singleton instance
172
+ export const sessionService = SessionService.getInstance();
173
+
@@ -0,0 +1,226 @@
1
+ /**
2
+ * TokenService - Single Source of Truth for Token Management
3
+ *
4
+ * Handles all token storage, retrieval, refresh, and validation.
5
+ * Used by HttpService, SocketService, and other services that need tokens.
6
+ *
7
+ * Architecture:
8
+ * - Single storage location (no duplication)
9
+ * - Automatic token refresh when expiring soon
10
+ * - Type-safe token payload handling
11
+ * - userId is always MongoDB ObjectId, never publicKey
12
+ */
13
+
14
+ import { jwtDecode } from 'jwt-decode';
15
+
16
+ /**
17
+ * AccessTokenPayload - Matches the token payload structure from API
18
+ * userId is always MongoDB ObjectId (24 hex characters), never publicKey
19
+ */
20
+ interface AccessTokenPayload {
21
+ userId: string; // MongoDB ObjectId - PRIMARY IDENTIFIER
22
+ sessionId: string; // Session UUID
23
+ deviceId: string; // Device identifier
24
+ type: 'access';
25
+ iat?: number; // Issued at (added by JWT)
26
+ exp?: number; // Expiration (added by JWT)
27
+ }
28
+
29
+ interface TokenStore {
30
+ accessToken: string | null;
31
+ refreshToken: string | null;
32
+ }
33
+
34
+ /**
35
+ * TokenService - Singleton pattern for global token management
36
+ */
37
+ class TokenService {
38
+ private static instance: TokenService;
39
+ private tokenStore: TokenStore = {
40
+ accessToken: null,
41
+ refreshToken: null,
42
+ };
43
+ private refreshPromise: Promise<void> | null = null;
44
+ private baseURL: string | null = null;
45
+
46
+ private constructor() {}
47
+
48
+ static getInstance(): TokenService {
49
+ if (!TokenService.instance) {
50
+ TokenService.instance = new TokenService();
51
+ }
52
+ return TokenService.instance;
53
+ }
54
+
55
+ /**
56
+ * Initialize TokenService with base URL for refresh requests
57
+ */
58
+ initialize(baseURL: string): void {
59
+ this.baseURL = baseURL;
60
+ }
61
+
62
+ /**
63
+ * Get current access token
64
+ */
65
+ getAccessToken(): string | null {
66
+ return this.tokenStore.accessToken;
67
+ }
68
+
69
+ /**
70
+ * Get current refresh token
71
+ */
72
+ getRefreshToken(): string | null {
73
+ return this.tokenStore.refreshToken;
74
+ }
75
+
76
+ /**
77
+ * Set tokens (called after login or token refresh)
78
+ */
79
+ setTokens(accessToken: string, refreshToken: string = ''): void {
80
+ this.tokenStore.accessToken = accessToken;
81
+ this.tokenStore.refreshToken = refreshToken || this.tokenStore.refreshToken;
82
+ }
83
+
84
+ /**
85
+ * Clear all tokens (called on logout)
86
+ */
87
+ clearTokens(): void {
88
+ this.tokenStore.accessToken = null;
89
+ this.tokenStore.refreshToken = null;
90
+ this.refreshPromise = null;
91
+ }
92
+
93
+ /**
94
+ * Check if access token exists
95
+ */
96
+ hasAccessToken(): boolean {
97
+ return !!this.tokenStore.accessToken;
98
+ }
99
+
100
+ /**
101
+ * Check if token is expiring soon (within 60 seconds)
102
+ */
103
+ isTokenExpiringSoon(): boolean {
104
+ const token = this.tokenStore.accessToken;
105
+ if (!token) return false;
106
+
107
+ try {
108
+ const decoded = jwtDecode<AccessTokenPayload>(token);
109
+ if (!decoded.exp) return false;
110
+
111
+ const currentTime = Math.floor(Date.now() / 1000);
112
+ return decoded.exp - currentTime < 60; // Expiring within 60 seconds
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Get userId from current access token
120
+ * Returns MongoDB ObjectId (never publicKey)
121
+ */
122
+ getUserIdFromToken(): string | null {
123
+ const token = this.tokenStore.accessToken;
124
+ if (!token) return null;
125
+
126
+ try {
127
+ const decoded = jwtDecode<AccessTokenPayload>(token);
128
+ return decoded.userId || null;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Refresh access token if expiring soon
136
+ * Returns promise that resolves when token is refreshed (or already valid)
137
+ */
138
+ async refreshTokenIfNeeded(): Promise<void> {
139
+ // If already refreshing, wait for that promise
140
+ if (this.refreshPromise) {
141
+ return this.refreshPromise;
142
+ }
143
+
144
+ // If token not expiring soon, no refresh needed
145
+ if (!this.isTokenExpiringSoon()) {
146
+ return;
147
+ }
148
+
149
+ // Start refresh
150
+ this.refreshPromise = this._performRefresh();
151
+
152
+ try {
153
+ await this.refreshPromise;
154
+ } finally {
155
+ this.refreshPromise = null;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Perform token refresh
161
+ */
162
+ private async _performRefresh(): Promise<void> {
163
+ const token = this.tokenStore.accessToken;
164
+ if (!token) {
165
+ throw new Error('No access token to refresh');
166
+ }
167
+
168
+ try {
169
+ const decoded = jwtDecode<AccessTokenPayload>(token);
170
+ if (!decoded.sessionId) {
171
+ throw new Error('Token missing sessionId');
172
+ }
173
+
174
+ if (!this.baseURL) {
175
+ throw new Error('TokenService not initialized with baseURL');
176
+ }
177
+
178
+ const refreshUrl = `${this.baseURL}/api/session/token/${decoded.sessionId}`;
179
+
180
+ const response = await fetch(refreshUrl, {
181
+ method: 'GET',
182
+ headers: { 'Accept': 'application/json' },
183
+ signal: AbortSignal.timeout(5000),
184
+ });
185
+
186
+ if (!response.ok) {
187
+ throw new Error(`Token refresh failed: ${response.status}`);
188
+ }
189
+
190
+ const { accessToken: newToken } = await response.json();
191
+
192
+ if (!newToken) {
193
+ throw new Error('No access token in refresh response');
194
+ }
195
+
196
+ // Validate new token has correct userId format (ObjectId)
197
+ const newDecoded = jwtDecode<AccessTokenPayload>(newToken);
198
+ if (newDecoded.userId && !/^[0-9a-fA-F]{24}$/.test(newDecoded.userId)) {
199
+ throw new Error(`Invalid userId format in refreshed token: ${newDecoded.userId.substring(0, 20)}...`);
200
+ }
201
+
202
+ this.setTokens(newToken);
203
+ } catch (error) {
204
+ // Clear tokens on refresh failure (likely expired or invalid)
205
+ this.clearTokens();
206
+ throw error;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Get authorization header with automatic refresh
212
+ */
213
+ async getAuthHeader(): Promise<string | null> {
214
+ // Refresh if needed
215
+ await this.refreshTokenIfNeeded().catch(() => {
216
+ // Ignore refresh errors, will use current token or return null
217
+ });
218
+
219
+ const token = this.tokenStore.accessToken;
220
+ return token ? `Bearer ${token}` : null;
221
+ }
222
+ }
223
+
224
+ // Export singleton instance
225
+ export const tokenService = TokenService.getInstance();
226
+
@@ -21,9 +21,23 @@ export interface OxyConfig {
21
21
  onRequestError?: (url: string, method: string, error: Error) => void;
22
22
  }
23
23
 
24
+ /**
25
+ * User Model
26
+ *
27
+ * IMPORTANT:
28
+ * - id: MongoDB ObjectId (24 hex characters) - PRIMARY IDENTIFIER for all internal operations
29
+ * - publicKey: Cryptographic public key (130 hex characters) - LOOKUP KEY for authentication and identity operations
30
+ *
31
+ * Never use publicKey as an ID. Always use id (ObjectId) for:
32
+ * - Database queries
33
+ * - Session userId
34
+ * - Token userId
35
+ * - Socket room names
36
+ * - API route parameters (unless explicitly doing publicKey lookup)
37
+ */
24
38
  export interface User {
25
- id: string;
26
- publicKey: string;
39
+ id: string; // MongoDB ObjectId - PRIMARY IDENTIFIER (always 24 hex chars)
40
+ publicKey: string; // Cryptographic public key - LOOKUP KEY (130 hex chars for secp256k1)
27
41
  username: string;
28
42
  email?: string;
29
43
  // Avatar file id (asset id)
@@ -1,9 +1,16 @@
1
+ /**
2
+ * Client Session Model
3
+ *
4
+ * IMPORTANT:
5
+ * - userId: MongoDB ObjectId (24 hex characters), never publicKey
6
+ * - Used for session management and user identification
7
+ */
1
8
  export interface ClientSession {
2
9
  sessionId: string;
3
10
  deviceId: string;
4
11
  expiresAt: string;
5
12
  lastActive: string;
6
- userId?: string;
13
+ userId?: string; // MongoDB ObjectId - PRIMARY IDENTIFIER (never publicKey)
7
14
  isCurrent?: boolean;
8
15
  }
9
16