@chipmobilesdk/rn-auth 0.3.0 → 0.3.1

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.
@@ -1,76 +1,76 @@
1
- /**
2
- * Classifies a native OIDC error into an `AuthErrorCode`.
3
- *
4
- * The rules are an ordered table rather than a chain of substring checks, so a
5
- * platform adapter (the iOS one, later) adds its own rows without touching the
6
- * engine. Order matters: the first matching rule wins.
7
- *
8
- * The default is deliberately not "session expired". Restore clears the stored
9
- * session only on `session-expired`, so anything this table cannot name — a
10
- * timeout, a 5xx, a discovery failure, a localized message — must keep the
11
- * session rather than sign the user out.
12
- */
13
-
14
- import { AuthError, type AuthErrorCode } from '../diagnostics/errors';
15
-
16
- export interface ClassificationRule {
17
- readonly code: AuthErrorCode;
18
- readonly messageKey: string;
19
- /** Matched against the native error's `code` and `message`, case-insensitively. */
20
- readonly pattern: RegExp;
21
- }
22
-
23
- /**
24
- * OAuth error codes (RFC 6749 §5.2, OIDC Core §3.1.2.6) and the Entra ID AADSTS
25
- * codes that mean the same thing, plus the transport failures app-auth reports.
26
- */
27
- export const DEFAULT_CLASSIFICATION_RULES: readonly ClassificationRule[] = [
28
- {
29
- code: 'provider-cancelled',
30
- messageKey: 'auth.error.cancelled',
31
- pattern: /cancel|user_cancel|authorization flow (was )?dismissed/i,
32
- },
33
- {
34
- code: 'session-expired',
35
- messageKey: 'auth.error.sessionExpired',
36
- // invalid_grant is the refresh-token verdict; the AADSTS codes are Entra's
37
- // expired, revoked, or inactive refresh token and password-changed cases.
38
- pattern:
39
- /\binvalid_grant\b|\binteraction_required\b|\blogin_required\b|AADSTS(700082|700084|50173|70043)\b/i,
40
- },
41
- {
42
- code: 'redirect-mismatch',
43
- messageKey: 'auth.error.redirectMismatch',
44
- pattern: /redirect_uri|redirect uri|redirect_mismatch|AADSTS50011\b/i,
45
- },
46
- {
47
- code: 'consent-denied',
48
- messageKey: 'auth.error.consentDenied',
49
- pattern: /\baccess_denied\b|\bconsent_required\b|AADSTS65001\b/i,
50
- },
51
- {
52
- code: 'network-unavailable',
53
- messageKey: 'auth.error.network',
54
- pattern:
55
- /network|connection|offline|timed? ?out|unable to resolve host|unreachable|\bserver_error\b|\btemporarily_unavailable\b|\b5\d\d\b/i,
56
- },
57
- ];
58
-
59
- /** Maps a native app-auth error to a classified AuthError without leaking tokens. */
60
- export function classifyAppAuthError(
61
- error: unknown,
62
- rules: readonly ClassificationRule[] = DEFAULT_CLASSIFICATION_RULES,
63
- ): AuthError {
64
- const message = error instanceof Error ? error.message : String(error);
65
- const nativeCode = (error as { code?: unknown } | undefined)?.code;
66
- const haystack = `${typeof nativeCode === 'string' ? nativeCode : ''} ${message}`;
67
- const rule = rules.find(candidate => candidate.pattern.test(haystack));
68
- const code = rule?.code ?? 'token-acquisition-failed';
69
- return new AuthError({
70
- code,
71
- messageKey: rule?.messageKey ?? 'auth.error.token',
72
- audience: code === 'redirect-mismatch' ? 'developer' : 'user',
73
- // AuthError redacts `detail`, so token-shaped values never survive.
74
- detail: message,
75
- });
76
- }
1
+ /**
2
+ * Classifies a native OIDC error into an `AuthErrorCode`.
3
+ *
4
+ * The rules are an ordered table rather than a chain of substring checks, so a
5
+ * platform adapter (the iOS one, later) adds its own rows without touching the
6
+ * engine. Order matters: the first matching rule wins.
7
+ *
8
+ * The default is deliberately not "session expired". Restore clears the stored
9
+ * session only on `session-expired`, so anything this table cannot name — a
10
+ * timeout, a 5xx, a discovery failure, a localized message — must keep the
11
+ * session rather than sign the user out.
12
+ */
13
+
14
+ import { AuthError, type AuthErrorCode } from '../diagnostics/errors';
15
+
16
+ export interface ClassificationRule {
17
+ readonly code: AuthErrorCode;
18
+ readonly messageKey: string;
19
+ /** Matched against the native error's `code` and `message`, case-insensitively. */
20
+ readonly pattern: RegExp;
21
+ }
22
+
23
+ /**
24
+ * OAuth error codes (RFC 6749 §5.2, OIDC Core §3.1.2.6) and the Entra ID AADSTS
25
+ * codes that mean the same thing, plus the transport failures app-auth reports.
26
+ */
27
+ export const DEFAULT_CLASSIFICATION_RULES: readonly ClassificationRule[] = [
28
+ {
29
+ code: 'provider-cancelled',
30
+ messageKey: 'auth.error.cancelled',
31
+ pattern: /cancel|user_cancel|authorization flow (was )?dismissed/i,
32
+ },
33
+ {
34
+ code: 'session-expired',
35
+ messageKey: 'auth.error.sessionExpired',
36
+ // invalid_grant is the refresh-token verdict; the AADSTS codes are Entra's
37
+ // expired, revoked, or inactive refresh token and password-changed cases.
38
+ pattern:
39
+ /\binvalid_grant\b|\binteraction_required\b|\blogin_required\b|AADSTS(700082|700084|50173|70043)\b/i,
40
+ },
41
+ {
42
+ code: 'redirect-mismatch',
43
+ messageKey: 'auth.error.redirectMismatch',
44
+ pattern: /redirect_uri|redirect uri|redirect_mismatch|AADSTS50011\b/i,
45
+ },
46
+ {
47
+ code: 'consent-denied',
48
+ messageKey: 'auth.error.consentDenied',
49
+ pattern: /\baccess_denied\b|\bconsent_required\b|AADSTS65001\b/i,
50
+ },
51
+ {
52
+ code: 'network-unavailable',
53
+ messageKey: 'auth.error.network',
54
+ pattern:
55
+ /network|connection|offline|timed? ?out|unable to resolve host|unreachable|\bserver_error\b|\btemporarily_unavailable\b|\b5\d\d\b/i,
56
+ },
57
+ ];
58
+
59
+ /** Maps a native app-auth error to a classified AuthError without leaking tokens. */
60
+ export function classifyAppAuthError(
61
+ error: unknown,
62
+ rules: readonly ClassificationRule[] = DEFAULT_CLASSIFICATION_RULES,
63
+ ): AuthError {
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ const nativeCode = (error as { code?: unknown } | undefined)?.code;
66
+ const haystack = `${typeof nativeCode === 'string' ? nativeCode : ''} ${message}`;
67
+ const rule = rules.find(candidate => candidate.pattern.test(haystack));
68
+ const code = rule?.code ?? 'token-acquisition-failed';
69
+ return new AuthError({
70
+ code,
71
+ messageKey: rule?.messageKey ?? 'auth.error.token',
72
+ audience: code === 'redirect-mismatch' ? 'developer' : 'user',
73
+ // AuthError redacts `detail`, so token-shaped values never survive.
74
+ detail: message,
75
+ });
76
+ }
@@ -1,73 +1,66 @@
1
- /**
2
- * Social provider routing (FR-002/FR-003, R3). Google and Apple are federated
3
- * inside the External ID tenant; the app presents the platform-correct action
4
- * and starts the user flow with a per-provider `domain_hint`.
5
- */
6
-
7
- import type {
8
- AuthConfig,
9
- DevicePlatform,
10
- ProviderConfig,
11
- ProviderId,
12
- } from '../config/types';
13
- import { AuthConfigError } from '../diagnostics/errors';
14
-
15
- /** Find the configured provider, ensuring it is enabled for the platform. */
16
- export function resolveProvider(
17
- config: AuthConfig,
18
- providerId: ProviderId,
19
- platform: DevicePlatform,
20
- ): ProviderConfig {
21
- const provider = config.providers.find(candidate => candidate.id === providerId);
22
- if (!provider) {
23
- throw new AuthConfigError(
24
- 'auth.error.configuration',
25
- `Provider "${providerId}" is not configured`,
26
- );
27
- }
28
- if (!provider.platforms.includes(platform)) {
29
- throw new AuthConfigError(
30
- 'auth.error.configuration',
31
- `Provider "${providerId}" is not enabled for platform "${platform}"`,
32
- );
33
- }
34
- if (provider.available === false) {
35
- throw new AuthConfigError(
36
- 'auth.error.configuration',
37
- `Provider "${providerId}" is marked unavailable`,
38
- );
39
- }
40
- return provider;
41
- }
42
-
43
- /** Build the OIDC additional parameters that route to the chosen IdP. */
44
- export function buildAdditionalParameters(
45
- provider: ProviderConfig,
46
- ): Record<string, string> {
47
- return { domain_hint: provider.domainHint };
48
- }
49
-
50
- /**
51
- * All providers to present on a given platform, in configuration array order
52
- * (FR-003/FR-004). Filtered to providers enabled for the platform and not marked
53
- * unavailable.
54
- */
55
- export function availableProvidersForPlatform(
56
- config: AuthConfig,
57
- platform: DevicePlatform,
58
- ): ProviderConfig[] {
59
- return config.providers.filter(
60
- provider => provider.platforms.includes(platform) && provider.available !== false,
61
- );
62
- }
63
-
64
- /**
65
- * The first available provider for a platform (UX-004, back-compat). Equals
66
- * `availableProvidersForPlatform(config, platform)[0]`.
67
- */
68
- export function defaultProviderForPlatform(
69
- config: AuthConfig,
70
- platform: DevicePlatform,
71
- ): ProviderConfig | undefined {
72
- return availableProvidersForPlatform(config, platform)[0];
73
- }
1
+ /**
2
+ * Social provider routing (FR-002/FR-003, R3). Google and Apple are federated
3
+ * inside the External ID tenant; the app presents the platform-correct action
4
+ * and starts the user flow with a per-provider `domain_hint`.
5
+ */
6
+
7
+ import type { AuthConfig, DevicePlatform, ProviderConfig, ProviderId } from '../config/types';
8
+ import { AuthConfigError } from '../diagnostics/errors';
9
+
10
+ /** Find the configured provider, ensuring it is enabled for the platform. */
11
+ export function resolveProvider(
12
+ config: AuthConfig,
13
+ providerId: ProviderId,
14
+ platform: DevicePlatform,
15
+ ): ProviderConfig {
16
+ const provider = config.providers.find(candidate => candidate.id === providerId);
17
+ if (!provider) {
18
+ throw new AuthConfigError(
19
+ 'auth.error.configuration',
20
+ `Provider "${providerId}" is not configured`,
21
+ );
22
+ }
23
+ if (!provider.platforms.includes(platform)) {
24
+ throw new AuthConfigError(
25
+ 'auth.error.configuration',
26
+ `Provider "${providerId}" is not enabled for platform "${platform}"`,
27
+ );
28
+ }
29
+ if (provider.available === false) {
30
+ throw new AuthConfigError(
31
+ 'auth.error.configuration',
32
+ `Provider "${providerId}" is marked unavailable`,
33
+ );
34
+ }
35
+ return provider;
36
+ }
37
+
38
+ /** Build the OIDC additional parameters that route to the chosen IdP. */
39
+ export function buildAdditionalParameters(provider: ProviderConfig): Record<string, string> {
40
+ return { domain_hint: provider.domainHint };
41
+ }
42
+
43
+ /**
44
+ * All providers to present on a given platform, in configuration array order
45
+ * (FR-003/FR-004). Filtered to providers enabled for the platform and not marked
46
+ * unavailable.
47
+ */
48
+ export function availableProvidersForPlatform(
49
+ config: AuthConfig,
50
+ platform: DevicePlatform,
51
+ ): ProviderConfig[] {
52
+ return config.providers.filter(
53
+ provider => provider.platforms.includes(platform) && provider.available !== false,
54
+ );
55
+ }
56
+
57
+ /**
58
+ * The first available provider for a platform (UX-004, back-compat). Equals
59
+ * `availableProvidersForPlatform(config, platform)[0]`.
60
+ */
61
+ export function defaultProviderForPlatform(
62
+ config: AuthConfig,
63
+ platform: DevicePlatform,
64
+ ): ProviderConfig | undefined {
65
+ return availableProvidersForPlatform(config, platform)[0];
66
+ }
@@ -1,47 +1,41 @@
1
- /**
2
- * React context boundary for the auth controller (LIB-002). Subscribes to the
3
- * controller and re-renders consumers on every state change.
4
- */
5
-
6
- import React, {
7
- createContext,
8
- useContext,
9
- useEffect,
10
- useMemo,
11
- useState,
12
- } from 'react';
13
- import type { Auth, AuthState } from '../config/types';
14
-
15
- interface AuthContextValue {
16
- auth: Auth;
17
- state: AuthState;
18
- }
19
-
20
- const AuthContext = createContext<AuthContextValue | null>(null);
21
-
22
- export interface AuthProviderProps {
23
- auth: Auth;
24
- children: React.ReactNode;
25
- }
26
-
27
- export function AuthProvider({ auth, children }: AuthProviderProps) {
28
- const [state, setState] = useState<AuthState>(() => auth.getState());
29
-
30
- useEffect(() => {
31
- // Sync immediately in case state changed between render and subscribe.
32
- setState(auth.getState());
33
- return auth.subscribe(setState);
34
- }, [auth]);
35
-
36
- const value = useMemo<AuthContextValue>(() => ({ auth, state }), [auth, state]);
37
-
38
- return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
39
- }
40
-
41
- export function useAuthContext(): AuthContextValue {
42
- const value = useContext(AuthContext);
43
- if (!value) {
44
- throw new Error('useAuth must be used within an <AuthProvider>.');
45
- }
46
- return value;
47
- }
1
+ /**
2
+ * React context boundary for the auth controller (LIB-002). Subscribes to the
3
+ * controller and re-renders consumers on every state change.
4
+ */
5
+
6
+ import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
7
+ import type { Auth, AuthState } from '../config/types';
8
+
9
+ interface AuthContextValue {
10
+ auth: Auth;
11
+ state: AuthState;
12
+ }
13
+
14
+ const AuthContext = createContext<AuthContextValue | null>(null);
15
+
16
+ export interface AuthProviderProps {
17
+ auth: Auth;
18
+ children: React.ReactNode;
19
+ }
20
+
21
+ export function AuthProvider({ auth, children }: AuthProviderProps) {
22
+ const [state, setState] = useState<AuthState>(() => auth.getState());
23
+
24
+ useEffect(() => {
25
+ // Sync immediately in case state changed between render and subscribe.
26
+ setState(auth.getState());
27
+ return auth.subscribe(setState);
28
+ }, [auth]);
29
+
30
+ const value = useMemo<AuthContextValue>(() => ({ auth, state }), [auth, state]);
31
+
32
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
33
+ }
34
+
35
+ export function useAuthContext(): AuthContextValue {
36
+ const value = useContext(AuthContext);
37
+ if (!value) {
38
+ throw new Error('useAuth must be used within an <AuthProvider>.');
39
+ }
40
+ return value;
41
+ }
@@ -1,91 +1,86 @@
1
- /**
2
- * Public hooks (LIB-002). All read from the AuthProvider context so consumers
3
- * re-render on state changes.
4
- */
5
-
6
- import { useCallback, useState } from 'react';
7
- import { useAuthContext } from './AuthProvider';
8
- import type {
9
- AccessTier,
10
- AccessTokenResult,
11
- AuthState,
12
- GlobalSignOutResult,
13
- PremiumGateResult,
14
- Profile,
15
- ProviderConfig,
16
- ProviderId,
17
- } from '../config/types';
18
- import { failClosedTier } from '../access/accessTier';
19
-
20
- export interface UseAuthResult {
21
- state: AuthState;
22
- signIn(providerId: ProviderId): Promise<AuthState>;
23
- signOut(): Promise<void>;
24
- signOutGlobal(): Promise<GlobalSignOutResult>;
25
- restore(): Promise<AuthState>;
26
- requirePremium(): PremiumGateResult;
27
- }
28
-
29
- export function useAuth(): UseAuthResult {
30
- const { auth, state } = useAuthContext();
31
- return {
32
- state,
33
- signIn: useCallback((providerId: ProviderId) => auth.signIn(providerId), [auth]),
34
- signOut: useCallback(() => auth.signOut(), [auth]),
35
- signOutGlobal: useCallback(() => auth.signOutGlobal(), [auth]),
36
- restore: useCallback(() => auth.restore(), [auth]),
37
- requirePremium: useCallback(() => auth.requirePremium(), [auth]),
38
- };
39
- }
40
-
41
- export function useProfile(): Profile | undefined {
42
- const { state } = useAuthContext();
43
- return state.profile;
44
- }
45
-
46
- export function useAccessTier(): AccessTier {
47
- const { state } = useAuthContext();
48
- return state.accessTier ?? failClosedTier();
49
- }
50
-
51
- /** The first provider to present on the current platform (UX-004), or undefined. */
52
- export function useSignInProvider(): ProviderConfig | undefined {
53
- const { auth } = useAuthContext();
54
- return auth.getPlatformProvider();
55
- }
56
-
57
- /** All available providers for the current platform, in configuration order (FR-003/FR-004). */
58
- export function useSignInProviders(): ProviderConfig[] {
59
- const { auth } = useAuthContext();
60
- return auth.getPlatformProviders();
61
- }
62
-
63
- export type UseAccessTokenStatus =
64
- | 'idle'
65
- | 'loading'
66
- | 'success'
67
- | 'denied'
68
- | 'unavailable'
69
- | 'error';
70
-
71
- export interface UseAccessTokenResult {
72
- status: UseAccessTokenStatus;
73
- token?: string;
74
- request(): Promise<AccessTokenResult>;
75
- }
76
-
77
- export function useAccessToken(resourceKey: string): UseAccessTokenResult {
78
- const { auth } = useAuthContext();
79
- const [status, setStatus] = useState<UseAccessTokenStatus>('idle');
80
- const [token, setToken] = useState<string | undefined>(undefined);
81
-
82
- const request = useCallback(async () => {
83
- setStatus('loading');
84
- const result = await auth.requestAccessToken(resourceKey);
85
- setStatus(result.status);
86
- setToken(result.token);
87
- return result;
88
- }, [auth, resourceKey]);
89
-
90
- return { status, token, request };
91
- }
1
+ /**
2
+ * Public hooks (LIB-002). All read from the AuthProvider context so consumers
3
+ * re-render on state changes.
4
+ */
5
+
6
+ import { useCallback, useState } from 'react';
7
+ import { useAuthContext } from './AuthProvider';
8
+ import type {
9
+ AccessTier,
10
+ AccessTokenResult,
11
+ AuthState,
12
+ GlobalSignOutResult,
13
+ PremiumGateResult,
14
+ Profile,
15
+ ProviderConfig,
16
+ ProviderId,
17
+ } from '../config/types';
18
+ import { failClosedTier } from '../access/accessTier';
19
+
20
+ export interface UseAuthResult {
21
+ state: AuthState;
22
+ signIn(providerId: ProviderId): Promise<AuthState>;
23
+ signOut(): Promise<void>;
24
+ signOutGlobal(): Promise<GlobalSignOutResult>;
25
+ restore(): Promise<AuthState>;
26
+ requirePremium(): PremiumGateResult;
27
+ }
28
+
29
+ export function useAuth(): UseAuthResult {
30
+ const { auth, state } = useAuthContext();
31
+ return {
32
+ state,
33
+ signIn: useCallback((providerId: ProviderId) => auth.signIn(providerId), [auth]),
34
+ signOut: useCallback(() => auth.signOut(), [auth]),
35
+ signOutGlobal: useCallback(() => auth.signOutGlobal(), [auth]),
36
+ restore: useCallback(() => auth.restore(), [auth]),
37
+ requirePremium: useCallback(() => auth.requirePremium(), [auth]),
38
+ };
39
+ }
40
+
41
+ export function useProfile(): Profile | undefined {
42
+ const { state } = useAuthContext();
43
+ return state.profile;
44
+ }
45
+
46
+ export function useAccessTier(): AccessTier {
47
+ const { state } = useAuthContext();
48
+ return state.accessTier ?? failClosedTier();
49
+ }
50
+
51
+ /** The first provider to present on the current platform (UX-004), or undefined. */
52
+ export function useSignInProvider(): ProviderConfig | undefined {
53
+ const { auth } = useAuthContext();
54
+ return auth.getPlatformProvider();
55
+ }
56
+
57
+ /** All available providers for the current platform, in configuration order (FR-003/FR-004). */
58
+ export function useSignInProviders(): ProviderConfig[] {
59
+ const { auth } = useAuthContext();
60
+ return auth.getPlatformProviders();
61
+ }
62
+
63
+ export type UseAccessTokenStatus =
64
+ 'idle' | 'loading' | 'success' | 'denied' | 'unavailable' | 'error';
65
+
66
+ export interface UseAccessTokenResult {
67
+ status: UseAccessTokenStatus;
68
+ token?: string;
69
+ request(): Promise<AccessTokenResult>;
70
+ }
71
+
72
+ export function useAccessToken(resourceKey: string): UseAccessTokenResult {
73
+ const { auth } = useAuthContext();
74
+ const [status, setStatus] = useState<UseAccessTokenStatus>('idle');
75
+ const [token, setToken] = useState<string | undefined>(undefined);
76
+
77
+ const request = useCallback(async () => {
78
+ setStatus('loading');
79
+ const result = await auth.requestAccessToken(resourceKey);
80
+ setStatus(result.status);
81
+ setToken(result.token);
82
+ return result;
83
+ }, [auth, resourceKey]);
84
+
85
+ return { status, token, request };
86
+ }