@oxyhq/core 3.4.0 → 3.4.2

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 (174) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/AuthManager.js +91 -319
  3. package/dist/cjs/CrossDomainAuth.js +19 -106
  4. package/dist/cjs/HttpService.js +49 -73
  5. package/dist/cjs/OxyServices.base.js +2 -2
  6. package/dist/cjs/i18n/index.js +7 -1
  7. package/dist/cjs/i18n/locales/ar-SA.json +18 -2
  8. package/dist/cjs/i18n/locales/ca-ES.json +18 -2
  9. package/dist/cjs/i18n/locales/de-DE.json +18 -2
  10. package/dist/cjs/i18n/locales/en-US.json +16 -2
  11. package/dist/cjs/i18n/locales/es-ES.json +16 -2
  12. package/dist/cjs/i18n/locales/fr-FR.json +18 -2
  13. package/dist/cjs/i18n/locales/it-IT.json +18 -2
  14. package/dist/cjs/i18n/locales/ja-JP.json +18 -2
  15. package/dist/cjs/i18n/locales/ko-KR.json +18 -2
  16. package/dist/cjs/i18n/locales/locales/ar-SA.json +18 -2
  17. package/dist/cjs/i18n/locales/locales/ca-ES.json +18 -2
  18. package/dist/cjs/i18n/locales/locales/de-DE.json +18 -2
  19. package/dist/cjs/i18n/locales/locales/en-US.json +17 -3
  20. package/dist/cjs/i18n/locales/locales/es-ES.json +16 -2
  21. package/dist/cjs/i18n/locales/locales/fr-FR.json +18 -2
  22. package/dist/cjs/i18n/locales/locales/it-IT.json +18 -2
  23. package/dist/cjs/i18n/locales/locales/ja-JP.json +18 -2
  24. package/dist/cjs/i18n/locales/locales/ko-KR.json +18 -2
  25. package/dist/cjs/i18n/locales/locales/pt-PT.json +18 -2
  26. package/dist/cjs/i18n/locales/locales/zh-CN.json +18 -2
  27. package/dist/cjs/i18n/locales/pt-PT.json +18 -2
  28. package/dist/cjs/i18n/locales/zh-CN.json +18 -2
  29. package/dist/cjs/mixins/OxyServices.auth.js +20 -63
  30. package/dist/cjs/mixins/OxyServices.fedcm.js +10 -12
  31. package/dist/cjs/mixins/OxyServices.popup.js +50 -299
  32. package/dist/cjs/mixins/OxyServices.redirect.js +84 -348
  33. package/dist/cjs/mixins/OxyServices.silent.js +204 -0
  34. package/dist/cjs/mixins/OxyServices.sso.js +4 -5
  35. package/dist/cjs/mixins/OxyServices.utility.js +6 -15
  36. package/dist/cjs/mixins/index.js +5 -6
  37. package/dist/cjs/server/index.js +21 -0
  38. package/dist/cjs/server/rateLimit.js +77 -0
  39. package/dist/cjs/shared/utils/debugUtils.js +1 -1
  40. package/dist/cjs/utils/accountUtils.js +4 -4
  41. package/dist/cjs/utils/authHelpers.js +21 -15
  42. package/dist/cjs/utils/coldBoot.js +3 -3
  43. package/dist/cjs/utils/fapiAutoDetect.js +1 -1
  44. package/dist/esm/.tsbuildinfo +1 -1
  45. package/dist/esm/AuthManager.js +91 -319
  46. package/dist/esm/CrossDomainAuth.js +19 -106
  47. package/dist/esm/HttpService.js +49 -73
  48. package/dist/esm/OxyServices.base.js +2 -2
  49. package/dist/esm/i18n/index.js +7 -1
  50. package/dist/esm/i18n/locales/ar-SA.json +18 -2
  51. package/dist/esm/i18n/locales/ca-ES.json +18 -2
  52. package/dist/esm/i18n/locales/de-DE.json +18 -2
  53. package/dist/esm/i18n/locales/en-US.json +16 -2
  54. package/dist/esm/i18n/locales/es-ES.json +16 -2
  55. package/dist/esm/i18n/locales/fr-FR.json +18 -2
  56. package/dist/esm/i18n/locales/it-IT.json +18 -2
  57. package/dist/esm/i18n/locales/ja-JP.json +18 -2
  58. package/dist/esm/i18n/locales/ko-KR.json +18 -2
  59. package/dist/esm/i18n/locales/locales/ar-SA.json +18 -2
  60. package/dist/esm/i18n/locales/locales/ca-ES.json +18 -2
  61. package/dist/esm/i18n/locales/locales/de-DE.json +18 -2
  62. package/dist/esm/i18n/locales/locales/en-US.json +17 -3
  63. package/dist/esm/i18n/locales/locales/es-ES.json +16 -2
  64. package/dist/esm/i18n/locales/locales/fr-FR.json +18 -2
  65. package/dist/esm/i18n/locales/locales/it-IT.json +18 -2
  66. package/dist/esm/i18n/locales/locales/ja-JP.json +18 -2
  67. package/dist/esm/i18n/locales/locales/ko-KR.json +18 -2
  68. package/dist/esm/i18n/locales/locales/pt-PT.json +18 -2
  69. package/dist/esm/i18n/locales/locales/zh-CN.json +18 -2
  70. package/dist/esm/i18n/locales/pt-PT.json +18 -2
  71. package/dist/esm/i18n/locales/zh-CN.json +18 -2
  72. package/dist/esm/mixins/OxyServices.auth.js +20 -63
  73. package/dist/esm/mixins/OxyServices.fedcm.js +10 -12
  74. package/dist/esm/mixins/OxyServices.popup.js +52 -301
  75. package/dist/esm/mixins/OxyServices.redirect.js +84 -349
  76. package/dist/esm/mixins/OxyServices.silent.js +202 -0
  77. package/dist/esm/mixins/OxyServices.sso.js +4 -5
  78. package/dist/esm/mixins/OxyServices.utility.js +6 -15
  79. package/dist/esm/mixins/index.js +5 -6
  80. package/dist/esm/server/index.js +17 -0
  81. package/dist/esm/server/rateLimit.js +71 -0
  82. package/dist/esm/shared/utils/debugUtils.js +1 -1
  83. package/dist/esm/utils/accountUtils.js +4 -4
  84. package/dist/esm/utils/authHelpers.js +21 -15
  85. package/dist/esm/utils/coldBoot.js +3 -3
  86. package/dist/esm/utils/fapiAutoDetect.js +1 -1
  87. package/dist/types/.tsbuildinfo +1 -1
  88. package/dist/types/AuthManager.d.ts +26 -53
  89. package/dist/types/AuthManagerTypes.d.ts +5 -9
  90. package/dist/types/CrossDomainAuth.d.ts +13 -52
  91. package/dist/types/HttpService.d.ts +9 -8
  92. package/dist/types/OxyServices.base.d.ts +1 -1
  93. package/dist/types/OxyServices.d.ts +4 -10
  94. package/dist/types/index.d.ts +1 -1
  95. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -1
  96. package/dist/types/mixins/OxyServices.appData.d.ts +1 -1
  97. package/dist/types/mixins/OxyServices.applications.d.ts +1 -1
  98. package/dist/types/mixins/OxyServices.assets.d.ts +1 -1
  99. package/dist/types/mixins/OxyServices.auth.d.ts +10 -31
  100. package/dist/types/mixins/OxyServices.contacts.d.ts +1 -1
  101. package/dist/types/mixins/OxyServices.devices.d.ts +1 -1
  102. package/dist/types/mixins/OxyServices.features.d.ts +1 -1
  103. package/dist/types/mixins/OxyServices.fedcm.d.ts +5 -5
  104. package/dist/types/mixins/OxyServices.language.d.ts +1 -1
  105. package/dist/types/mixins/OxyServices.location.d.ts +1 -1
  106. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -1
  107. package/dist/types/mixins/OxyServices.payment.d.ts +1 -1
  108. package/dist/types/mixins/OxyServices.popup.d.ts +18 -120
  109. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -1
  110. package/dist/types/mixins/OxyServices.redirect.d.ts +13 -174
  111. package/dist/types/mixins/OxyServices.reputation.d.ts +1 -1
  112. package/dist/types/mixins/OxyServices.security.d.ts +1 -1
  113. package/dist/types/mixins/OxyServices.silent.d.ts +131 -0
  114. package/dist/types/mixins/OxyServices.sso.d.ts +4 -5
  115. package/dist/types/mixins/OxyServices.topics.d.ts +1 -1
  116. package/dist/types/mixins/OxyServices.user.d.ts +1 -1
  117. package/dist/types/mixins/OxyServices.utility.d.ts +3 -8
  118. package/dist/types/mixins/OxyServices.workspaces.d.ts +1 -1
  119. package/dist/types/mixins/index.d.ts +3 -3
  120. package/dist/types/models/interfaces.d.ts +5 -16
  121. package/dist/types/models/session.d.ts +0 -2
  122. package/dist/types/server/index.d.ts +18 -0
  123. package/dist/types/server/rateLimit.d.ts +40 -0
  124. package/dist/types/shared/utils/debugUtils.d.ts +1 -1
  125. package/dist/types/utils/authHelpers.d.ts +4 -3
  126. package/dist/types/utils/coldBoot.d.ts +2 -2
  127. package/dist/types/utils/fapiAutoDetect.d.ts +1 -1
  128. package/package.json +25 -3
  129. package/src/AuthManager.ts +100 -370
  130. package/src/AuthManagerTypes.ts +5 -9
  131. package/src/CrossDomainAuth.ts +22 -129
  132. package/src/HttpService.ts +55 -73
  133. package/src/OxyServices.base.ts +2 -3
  134. package/src/OxyServices.ts +9 -11
  135. package/src/__tests__/authManager.cookiePath.test.ts +19 -17
  136. package/src/__tests__/authManager.security.test.ts +7 -3
  137. package/src/__tests__/crossDomainAuth.test.ts +26 -118
  138. package/src/i18n/index.ts +7 -1
  139. package/src/i18n/locales/ar-SA.json +18 -2
  140. package/src/i18n/locales/ca-ES.json +18 -2
  141. package/src/i18n/locales/de-DE.json +18 -2
  142. package/src/i18n/locales/en-US.json +17 -3
  143. package/src/i18n/locales/es-ES.json +16 -2
  144. package/src/i18n/locales/fr-FR.json +18 -2
  145. package/src/i18n/locales/it-IT.json +18 -2
  146. package/src/i18n/locales/ja-JP.json +18 -2
  147. package/src/i18n/locales/ko-KR.json +18 -2
  148. package/src/i18n/locales/pt-PT.json +18 -2
  149. package/src/i18n/locales/zh-CN.json +18 -2
  150. package/src/index.ts +1 -1
  151. package/src/mixins/OxyServices.auth.ts +23 -75
  152. package/src/mixins/OxyServices.fedcm.ts +10 -12
  153. package/src/mixins/OxyServices.redirect.ts +82 -371
  154. package/src/mixins/OxyServices.silent.ts +272 -0
  155. package/src/mixins/OxyServices.sso.ts +5 -6
  156. package/src/mixins/OxyServices.utility.ts +9 -22
  157. package/src/mixins/__tests__/appData.test.ts +1 -1
  158. package/src/mixins/__tests__/onTokensChanged.test.ts +1 -1
  159. package/src/mixins/__tests__/reputation.test.ts +1 -1
  160. package/src/mixins/__tests__/serviceAuth.test.ts +7 -5
  161. package/src/mixins/__tests__/silent.test.ts +102 -0
  162. package/src/mixins/__tests__/verifyChallenge.test.ts +9 -14
  163. package/src/mixins/index.ts +6 -8
  164. package/src/models/interfaces.ts +5 -16
  165. package/src/models/session.ts +1 -3
  166. package/src/server/index.ts +19 -0
  167. package/src/server/rateLimit.ts +170 -0
  168. package/src/shared/utils/debugUtils.ts +1 -1
  169. package/src/utils/accountUtils.ts +4 -4
  170. package/src/utils/authHelpers.ts +23 -15
  171. package/src/utils/coldBoot.ts +4 -4
  172. package/src/utils/fapiAutoDetect.ts +1 -1
  173. package/src/mixins/OxyServices.popup.ts +0 -631
  174. package/src/mixins/__tests__/popup.test.ts +0 -374
@@ -6,8 +6,7 @@
6
6
  * selects the best authentication method based on browser capabilities:
7
7
  *
8
8
  * 1. FedCM (if supported) - Modern, Google-style browser-native auth
9
- * 2. Popup (fallback) - OAuth2-style popup window
10
- * 3. Redirect (final fallback) - Traditional full-page redirect
9
+ * 2. Redirect (fallback) - Tokenless central SSO full-page redirect
11
10
  *
12
11
  * Usage:
13
12
  * ```typescript
@@ -18,8 +17,8 @@
18
17
  * // Automatic method selection
19
18
  * const session = await auth.signIn();
20
19
  *
21
- * // Or use specific method
22
- * const session = await auth.signInWithPopup();
20
+ * // Or use a specific method
21
+ * auth.signInWithRedirect();
23
22
  * ```
24
23
  */
25
24
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -35,53 +34,23 @@ class CrossDomainAuth {
35
34
  *
36
35
  * Tries methods in this order:
37
36
  * 1. FedCM (if supported and not in private browsing)
38
- * 2. Popup (if not blocked)
39
- * 3. Redirect (always works)
37
+ * 2. Redirect (always works)
40
38
  *
41
39
  * @param options - Authentication options
42
40
  * @returns Session with user data and access token
43
41
  */
44
42
  async signIn(options = {}) {
45
43
  const method = options.method || 'auto';
46
- // If specific method requested, use it directly. The caller MAY have
47
- // pre-opened a popup on the raw click (the standard pattern in
48
- // WebOxyProvider / services useAuth). For the FedCM and redirect paths
49
- // that popup is unused — close it so it doesn't linger as an orphaned
50
- // blank window. Close in both success and failure paths.
51
44
  if (method === 'fedcm') {
52
- try {
53
- const session = await this.signInWithFedCM(options);
54
- this.closeOrphanPopup(options.popup);
55
- return session;
56
- }
57
- catch (error) {
58
- this.closeOrphanPopup(options.popup);
59
- throw error;
60
- }
61
- }
62
- if (method === 'popup') {
63
- return this.signInWithPopup(options);
45
+ return this.signInWithFedCM(options);
64
46
  }
65
47
  if (method === 'redirect') {
66
- this.closeOrphanPopup(options.popup);
67
48
  this.signInWithRedirect(options);
68
49
  return null; // Redirect doesn't return immediately
69
50
  }
70
- // Auto mode: Try methods in order of preference
51
+ // Auto mode: try methods in order of preference.
71
52
  return this.autoSignIn(options);
72
53
  }
73
- /**
74
- * Close a caller-supplied popup window that is no longer needed (e.g. the
75
- * resolved auth method didn't end up using it). Safe against null / already
76
- * closed handles.
77
- *
78
- * @private
79
- */
80
- closeOrphanPopup(popup) {
81
- if (popup && !popup.closed) {
82
- popup.close();
83
- }
84
- }
85
54
  /**
86
55
  * Automatic sign-in with progressive enhancement
87
56
  *
@@ -92,27 +61,13 @@ class CrossDomainAuth {
92
61
  if (this.isFedCMSupported()) {
93
62
  try {
94
63
  options.onMethodSelected?.('fedcm');
95
- const session = await this.signInWithFedCM(options);
96
- // FedCM succeeded — close the pre-opened popup so it doesn't linger
97
- // as an orphaned blank window.
98
- this.closeOrphanPopup(options.popup);
99
- return session;
64
+ return await this.signInWithFedCM(options);
100
65
  }
101
66
  catch (error) {
102
- loggerUtils_1.logger.warn('FedCM failed, trying popup', { component: 'CrossDomainAuth', method: 'autoSignIn' }, error);
67
+ loggerUtils_1.logger.warn('FedCM failed, falling back to redirect', { component: 'CrossDomainAuth', method: 'autoSignIn' }, error);
103
68
  }
104
69
  }
105
- // 2. Try popup (good UX, widely supported)
106
- try {
107
- options.onMethodSelected?.('popup');
108
- return await this.signInWithPopup(options);
109
- }
110
- catch (error) {
111
- loggerUtils_1.logger.warn('Popup failed, falling back to redirect', { component: 'CrossDomainAuth', method: 'autoSignIn' }, error);
112
- // Popup path failed — close the pre-opened popup before redirecting.
113
- this.closeOrphanPopup(options.popup);
114
- }
115
- // 3. Fallback to redirect (always works)
70
+ // 2. Fallback to redirect (always works)
116
71
  options.onMethodSelected?.('redirect');
117
72
  this.signInWithRedirect(options);
118
73
  return null;
@@ -120,26 +75,13 @@ class CrossDomainAuth {
120
75
  /**
121
76
  * Sign in using FedCM (Federated Credential Management)
122
77
  *
123
- * Best method - browser-native, no popups, Google-like experience
78
+ * Best method - browser-native, Google-like experience
124
79
  */
125
80
  async signInWithFedCM(options = {}) {
126
81
  return this.oxyServices.signInWithFedCM({
127
82
  context: options.isSignup ? 'signup' : 'signin',
128
83
  });
129
84
  }
130
- /**
131
- * Sign in using popup window
132
- *
133
- * Good method - preserves app state, no full page reload
134
- */
135
- async signInWithPopup(options = {}) {
136
- return this.oxyServices.signInWithPopup({
137
- mode: options.isSignup ? 'signup' : 'login',
138
- width: options.popupDimensions?.width,
139
- height: options.popupDimensions?.height,
140
- popup: options.popup ?? undefined,
141
- });
142
- }
143
85
  /**
144
86
  * Sign in using full-page redirect
145
87
  *
@@ -163,7 +105,7 @@ class CrossDomainAuth {
163
105
  * Silent sign-in (check for existing session)
164
106
  *
165
107
  * Tries to automatically sign in without user interaction.
166
- * Works with both FedCM and popup/iframe methods.
108
+ * Works with FedCM and iframe-based silent auth.
167
109
  *
168
110
  * @returns Session if user is already signed in, null otherwise
169
111
  */
@@ -190,22 +132,13 @@ class CrossDomainAuth {
190
132
  }
191
133
  }
192
134
  /**
193
- * Restore session from storage
135
+ * Restore session from storage.
194
136
  *
195
- * For redirect method - restores previously authenticated session from localStorage
137
+ * Access tokens are no longer persisted in browser storage; providers restore
138
+ * through refresh cookies / SSO code exchange instead.
196
139
  */
197
140
  restoreSession() {
198
- return this.oxyServices.restoreSession?.() || false;
199
- }
200
- /**
201
- * Open a blank popup SYNCHRONOUSLY (call from a raw user-gesture handler
202
- * BEFORE any `await`). Returns `null` if the popup was blocked. Pass the
203
- * handle into `signIn({ popup })` / `signInWithPopup({ popup })` so the
204
- * popup is not blocked by Chrome after any prior `await` consumed the
205
- * transient user activation. Delegates to `OxyServices.openBlankPopup`.
206
- */
207
- openBlankPopup(width, height) {
208
- return this.oxyServices.openBlankPopup(width, height);
141
+ return false;
209
142
  }
210
143
  /**
211
144
  * Check if FedCM is supported in current browser
@@ -229,8 +162,8 @@ class CrossDomainAuth {
229
162
  }
230
163
  if (typeof window !== 'undefined') {
231
164
  return {
232
- method: 'popup',
233
- reason: 'Browser environment - popup preserves app state',
165
+ method: 'redirect',
166
+ reason: 'Browser environment - redirect SSO works without token callback URLs',
234
167
  };
235
168
  }
236
169
  return {
@@ -243,8 +176,7 @@ class CrossDomainAuth {
243
176
  *
244
177
  * This handles:
245
178
  * 1. Redirect callback (if returning from auth.oxy.so)
246
- * 2. Session restoration (from localStorage)
247
- * 3. Silent sign-in (check for existing SSO session)
179
+ * 2. Silent sign-in (check for existing SSO session)
248
180
  *
249
181
  * @returns Session if user is authenticated, null otherwise
250
182
  */
@@ -254,26 +186,7 @@ class CrossDomainAuth {
254
186
  if (callbackSession) {
255
187
  return callbackSession;
256
188
  }
257
- // 2. Try to restore existing session from storage
258
- const restored = this.restoreSession();
259
- if (restored) {
260
- // Verify session is still valid by fetching user
261
- try {
262
- const user = await this.oxyServices.getCurrentUser();
263
- if (user) {
264
- return {
265
- sessionId: this.oxyServices.getStoredSessionId?.() || '',
266
- deviceId: '',
267
- expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
268
- user,
269
- };
270
- }
271
- }
272
- catch (error) {
273
- loggerUtils_1.logger.debug('stored session invalid', { component: 'CrossDomainAuth', method: 'initialize' }, error);
274
- }
275
- }
276
- // 3. Try silent sign-in (check for SSO session at auth.oxy.so)
189
+ // 2. Try silent sign-in (check for SSO session at auth.oxy.so)
277
190
  return await this.silentSignIn();
278
191
  }
279
192
  }
@@ -60,23 +60,17 @@ function fnv1a32(str) {
60
60
  class TokenStore {
61
61
  constructor() {
62
62
  this.accessToken = null;
63
- this.refreshToken = null;
64
63
  this.csrfToken = null;
65
64
  this.csrfTokenFetchPromise = null;
66
65
  }
67
- setTokens(accessToken, refreshToken = '') {
66
+ setTokens(accessToken) {
68
67
  this.accessToken = accessToken;
69
- this.refreshToken = refreshToken;
70
68
  }
71
69
  getAccessToken() {
72
70
  return this.accessToken;
73
71
  }
74
- getRefreshToken() {
75
- return this.refreshToken;
76
- }
77
72
  clearTokens() {
78
73
  this.accessToken = null;
79
- this.refreshToken = null;
80
74
  }
81
75
  hasAccessToken() {
82
76
  return !!this.accessToken;
@@ -108,13 +102,12 @@ class HttpService {
108
102
  constructor(config) {
109
103
  this.tokenRefreshPromise = null;
110
104
  this.tokenRefreshCooldownUntil = 0;
111
- this._onTokenRefreshed = null;
105
+ this.authRefreshHandler = null;
112
106
  /**
113
107
  * Fan-out listeners notified on EVERY access-token change on this instance:
114
- * explicit `setTokens`, `clearTokens`, a successful silent refresh, and the
115
- * internal 401-driven clear. Unlike the single-slot `_onTokenRefreshed`
116
- * (owned by AuthManager for the refresh path only), this is a Set so multiple
117
- * independent observers can mirror token state without clobbering each other.
108
+ * explicit `setTokens`, `clearTokens`, an AuthManager-owned refresh, and the
109
+ * internal 401-driven clear. This is a Set so multiple independent observers
110
+ * can mirror token state without clobbering each other.
118
111
  *
119
112
  * Each listener receives the resulting access token, or `null` when cleared.
120
113
  */
@@ -301,23 +294,13 @@ class HttpService {
301
294
  clearTimeout(timeoutId);
302
295
  // Handle response
303
296
  if (!response.ok) {
304
- // On 401, attempt token refresh and retry once before giving up
297
+ // On 401, delegate refresh to AuthManager and retry once before
298
+ // giving up. HttpService deliberately does not know any session
299
+ // routes; the AuthManager is the single session authority.
305
300
  if (response.status === 401 && !config._isAuthRetry) {
306
- const currentToken = this.tokenStore.getAccessToken();
307
- if (currentToken) {
308
- try {
309
- const decoded = (0, jwt_decode_1.jwtDecode)(currentToken);
310
- if (decoded.sessionId) {
311
- const refreshResult = await this._refreshTokenFromSession(decoded.sessionId);
312
- if (refreshResult) {
313
- // Retry the request with the new token
314
- return this.request({ ...config, _isAuthRetry: true, retry: false });
315
- }
316
- }
317
- }
318
- catch {
319
- // Token decode failed, fall through to clear
320
- }
301
+ const refreshed = await this.refreshAccessToken('response-401');
302
+ if (refreshed) {
303
+ return this.request({ ...config, _isAuthRetry: true, retry: false });
321
304
  }
322
305
  // Refresh failed or no token — clear tokens and stale CSRF
323
306
  this.tokenStore.clearTokens();
@@ -344,7 +327,7 @@ class HttpService {
344
327
  if (contentType && contentType.includes('application/json')) {
345
328
  try {
346
329
  const errorData = await response.json();
347
- // Check both 'message' and 'error' fields for backwards compatibility
330
+ // Accept either structured error field from API responses.
348
331
  if (errorData?.message) {
349
332
  errorMessage = errorData.message;
350
333
  }
@@ -711,26 +694,10 @@ class HttpService {
711
694
  const decoded = (0, jwt_decode_1.jwtDecode)(accessToken);
712
695
  const currentTime = Math.floor(Date.now() / 1000);
713
696
  // If token expires in less than 60 seconds, refresh it
714
- if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
715
- // Skip if we recently failed a refresh (15s cooldown to prevent storms)
716
- if (Date.now() < this.tokenRefreshCooldownUntil) {
717
- return `Bearer ${accessToken}`;
718
- }
719
- // Deduplicate concurrent refresh attempts. The promise is shared
720
- // across all concurrent callers and cleared only after it settles,
721
- // so every awaiter receives the same result.
722
- if (!this.tokenRefreshPromise) {
723
- this.tokenRefreshPromise = this._refreshTokenFromSession(decoded.sessionId)
724
- .then((result) => {
725
- if (!result)
726
- this.tokenRefreshCooldownUntil = Date.now() + 15000;
727
- return result;
728
- })
729
- .finally(() => { this.tokenRefreshPromise = null; });
730
- }
731
- const result = await this.tokenRefreshPromise;
732
- if (result)
733
- return result;
697
+ if (decoded.exp && decoded.exp - currentTime < 60) {
698
+ const refreshed = await this.refreshAccessToken('preflight');
699
+ if (refreshed)
700
+ return `Bearer ${refreshed}`;
734
701
  // Refresh failed — don't use the expired token (would cause 401 loop)
735
702
  return null;
736
703
  }
@@ -741,28 +708,37 @@ class HttpService {
741
708
  return null;
742
709
  }
743
710
  }
744
- async _refreshTokenFromSession(sessionId) {
745
- try {
746
- const refreshUrl = `${this.baseURL}/session/token/${sessionId}`;
747
- const response = await fetch(refreshUrl, {
748
- method: 'GET',
749
- headers: { 'Accept': 'application/json' },
750
- signal: AbortSignal.timeout(5000),
751
- credentials: 'include',
752
- });
753
- if (response.ok) {
754
- const { accessToken: newToken } = await response.json();
755
- this.tokenStore.setTokens(newToken);
756
- this._onTokenRefreshed?.(newToken);
757
- this.notifyTokenChange();
758
- this.logger.debug('Token refreshed');
759
- return `Bearer ${newToken}`;
760
- }
711
+ async refreshAccessToken(reason) {
712
+ if (!this.authRefreshHandler) {
713
+ return null;
714
+ }
715
+ if (Date.now() < this.tokenRefreshCooldownUntil) {
716
+ return null;
761
717
  }
762
- catch (refreshError) {
763
- this.logger.warn('Token refresh failed, using current token');
718
+ if (!this.tokenRefreshPromise) {
719
+ this.tokenRefreshPromise = this.authRefreshHandler(reason)
720
+ .then((newToken) => {
721
+ if (!newToken) {
722
+ this.tokenRefreshCooldownUntil = Date.now() + 15000;
723
+ return null;
724
+ }
725
+ if (this.tokenStore.getAccessToken() !== newToken) {
726
+ this.tokenStore.setTokens(newToken);
727
+ this.notifyTokenChange();
728
+ }
729
+ this.logger.debug('Token refreshed via AuthManager');
730
+ return newToken;
731
+ })
732
+ .catch((error) => {
733
+ this.logger.warn('Token refresh failed:', error);
734
+ this.tokenRefreshCooldownUntil = Date.now() + 15000;
735
+ return null;
736
+ })
737
+ .finally(() => {
738
+ this.tokenRefreshPromise = null;
739
+ });
764
740
  }
765
- return null;
741
+ return this.tokenRefreshPromise;
766
742
  }
767
743
  /**
768
744
  * Unwrap standardized API response format
@@ -818,12 +794,12 @@ class HttpService {
818
794
  return this._actingAsUserId;
819
795
  }
820
796
  // Token management
821
- setTokens(accessToken, refreshToken = '') {
822
- this.tokenStore.setTokens(accessToken, refreshToken);
797
+ setTokens(accessToken) {
798
+ this.tokenStore.setTokens(accessToken);
823
799
  this.notifyTokenChange();
824
800
  }
825
- set onTokenRefreshed(callback) {
826
- this._onTokenRefreshed = callback;
801
+ setAuthRefreshHandler(handler) {
802
+ this.authRefreshHandler = handler;
827
803
  }
828
804
  clearTokens() {
829
805
  this.tokenStore.clearTokens();
@@ -130,8 +130,8 @@ class OxyServicesBase {
130
130
  /**
131
131
  * Set authentication tokens
132
132
  */
133
- setTokens(accessToken, refreshToken = '') {
134
- this.httpService.setTokens(accessToken, refreshToken);
133
+ setTokens(accessToken) {
134
+ this.httpService.setTokens(accessToken);
135
135
  }
136
136
  /**
137
137
  * Clear stored authentication tokens
@@ -48,8 +48,14 @@ function translate(locale, key, vars) {
48
48
  const lang = locale && DICTS[locale] ? locale : FALLBACK;
49
49
  const dict = DICTS[lang] || DICTS[FALLBACK];
50
50
  let val = getNested(dict, key);
51
+ // Per-key fallback to the English dictionary when a key is missing from the
52
+ // resolved (non-English) locale. Without this, a key present in en-US but not
53
+ // yet translated in e.g. es-ES would render the raw dotted key to users.
54
+ if (typeof val !== 'string' && lang !== FALLBACK) {
55
+ val = getNested(DICTS[FALLBACK], key);
56
+ }
51
57
  if (typeof val !== 'string')
52
- return key; // fallback to key if missing
58
+ return key; // last resort: echo the key when truly absent everywhere
53
59
  if (vars) {
54
60
  Object.keys(vars).forEach(k => {
55
61
  const token = `{{${k}}}`;
@@ -103,7 +103,9 @@
103
103
  "createAccount": "إنشاء حساب",
104
104
  "signIn": "تسجيل الدخول",
105
105
  "verify": "التحقق",
106
- "resetPassword": "إعادة تعيين كلمة المرور"
106
+ "resetPassword": "إعادة تعيين كلمة المرور",
107
+ "signedOut": "تم تسجيل الخروج",
108
+ "close": "إغلاق"
107
109
  },
108
110
  "links": {
109
111
  "recoverAccount": "استعادة حسابك",
@@ -115,7 +117,10 @@
115
117
  "password": "كلمة المرور",
116
118
  "confirmPassword": "تأكيد كلمة المرور"
117
119
  },
118
- "revoke": "Revoke"
120
+ "revoke": "Revoke",
121
+ "errors": {
122
+ "signOutAllFailed": "حدثت مشكلة أثناء تسجيل الخروج من جميع الحسابات. يرجى المحاولة مرة أخرى."
123
+ }
119
124
  },
120
125
  "notifications": {
121
126
  "title": "Notifications",
@@ -198,5 +203,16 @@
198
203
  "revoked": "Revoked access for {{name}}",
199
204
  "revokeFailed": "Failed to revoke access"
200
205
  }
206
+ },
207
+ "accountMenu": {
208
+ "label": "قائمة الحساب",
209
+ "manage": "إدارة حساب Oxy الخاص بك",
210
+ "addAnother": "إضافة حساب آخر",
211
+ "signOutAll": "تسجيل الخروج من جميع الحسابات",
212
+ "open": "قائمة الحساب",
213
+ "openHint": "يفتح قائمة الحساب",
214
+ "openWithUser": "قائمة حساب {{name}}",
215
+ "switching": "جارٍ تبديل الحساب…",
216
+ "signOutAccount": "تسجيل خروج {{name}}"
201
217
  }
202
218
  }
@@ -103,7 +103,9 @@
103
103
  "createAccount": "Crear compte",
104
104
  "signIn": "Iniciar sessió",
105
105
  "verify": "Verificar",
106
- "resetPassword": "Restablir contrasenya"
106
+ "resetPassword": "Restablir contrasenya",
107
+ "signedOut": "Sessió tancada",
108
+ "close": "Tanca"
107
109
  },
108
110
  "links": {
109
111
  "recoverAccount": "Recuperar el teu compte",
@@ -115,7 +117,10 @@
115
117
  "password": "Contrasenya",
116
118
  "confirmPassword": "Confirmar contrasenya"
117
119
  },
118
- "revoke": "Revoke"
120
+ "revoke": "Revoke",
121
+ "errors": {
122
+ "signOutAllFailed": "Hi ha hagut un problema en tancar la sessió de tots els comptes. Torna-ho a provar."
123
+ }
119
124
  },
120
125
  "notifications": {
121
126
  "title": "Notifications",
@@ -198,5 +203,16 @@
198
203
  "revoked": "Revoked access for {{name}}",
199
204
  "revokeFailed": "Failed to revoke access"
200
205
  }
206
+ },
207
+ "accountMenu": {
208
+ "label": "Menú del compte",
209
+ "manage": "Gestiona el teu compte d'Oxy",
210
+ "addAnother": "Afegeix un altre compte",
211
+ "signOutAll": "Tanca la sessió de tots els comptes",
212
+ "open": "Menú del compte",
213
+ "openHint": "Obre el menú del compte",
214
+ "openWithUser": "Menú del compte de {{name}}",
215
+ "switching": "Canviant de compte…",
216
+ "signOutAccount": "Tanca la sessió de {{name}}"
201
217
  }
202
218
  }
@@ -103,7 +103,9 @@
103
103
  "createAccount": "Konto erstellen",
104
104
  "signIn": "Anmelden",
105
105
  "verify": "Überprüfen",
106
- "resetPassword": "Passwort zurücksetzen"
106
+ "resetPassword": "Passwort zurücksetzen",
107
+ "signedOut": "Abgemeldet",
108
+ "close": "Schließen"
107
109
  },
108
110
  "links": {
109
111
  "recoverAccount": "Ihr Konto wiederherstellen",
@@ -115,7 +117,10 @@
115
117
  "password": "Passwort",
116
118
  "confirmPassword": "Passwort bestätigen"
117
119
  },
118
- "revoke": "Revoke"
120
+ "revoke": "Revoke",
121
+ "errors": {
122
+ "signOutAllFailed": "Beim Abmelden von allen Konten ist ein Problem aufgetreten. Bitte versuchen Sie es erneut."
123
+ }
119
124
  },
120
125
  "notifications": {
121
126
  "title": "Notifications",
@@ -198,5 +203,16 @@
198
203
  "revoked": "Revoked access for {{name}}",
199
204
  "revokeFailed": "Failed to revoke access"
200
205
  }
206
+ },
207
+ "accountMenu": {
208
+ "label": "Kontomenü",
209
+ "manage": "Ihr Oxy-Konto verwalten",
210
+ "addAnother": "Weiteres Konto hinzufügen",
211
+ "signOutAll": "Von allen Konten abmelden",
212
+ "open": "Kontomenü",
213
+ "openHint": "Öffnet das Kontomenü",
214
+ "openWithUser": "Kontomenü für {{name}}",
215
+ "switching": "Konto wird gewechselt…",
216
+ "signOutAccount": "{{name}} abmelden"
201
217
  }
202
218
  }
@@ -1038,7 +1038,8 @@
1038
1038
  },
1039
1039
  "common": {
1040
1040
  "errors": {
1041
- "signOutFailed": "There was a problem signing you out. Please try again."
1041
+ "signOutFailed": "There was a problem signing you out. Please try again.",
1042
+ "signOutAllFailed": "There was a problem signing out of all accounts. Please try again."
1042
1043
  },
1043
1044
  "confirms": {
1044
1045
  "signOut": "Are you sure you want to sign out?",
@@ -1057,7 +1058,9 @@
1057
1058
  "signIn": "Sign In",
1058
1059
  "signOut": "Sign Out",
1059
1060
  "verify": "Verify",
1060
- "resetPassword": "Reset Password"
1061
+ "resetPassword": "Reset Password",
1062
+ "signedOut": "Signed out",
1063
+ "close": "Close"
1061
1064
  },
1062
1065
  "cancel": "Cancel",
1063
1066
  "revoke": "Revoke",
@@ -1495,5 +1498,16 @@
1495
1498
  "revoked": "Revoked access for {{name}}",
1496
1499
  "revokeFailed": "Failed to revoke access"
1497
1500
  }
1501
+ },
1502
+ "accountMenu": {
1503
+ "label": "Account menu",
1504
+ "manage": "Manage your Oxy Account",
1505
+ "addAnother": "Add another account",
1506
+ "signOutAll": "Sign out of all accounts",
1507
+ "open": "Account menu",
1508
+ "openHint": "Opens the account menu",
1509
+ "openWithUser": "Account menu for {{name}}",
1510
+ "switching": "Switching account…",
1511
+ "signOutAccount": "Sign out {{name}}"
1498
1512
  }
1499
1513
  }
@@ -281,7 +281,9 @@
281
281
  "signIn": "Entrar",
282
282
  "signOut": "Cerrar sesión",
283
283
  "verify": "Verificar",
284
- "resetPassword": "Restablecer contraseña"
284
+ "resetPassword": "Restablecer contraseña",
285
+ "signedOut": "Sesión cerrada",
286
+ "close": "Cerrar"
285
287
  },
286
288
  "cancel": "Cancelar",
287
289
  "revoke": "Revocar",
@@ -302,7 +304,8 @@
302
304
  "confirmPassword": "Confirmar contraseña"
303
305
  },
304
306
  "errors": {
305
- "signOutFailed": "Hubo un problema al cerrar sesión. Inténtalo de nuevo."
307
+ "signOutFailed": "Hubo un problema al cerrar sesión. Inténtalo de nuevo.",
308
+ "signOutAllFailed": "Hubo un problema al cerrar sesión en todas las cuentas. Inténtalo de nuevo."
306
309
  },
307
310
  "confirms": {
308
311
  "signOut": "¿Seguro que quieres cerrar sesión?",
@@ -1495,5 +1498,16 @@
1495
1498
  "revoked": "Acceso revocado para {{name}}",
1496
1499
  "revokeFailed": "No se ha podido revocar el acceso"
1497
1500
  }
1501
+ },
1502
+ "accountMenu": {
1503
+ "label": "Menú de cuenta",
1504
+ "manage": "Gestiona tu cuenta de Oxy",
1505
+ "addAnother": "Añadir otra cuenta",
1506
+ "signOutAll": "Cerrar sesión en todas las cuentas",
1507
+ "open": "Menú de cuenta",
1508
+ "openHint": "Abre el menú de cuenta",
1509
+ "openWithUser": "Menú de cuenta de {{name}}",
1510
+ "switching": "Cambiando de cuenta…",
1511
+ "signOutAccount": "Cerrar sesión de {{name}}"
1498
1512
  }
1499
1513
  }