@oxyhq/core 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/AuthManager.js +35 -10
- package/dist/cjs/CrossDomainAuth.js +2 -2
- package/dist/cjs/HttpService.js +40 -24
- package/dist/cjs/OxyServices.base.js +16 -3
- package/dist/cjs/crypto/keyManager.js +29 -24
- package/dist/cjs/crypto/polyfill.js +6 -1
- package/dist/cjs/crypto/signatureService.js +40 -31
- package/dist/cjs/i18n/index.js +36 -45
- package/dist/cjs/i18n/locales/ar-SA.json +114 -115
- package/dist/cjs/i18n/locales/ca-ES.json +114 -115
- package/dist/cjs/i18n/locales/de-DE.json +114 -115
- package/dist/cjs/i18n/locales/en-US.json +936 -936
- package/dist/cjs/i18n/locales/es-ES.json +924 -924
- package/dist/cjs/i18n/locales/fr-FR.json +114 -115
- package/dist/cjs/i18n/locales/it-IT.json +114 -115
- package/dist/cjs/i18n/locales/ja-JP.json +1 -1
- package/dist/cjs/i18n/locales/ko-KR.json +114 -115
- package/dist/cjs/i18n/locales/locales/ar-SA.json +120 -0
- package/dist/cjs/i18n/locales/locales/ca-ES.json +120 -0
- package/dist/cjs/i18n/locales/locales/de-DE.json +120 -0
- package/dist/cjs/i18n/locales/locales/en-US.json +956 -0
- package/dist/cjs/i18n/locales/locales/es-ES.json +944 -0
- package/dist/cjs/i18n/locales/locales/fr-FR.json +120 -0
- package/dist/cjs/i18n/locales/locales/it-IT.json +120 -0
- package/dist/cjs/i18n/locales/locales/ja-JP.json +119 -0
- package/dist/cjs/i18n/locales/locales/ko-KR.json +120 -0
- package/dist/cjs/i18n/locales/locales/pt-PT.json +120 -0
- package/dist/cjs/i18n/locales/locales/zh-CN.json +120 -0
- package/dist/cjs/i18n/locales/pt-PT.json +114 -115
- package/dist/cjs/i18n/locales/zh-CN.json +114 -115
- package/dist/cjs/mixins/OxyServices.fedcm.js +21 -45
- package/dist/cjs/mixins/OxyServices.language.js +5 -2
- package/dist/cjs/mixins/OxyServices.popup.js +16 -6
- package/dist/cjs/mixins/OxyServices.privacy.js +2 -1
- package/dist/cjs/mixins/OxyServices.redirect.js +16 -6
- package/dist/cjs/mixins/OxyServices.security.js +3 -2
- package/dist/cjs/shared/utils/debugUtils.js +8 -1
- package/dist/cjs/utils/deviceManager.js +4 -6
- package/dist/cjs/utils/platform.js +3 -2
- package/dist/esm/AuthManager.js +35 -10
- package/dist/esm/CrossDomainAuth.js +2 -2
- package/dist/esm/HttpService.js +40 -24
- package/dist/esm/OxyServices.base.js +16 -3
- package/dist/esm/crypto/keyManager.js +29 -24
- package/dist/esm/crypto/polyfill.js +6 -1
- package/dist/esm/crypto/signatureService.js +40 -31
- package/dist/esm/i18n/index.js +11 -23
- package/dist/esm/i18n/locales/ar-SA.json +114 -115
- package/dist/esm/i18n/locales/ca-ES.json +114 -115
- package/dist/esm/i18n/locales/de-DE.json +114 -115
- package/dist/esm/i18n/locales/en-US.json +936 -936
- package/dist/esm/i18n/locales/es-ES.json +924 -924
- package/dist/esm/i18n/locales/fr-FR.json +114 -115
- package/dist/esm/i18n/locales/it-IT.json +114 -115
- package/dist/esm/i18n/locales/ja-JP.json +1 -1
- package/dist/esm/i18n/locales/ko-KR.json +114 -115
- package/dist/esm/i18n/locales/locales/ar-SA.json +120 -0
- package/dist/esm/i18n/locales/locales/ca-ES.json +120 -0
- package/dist/esm/i18n/locales/locales/de-DE.json +120 -0
- package/dist/esm/i18n/locales/locales/en-US.json +956 -0
- package/dist/esm/i18n/locales/locales/es-ES.json +944 -0
- package/dist/esm/i18n/locales/locales/fr-FR.json +120 -0
- package/dist/esm/i18n/locales/locales/it-IT.json +120 -0
- package/dist/esm/i18n/locales/locales/ja-JP.json +119 -0
- package/dist/esm/i18n/locales/locales/ko-KR.json +120 -0
- package/dist/esm/i18n/locales/locales/pt-PT.json +120 -0
- package/dist/esm/i18n/locales/locales/zh-CN.json +120 -0
- package/dist/esm/i18n/locales/pt-PT.json +114 -115
- package/dist/esm/i18n/locales/zh-CN.json +114 -115
- package/dist/esm/mixins/OxyServices.fedcm.js +21 -45
- package/dist/esm/mixins/OxyServices.language.js +5 -2
- package/dist/esm/mixins/OxyServices.popup.js +16 -6
- package/dist/esm/mixins/OxyServices.privacy.js +2 -1
- package/dist/esm/mixins/OxyServices.redirect.js +16 -6
- package/dist/esm/mixins/OxyServices.security.js +3 -2
- package/dist/esm/shared/utils/debugUtils.js +8 -1
- package/dist/esm/utils/deviceManager.js +4 -6
- package/dist/esm/utils/platform.js +3 -2
- package/dist/types/AuthManager.d.ts +4 -1
- package/dist/types/CrossDomainAuth.d.ts +2 -2
- package/dist/types/HttpService.d.ts +2 -0
- package/dist/types/OxyServices.base.d.ts +4 -1
- package/dist/types/OxyServices.d.ts +13 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/mixins/OxyServices.analytics.d.ts +2 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +2 -0
- package/dist/types/mixins/OxyServices.auth.d.ts +2 -0
- package/dist/types/mixins/OxyServices.developer.d.ts +2 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +2 -0
- package/dist/types/mixins/OxyServices.features.d.ts +2 -0
- package/dist/types/mixins/OxyServices.fedcm.d.ts +4 -2
- package/dist/types/mixins/OxyServices.karma.d.ts +2 -0
- package/dist/types/mixins/OxyServices.language.d.ts +2 -0
- package/dist/types/mixins/OxyServices.location.d.ts +2 -0
- package/dist/types/mixins/OxyServices.payment.d.ts +2 -0
- package/dist/types/mixins/OxyServices.popup.d.ts +2 -0
- package/dist/types/mixins/OxyServices.privacy.d.ts +2 -0
- package/dist/types/mixins/OxyServices.redirect.d.ts +2 -0
- package/dist/types/mixins/OxyServices.security.d.ts +2 -0
- package/dist/types/mixins/OxyServices.user.d.ts +2 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +2 -0
- package/package.json +1 -2
- package/src/AuthManager.ts +42 -16
- package/src/CrossDomainAuth.ts +2 -2
- package/src/HttpService.ts +40 -26
- package/src/OxyServices.base.ts +21 -4
- package/src/OxyServices.ts +23 -2
- package/src/crypto/keyManager.ts +30 -25
- package/src/crypto/polyfill.ts +6 -1
- package/src/crypto/signatureService.ts +43 -37
- package/src/i18n/index.ts +33 -45
- package/src/index.ts +3 -0
- package/src/mixins/OxyServices.fedcm.ts +22 -48
- package/src/mixins/OxyServices.language.ts +6 -3
- package/src/mixins/OxyServices.popup.ts +16 -6
- package/src/mixins/OxyServices.privacy.ts +2 -1
- package/src/mixins/OxyServices.redirect.ts +16 -6
- package/src/mixins/OxyServices.security.ts +3 -2
- package/src/shared/utils/__tests__/debugUtils.test.ts +55 -0
- package/src/shared/utils/debugUtils.ts +6 -1
- package/src/utils/deviceManager.ts +5 -6
- package/src/utils/platform.ts +3 -2
package/src/AuthManager.ts
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
import type { OxyServices } from './OxyServices';
|
|
11
11
|
import type { HttpService } from './HttpService';
|
|
12
12
|
import type { SessionLoginResponse, MinimalUserData } from './models/session';
|
|
13
|
+
import { retryAsync } from './utils/asyncUtils';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
* OxyServices
|
|
16
|
+
* OxyServices already declares revokeFedCMCredential via mixin type augmentation.
|
|
17
|
+
* This alias is kept for readability in the signOut method.
|
|
16
18
|
*/
|
|
17
|
-
|
|
18
|
-
revokeFedCMCredential?: () => Promise<void>;
|
|
19
|
-
}
|
|
19
|
+
type OxyServicesWithFedCM = OxyServices;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Storage adapter interface for platform-agnostic storage.
|
|
@@ -142,6 +142,7 @@ export class AuthManager {
|
|
|
142
142
|
private listeners: Set<AuthStateChangeCallback> = new Set();
|
|
143
143
|
private currentUser: MinimalUserData | null = null;
|
|
144
144
|
private refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
145
|
+
private refreshPromise: Promise<boolean> | null = null;
|
|
145
146
|
private config: Required<AuthManagerConfig>;
|
|
146
147
|
|
|
147
148
|
constructor(oxyServices: OxyServices, config: AuthManagerConfig = {}) {
|
|
@@ -261,28 +262,53 @@ export class AuthManager {
|
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
/**
|
|
264
|
-
* Refresh the access token.
|
|
265
|
+
* Refresh the access token. Deduplicates concurrent calls so only one
|
|
266
|
+
* refresh request is in-flight at a time.
|
|
265
267
|
*/
|
|
266
268
|
async refreshToken(): Promise<boolean> {
|
|
269
|
+
// If a refresh is already in-flight, return the same promise
|
|
270
|
+
if (this.refreshPromise) {
|
|
271
|
+
return this.refreshPromise;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.refreshPromise = this._doRefreshToken();
|
|
275
|
+
try {
|
|
276
|
+
return await this.refreshPromise;
|
|
277
|
+
} finally {
|
|
278
|
+
this.refreshPromise = null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private async _doRefreshToken(): Promise<boolean> {
|
|
267
283
|
const refreshToken = await this.storage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
268
284
|
if (!refreshToken) {
|
|
269
285
|
return false;
|
|
270
286
|
}
|
|
271
287
|
|
|
272
288
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
289
|
+
await retryAsync(
|
|
290
|
+
async () => {
|
|
291
|
+
const httpService = this.oxyServices.httpService as HttpService;
|
|
292
|
+
const response = await httpService.request<SessionLoginResponse>({
|
|
293
|
+
method: 'POST',
|
|
294
|
+
url: '/api/auth/refresh',
|
|
295
|
+
data: { refreshToken },
|
|
296
|
+
cache: false,
|
|
297
|
+
});
|
|
298
|
+
await this.handleAuthSuccess(response, 'credentials');
|
|
299
|
+
},
|
|
300
|
+
2, // 2 retries = 3 total attempts
|
|
301
|
+
1000, // 1s base delay with exponential backoff + jitter
|
|
302
|
+
(error: any) => {
|
|
303
|
+
// Don't retry on 4xx client errors (invalid/revoked token)
|
|
304
|
+
const status = error?.status ?? error?.response?.status;
|
|
305
|
+
if (status && status >= 400 && status < 500) return false;
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
);
|
|
283
309
|
return true;
|
|
284
310
|
} catch {
|
|
285
|
-
//
|
|
311
|
+
// All retry attempts exhausted, clear session
|
|
286
312
|
await this.clearSession();
|
|
287
313
|
this.currentUser = null;
|
|
288
314
|
this.notifyListeners();
|
package/src/CrossDomainAuth.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
12
|
* ```typescript
|
|
13
|
-
* import { CrossDomainAuth } from '@oxyhq/
|
|
13
|
+
* import { CrossDomainAuth } from '@oxyhq/core';
|
|
14
14
|
*
|
|
15
15
|
* const auth = new CrossDomainAuth(oxyServices);
|
|
16
16
|
*
|
|
@@ -287,7 +287,7 @@ export class CrossDomainAuth {
|
|
|
287
287
|
*
|
|
288
288
|
* @example
|
|
289
289
|
* ```typescript
|
|
290
|
-
* import { createCrossDomainAuth } from '@oxyhq/
|
|
290
|
+
* import { createCrossDomainAuth } from '@oxyhq/core';
|
|
291
291
|
*
|
|
292
292
|
* const oxyServices = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
293
293
|
* const auth = createCrossDomainAuth(oxyServices);
|
package/src/HttpService.ts
CHANGED
|
@@ -17,6 +17,7 @@ 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 { isDev } from './shared/utils/debugUtils';
|
|
20
21
|
import { jwtDecode } from 'jwt-decode';
|
|
21
22
|
import { isNative, getPlatformOS } from './utils/platform';
|
|
22
23
|
import type { OxyConfig } from './models/interfaces';
|
|
@@ -130,6 +131,7 @@ export class HttpService {
|
|
|
130
131
|
private requestQueue: RequestQueue;
|
|
131
132
|
private logger: SimpleLogger;
|
|
132
133
|
private config: OxyConfig;
|
|
134
|
+
private tokenRefreshPromise: Promise<string | null> | null = null;
|
|
133
135
|
|
|
134
136
|
// Performance monitoring
|
|
135
137
|
private requestMetrics = {
|
|
@@ -279,7 +281,7 @@ export class HttpService {
|
|
|
279
281
|
}
|
|
280
282
|
|
|
281
283
|
// Debug logging for CSRF issues
|
|
282
|
-
if (isStateChangingMethod &&
|
|
284
|
+
if (isStateChangingMethod && isDev()) {
|
|
283
285
|
console.log('[HttpService] CSRF Debug:', {
|
|
284
286
|
url,
|
|
285
287
|
method,
|
|
@@ -484,20 +486,20 @@ export class HttpService {
|
|
|
484
486
|
// Return cached token if available
|
|
485
487
|
const cachedToken = this.tokenStore.getCsrfToken();
|
|
486
488
|
if (cachedToken) {
|
|
487
|
-
if (
|
|
489
|
+
if (isDev()) console.log('[HttpService] Using cached CSRF token');
|
|
488
490
|
return cachedToken;
|
|
489
491
|
}
|
|
490
492
|
|
|
491
493
|
// Deduplicate concurrent CSRF token fetches
|
|
492
494
|
const existingPromise = this.tokenStore.getCsrfTokenFetchPromise();
|
|
493
495
|
if (existingPromise) {
|
|
494
|
-
if (
|
|
496
|
+
if (isDev()) console.log('[HttpService] Waiting for existing CSRF fetch');
|
|
495
497
|
return existingPromise;
|
|
496
498
|
}
|
|
497
499
|
|
|
498
500
|
const fetchPromise = (async () => {
|
|
499
501
|
try {
|
|
500
|
-
if (
|
|
502
|
+
if (isDev()) console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/api/csrf-token`);
|
|
501
503
|
|
|
502
504
|
// Use AbortController for timeout (more compatible than AbortSignal.timeout)
|
|
503
505
|
const controller = new AbortController();
|
|
@@ -512,11 +514,11 @@ export class HttpService {
|
|
|
512
514
|
|
|
513
515
|
clearTimeout(timeoutId);
|
|
514
516
|
|
|
515
|
-
if (
|
|
517
|
+
if (isDev()) console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
|
|
516
518
|
|
|
517
519
|
if (response.ok) {
|
|
518
520
|
const data = await response.json() as { csrfToken?: string };
|
|
519
|
-
if (
|
|
521
|
+
if (isDev()) console.log('[HttpService] CSRF response data:', data);
|
|
520
522
|
const token = data.csrfToken || null;
|
|
521
523
|
this.tokenStore.setCsrfToken(token);
|
|
522
524
|
this.logger.debug('CSRF token fetched');
|
|
@@ -531,11 +533,11 @@ export class HttpService {
|
|
|
531
533
|
return headerToken;
|
|
532
534
|
}
|
|
533
535
|
|
|
534
|
-
if (
|
|
536
|
+
if (isDev()) console.log('[HttpService] CSRF fetch failed with status:', response.status);
|
|
535
537
|
this.logger.warn('Failed to fetch CSRF token:', response.status);
|
|
536
538
|
return null;
|
|
537
539
|
} catch (error) {
|
|
538
|
-
if (
|
|
540
|
+
if (isDev()) console.log('[HttpService] CSRF fetch error:', error);
|
|
539
541
|
this.logger.warn('CSRF token fetch error:', error);
|
|
540
542
|
return null;
|
|
541
543
|
} finally {
|
|
@@ -562,25 +564,15 @@ export class HttpService {
|
|
|
562
564
|
|
|
563
565
|
// If token expires in less than 60 seconds, refresh it
|
|
564
566
|
if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
|
|
567
|
+
// Deduplicate concurrent refresh attempts
|
|
568
|
+
if (!this.tokenRefreshPromise) {
|
|
569
|
+
this.tokenRefreshPromise = this._refreshTokenFromSession(decoded.sessionId);
|
|
570
|
+
}
|
|
565
571
|
try {
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
method: 'GET',
|
|
571
|
-
headers: { 'Accept': 'application/json' },
|
|
572
|
-
signal: AbortSignal.timeout(5000),
|
|
573
|
-
credentials: 'include', // Include cookies for cross-origin requests
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
if (response.ok) {
|
|
577
|
-
const { accessToken: newToken } = await response.json();
|
|
578
|
-
this.tokenStore.setTokens(newToken);
|
|
579
|
-
this.logger.debug('Token refreshed');
|
|
580
|
-
return `Bearer ${newToken}`;
|
|
581
|
-
}
|
|
582
|
-
} catch (refreshError) {
|
|
583
|
-
this.logger.warn('Token refresh failed, using current token');
|
|
572
|
+
const result = await this.tokenRefreshPromise;
|
|
573
|
+
if (result) return result;
|
|
574
|
+
} finally {
|
|
575
|
+
this.tokenRefreshPromise = null;
|
|
584
576
|
}
|
|
585
577
|
}
|
|
586
578
|
|
|
@@ -591,6 +583,28 @@ export class HttpService {
|
|
|
591
583
|
}
|
|
592
584
|
}
|
|
593
585
|
|
|
586
|
+
private async _refreshTokenFromSession(sessionId: string): Promise<string | null> {
|
|
587
|
+
try {
|
|
588
|
+
const refreshUrl = `${this.baseURL}/api/session/token/${sessionId}`;
|
|
589
|
+
const response = await fetch(refreshUrl, {
|
|
590
|
+
method: 'GET',
|
|
591
|
+
headers: { 'Accept': 'application/json' },
|
|
592
|
+
signal: AbortSignal.timeout(5000),
|
|
593
|
+
credentials: 'include',
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
if (response.ok) {
|
|
597
|
+
const { accessToken: newToken } = await response.json();
|
|
598
|
+
this.tokenStore.setTokens(newToken);
|
|
599
|
+
this.logger.debug('Token refreshed');
|
|
600
|
+
return `Bearer ${newToken}`;
|
|
601
|
+
}
|
|
602
|
+
} catch (refreshError) {
|
|
603
|
+
this.logger.warn('Token refresh failed, using current token');
|
|
604
|
+
}
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
594
608
|
/**
|
|
595
609
|
* Unwrap standardized API response format
|
|
596
610
|
*/
|
package/src/OxyServices.base.ts
CHANGED
|
@@ -131,21 +131,38 @@ export class OxyServicesBase {
|
|
|
131
131
|
*/
|
|
132
132
|
public clearTokens(): void {
|
|
133
133
|
this.httpService.clearTokens();
|
|
134
|
+
this._cachedUserId = undefined;
|
|
135
|
+
this._cachedAccessToken = null;
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
/** @internal */ _cachedUserId: string | null | undefined = undefined;
|
|
139
|
+
/** @internal */ _cachedAccessToken: string | null = null;
|
|
140
|
+
|
|
136
141
|
/**
|
|
137
|
-
* Get the current user ID from the access token
|
|
142
|
+
* Get the current user ID from the access token.
|
|
143
|
+
* Caches the decoded value and invalidates when the token changes.
|
|
138
144
|
*/
|
|
139
145
|
public getCurrentUserId(): string | null {
|
|
140
146
|
const accessToken = this.httpService.getAccessToken();
|
|
147
|
+
|
|
148
|
+
// Return cached value if token hasn't changed
|
|
149
|
+
if (accessToken === this._cachedAccessToken && this._cachedUserId !== undefined) {
|
|
150
|
+
return this._cachedUserId;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this._cachedAccessToken = accessToken;
|
|
154
|
+
|
|
141
155
|
if (!accessToken) {
|
|
156
|
+
this._cachedUserId = null;
|
|
142
157
|
return null;
|
|
143
158
|
}
|
|
144
|
-
|
|
159
|
+
|
|
145
160
|
try {
|
|
146
161
|
const decoded = jwtDecode<JwtPayload>(accessToken);
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
this._cachedUserId = decoded.userId || decoded.id || null;
|
|
163
|
+
return this._cachedUserId;
|
|
164
|
+
} catch {
|
|
165
|
+
this._cachedUserId = null;
|
|
149
166
|
return null;
|
|
150
167
|
}
|
|
151
168
|
}
|
package/src/OxyServices.ts
CHANGED
|
@@ -58,6 +58,10 @@
|
|
|
58
58
|
*/
|
|
59
59
|
import { OxyServicesBase, type OxyConfig } from './OxyServices.base';
|
|
60
60
|
import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
|
|
61
|
+
import type { SessionLoginResponse } from './models/session';
|
|
62
|
+
import type { FedCMAuthOptions, FedCMConfig } from './mixins/OxyServices.fedcm';
|
|
63
|
+
import type { PopupAuthOptions } from './mixins/OxyServices.popup';
|
|
64
|
+
import type { RedirectAuthOptions } from './mixins/OxyServices.redirect';
|
|
61
65
|
|
|
62
66
|
// Import mixin composition helper
|
|
63
67
|
import { composeOxyServices } from './mixins';
|
|
@@ -106,8 +110,25 @@ export class OxyServices extends (OxyServicesComposed as any) {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
// Type augmentation to expose mixin methods to TypeScript
|
|
109
|
-
// This allows proper type checking while avoiding complex mixin type inference
|
|
110
|
-
|
|
113
|
+
// This allows proper type checking while avoiding complex mixin type inference.
|
|
114
|
+
// Explicit declarations are added for cross-domain auth methods that downstream
|
|
115
|
+
// packages (auth-sdk, services) need without casting to `any`.
|
|
116
|
+
export interface OxyServices extends InstanceType<ReturnType<typeof composeOxyServices>> {
|
|
117
|
+
// FedCM authentication
|
|
118
|
+
isFedCMSupported(): boolean;
|
|
119
|
+
signInWithFedCM(options?: FedCMAuthOptions): Promise<SessionLoginResponse>;
|
|
120
|
+
silentSignInWithFedCM(): Promise<SessionLoginResponse | null>;
|
|
121
|
+
revokeFedCMCredential(): Promise<void>;
|
|
122
|
+
getFedCMConfig(): FedCMConfig;
|
|
123
|
+
|
|
124
|
+
// Popup authentication
|
|
125
|
+
signInWithPopup(options?: PopupAuthOptions): Promise<SessionLoginResponse>;
|
|
126
|
+
signUpWithPopup(options?: PopupAuthOptions): Promise<SessionLoginResponse>;
|
|
127
|
+
|
|
128
|
+
// Redirect authentication
|
|
129
|
+
signInWithRedirect(options?: RedirectAuthOptions): void;
|
|
130
|
+
signUpWithRedirect(options?: RedirectAuthOptions): void;
|
|
131
|
+
}
|
|
111
132
|
|
|
112
133
|
// Re-export error classes for convenience
|
|
113
134
|
export { OxyAuthenticationError, OxyAuthenticationTimeoutError };
|
package/src/crypto/keyManager.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { ec as EC } from 'elliptic';
|
|
|
9
9
|
import type { ECKeyPair } from 'elliptic';
|
|
10
10
|
import { isWeb, isIOS, isAndroid } from '../utils/platform';
|
|
11
11
|
import { logger } from '../utils/loggerUtils';
|
|
12
|
+
import { isDev } from '../shared/utils/debugUtils';
|
|
12
13
|
|
|
13
14
|
// Lazy imports for React Native specific modules
|
|
14
15
|
let SecureStore: typeof import('expo-secure-store') | null = null;
|
|
@@ -49,7 +50,9 @@ const ANDROID_ACCOUNT_TYPE = 'com.oxy.account';
|
|
|
49
50
|
async function initSecureStore(): Promise<typeof import('expo-secure-store')> {
|
|
50
51
|
if (!SecureStore) {
|
|
51
52
|
try {
|
|
52
|
-
|
|
53
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
54
|
+
const moduleName = 'expo-secure-store';
|
|
55
|
+
SecureStore = await import(moduleName);
|
|
53
56
|
} catch (error) {
|
|
54
57
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
55
58
|
throw new Error(`Failed to load expo-secure-store: ${errorMessage}. Make sure expo-secure-store is installed and properly configured.`);
|
|
@@ -85,9 +88,11 @@ function isWebPlatform(): boolean {
|
|
|
85
88
|
|
|
86
89
|
async function initExpoCrypto(): Promise<typeof import('expo-crypto')> {
|
|
87
90
|
if (!ExpoCrypto) {
|
|
88
|
-
|
|
91
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
92
|
+
const moduleName = 'expo-crypto';
|
|
93
|
+
ExpoCrypto = await import(moduleName);
|
|
89
94
|
}
|
|
90
|
-
return ExpoCrypto
|
|
95
|
+
return ExpoCrypto!;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
/**
|
|
@@ -238,7 +243,7 @@ export class KeyManager {
|
|
|
238
243
|
KeyManager.cachedSharedPublicKey = publicKey;
|
|
239
244
|
KeyManager.cachedHasSharedIdentity = true;
|
|
240
245
|
|
|
241
|
-
if (
|
|
246
|
+
if (isDev()) {
|
|
242
247
|
logger.debug('Shared identity created successfully', { component: 'KeyManager' });
|
|
243
248
|
}
|
|
244
249
|
|
|
@@ -277,7 +282,7 @@ export class KeyManager {
|
|
|
277
282
|
|
|
278
283
|
return publicKey;
|
|
279
284
|
} catch (error) {
|
|
280
|
-
if (
|
|
285
|
+
if (isDev()) {
|
|
281
286
|
logger.warn('Failed to get shared public key', { component: 'KeyManager' }, error);
|
|
282
287
|
}
|
|
283
288
|
KeyManager.cachedSharedPublicKey = null;
|
|
@@ -312,7 +317,7 @@ export class KeyManager {
|
|
|
312
317
|
|
|
313
318
|
return privateKey;
|
|
314
319
|
} catch (error) {
|
|
315
|
-
if (
|
|
320
|
+
if (isDev()) {
|
|
316
321
|
logger.warn('Failed to get shared private key', { component: 'KeyManager' }, error);
|
|
317
322
|
}
|
|
318
323
|
return null;
|
|
@@ -343,7 +348,7 @@ export class KeyManager {
|
|
|
343
348
|
|
|
344
349
|
return hasShared;
|
|
345
350
|
} catch (error) {
|
|
346
|
-
if (
|
|
351
|
+
if (isDev()) {
|
|
347
352
|
logger.warn('Failed to check shared identity', { component: 'KeyManager' }, error);
|
|
348
353
|
}
|
|
349
354
|
KeyManager.cachedHasSharedIdentity = false;
|
|
@@ -392,7 +397,7 @@ export class KeyManager {
|
|
|
392
397
|
KeyManager.cachedSharedPublicKey = publicKey;
|
|
393
398
|
KeyManager.cachedHasSharedIdentity = true;
|
|
394
399
|
|
|
395
|
-
if (
|
|
400
|
+
if (isDev()) {
|
|
396
401
|
logger.debug('Shared identity imported successfully', { component: 'KeyManager' });
|
|
397
402
|
}
|
|
398
403
|
|
|
@@ -431,11 +436,11 @@ export class KeyManager {
|
|
|
431
436
|
await store.setItemAsync(STORAGE_KEYS.SHARED_SESSION_TOKEN, accessToken);
|
|
432
437
|
}
|
|
433
438
|
|
|
434
|
-
if (
|
|
439
|
+
if (isDev()) {
|
|
435
440
|
logger.debug('Shared session stored successfully', { component: 'KeyManager' });
|
|
436
441
|
}
|
|
437
442
|
} catch (error) {
|
|
438
|
-
if (
|
|
443
|
+
if (isDev()) {
|
|
439
444
|
logger.error('Failed to store shared session', error, { component: 'KeyManager' });
|
|
440
445
|
}
|
|
441
446
|
throw error;
|
|
@@ -479,7 +484,7 @@ export class KeyManager {
|
|
|
479
484
|
|
|
480
485
|
return { sessionId, accessToken };
|
|
481
486
|
} catch (error) {
|
|
482
|
-
if (
|
|
487
|
+
if (isDev()) {
|
|
483
488
|
logger.warn('Failed to get shared session', { component: 'KeyManager' }, error);
|
|
484
489
|
}
|
|
485
490
|
return null;
|
|
@@ -512,11 +517,11 @@ export class KeyManager {
|
|
|
512
517
|
await store.deleteItemAsync(STORAGE_KEYS.SHARED_SESSION_TOKEN);
|
|
513
518
|
}
|
|
514
519
|
|
|
515
|
-
if (
|
|
520
|
+
if (isDev()) {
|
|
516
521
|
logger.debug('Shared session cleared successfully', { component: 'KeyManager' });
|
|
517
522
|
}
|
|
518
523
|
} catch (error) {
|
|
519
|
-
if (
|
|
524
|
+
if (isDev()) {
|
|
520
525
|
logger.error('Failed to clear shared session', error, { component: 'KeyManager' });
|
|
521
526
|
}
|
|
522
527
|
}
|
|
@@ -540,7 +545,7 @@ export class KeyManager {
|
|
|
540
545
|
// Check if we already have a shared identity
|
|
541
546
|
const hasShared = await KeyManager.hasSharedIdentity();
|
|
542
547
|
if (hasShared) {
|
|
543
|
-
if (
|
|
548
|
+
if (isDev()) {
|
|
544
549
|
logger.debug('Shared identity already exists, skipping migration', { component: 'KeyManager' });
|
|
545
550
|
}
|
|
546
551
|
return true;
|
|
@@ -549,7 +554,7 @@ export class KeyManager {
|
|
|
549
554
|
// Get local identity
|
|
550
555
|
const privateKey = await KeyManager.getPrivateKey();
|
|
551
556
|
if (!privateKey) {
|
|
552
|
-
if (
|
|
557
|
+
if (isDev()) {
|
|
553
558
|
logger.debug('No local identity to migrate', { component: 'KeyManager' });
|
|
554
559
|
}
|
|
555
560
|
return false;
|
|
@@ -558,13 +563,13 @@ export class KeyManager {
|
|
|
558
563
|
// Import to shared storage
|
|
559
564
|
await KeyManager.importSharedIdentity(privateKey);
|
|
560
565
|
|
|
561
|
-
if (
|
|
566
|
+
if (isDev()) {
|
|
562
567
|
logger.debug('Successfully migrated local identity to shared identity', { component: 'KeyManager' });
|
|
563
568
|
}
|
|
564
569
|
|
|
565
570
|
return true;
|
|
566
571
|
} catch (error) {
|
|
567
|
-
if (
|
|
572
|
+
if (isDev()) {
|
|
568
573
|
logger.error('Failed to migrate to shared identity', error, { component: 'KeyManager' });
|
|
569
574
|
}
|
|
570
575
|
return false;
|
|
@@ -635,7 +640,7 @@ export class KeyManager {
|
|
|
635
640
|
} catch (error) {
|
|
636
641
|
// If secure store is not available, return null (no identity)
|
|
637
642
|
// This allows the app to continue functioning even if secure store fails to load
|
|
638
|
-
if (
|
|
643
|
+
if (isDev()) {
|
|
639
644
|
logger.warn('Failed to access secure store', { component: 'KeyManager' }, error);
|
|
640
645
|
}
|
|
641
646
|
return null;
|
|
@@ -665,7 +670,7 @@ export class KeyManager {
|
|
|
665
670
|
// If secure store is not available, return null (no identity)
|
|
666
671
|
// Cache null to avoid repeated failed attempts
|
|
667
672
|
KeyManager.cachedPublicKey = null;
|
|
668
|
-
if (
|
|
673
|
+
if (isDev()) {
|
|
669
674
|
logger.warn('Failed to access secure store', { component: 'KeyManager' }, error);
|
|
670
675
|
}
|
|
671
676
|
return null;
|
|
@@ -695,7 +700,7 @@ export class KeyManager {
|
|
|
695
700
|
// If we can't check, assume no identity (safer default)
|
|
696
701
|
// Cache false to avoid repeated failed attempts
|
|
697
702
|
KeyManager.cachedHasIdentity = false;
|
|
698
|
-
if (
|
|
703
|
+
if (isDev()) {
|
|
699
704
|
logger.warn('Failed to check identity', { component: 'KeyManager' }, error);
|
|
700
705
|
}
|
|
701
706
|
return false;
|
|
@@ -736,11 +741,11 @@ export class KeyManager {
|
|
|
736
741
|
if (!skipBackup) {
|
|
737
742
|
try {
|
|
738
743
|
const backupSuccess = await KeyManager.backupIdentity();
|
|
739
|
-
if (!backupSuccess &&
|
|
744
|
+
if (!backupSuccess && isDev()) {
|
|
740
745
|
logger.warn('Failed to backup identity before deletion - proceeding anyway', { component: 'KeyManager' });
|
|
741
746
|
}
|
|
742
747
|
} catch (backupError) {
|
|
743
|
-
if (
|
|
748
|
+
if (isDev()) {
|
|
744
749
|
logger.warn('Failed to backup identity before deletion', { component: 'KeyManager' }, backupError);
|
|
745
750
|
}
|
|
746
751
|
}
|
|
@@ -790,7 +795,7 @@ export class KeyManager {
|
|
|
790
795
|
|
|
791
796
|
return true;
|
|
792
797
|
} catch (error) {
|
|
793
|
-
if (
|
|
798
|
+
if (isDev()) {
|
|
794
799
|
logger.error('Failed to backup identity', error, { component: 'KeyManager' });
|
|
795
800
|
}
|
|
796
801
|
return false;
|
|
@@ -836,7 +841,7 @@ export class KeyManager {
|
|
|
836
841
|
|
|
837
842
|
return true;
|
|
838
843
|
} catch (error) {
|
|
839
|
-
if (
|
|
844
|
+
if (isDev()) {
|
|
840
845
|
logger.error('Identity integrity check failed', error, { component: 'KeyManager' });
|
|
841
846
|
}
|
|
842
847
|
return false;
|
|
@@ -893,7 +898,7 @@ export class KeyManager {
|
|
|
893
898
|
|
|
894
899
|
return false;
|
|
895
900
|
} catch (error) {
|
|
896
|
-
if (
|
|
901
|
+
if (isDev()) {
|
|
897
902
|
logger.error('Failed to restore identity from backup', error, { component: 'KeyManager' });
|
|
898
903
|
}
|
|
899
904
|
return false;
|
package/src/crypto/polyfill.ts
CHANGED
|
@@ -37,7 +37,12 @@ function getRandomBytesSync(byteCount: number): Uint8Array {
|
|
|
37
37
|
if (!expoCryptoLoadAttempted) {
|
|
38
38
|
expoCryptoLoadAttempted = true;
|
|
39
39
|
try {
|
|
40
|
-
|
|
40
|
+
// Only use require() in CJS environments (Metro/Node). In ESM (Vite/browser),
|
|
41
|
+
// crypto.getRandomValues exists natively so this code path is never reached.
|
|
42
|
+
if (typeof require !== 'undefined') {
|
|
43
|
+
const moduleName = 'expo-crypto';
|
|
44
|
+
expoCryptoModule = require(moduleName);
|
|
45
|
+
}
|
|
41
46
|
} catch {
|
|
42
47
|
// expo-crypto not available — expected in non-RN environments
|
|
43
48
|
}
|