@chipmobilesdk/rn-auth 0.3.1 → 0.5.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.
@@ -9,7 +9,15 @@
9
9
  * can never be undone by a request that was already in flight.
10
10
  */
11
11
 
12
- import { Platform } from 'react-native';
12
+ import {
13
+ createDiagnosticChannel,
14
+ createListenerSet,
15
+ currentPlatform,
16
+ shallowEqual,
17
+ } from '@chipmobilesdk/rn-core';
18
+
19
+ import type { AuthMessageMap } from '../diagnostics/messageKeys';
20
+ import { resolveMessages } from '../diagnostics/messageKeys';
13
21
  import type {
14
22
  Auth,
15
23
  AuthConfig,
@@ -31,10 +39,10 @@ import {
31
39
  } from '../oidc/socialRouting';
32
40
  import { mapClaimsToProfile } from './profile';
33
41
  import { evaluateAccessTier, failClosedTier } from '../access/accessTier';
34
- import { restoreSession, type PersistedSession } from './session';
42
+ import { restoreSession, sameSession, storedProfileFrom, type PersistedSession } from './session';
35
43
  import { AUTH_STORAGE_KEY } from '../tokens/secureStore';
36
44
  import { createAccessTokenManager } from '../tokens/accessToken';
37
- import { AuthError, toAuthError } from '../diagnostics/errors';
45
+ import { AuthError, toAuthError, type AuthErrorCode } from '../diagnostics/errors';
38
46
 
39
47
  const SIGNED_OUT: Partial<AuthState> = {
40
48
  status: 'signed-out',
@@ -48,17 +56,23 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
48
56
  // Throws AuthConfigError before anything else (FR-005, US4 scenario 2).
49
57
  validateConfig(config);
50
58
 
51
- const platform: DevicePlatform = options.platform ?? (Platform?.OS as DevicePlatform) ?? 'ios';
59
+ // rn-core's one platform source (CORE-09). Outside Android and iOS (tests on
60
+ // Node, web) the providers are resolved as for iOS, as before.
61
+ const detected = currentPlatform();
62
+ const platform: DevicePlatform =
63
+ options.platform ?? (detected === 'unsupported' ? 'ios' : detected);
52
64
  const secureStore = config.secureStore;
53
65
  const authClient = config.authClient ?? createAppAuthClient();
54
66
  const resources = config.resources ?? [];
67
+ // Declared since 0.1.0 and read by nothing until now (AUTH-15).
68
+ const messages = resolveMessages(config.messages);
55
69
 
56
- let state: AuthState = { status: 'loading', restore: 'pending' };
70
+ let state: AuthState = Object.freeze({ status: 'loading', restore: 'pending' });
57
71
  let refreshToken: string | null = null;
58
72
  /** Kept for the end-session `id_token_hint`; without it a global sign-out is a no-op. */
59
73
  let idToken: string | null = null;
60
74
  let sessionEpoch = 0;
61
- const listeners = new Set<(state: AuthState) => void>();
75
+ const listeners = createListenerSet<[AuthState]>();
62
76
  /**
63
77
  * Every Keychain write and delete, in order. A rotation persisted while a
64
78
  * sign-out runs must not land after the sign-out's delete and resurrect the
@@ -74,12 +88,29 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
74
88
  return next;
75
89
  };
76
90
 
77
- const writeSession = (session: PersistedSession, epoch: number): Promise<void> =>
91
+ /** The artifact as last written or read, so an identical write is skipped. */
92
+ let storedSession: PersistedSession | null = null;
93
+
94
+ const writeSession = (
95
+ session: { refreshToken: string; profile: Profile; idToken?: string | undefined },
96
+ epoch: number,
97
+ ): Promise<void> =>
78
98
  enqueueStoreOp(async () => {
79
99
  if (epoch !== sessionEpoch) {
80
100
  return; // The session this belonged to has already ended.
81
101
  }
82
- await secureStore.setItem(AUTH_STORAGE_KEY, JSON.stringify(session));
102
+ const artifact: PersistedSession = {
103
+ refreshToken: session.refreshToken,
104
+ profile: storedProfileFrom(session.profile),
105
+ idToken: session.idToken,
106
+ };
107
+ // A restore that rotated nothing used to write the Keychain again on
108
+ // every launch, on the startup path (AUTH-23).
109
+ if (storedSession && sameSession(storedSession, artifact)) {
110
+ return;
111
+ }
112
+ await secureStore.setItem(AUTH_STORAGE_KEY, JSON.stringify(artifact));
113
+ storedSession = artifact;
83
114
  });
84
115
 
85
116
  const tokenManager = createAccessTokenManager({
@@ -114,21 +145,30 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
114
145
  },
115
146
  });
116
147
 
148
+ /**
149
+ * The current snapshot, the same object until the state changes, so React reads
150
+ * it through `useSyncExternalStore` without re-rendering for nothing (PERF-02).
151
+ * Frozen: a consumer cannot change the machine's state by mutating it.
152
+ */
117
153
  function getState(): AuthState {
118
- return { ...state };
154
+ return state;
119
155
  }
120
156
 
121
157
  function setState(patch: Partial<AuthState>): void {
122
- state = { ...state, ...patch };
123
- // Snapshot, and isolate each listener: a subscriber that throws must not
124
- // stop the others or turn a completed sign-in into an error (CORE-03).
125
- for (const listener of [...listeners]) {
126
- try {
127
- listener(getState());
128
- } catch {
129
- /* consumer bug; the state transition itself already happened */
130
- }
158
+ const next = { ...state, ...patch };
159
+ // No change, no new snapshot and no notification.
160
+ if (shallowEqual(state, next)) return;
161
+ state = Object.freeze(next);
162
+ if (patch.error) {
163
+ // The code and audience only; `detail` is already redacted but stays local.
164
+ channel.emit(patch.error.code, patch.error.audience === 'user' ? 'info' : 'warn', {
165
+ status: state.status,
166
+ retryable: patch.error.retryable,
167
+ });
131
168
  }
169
+ // Isolated: a subscriber that throws neither stops the others nor turns a
170
+ // completed sign-in into an error; rn-core reports it instead (CORE-03).
171
+ listeners.notify(getState());
132
172
  }
133
173
 
134
174
  /** Ends the local session: memory, cached access tokens, and the stored artifact. */
@@ -136,6 +176,7 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
136
176
  sessionEpoch += 1;
137
177
  refreshToken = null;
138
178
  idToken = null;
179
+ storedSession = null;
139
180
  tokenManager.clear();
140
181
  await enqueueStoreOp(() => secureStore.removeItem(AUTH_STORAGE_KEY));
141
182
  }
@@ -143,12 +184,7 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
143
184
  return {
144
185
  getState,
145
186
 
146
- subscribe(listener) {
147
- listeners.add(listener);
148
- return () => {
149
- listeners.delete(listener);
150
- };
151
- },
187
+ subscribe: listener => listeners.add(listener),
152
188
 
153
189
  async restore() {
154
190
  sessionEpoch += 1;
@@ -179,6 +215,8 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
179
215
  return getState();
180
216
  }
181
217
 
218
+ storedSession = result.persisted ?? null;
219
+
182
220
  if (result.outcome === 'restored' && result.profile && result.refreshToken) {
183
221
  refreshToken = result.refreshToken;
184
222
  idToken = result.idToken ?? null;
@@ -320,16 +358,17 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
320
358
  return result;
321
359
  },
322
360
 
323
- async requestAccessToken(resourceKey) {
361
+ async requestAccessToken(resourceKey, tokenOptions) {
324
362
  const epoch = sessionEpoch;
325
363
  // Only a real network refresh is a `token-refreshing` phase; a cache hit
326
364
  // would otherwise flash every subscriber twice for nothing.
327
365
  const refreshing =
328
- state.status === 'authenticated' && !tokenManager.hasFreshToken(resourceKey);
366
+ state.status === 'authenticated' &&
367
+ (tokenOptions?.forceRefresh === true || !tokenManager.hasFreshToken(resourceKey));
329
368
  if (refreshing) {
330
369
  setState({ status: 'token-refreshing' });
331
370
  }
332
- const result = await tokenManager.request(resourceKey);
371
+ const result = await tokenManager.request(resourceKey, tokenOptions);
333
372
  // Restored only if nothing else moved the session meanwhile: a sign-out
334
373
  // during the refresh used to be overwritten back to `authenticated`.
335
374
  if (refreshing && epoch === sessionEpoch && state.status === 'token-refreshing') {
@@ -342,6 +381,11 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
342
381
  return state.accessTier ?? failClosedTier();
343
382
  },
344
383
 
384
+ canAccessPremium(): PremiumGateResult {
385
+ const tier = state.accessTier ?? failClosedTier();
386
+ return { allowed: tier.tier === 'premium' && tier.verification === 'verified', tier };
387
+ },
388
+
345
389
  requirePremium(): PremiumGateResult {
346
390
  const tier = state.accessTier ?? failClosedTier();
347
391
  const allowed = tier.tier === 'premium' && tier.verification === 'verified';
@@ -351,6 +395,10 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
351
395
  return { allowed, tier };
352
396
  },
353
397
 
398
+ getMessages(): AuthMessageMap {
399
+ return messages;
400
+ },
401
+
354
402
  getPlatformProvider() {
355
403
  return defaultProviderForPlatform(config, platform);
356
404
  },
@@ -362,3 +410,5 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
362
410
  }
363
411
 
364
412
  export type { Profile };
413
+ /** Every error the machine lands in also goes to the app's SDK sinks (CORE-08). */
414
+ const channel = createDiagnosticChannel<AuthErrorCode>('rn-auth');
@@ -41,7 +41,12 @@ export function mapClaimsToProfile(
41
41
  };
42
42
  }
43
43
 
44
- /** Describe the external provider identity behind the profile (FR-004 edge cases). */
44
+ /**
45
+ * Describe the external provider identity behind the profile (FR-004 edge
46
+ * cases). `emailBehavior` is what a screen needs to explain an address the user
47
+ * did not choose: Apple's private relay forwards mail and is not the address
48
+ * they type anywhere else.
49
+ */
45
50
  export function describeProviderIdentity(
46
51
  claims: Record<string, unknown>,
47
52
  providerId?: ProviderId,
@@ -22,11 +22,48 @@ import { mapClaimsToProfile } from './profile';
22
22
  import { evaluateAccessTier, failClosedTier } from '../access/accessTier';
23
23
  import { isAuthError } from '../diagnostics/errors';
24
24
 
25
+ /**
26
+ * The identity in the stored artifact: who, not what the token said.
27
+ *
28
+ * The whole claim set used to be written to the Keychain on every launch — all
29
+ * of it, groups included — for a value nothing reads back: the access tier is
30
+ * recomputed from fresh claims, and a restore that cannot reach the provider
31
+ * fails closed anyway (AUTH-23).
32
+ */
33
+ export type StoredProfile = Omit<Profile, 'claims'>;
34
+
25
35
  export interface PersistedSession {
26
36
  refreshToken: string;
27
- profile: Profile;
37
+ profile: StoredProfile;
28
38
  /** The id token hint for end-session (AUTH-04). Absent in artifacts written before 0.2.2. */
29
- idToken?: string;
39
+ idToken?: string | undefined;
40
+ }
41
+
42
+ /** The artifact's identity as a profile. Artifacts written before 0.4.0 carry claims; they are kept. */
43
+ export function profileFromStored(stored: StoredProfile | Profile): Profile {
44
+ return { ...stored, claims: 'claims' in stored ? (stored.claims ?? {}) : {} };
45
+ }
46
+
47
+ /** What goes to the store: everything but the claims. */
48
+ export function storedProfileFrom(profile: Profile): StoredProfile {
49
+ return {
50
+ tenantUserId: profile.tenantUserId,
51
+ displayName: profile.displayName,
52
+ email: profile.email,
53
+ provider: profile.provider,
54
+ };
55
+ }
56
+
57
+ /** True when the artifact would be byte-identical, so the write can be skipped. */
58
+ export function sameSession(a: PersistedSession, b: PersistedSession): boolean {
59
+ return (
60
+ a.refreshToken === b.refreshToken &&
61
+ a.idToken === b.idToken &&
62
+ a.profile.tenantUserId === b.profile.tenantUserId &&
63
+ a.profile.displayName === b.profile.displayName &&
64
+ a.profile.email === b.profile.email &&
65
+ a.profile.provider === b.profile.provider
66
+ );
30
67
  }
31
68
 
32
69
  export interface RestoreDeps {
@@ -42,9 +79,11 @@ export interface RestoreDeps {
42
79
  export interface RestoreResult {
43
80
  outcome: RestoreStatus;
44
81
  profile?: Profile;
82
+ /** The artifact this restore read, so a rewrite that changes nothing can be skipped. */
83
+ persisted?: PersistedSession;
45
84
  accessTier?: AccessTier;
46
85
  refreshToken?: string;
47
- idToken?: string;
86
+ idToken?: string | undefined;
48
87
  }
49
88
 
50
89
  export function parsePersistedSession(raw: string | null): PersistedSession | null {
@@ -82,7 +121,13 @@ export async function restoreSession(deps: RestoreDeps): Promise<RestoreResult>
82
121
  const hasClaims = Object.keys(tokens.claims).length > 0;
83
122
  return {
84
123
  outcome: 'restored',
85
- profile: hasClaims ? mapClaimsToProfile(tokens.claims) : persisted.profile,
124
+ persisted,
125
+ // The provider id came from the sign-in and is not in the claims: a
126
+ // refresh answered `idp: 'google.com'`, so a restored profile used to
127
+ // report a different provider than the same session did before (AUTH-10).
128
+ profile: hasClaims
129
+ ? mapClaimsToProfile(tokens.claims, persisted.profile.provider)
130
+ : profileFromStored(persisted.profile),
86
131
  accessTier: hasClaims ? evaluateAccessTier(tokens.claims, deps.mapping) : failClosedTier(),
87
132
  refreshToken: tokens.refreshToken || persisted.refreshToken,
88
133
  idToken: tokens.idToken || persisted.idToken,
@@ -96,7 +141,8 @@ export async function restoreSession(deps: RestoreDeps): Promise<RestoreResult>
96
141
  // read-only and fail closed on entitlement; the next restore tries again.
97
142
  return {
98
143
  outcome: 'offline',
99
- profile: persisted.profile,
144
+ persisted,
145
+ profile: profileFromStored(persisted.profile),
100
146
  accessTier: failClosedTier(),
101
147
  refreshToken: persisted.refreshToken,
102
148
  idToken: persisted.idToken,
@@ -5,6 +5,10 @@
5
5
  * in logs or error objects (FR-014).
6
6
  */
7
7
 
8
+ import { COMMON_PATTERNS, SdkError, redactPatterns } from '@chipmobilesdk/rn-core';
9
+
10
+ import type { AuthMessageKey } from './messageKeys';
11
+
8
12
  export type AuthErrorCode =
9
13
  | 'configuration-error'
10
14
  | 'provider-cancelled'
@@ -12,53 +16,63 @@ export type AuthErrorCode =
12
16
  | 'token-acquisition-failed'
13
17
  | 'session-expired'
14
18
  | 'consent-denied'
15
- | 'entitlement-unverified'
16
19
  | 'network-unavailable';
17
20
 
18
21
  export type AuthErrorAudience = 'user' | 'developer';
19
22
 
20
23
  export interface AuthErrorArgs {
21
24
  code: AuthErrorCode;
22
- messageKey: string;
25
+ /** A key of this package's catalogue: the copy an app shows for this error. */
26
+ messageKey: AuthMessageKey;
23
27
  audience?: AuthErrorAudience;
24
- detail?: string;
28
+ detail?: string | undefined;
25
29
  }
26
30
 
31
+ /** JWT-shaped values: three base64url segments separated by dots. */
32
+ const JWT_PATTERN = /[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g;
33
+ /** Any other long opaque token-like run; shorter than rn-core's payload run on purpose. */
34
+ const TOKEN_PATTERN = /[A-Za-z0-9_-]{24,}/g;
35
+
27
36
  /**
28
- * Replace token-like substrings (long URL-safe base64 / JWT segments) with a
29
- * placeholder so diagnostics never leak secrets or token values (FR-014).
37
+ * Replace token-like substrings with a placeholder so diagnostics never leak
38
+ * secrets or token values (FR-014): JWTs become `[redacted-token]`; URLs, e-mail
39
+ * addresses, UUIDs and other opaque runs become `[redacted]` (rn-core's patterns).
30
40
  */
31
41
  export function redact(value: string | undefined): string | undefined {
32
42
  if (!value) {
33
43
  return value;
34
44
  }
35
- return (
36
- value
37
- // JWT-shaped values: three base64url segments separated by dots.
38
- .replace(/[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, '[redacted-token]')
39
- // Any other long opaque token-like run.
40
- .replace(/[A-Za-z0-9_-]{24,}/g, '[redacted]')
41
- );
45
+ return redactPatterns(value.replace(JWT_PATTERN, '[redacted-token]'), [
46
+ ...COMMON_PATTERNS,
47
+ TOKEN_PATTERN,
48
+ ]);
42
49
  }
43
50
 
44
- export class AuthError extends Error {
45
- readonly code: AuthErrorCode;
51
+ /** Retrying can only help when the network was the problem. */
52
+ const RETRYABLE: ReadonlySet<AuthErrorCode> = new Set<AuthErrorCode>(['network-unavailable']);
53
+
54
+ /** The package's error; an `SdkError` (`@chipmobilesdk/rn-core`) with `pkg: 'rn-auth'`. */
55
+ export class AuthError extends SdkError<AuthErrorCode> {
46
56
  readonly audience: AuthErrorAudience;
47
- readonly messageKey: string;
48
- readonly detail?: string;
57
+ readonly messageKey: AuthMessageKey;
58
+ readonly detail?: string | undefined;
49
59
 
50
60
  constructor(args: AuthErrorArgs) {
51
- super(args.messageKey);
61
+ const detail = redact(args.detail);
62
+ super('rn-auth', args.code, {
63
+ message: args.messageKey,
64
+ retryable: RETRYABLE.has(args.code),
65
+ redactedCause: detail,
66
+ });
52
67
  this.name = 'AuthError';
53
- this.code = args.code;
54
68
  this.audience = args.audience ?? 'developer';
55
69
  this.messageKey = args.messageKey;
56
- this.detail = redact(args.detail);
70
+ this.detail = detail;
57
71
  }
58
72
  }
59
73
 
60
74
  export class AuthConfigError extends AuthError {
61
- constructor(messageKey: string, detail?: string) {
75
+ constructor(messageKey: AuthMessageKey, detail?: string) {
62
76
  super({ code: 'configuration-error', messageKey, audience: 'developer', detail });
63
77
  this.name = 'AuthConfigError';
64
78
  }
@@ -72,7 +86,7 @@ export function isAuthError(value: unknown): value is AuthError {
72
86
  export function toAuthError(
73
87
  value: unknown,
74
88
  fallbackCode: AuthErrorCode = 'token-acquisition-failed',
75
- fallbackMessageKey = 'auth.error.token',
89
+ fallbackMessageKey: AuthMessageKey = 'auth.error.token',
76
90
  ): AuthError {
77
91
  if (isAuthError(value)) {
78
92
  return value;
@@ -5,6 +5,8 @@
5
5
  * developer/tenant-configuration copy are kept separate (UX-005).
6
6
  */
7
7
 
8
+ import { defineMessages } from '@chipmobilesdk/rn-core';
9
+
8
10
  export interface AuthMessageMap {
9
11
  // User-facing actions and states
10
12
  'auth.action.continueGoogle': string;
@@ -32,28 +34,39 @@ export interface AuthMessageMap {
32
34
 
33
35
  export type AuthMessageKey = keyof AuthMessageMap;
34
36
 
35
- export const DEFAULT_MESSAGES: AuthMessageMap = {
36
- 'auth.action.continueGoogle': 'Continue with Google',
37
- 'auth.action.continueApple': 'Continue with Apple',
38
- 'auth.action.continueFacebook': 'Continue with Facebook',
39
- 'auth.state.signedOut': 'Sign in to continue',
40
- 'auth.state.authenticating': 'Signing you in…',
41
- 'auth.state.signedIn': 'You are signed in',
42
- 'auth.state.signingOut': 'Signing out…',
43
- 'auth.tier.free': 'Free',
44
- 'auth.tier.premium': 'Premium',
45
- 'auth.tier.unverified': 'Membership could not be verified',
46
- 'auth.error.cancelled': 'Sign-in was cancelled. You can try again.',
47
- 'auth.error.sessionExpired': 'Your session expired. Please sign in again.',
48
- 'auth.error.entitlement': 'This feature requires a verified premium membership.',
49
- 'auth.error.network': 'No connection. Some features are limited until you reconnect.',
50
- 'auth.error.configuration': 'Authentication is not configured correctly.',
51
- 'auth.error.redirectMismatch': 'The sign-in redirect does not match the app configuration.',
52
- 'auth.error.token': 'We could not complete the request. Please try again.',
53
- 'auth.error.consentDenied': 'Access to this resource was denied.',
54
- };
37
+ /**
38
+ * The catalog, as an rn-core message set: `user` copy may reach an end user,
39
+ * `developer` copy describes a tenant or configuration fault (UX-005). Register
40
+ * `authMessages.user` with rn-i18n to translate it like app copy.
41
+ */
42
+ export const authMessages = defineMessages({
43
+ user: {
44
+ 'auth.action.continueGoogle': 'Continue with Google',
45
+ 'auth.action.continueApple': 'Continue with Apple',
46
+ 'auth.action.continueFacebook': 'Continue with Facebook',
47
+ 'auth.state.signedOut': 'Sign in to continue',
48
+ 'auth.state.authenticating': 'Signing you in…',
49
+ 'auth.state.signedIn': 'You are signed in',
50
+ 'auth.state.signingOut': 'Signing out…',
51
+ 'auth.tier.free': 'Free',
52
+ 'auth.tier.premium': 'Premium',
53
+ 'auth.tier.unverified': 'Membership could not be verified',
54
+ 'auth.error.cancelled': 'Sign-in was cancelled. You can try again.',
55
+ 'auth.error.sessionExpired': 'Your session expired. Please sign in again.',
56
+ 'auth.error.entitlement': 'This feature requires a verified premium membership.',
57
+ 'auth.error.network': 'No connection. Some features are limited until you reconnect.',
58
+ },
59
+ developer: {
60
+ 'auth.error.configuration': 'Authentication is not configured correctly.',
61
+ 'auth.error.redirectMismatch': 'The sign-in redirect does not match the app configuration.',
62
+ 'auth.error.token': 'We could not complete the request. Please try again.',
63
+ 'auth.error.consentDenied': 'Access to this resource was denied.',
64
+ },
65
+ });
66
+
67
+ export const DEFAULT_MESSAGES: AuthMessageMap = authMessages.defaults;
55
68
 
56
69
  /** Resolve a message, preferring app overrides over the English defaults. */
57
70
  export function resolveMessages(overrides?: Partial<AuthMessageMap>): AuthMessageMap {
58
- return { ...DEFAULT_MESSAGES, ...(overrides ?? {}) };
71
+ return authMessages.resolve(overrides);
59
72
  }
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export type {
25
25
  ProviderIdentity,
26
26
  ProviderConfig,
27
27
  ProviderId,
28
+ KnownProviderId,
28
29
  DevicePlatform,
29
30
  ResourceConfig,
30
31
  ClaimMatcher,
@@ -37,11 +38,16 @@ export type {
37
38
  } from './config/types';
38
39
 
39
40
  // Ports + default adapters
41
+ export { describeProviderIdentity, mapClaimsToProfile } from './core/profile';
42
+
43
+ export type { AccessTokenRequestOptions } from './tokens/accessToken';
40
44
  export type { SecureTokenStore } from './tokens/secureStore';
41
45
  export { createKeychainSecureStore, AUTH_STORAGE_KEY } from './tokens/secureStore';
42
46
  export type {
43
47
  OidcAuthClient,
44
48
  OidcTokens,
49
+ OidcServiceConfiguration,
50
+ AppAuthClientOptions,
45
51
  AuthorizeParams,
46
52
  RefreshParams,
47
53
  EndSessionParams,
@@ -54,14 +60,16 @@ export { evaluateAccessTier, failClosedTier } from './access/accessTier';
54
60
  // Diagnostics
55
61
  export { AuthError, AuthConfigError, isAuthError, redact } from './diagnostics/errors';
56
62
  export type { AuthErrorCode, AuthErrorAudience } from './diagnostics/errors';
57
- export { DEFAULT_MESSAGES, resolveMessages } from './diagnostics/messageKeys';
63
+ export { DEFAULT_MESSAGES, authMessages, resolveMessages } from './diagnostics/messageKeys';
58
64
  export type { AuthMessageMap, AuthMessageKey } from './diagnostics/messageKeys';
59
65
 
60
66
  // React bindings
61
67
  export { AuthProvider } from './react/AuthProvider';
68
+ export { useAuthSelector } from './react/hooks';
62
69
  export type { AuthProviderProps } from './react/AuthProvider';
63
70
  export {
64
71
  useAuth,
72
+ useAuthMessages,
65
73
  useProfile,
66
74
  useAccessTier,
67
75
  useAccessToken,
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `@chipmobilesdk/rn-auth/keychain`: the default secure token store over
3
+ * react-native-keychain (AUTH-09). Its own entrypoint, so an app with its own
4
+ * secure storage never reaches this module.
5
+ */
6
+ export { createKeychainSecureStore, type KeychainSecureStoreOptions } from './tokens/secureStore';