@oxyhq/core 1.2.3 → 1.2.4

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.
@@ -111,6 +111,10 @@ class AuthManager {
111
111
  refreshBuffer: config.refreshBuffer ?? 5 * 60 * 1000, // 5 minutes
112
112
  };
113
113
  this.storage = this.config.storage;
114
+ // Persist tokens to storage when HttpService refreshes them automatically
115
+ this.oxyServices.httpService.onTokenRefreshed = (accessToken) => {
116
+ this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
117
+ };
114
118
  }
115
119
  /**
116
120
  * Get default storage based on environment.
@@ -86,6 +86,7 @@ class TokenStore {
86
86
  class HttpService {
87
87
  constructor(config) {
88
88
  this.tokenRefreshPromise = null;
89
+ this._onTokenRefreshed = null;
89
90
  // Performance monitoring
90
91
  this.requestMetrics = {
91
92
  totalRequests: 0,
@@ -228,7 +229,25 @@ class HttpService {
228
229
  clearTimeout(timeoutId);
229
230
  // Handle response
230
231
  if (!response.ok) {
231
- if (response.status === 401) {
232
+ // On 401, attempt token refresh and retry once before giving up
233
+ if (response.status === 401 && !config._isAuthRetry) {
234
+ const currentToken = this.tokenStore.getAccessToken();
235
+ if (currentToken) {
236
+ try {
237
+ const decoded = (0, jwt_decode_1.jwtDecode)(currentToken);
238
+ if (decoded.sessionId) {
239
+ const refreshResult = await this._refreshTokenFromSession(decoded.sessionId);
240
+ if (refreshResult) {
241
+ // Retry the request with the new token
242
+ return this.request({ ...config, _isAuthRetry: true, retry: false });
243
+ }
244
+ }
245
+ }
246
+ catch {
247
+ // Token decode failed, fall through to clear
248
+ }
249
+ }
250
+ // Refresh failed or no token — clear tokens
232
251
  this.tokenStore.clearTokens();
233
252
  }
234
253
  // Try to parse error response (handle empty/malformed JSON)
@@ -481,6 +500,7 @@ class HttpService {
481
500
  if (response.ok) {
482
501
  const { accessToken: newToken } = await response.json();
483
502
  this.tokenStore.setTokens(newToken);
503
+ this._onTokenRefreshed?.(newToken);
484
504
  this.logger.debug('Token refreshed');
485
505
  return `Bearer ${newToken}`;
486
506
  }
@@ -587,6 +607,9 @@ class HttpService {
587
607
  setTokens(accessToken, refreshToken = '') {
588
608
  this.tokenStore.setTokens(accessToken, refreshToken);
589
609
  }
610
+ set onTokenRefreshed(callback) {
611
+ this._onTokenRefreshed = callback;
612
+ }
590
613
  clearTokens() {
591
614
  this.tokenStore.clearTokens();
592
615
  this.tokenStore.clearCsrfToken();
@@ -107,6 +107,10 @@ export class AuthManager {
107
107
  refreshBuffer: config.refreshBuffer ?? 5 * 60 * 1000, // 5 minutes
108
108
  };
109
109
  this.storage = this.config.storage;
110
+ // Persist tokens to storage when HttpService refreshes them automatically
111
+ this.oxyServices.httpService.onTokenRefreshed = (accessToken) => {
112
+ this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
113
+ };
110
114
  }
111
115
  /**
112
116
  * Get default storage based on environment.
@@ -83,6 +83,7 @@ class TokenStore {
83
83
  export class HttpService {
84
84
  constructor(config) {
85
85
  this.tokenRefreshPromise = null;
86
+ this._onTokenRefreshed = null;
86
87
  // Performance monitoring
87
88
  this.requestMetrics = {
88
89
  totalRequests: 0,
@@ -225,7 +226,25 @@ export class HttpService {
225
226
  clearTimeout(timeoutId);
226
227
  // Handle response
227
228
  if (!response.ok) {
228
- if (response.status === 401) {
229
+ // On 401, attempt token refresh and retry once before giving up
230
+ if (response.status === 401 && !config._isAuthRetry) {
231
+ const currentToken = this.tokenStore.getAccessToken();
232
+ if (currentToken) {
233
+ try {
234
+ const decoded = jwtDecode(currentToken);
235
+ if (decoded.sessionId) {
236
+ const refreshResult = await this._refreshTokenFromSession(decoded.sessionId);
237
+ if (refreshResult) {
238
+ // Retry the request with the new token
239
+ return this.request({ ...config, _isAuthRetry: true, retry: false });
240
+ }
241
+ }
242
+ }
243
+ catch {
244
+ // Token decode failed, fall through to clear
245
+ }
246
+ }
247
+ // Refresh failed or no token — clear tokens
229
248
  this.tokenStore.clearTokens();
230
249
  }
231
250
  // Try to parse error response (handle empty/malformed JSON)
@@ -478,6 +497,7 @@ export class HttpService {
478
497
  if (response.ok) {
479
498
  const { accessToken: newToken } = await response.json();
480
499
  this.tokenStore.setTokens(newToken);
500
+ this._onTokenRefreshed?.(newToken);
481
501
  this.logger.debug('Token refreshed');
482
502
  return `Bearer ${newToken}`;
483
503
  }
@@ -584,6 +604,9 @@ export class HttpService {
584
604
  setTokens(accessToken, refreshToken = '') {
585
605
  this.tokenStore.setTokens(accessToken, refreshToken);
586
606
  }
607
+ set onTokenRefreshed(callback) {
608
+ this._onTokenRefreshed = callback;
609
+ }
587
610
  clearTokens() {
588
611
  this.tokenStore.clearTokens();
589
612
  this.tokenStore.clearCsrfToken();
@@ -28,6 +28,8 @@ interface RequestConfig extends RequestOptions {
28
28
  url: string;
29
29
  data?: unknown;
30
30
  params?: Record<string, unknown>;
31
+ /** @internal Used to prevent infinite auth retry loops */
32
+ _isAuthRetry?: boolean;
31
33
  }
32
34
  /**
33
35
  * Unified HTTP Service
@@ -44,6 +46,7 @@ export declare class HttpService {
44
46
  private logger;
45
47
  private config;
46
48
  private tokenRefreshPromise;
49
+ private _onTokenRefreshed;
47
50
  private requestMetrics;
48
51
  constructor(config: OxyConfig);
49
52
  /**
@@ -140,6 +143,7 @@ export declare class HttpService {
140
143
  data: T;
141
144
  }>;
142
145
  setTokens(accessToken: string, refreshToken?: string): void;
146
+ set onTokenRefreshed(callback: ((accessToken: string) => void) | null);
143
147
  clearTokens(): void;
144
148
  getAccessToken(): string | null;
145
149
  hasAccessToken(): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -153,6 +153,11 @@ export class AuthManager {
153
153
  refreshBuffer: config.refreshBuffer ?? 5 * 60 * 1000, // 5 minutes
154
154
  };
155
155
  this.storage = this.config.storage;
156
+
157
+ // Persist tokens to storage when HttpService refreshes them automatically
158
+ this.oxyServices.httpService.onTokenRefreshed = (accessToken: string) => {
159
+ this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
160
+ };
156
161
  }
157
162
 
158
163
  /**
@@ -52,6 +52,8 @@ interface RequestConfig extends RequestOptions {
52
52
  url: string;
53
53
  data?: unknown;
54
54
  params?: Record<string, unknown>;
55
+ /** @internal Used to prevent infinite auth retry loops */
56
+ _isAuthRetry?: boolean;
55
57
  }
56
58
 
57
59
  /**
@@ -132,6 +134,7 @@ export class HttpService {
132
134
  private logger: SimpleLogger;
133
135
  private config: OxyConfig;
134
136
  private tokenRefreshPromise: Promise<string | null> | null = null;
137
+ private _onTokenRefreshed: ((accessToken: string) => void) | null = null;
135
138
 
136
139
  // Performance monitoring
137
140
  private requestMetrics = {
@@ -322,10 +325,27 @@ export class HttpService {
322
325
 
323
326
  // Handle response
324
327
  if (!response.ok) {
325
- if (response.status === 401) {
328
+ // On 401, attempt token refresh and retry once before giving up
329
+ if (response.status === 401 && !config._isAuthRetry) {
330
+ const currentToken = this.tokenStore.getAccessToken();
331
+ if (currentToken) {
332
+ try {
333
+ const decoded = jwtDecode<JwtPayload>(currentToken);
334
+ if (decoded.sessionId) {
335
+ const refreshResult = await this._refreshTokenFromSession(decoded.sessionId);
336
+ if (refreshResult) {
337
+ // Retry the request with the new token
338
+ return this.request<T>({ ...config, _isAuthRetry: true, retry: false });
339
+ }
340
+ }
341
+ } catch {
342
+ // Token decode failed, fall through to clear
343
+ }
344
+ }
345
+ // Refresh failed or no token — clear tokens
326
346
  this.tokenStore.clearTokens();
327
347
  }
328
-
348
+
329
349
  // Try to parse error response (handle empty/malformed JSON)
330
350
  let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
331
351
  const contentType = response.headers.get('content-type');
@@ -343,10 +363,10 @@ export class HttpService {
343
363
  this.logger.warn('Failed to parse error response JSON:', parseError);
344
364
  }
345
365
  }
346
-
347
- const error = new Error(errorMessage) as Error & {
348
- status?: number;
349
- response?: { status: number; statusText: string }
366
+
367
+ const error = new Error(errorMessage) as Error & {
368
+ status?: number;
369
+ response?: { status: number; statusText: string }
350
370
  };
351
371
  error.status = response.status;
352
372
  error.response = { status: response.status, statusText: response.statusText };
@@ -596,6 +616,7 @@ export class HttpService {
596
616
  if (response.ok) {
597
617
  const { accessToken: newToken } = await response.json();
598
618
  this.tokenStore.setTokens(newToken);
619
+ this._onTokenRefreshed?.(newToken);
599
620
  this.logger.debug('Token refreshed');
600
621
  return `Bearer ${newToken}`;
601
622
  }
@@ -712,6 +733,10 @@ export class HttpService {
712
733
  this.tokenStore.setTokens(accessToken, refreshToken);
713
734
  }
714
735
 
736
+ set onTokenRefreshed(callback: ((accessToken: string) => void) | null) {
737
+ this._onTokenRefreshed = callback;
738
+ }
739
+
715
740
  clearTokens(): void {
716
741
  this.tokenStore.clearTokens();
717
742
  this.tokenStore.clearCsrfToken();