From 865f43cf8dde7ef199dd3621e5bb3305aa225054 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 17 Dec 2025 15:31:25 +0530 Subject: [PATCH 1/3] feat: export DPoP error code constants and add tests for error code constants --- README.md | 70 ++++- src/core/models/DPoPError.ts | 41 ++- src/core/models/__tests__/ErrorCodes.spec.ts | 261 +++++++++++++++++++ src/core/models/index.ts | 9 +- src/index.ts | 3 + 5 files changed, 367 insertions(+), 17 deletions(-) create mode 100644 src/core/models/__tests__/ErrorCodes.spec.ts diff --git a/README.md b/README.md index 07c0ba14..ca37f896 100644 --- a/README.md +++ b/README.md @@ -640,13 +640,51 @@ try { **Platform agnostic errors:** -You can access the platform agnostic generic error codes as below : +You can access the platform agnostic generic error codes as below: ```js try { const credentials = await auth0.credentialsManager.getCredentials(); } catch (error) { - console.log(e.type); + console.log(error.type); +} +``` + +**Using Error Code Constants (Recommended)** + +For better type safety and autocompletion, you can use the exported error code constants: + +```js +import { + CredentialsManagerError, + CredentialsManagerErrorCodes, +} from 'react-native-auth0'; + +try { + const credentials = await auth0.credentialsManager.getCredentials(); +} catch (error) { + if (error instanceof CredentialsManagerError) { + switch (error.type) { + case CredentialsManagerErrorCodes.NO_CREDENTIALS: + console.log('No credentials stored. User needs to log in.'); + break; + case CredentialsManagerErrorCodes.NO_REFRESH_TOKEN: + console.log( + 'No refresh token available. Request offline_access scope during login.' + ); + break; + case CredentialsManagerErrorCodes.RENEW_FAILED: + console.log( + 'Failed to refresh credentials. Re-authentication may be required.' + ); + break; + case CredentialsManagerErrorCodes.BIOMETRICS_FAILED: + console.log('Biometric authentication failed.'); + break; + default: + console.error('Credentials error:', error.message); + } + } } ``` @@ -707,6 +745,34 @@ try { } ``` +**Using Error Code Constants (Recommended)** + +For better type safety and autocompletion, you can use the exported error code constants: + +```javascript +import { WebAuthError, WebAuthErrorCodes } from 'react-native-auth0'; + +try { + await auth0.webAuth.authorize(); +} catch (e) { + if (e instanceof WebAuthError) { + switch (e.type) { + case WebAuthErrorCodes.USER_CANCELLED: + console.log('User cancelled the login.'); + break; + case WebAuthErrorCodes.NETWORK_ERROR: + console.log('Network error occurred. Please check your connection.'); + break; + case WebAuthErrorCodes.BROWSER_NOT_AVAILABLE: + console.log('No browser available on this device.'); + break; + default: + console.error('Authentication error:', e.message); + } + } +} +``` + | Platform-Agnostic | Description | Android Native Error | iOS Native Error | Web Error Code | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------- | ------------------------------------ | | `USER_CANCELLED` | The user actively cancelled the web authentication flow. | `a0.session.user_cancelled` | `USER_CANCELLED` | `cancelled` | diff --git a/src/core/models/DPoPError.ts b/src/core/models/DPoPError.ts index 9f664d14..2cd34451 100644 --- a/src/core/models/DPoPError.ts +++ b/src/core/models/DPoPError.ts @@ -1,7 +1,9 @@ import { AuthError } from './AuthError'; -const ERROR_CODE_MAP: Record = { - // --- DPoP-specific error codes --- +/** + * Public constants exposing all possible DPoP error codes. + */ +export const DPoPErrorCodes = { DPOP_GENERATION_FAILED: 'DPOP_GENERATION_FAILED', DPOP_PROOF_FAILED: 'DPOP_PROOF_FAILED', DPOP_KEY_GENERATION_FAILED: 'DPOP_KEY_GENERATION_FAILED', @@ -11,24 +13,38 @@ const ERROR_CODE_MAP: Record = { DPOP_INVALID_TOKEN_TYPE: 'DPOP_INVALID_TOKEN_TYPE', DPOP_MISSING_PARAMETER: 'DPOP_MISSING_PARAMETER', DPOP_CLEAR_KEY_FAILED: 'DPOP_CLEAR_KEY_FAILED', + UNKNOWN_DPOP_ERROR: 'UNKNOWN_DPOP_ERROR', +} as const; + +const ERROR_CODE_MAP: Record = { + // --- DPoP-specific error codes --- + DPOP_GENERATION_FAILED: DPoPErrorCodes.DPOP_GENERATION_FAILED, + DPOP_PROOF_FAILED: DPoPErrorCodes.DPOP_PROOF_FAILED, + DPOP_KEY_GENERATION_FAILED: DPoPErrorCodes.DPOP_KEY_GENERATION_FAILED, + DPOP_KEY_STORAGE_FAILED: DPoPErrorCodes.DPOP_KEY_STORAGE_FAILED, + DPOP_KEY_RETRIEVAL_FAILED: DPoPErrorCodes.DPOP_KEY_RETRIEVAL_FAILED, + DPOP_NONCE_MISMATCH: DPoPErrorCodes.DPOP_NONCE_MISMATCH, + DPOP_INVALID_TOKEN_TYPE: DPoPErrorCodes.DPOP_INVALID_TOKEN_TYPE, + DPOP_MISSING_PARAMETER: DPoPErrorCodes.DPOP_MISSING_PARAMETER, + DPOP_CLEAR_KEY_FAILED: DPoPErrorCodes.DPOP_CLEAR_KEY_FAILED, // --- Native platform mappings --- // iOS - DPOP_KEY_NOT_FOUND: 'DPOP_KEY_RETRIEVAL_FAILED', - DPOP_KEYCHAIN_ERROR: 'DPOP_KEY_STORAGE_FAILED', + DPOP_KEY_NOT_FOUND: DPoPErrorCodes.DPOP_KEY_RETRIEVAL_FAILED, + DPOP_KEYCHAIN_ERROR: DPoPErrorCodes.DPOP_KEY_STORAGE_FAILED, // Android - DPOP_KEYSTORE_ERROR: 'DPOP_KEY_STORAGE_FAILED', - DPOP_CRYPTO_ERROR: 'DPOP_KEY_GENERATION_FAILED', + DPOP_KEYSTORE_ERROR: DPoPErrorCodes.DPOP_KEY_STORAGE_FAILED, + DPOP_CRYPTO_ERROR: DPoPErrorCodes.DPOP_KEY_GENERATION_FAILED, // Web - dpop_generation_failed: 'DPOP_GENERATION_FAILED', - dpop_proof_failed: 'DPOP_PROOF_FAILED', - dpop_key_error: 'DPOP_KEY_GENERATION_FAILED', + dpop_generation_failed: DPoPErrorCodes.DPOP_GENERATION_FAILED, + dpop_proof_failed: DPoPErrorCodes.DPOP_PROOF_FAILED, + dpop_key_error: DPoPErrorCodes.DPOP_KEY_GENERATION_FAILED, // --- Generic fallback --- - UNKNOWN: 'UNKNOWN_DPOP_ERROR', - OTHER: 'UNKNOWN_DPOP_ERROR', + UNKNOWN: DPoPErrorCodes.UNKNOWN_DPOP_ERROR, + OTHER: DPoPErrorCodes.UNKNOWN_DPOP_ERROR, }; /** @@ -86,6 +102,7 @@ export class DPoPError extends AuthError { }); // Map the original error code to a normalized type - this.type = ERROR_CODE_MAP[originalError.code] || 'UNKNOWN_DPOP_ERROR'; + this.type = + ERROR_CODE_MAP[originalError.code] || DPoPErrorCodes.UNKNOWN_DPOP_ERROR; } } diff --git a/src/core/models/__tests__/ErrorCodes.spec.ts b/src/core/models/__tests__/ErrorCodes.spec.ts new file mode 100644 index 00000000..71171e68 --- /dev/null +++ b/src/core/models/__tests__/ErrorCodes.spec.ts @@ -0,0 +1,261 @@ +import { + WebAuthErrorCodes, + CredentialsManagerErrorCodes, + DPoPErrorCodes, +} from '../'; + +describe('Error Code Constants', () => { + describe('WebAuthErrorCodes', () => { + it('should export all expected error code constants', () => { + // Verify that all expected codes are defined + expect(WebAuthErrorCodes.USER_CANCELLED).toBe('USER_CANCELLED'); + expect(WebAuthErrorCodes.ACCESS_DENIED).toBe('ACCESS_DENIED'); + expect(WebAuthErrorCodes.NETWORK_ERROR).toBe('NETWORK_ERROR'); + expect(WebAuthErrorCodes.ID_TOKEN_VALIDATION_FAILED).toBe( + 'ID_TOKEN_VALIDATION_FAILED' + ); + expect(WebAuthErrorCodes.BIOMETRICS_CONFIGURATION_ERROR).toBe( + 'BIOMETRICS_CONFIGURATION_ERROR' + ); + expect(WebAuthErrorCodes.BROWSER_NOT_AVAILABLE).toBe( + 'BROWSER_NOT_AVAILABLE' + ); + expect(WebAuthErrorCodes.FAILED_TO_LOAD_URL).toBe('FAILED_TO_LOAD_URL'); + expect(WebAuthErrorCodes.BROWSER_TERMINATED).toBe('BROWSER_TERMINATED'); + expect(WebAuthErrorCodes.NO_BUNDLE_IDENTIFIER).toBe( + 'NO_BUNDLE_IDENTIFIER' + ); + expect(WebAuthErrorCodes.TRANSACTION_ACTIVE_ALREADY).toBe( + 'TRANSACTION_ACTIVE_ALREADY' + ); + expect(WebAuthErrorCodes.NO_AUTHORIZATION_CODE).toBe( + 'NO_AUTHORIZATION_CODE' + ); + expect(WebAuthErrorCodes.PKCE_NOT_ALLOWED).toBe('PKCE_NOT_ALLOWED'); + expect(WebAuthErrorCodes.INVALID_INVITATION_URL).toBe( + 'INVALID_INVITATION_URL' + ); + expect(WebAuthErrorCodes.INVALID_STATE).toBe('INVALID_STATE'); + expect(WebAuthErrorCodes.TIMEOUT_ERROR).toBe('TIMEOUT_ERROR'); + expect(WebAuthErrorCodes.CONSENT_REQUIRED).toBe('CONSENT_REQUIRED'); + expect(WebAuthErrorCodes.INVALID_CONFIGURATION).toBe( + 'INVALID_CONFIGURATION' + ); + expect(WebAuthErrorCodes.UNKNOWN_ERROR).toBe('UNKNOWN_ERROR'); + }); + + it('should have exactly 18 error codes', () => { + const keys = Object.keys(WebAuthErrorCodes); + expect(keys).toHaveLength(18); + }); + + it('should be immutable (as const)', () => { + // TypeScript should enforce this at compile time, + // but we can verify the object is defined + expect(WebAuthErrorCodes).toBeDefined(); + expect(typeof WebAuthErrorCodes).toBe('object'); + }); + + it('should be usable in switch statements', () => { + const testErrorType = 'USER_CANCELLED'; + let result = ''; + + switch (testErrorType) { + case WebAuthErrorCodes.USER_CANCELLED: + result = 'cancelled'; + break; + case WebAuthErrorCodes.NETWORK_ERROR: + result = 'network'; + break; + default: + result = 'unknown'; + } + + expect(result).toBe('cancelled'); + }); + }); + + describe('CredentialsManagerErrorCodes', () => { + it('should export all expected error code constants', () => { + expect(CredentialsManagerErrorCodes.INVALID_CREDENTIALS).toBe( + 'INVALID_CREDENTIALS' + ); + expect(CredentialsManagerErrorCodes.NO_CREDENTIALS).toBe( + 'NO_CREDENTIALS' + ); + expect(CredentialsManagerErrorCodes.NO_REFRESH_TOKEN).toBe( + 'NO_REFRESH_TOKEN' + ); + expect(CredentialsManagerErrorCodes.RENEW_FAILED).toBe('RENEW_FAILED'); + expect(CredentialsManagerErrorCodes.STORE_FAILED).toBe('STORE_FAILED'); + expect(CredentialsManagerErrorCodes.REVOKE_FAILED).toBe('REVOKE_FAILED'); + expect(CredentialsManagerErrorCodes.LARGE_MIN_TTL).toBe('LARGE_MIN_TTL'); + expect(CredentialsManagerErrorCodes.CREDENTIAL_MANAGER_ERROR).toBe( + 'CREDENTIAL_MANAGER_ERROR' + ); + expect(CredentialsManagerErrorCodes.BIOMETRICS_FAILED).toBe( + 'BIOMETRICS_FAILED' + ); + expect(CredentialsManagerErrorCodes.NO_NETWORK).toBe('NO_NETWORK'); + expect(CredentialsManagerErrorCodes.API_ERROR).toBe('API_ERROR'); + expect(CredentialsManagerErrorCodes.API_EXCHANGE_FAILED).toBe( + 'API_EXCHANGE_FAILED' + ); + expect(CredentialsManagerErrorCodes.INCOMPATIBLE_DEVICE).toBe( + 'INCOMPATIBLE_DEVICE' + ); + expect(CredentialsManagerErrorCodes.CRYPTO_EXCEPTION).toBe( + 'CRYPTO_EXCEPTION' + ); + expect(CredentialsManagerErrorCodes.UNKNOWN_ERROR).toBe('UNKNOWN_ERROR'); + }); + + it('should have exactly 15 error codes', () => { + const keys = Object.keys(CredentialsManagerErrorCodes); + expect(keys).toHaveLength(15); + }); + + it('should be immutable (as const)', () => { + expect(CredentialsManagerErrorCodes).toBeDefined(); + expect(typeof CredentialsManagerErrorCodes).toBe('object'); + }); + + it('should be usable in switch statements', () => { + const testErrorType = 'NO_CREDENTIALS'; + let result = ''; + + switch (testErrorType) { + case CredentialsManagerErrorCodes.NO_CREDENTIALS: + result = 'no_creds'; + break; + case CredentialsManagerErrorCodes.NO_REFRESH_TOKEN: + result = 'no_refresh'; + break; + default: + result = 'unknown'; + } + + expect(result).toBe('no_creds'); + }); + }); + + describe('DPoPErrorCodes', () => { + it('should export all expected error code constants', () => { + expect(DPoPErrorCodes.DPOP_GENERATION_FAILED).toBe( + 'DPOP_GENERATION_FAILED' + ); + expect(DPoPErrorCodes.DPOP_PROOF_FAILED).toBe('DPOP_PROOF_FAILED'); + expect(DPoPErrorCodes.DPOP_KEY_GENERATION_FAILED).toBe( + 'DPOP_KEY_GENERATION_FAILED' + ); + expect(DPoPErrorCodes.DPOP_KEY_STORAGE_FAILED).toBe( + 'DPOP_KEY_STORAGE_FAILED' + ); + expect(DPoPErrorCodes.DPOP_KEY_RETRIEVAL_FAILED).toBe( + 'DPOP_KEY_RETRIEVAL_FAILED' + ); + expect(DPoPErrorCodes.DPOP_NONCE_MISMATCH).toBe('DPOP_NONCE_MISMATCH'); + expect(DPoPErrorCodes.DPOP_INVALID_TOKEN_TYPE).toBe( + 'DPOP_INVALID_TOKEN_TYPE' + ); + expect(DPoPErrorCodes.DPOP_MISSING_PARAMETER).toBe( + 'DPOP_MISSING_PARAMETER' + ); + expect(DPoPErrorCodes.DPOP_CLEAR_KEY_FAILED).toBe( + 'DPOP_CLEAR_KEY_FAILED' + ); + expect(DPoPErrorCodes.UNKNOWN_DPOP_ERROR).toBe('UNKNOWN_DPOP_ERROR'); + }); + + it('should have exactly 10 error codes', () => { + const keys = Object.keys(DPoPErrorCodes); + expect(keys).toHaveLength(10); + }); + + it('should be immutable (as const)', () => { + expect(DPoPErrorCodes).toBeDefined(); + expect(typeof DPoPErrorCodes).toBe('object'); + }); + + it('should be usable in switch statements', () => { + const testErrorType = 'DPOP_GENERATION_FAILED'; + let result = ''; + + switch (testErrorType) { + case DPoPErrorCodes.DPOP_GENERATION_FAILED: + result = 'generation_failed'; + break; + case DPoPErrorCodes.DPOP_KEY_STORAGE_FAILED: + result = 'storage_failed'; + break; + default: + result = 'unknown'; + } + + expect(result).toBe('generation_failed'); + }); + }); + + describe('Cross-Error-Code Uniqueness', () => { + it('should not have overlapping error codes between WebAuth and CredentialsManager', () => { + const webAuthCodes = new Set(Object.values(WebAuthErrorCodes)); + const credentialsManagerCodes = new Set( + Object.values(CredentialsManagerErrorCodes) + ); + + // Only UNKNOWN_ERROR should overlap intentionally + const webAuthArray = Array.from(webAuthCodes); + const credentialsArray = Array.from(credentialsManagerCodes); + + const overlaps = webAuthArray.filter((code) => + credentialsArray.includes(code) + ); + + // Only UNKNOWN_ERROR should be shared + expect(overlaps).toEqual(['UNKNOWN_ERROR']); + }); + + it('should not have overlapping error codes between DPoP and other error types', () => { + const dpopCodes = new Set(Object.values(DPoPErrorCodes)); + const webAuthCodes = new Set(Object.values(WebAuthErrorCodes)); + const credentialsManagerCodes = new Set( + Object.values(CredentialsManagerErrorCodes) + ); + + const dpopArray = Array.from(dpopCodes); + const webAuthArray = Array.from(webAuthCodes); + const credentialsArray = Array.from(credentialsManagerCodes); + + const webAuthOverlaps = dpopArray.filter((code) => + webAuthArray.includes(code) + ); + const credentialsOverlaps = dpopArray.filter((code) => + credentialsArray.includes(code) + ); + + // No overlaps expected + expect(webAuthOverlaps).toEqual([]); + expect(credentialsOverlaps).toEqual([]); + }); + }); + + describe('TypeScript Type Safety', () => { + it('should allow type-safe comparisons with error types', () => { + // This test verifies TypeScript compilation works correctly + // At runtime, we just verify the values match + const mockErrorType: string = 'USER_CANCELLED'; + + // This should compile without errors in TypeScript + const isUserCancelled = + mockErrorType === WebAuthErrorCodes.USER_CANCELLED; + expect(isUserCancelled).toBe(true); + }); + + it('should provide autocomplete-friendly constant access', () => { + // Verify all constants are accessible and have string values + expect(typeof WebAuthErrorCodes.USER_CANCELLED).toBe('string'); + expect(typeof CredentialsManagerErrorCodes.NO_CREDENTIALS).toBe('string'); + expect(typeof DPoPErrorCodes.DPOP_GENERATION_FAILED).toBe('string'); + }); + }); +}); diff --git a/src/core/models/index.ts b/src/core/models/index.ts index 81e84a8b..d6d3857b 100644 --- a/src/core/models/index.ts +++ b/src/core/models/index.ts @@ -2,6 +2,9 @@ export { AuthError } from './AuthError'; export { Credentials } from './Credentials'; export { Auth0User } from './Auth0User'; export { ApiCredentials } from './ApiCredentials'; -export { CredentialsManagerError } from './CredentialsManagerError'; -export { WebAuthError } from './WebAuthError'; -export { DPoPError } from './DPoPError'; +export { + CredentialsManagerError, + CredentialsManagerErrorCodes, +} from './CredentialsManagerError'; +export { WebAuthError, WebAuthErrorCodes } from './WebAuthError'; +export { DPoPError, DPoPErrorCodes } from './DPoPError'; diff --git a/src/index.ts b/src/index.ts index aaf6d89b..28339a14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,11 @@ export { AuthError, CredentialsManagerError, + CredentialsManagerErrorCodes, WebAuthError, + WebAuthErrorCodes, DPoPError, + DPoPErrorCodes, } from './core/models'; export { TimeoutError } from './core/utils/fetchWithTimeout'; export { TokenType } from './types/common'; From 90def86d9b262e39ad7ab38c315d3e1365ce06cf Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 17 Dec 2025 17:23:56 +0530 Subject: [PATCH 2/3] feat: enhance error handling in CredentialsScreen and HomeScreen using type-safe error codes --- .../src/screens/hooks/CredentialsScreen.tsx | 30 ++++++++++++++++++- example/src/screens/hooks/Home.tsx | 24 ++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/example/src/screens/hooks/CredentialsScreen.tsx b/example/src/screens/hooks/CredentialsScreen.tsx index 98269ccb..48e2da4f 100644 --- a/example/src/screens/hooks/CredentialsScreen.tsx +++ b/example/src/screens/hooks/CredentialsScreen.tsx @@ -8,7 +8,13 @@ import { Linking, Alert, } from 'react-native'; -import { useAuth0, Credentials, ApiCredentials } from 'react-native-auth0'; +import { + useAuth0, + Credentials, + ApiCredentials, + CredentialsManagerError, + CredentialsManagerErrorCodes, +} from 'react-native-auth0'; import Button from '../../components/Button'; import Header from '../../components/Header'; import Result from '../../components/Result'; @@ -41,6 +47,28 @@ const CredentialsScreen = () => { setResult(res ?? { success: `${title} completed` }); } catch (e) { setError(e as Error); + // Demonstrate usage of CredentialsManagerErrorCodes for type-safe error handling + if (e instanceof CredentialsManagerError) { + const credError: CredentialsManagerError = e; + switch (credError.type) { + case CredentialsManagerErrorCodes.NO_CREDENTIALS: + Alert.alert( + 'No Credentials', + 'No credentials are stored. Please log in first.' + ); + break; + case CredentialsManagerErrorCodes.NO_REFRESH_TOKEN: + Alert.alert( + 'No Refresh Token', + 'Refresh token is not available. Make sure to request the "offline_access" scope during login.' + ); + break; + default: + console.log( + `Credentials error: ${credError.type} - ${credError.message}` + ); + } + } } }; diff --git a/example/src/screens/hooks/Home.tsx b/example/src/screens/hooks/Home.tsx index 351c347f..ce09fd37 100644 --- a/example/src/screens/hooks/Home.tsx +++ b/example/src/screens/hooks/Home.tsx @@ -7,7 +7,7 @@ import { StyleSheet, Alert, } from 'react-native'; -import { useAuth0 } from 'react-native-auth0'; +import { useAuth0, WebAuthError, WebAuthErrorCodes } from 'react-native-auth0'; import Button from '../../components/Button'; import Header from '../../components/Header'; import LabeledInput from '../../components/LabeledInput'; @@ -37,6 +37,28 @@ const HomeScreen = () => { }); } catch (e) { console.log('Login error: ', e); + // Demonstrate usage of WebAuthErrorCodes for type-safe error handling + if (e instanceof WebAuthError) { + const webAuthError: WebAuthError = e; + switch (webAuthError.type) { + case WebAuthErrorCodes.USER_CANCELLED: + Alert.alert( + 'Login Cancelled', + 'You cancelled the login process. Please try again when ready.' + ); + break; + case WebAuthErrorCodes.TIMEOUT_ERROR: + Alert.alert( + 'Login Timeout', + 'The login process timed out. Please try again.' + ); + break; + default: + Alert.alert('Authentication Error', webAuthError.message); + } + } else { + Alert.alert('Error', 'An unexpected error occurred during login.'); + } } }; From 76802bead7e749c0b3f5dc56d8e07301737d7210 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 17 Dec 2025 17:48:53 +0530 Subject: [PATCH 3/3] feat: doc update for error code constants for Credentials Manager, DPoP, and WebAuth operations --- src/core/models/CredentialsManagerError.ts | 46 +++++++++++++++++++++- src/core/models/DPoPError.ts | 44 ++++++++++++++++++++- src/core/models/WebAuthError.ts | 45 ++++++++++++++++++++- src/exports/enums.ts | 9 +++++ 4 files changed, 141 insertions(+), 3 deletions(-) diff --git a/src/core/models/CredentialsManagerError.ts b/src/core/models/CredentialsManagerError.ts index 6b3b056e..4a1c40ab 100644 --- a/src/core/models/CredentialsManagerError.ts +++ b/src/core/models/CredentialsManagerError.ts @@ -1,23 +1,67 @@ import { AuthError } from './AuthError'; /** - * Public constants exposing all possible CredentialsManager error codes. + * Platform-agnostic error code constants for Credentials Manager operations. + * + * Use these constants for type-safe error handling when working with credentials operations + * like getCredentials, saveCredentials, clearCredentials, and getApiCredentials. + * Each constant corresponds to a specific error type in the {@link CredentialsManagerError.type} property. + * + * @example + * ```typescript + * import { CredentialsManagerError, CredentialsManagerErrorCodes } from 'react-native-auth0'; + * + * try { + * const credentials = await auth0.credentialsManager.getCredentials(); + * } catch (e) { + * if (e instanceof CredentialsManagerError) { + * switch (e.type) { + * case CredentialsManagerErrorCodes.NO_CREDENTIALS: + * // User needs to log in + * break; + * case CredentialsManagerErrorCodes.NO_REFRESH_TOKEN: + * // Request offline_access scope during login + * break; + * case CredentialsManagerErrorCodes.RENEW_FAILED: + * // Token refresh failed - may need re-authentication + * break; + * } + * } + * } + * ``` + * + * @see {@link CredentialsManagerError} */ export const CredentialsManagerErrorCodes = { + /** Stored credentials are invalid or corrupted */ INVALID_CREDENTIALS: 'INVALID_CREDENTIALS', + /** No credentials are stored - user needs to log in */ NO_CREDENTIALS: 'NO_CREDENTIALS', + /** Refresh token is not available - ensure offline_access scope was requested */ NO_REFRESH_TOKEN: 'NO_REFRESH_TOKEN', + /** Failed to refresh credentials using refresh token */ RENEW_FAILED: 'RENEW_FAILED', + /** Failed to store credentials securely */ STORE_FAILED: 'STORE_FAILED', + /** Failed to revoke refresh token */ REVOKE_FAILED: 'REVOKE_FAILED', + /** Requested minimum TTL exceeds token lifetime */ LARGE_MIN_TTL: 'LARGE_MIN_TTL', + /** Generic credentials manager error */ CREDENTIAL_MANAGER_ERROR: 'CREDENTIAL_MANAGER_ERROR', + /** Biometric authentication failed */ BIOMETRICS_FAILED: 'BIOMETRICS_FAILED', + /** Network connectivity issue */ NO_NETWORK: 'NO_NETWORK', + /** Generic API error */ API_ERROR: 'API_ERROR', + /** Failed to exchange refresh token for API-specific credentials (MRRT) */ API_EXCHANGE_FAILED: 'API_EXCHANGE_FAILED', + /** Device is incompatible with secure storage requirements */ INCOMPATIBLE_DEVICE: 'INCOMPATIBLE_DEVICE', + /** Cryptographic operation failed */ CRYPTO_EXCEPTION: 'CRYPTO_EXCEPTION', + /** Unknown or uncategorized error */ UNKNOWN_ERROR: 'UNKNOWN_ERROR', } as const; diff --git a/src/core/models/DPoPError.ts b/src/core/models/DPoPError.ts index 2cd34451..1c513fa5 100644 --- a/src/core/models/DPoPError.ts +++ b/src/core/models/DPoPError.ts @@ -1,18 +1,60 @@ import { AuthError } from './AuthError'; /** - * Public constants exposing all possible DPoP error codes. + * Platform-agnostic error code constants for DPoP (Demonstrating Proof-of-Possession) operations. + * + * Use these constants for type-safe error handling when working with DPoP-bound tokens. + * DPoP enhances OAuth 2.0 security by binding tokens to cryptographic keys. + * Each constant corresponds to a specific error type in the {@link DPoPError.type} property. + * + * @example + * ```typescript + * import { DPoPError, DPoPErrorCodes } from 'react-native-auth0'; + * + * try { + * const headers = await auth0.getDPoPHeaders({ + * url: 'https://api.example.com/data', + * method: 'GET', + * accessToken: credentials.accessToken, + * tokenType: credentials.tokenType + * }); + * } catch (e) { + * if (e instanceof DPoPError) { + * switch (e.type) { + * case DPoPErrorCodes.DPOP_KEY_GENERATION_FAILED: + * // Failed to generate DPoP key pair + * break; + * case DPoPErrorCodes.DPOP_PROOF_FAILED: + * // Failed to create DPoP proof + * break; + * } + * } + * } + * ``` + * + * @see {@link DPoPError} + * @see {@link https://datatracker.ietf.org/doc/html/rfc9449|RFC 9449 - OAuth 2.0 DPoP} */ export const DPoPErrorCodes = { + /** Failed to generate DPoP proof JWT */ DPOP_GENERATION_FAILED: 'DPOP_GENERATION_FAILED', + /** DPoP proof validation or creation failed */ DPOP_PROOF_FAILED: 'DPOP_PROOF_FAILED', + /** Failed to generate DPoP key pair */ DPOP_KEY_GENERATION_FAILED: 'DPOP_KEY_GENERATION_FAILED', + /** Failed to store DPoP key securely (keychain/keystore) */ DPOP_KEY_STORAGE_FAILED: 'DPOP_KEY_STORAGE_FAILED', + /** Failed to retrieve stored DPoP key */ DPOP_KEY_RETRIEVAL_FAILED: 'DPOP_KEY_RETRIEVAL_FAILED', + /** DPoP nonce mismatch - server rejected the proof */ DPOP_NONCE_MISMATCH: 'DPOP_NONCE_MISMATCH', + /** Invalid token type for DPoP operation */ DPOP_INVALID_TOKEN_TYPE: 'DPOP_INVALID_TOKEN_TYPE', + /** Required DPoP parameter is missing */ DPOP_MISSING_PARAMETER: 'DPOP_MISSING_PARAMETER', + /** Failed to clear/delete DPoP key */ DPOP_CLEAR_KEY_FAILED: 'DPOP_CLEAR_KEY_FAILED', + /** Unknown or uncategorized DPoP error */ UNKNOWN_DPOP_ERROR: 'UNKNOWN_DPOP_ERROR', } as const; diff --git a/src/core/models/WebAuthError.ts b/src/core/models/WebAuthError.ts index e7c50a8f..13ca702c 100644 --- a/src/core/models/WebAuthError.ts +++ b/src/core/models/WebAuthError.ts @@ -1,26 +1,69 @@ import { AuthError } from './AuthError'; /** - * Public constants exposing all possible WebAuth error codes. + * Platform-agnostic error code constants for WebAuth operations. + * + * Use these constants for type-safe error handling when working with WebAuth errors. + * Each constant corresponds to a specific error type in the {@link WebAuthError.type} property. + * + * @example + * ```typescript + * import { WebAuthError, WebAuthErrorCodes } from 'react-native-auth0'; + * + * try { + * await auth0.webAuth.authorize(); + * } catch (e) { + * if (e instanceof WebAuthError) { + * switch (e.type) { + * case WebAuthErrorCodes.USER_CANCELLED: + * // User cancelled the authentication + * break; + * case WebAuthErrorCodes.NETWORK_ERROR: + * // Network connectivity issue + * break; + * } + * } + * } + * ``` + * + * @see {@link WebAuthError} */ export const WebAuthErrorCodes = { + /** User actively cancelled the authentication flow */ USER_CANCELLED: 'USER_CANCELLED', + /** Authentication was denied by user or Auth0 (rules, actions, policies) */ ACCESS_DENIED: 'ACCESS_DENIED', + /** Network error occurred during authentication */ NETWORK_ERROR: 'NETWORK_ERROR', + /** ID token validation failed (signature, issuer, audience, nonce) */ ID_TOKEN_VALIDATION_FAILED: 'ID_TOKEN_VALIDATION_FAILED', + /** Biometric configuration error */ BIOMETRICS_CONFIGURATION_ERROR: 'BIOMETRICS_CONFIGURATION_ERROR', + /** No compatible browser available on device */ BROWSER_NOT_AVAILABLE: 'BROWSER_NOT_AVAILABLE', + /** Authorization URL failed to load in browser */ FAILED_TO_LOAD_URL: 'FAILED_TO_LOAD_URL', + /** Browser was closed unexpectedly */ BROWSER_TERMINATED: 'BROWSER_TERMINATED', + /** Native bundle identifier could not be retrieved (iOS) */ NO_BUNDLE_IDENTIFIER: 'NO_BUNDLE_IDENTIFIER', + /** Another authentication transaction is already active */ TRANSACTION_ACTIVE_ALREADY: 'TRANSACTION_ACTIVE_ALREADY', + /** Authorization code missing from callback URL */ NO_AUTHORIZATION_CODE: 'NO_AUTHORIZATION_CODE', + /** PKCE is required but not enabled in Auth0 Application */ PKCE_NOT_ALLOWED: 'PKCE_NOT_ALLOWED', + /** Organization invitation URL is malformed */ INVALID_INVITATION_URL: 'INVALID_INVITATION_URL', + /** State parameter mismatch (potential CSRF attack) */ INVALID_STATE: 'INVALID_STATE', + /** Authentication flow timed out */ TIMEOUT_ERROR: 'TIMEOUT_ERROR', + /** User consent required for requested scopes */ CONSENT_REQUIRED: 'CONSENT_REQUIRED', + /** Auth0 Application is misconfigured */ INVALID_CONFIGURATION: 'INVALID_CONFIGURATION', + /** Unknown or uncategorized error */ UNKNOWN_ERROR: 'UNKNOWN_ERROR', } as const; diff --git a/src/exports/enums.ts b/src/exports/enums.ts index 5b4987c8..56259a1c 100644 --- a/src/exports/enums.ts +++ b/src/exports/enums.ts @@ -4,3 +4,12 @@ export { LocalAuthenticationStrategy, BiometricPolicy, } from '../types/platform-specific'; + +/** + * Error code constants for type-safe error handling. + */ +export { + WebAuthErrorCodes, + CredentialsManagerErrorCodes, + DPoPErrorCodes, +} from '../core/models';