@chipmobilesdk/rn-auth 0.4.0 → 0.6.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.
Files changed (180) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +74 -14
  3. package/lib/commonjs/access/accessTier.js +111 -0
  4. package/lib/commonjs/access/accessTier.js.map +1 -0
  5. package/lib/commonjs/app-auth.js +13 -0
  6. package/lib/commonjs/app-auth.js.map +1 -0
  7. package/lib/commonjs/config/types.js +50 -0
  8. package/lib/commonjs/config/types.js.map +1 -0
  9. package/lib/commonjs/config/validateConfig.js +114 -0
  10. package/lib/commonjs/config/validateConfig.js.map +1 -0
  11. package/lib/commonjs/core/authMachine.js +406 -0
  12. package/lib/commonjs/core/authMachine.js.map +1 -0
  13. package/lib/commonjs/core/profile.js +59 -0
  14. package/lib/commonjs/core/profile.js.map +1 -0
  15. package/lib/commonjs/core/session.js +125 -0
  16. package/lib/commonjs/core/session.js.map +1 -0
  17. package/lib/commonjs/diagnostics/errors.js +83 -0
  18. package/lib/commonjs/diagnostics/errors.js.map +1 -0
  19. package/lib/commonjs/diagnostics/messageKeys.js +51 -0
  20. package/lib/commonjs/diagnostics/messageKeys.js.map +1 -0
  21. package/lib/commonjs/index.js +175 -0
  22. package/lib/commonjs/index.js.map +1 -0
  23. package/lib/commonjs/keychain.js +13 -0
  24. package/lib/commonjs/keychain.js.map +1 -0
  25. package/lib/commonjs/oidc/authClient.js +157 -0
  26. package/lib/commonjs/oidc/authClient.js.map +1 -0
  27. package/lib/commonjs/oidc/classifyError.js +65 -0
  28. package/lib/commonjs/oidc/classifyError.js.map +1 -0
  29. package/lib/commonjs/oidc/socialRouting.js +58 -0
  30. package/lib/commonjs/oidc/socialRouting.js.map +1 -0
  31. package/lib/commonjs/package.json +1 -0
  32. package/lib/commonjs/react/AuthProvider.js +37 -0
  33. package/lib/commonjs/react/AuthProvider.js.map +1 -0
  34. package/lib/commonjs/react/hooks.js +122 -0
  35. package/lib/commonjs/react/hooks.js.map +1 -0
  36. package/lib/commonjs/testing/conformance.js +148 -0
  37. package/lib/commonjs/testing/conformance.js.map +1 -0
  38. package/lib/commonjs/testing/index.js +53 -0
  39. package/lib/commonjs/testing/index.js.map +1 -0
  40. package/lib/commonjs/tokens/accessToken.js +165 -0
  41. package/lib/commonjs/tokens/accessToken.js.map +1 -0
  42. package/lib/commonjs/tokens/secureStore.js +60 -0
  43. package/lib/commonjs/tokens/secureStore.js.map +1 -0
  44. package/lib/module/access/accessTier.js +106 -0
  45. package/lib/module/access/accessTier.js.map +1 -0
  46. package/lib/module/app-auth.js +9 -0
  47. package/lib/module/app-auth.js.map +1 -0
  48. package/lib/module/config/types.js +46 -0
  49. package/lib/module/config/types.js.map +1 -0
  50. package/lib/module/config/validateConfig.js +110 -0
  51. package/lib/module/config/validateConfig.js.map +1 -0
  52. package/lib/module/core/authMachine.js +402 -0
  53. package/lib/module/core/authMachine.js.map +1 -0
  54. package/lib/module/core/profile.js +54 -0
  55. package/lib/module/core/profile.js.map +1 -0
  56. package/lib/module/core/session.js +118 -0
  57. package/lib/module/core/session.js.map +1 -0
  58. package/lib/module/diagnostics/errors.js +74 -0
  59. package/lib/module/diagnostics/errors.js.map +1 -0
  60. package/lib/module/diagnostics/messageKeys.js +46 -0
  61. package/lib/module/diagnostics/messageKeys.js.map +1 -0
  62. package/lib/module/index.js +33 -0
  63. package/lib/module/index.js.map +1 -0
  64. package/lib/module/keychain.js +9 -0
  65. package/lib/module/keychain.js.map +1 -0
  66. package/lib/module/oidc/authClient.js +152 -0
  67. package/lib/module/oidc/authClient.js.map +1 -0
  68. package/lib/module/oidc/classifyError.js +60 -0
  69. package/lib/module/oidc/classifyError.js.map +1 -0
  70. package/lib/module/oidc/socialRouting.js +52 -0
  71. package/lib/module/oidc/socialRouting.js.map +1 -0
  72. package/lib/module/package.json +1 -0
  73. package/lib/module/react/AuthProvider.js +31 -0
  74. package/lib/module/react/AuthProvider.js.map +1 -0
  75. package/lib/module/react/hooks.js +111 -0
  76. package/lib/module/react/hooks.js.map +1 -0
  77. package/lib/module/testing/conformance.js +140 -0
  78. package/lib/module/testing/conformance.js.map +1 -0
  79. package/lib/module/testing/index.js +25 -0
  80. package/lib/module/testing/index.js.map +1 -0
  81. package/lib/module/tokens/accessToken.js +161 -0
  82. package/lib/module/tokens/accessToken.js.map +1 -0
  83. package/lib/module/tokens/secureStore.js +56 -0
  84. package/lib/module/tokens/secureStore.js.map +1 -0
  85. package/lib/typescript/commonjs/access/accessTier.d.ts +14 -0
  86. package/lib/typescript/commonjs/access/accessTier.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/app-auth.d.ts +7 -0
  88. package/lib/typescript/commonjs/app-auth.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/config/types.d.ts +171 -0
  90. package/lib/typescript/commonjs/config/types.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/config/validateConfig.d.ts +8 -0
  92. package/lib/typescript/commonjs/config/validateConfig.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/core/authMachine.d.ts +14 -0
  94. package/lib/typescript/commonjs/core/authMachine.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/core/profile.d.ts +15 -0
  96. package/lib/typescript/commonjs/core/profile.d.ts.map +1 -0
  97. package/lib/typescript/commonjs/core/session.d.ts +60 -0
  98. package/lib/typescript/commonjs/core/session.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/diagnostics/errors.d.ts +37 -0
  100. package/lib/typescript/commonjs/diagnostics/errors.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/diagnostics/messageKeys.d.ts +57 -0
  102. package/lib/typescript/commonjs/diagnostics/messageKeys.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/index.d.ts +27 -0
  104. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/keychain.d.ts +7 -0
  106. package/lib/typescript/commonjs/keychain.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/oidc/authClient.d.ts +75 -0
  108. package/lib/typescript/commonjs/oidc/authClient.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/oidc/classifyError.d.ts +28 -0
  110. package/lib/typescript/commonjs/oidc/classifyError.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/oidc/socialRouting.d.ts +22 -0
  112. package/lib/typescript/commonjs/oidc/socialRouting.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/package.json +1 -0
  114. package/lib/typescript/commonjs/react/AuthProvider.d.ts +17 -0
  115. package/lib/typescript/commonjs/react/AuthProvider.d.ts.map +1 -0
  116. package/lib/typescript/commonjs/react/hooks.d.ts +42 -0
  117. package/lib/typescript/commonjs/react/hooks.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/testing/conformance.d.ts +32 -0
  119. package/lib/typescript/commonjs/testing/conformance.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/testing/index.d.ts +11 -0
  121. package/lib/typescript/commonjs/testing/index.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/tokens/accessToken.d.ts +45 -0
  123. package/lib/typescript/commonjs/tokens/accessToken.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/tokens/secureStore.d.ts +26 -0
  125. package/lib/typescript/commonjs/tokens/secureStore.d.ts.map +1 -0
  126. package/lib/typescript/module/access/accessTier.d.ts +14 -0
  127. package/lib/typescript/module/access/accessTier.d.ts.map +1 -0
  128. package/lib/typescript/module/app-auth.d.ts +7 -0
  129. package/lib/typescript/module/app-auth.d.ts.map +1 -0
  130. package/lib/typescript/module/config/types.d.ts +171 -0
  131. package/lib/typescript/module/config/types.d.ts.map +1 -0
  132. package/lib/typescript/module/config/validateConfig.d.ts +8 -0
  133. package/lib/typescript/module/config/validateConfig.d.ts.map +1 -0
  134. package/lib/typescript/module/core/authMachine.d.ts +14 -0
  135. package/lib/typescript/module/core/authMachine.d.ts.map +1 -0
  136. package/lib/typescript/module/core/profile.d.ts +15 -0
  137. package/lib/typescript/module/core/profile.d.ts.map +1 -0
  138. package/lib/typescript/module/core/session.d.ts +60 -0
  139. package/lib/typescript/module/core/session.d.ts.map +1 -0
  140. package/lib/typescript/module/diagnostics/errors.d.ts +37 -0
  141. package/lib/typescript/module/diagnostics/errors.d.ts.map +1 -0
  142. package/lib/typescript/module/diagnostics/messageKeys.d.ts +57 -0
  143. package/lib/typescript/module/diagnostics/messageKeys.d.ts.map +1 -0
  144. package/lib/typescript/module/index.d.ts +27 -0
  145. package/lib/typescript/module/index.d.ts.map +1 -0
  146. package/lib/typescript/module/keychain.d.ts +7 -0
  147. package/lib/typescript/module/keychain.d.ts.map +1 -0
  148. package/lib/typescript/module/oidc/authClient.d.ts +75 -0
  149. package/lib/typescript/module/oidc/authClient.d.ts.map +1 -0
  150. package/lib/typescript/module/oidc/classifyError.d.ts +28 -0
  151. package/lib/typescript/module/oidc/classifyError.d.ts.map +1 -0
  152. package/lib/typescript/module/oidc/socialRouting.d.ts +22 -0
  153. package/lib/typescript/module/oidc/socialRouting.d.ts.map +1 -0
  154. package/lib/typescript/module/package.json +1 -0
  155. package/lib/typescript/module/react/AuthProvider.d.ts +17 -0
  156. package/lib/typescript/module/react/AuthProvider.d.ts.map +1 -0
  157. package/lib/typescript/module/react/hooks.d.ts +42 -0
  158. package/lib/typescript/module/react/hooks.d.ts.map +1 -0
  159. package/lib/typescript/module/testing/conformance.d.ts +32 -0
  160. package/lib/typescript/module/testing/conformance.d.ts.map +1 -0
  161. package/lib/typescript/module/testing/index.d.ts +11 -0
  162. package/lib/typescript/module/testing/index.d.ts.map +1 -0
  163. package/lib/typescript/module/tokens/accessToken.d.ts +45 -0
  164. package/lib/typescript/module/tokens/accessToken.d.ts.map +1 -0
  165. package/lib/typescript/module/tokens/secureStore.d.ts +26 -0
  166. package/lib/typescript/module/tokens/secureStore.d.ts.map +1 -0
  167. package/package.json +68 -21
  168. package/skills/sdk-rn-auth/SKILL.md +35 -2
  169. package/src/config/types.ts +53 -5
  170. package/src/config/validateConfig.ts +32 -1
  171. package/src/core/authMachine.ts +40 -6
  172. package/src/core/profile.ts +6 -1
  173. package/src/core/session.ts +49 -3
  174. package/src/diagnostics/errors.ts +7 -5
  175. package/src/index.ts +7 -0
  176. package/src/oidc/authClient.ts +19 -1
  177. package/src/oidc/classifyError.ts +2 -1
  178. package/src/oidc/socialRouting.ts +3 -1
  179. package/src/react/hooks.ts +53 -14
  180. package/src/tokens/accessToken.ts +20 -9
@@ -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,13 +22,50 @@ 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
39
  idToken?: string | undefined;
30
40
  }
31
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
+ );
67
+ }
68
+
32
69
  export interface RestoreDeps {
33
70
  secureStore: SecureTokenStore;
34
71
  authority: string;
@@ -42,6 +79,8 @@ 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
86
  idToken?: string | undefined;
@@ -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,
@@ -7,6 +7,8 @@
7
7
 
8
8
  import { COMMON_PATTERNS, SdkError, redactPatterns } from '@chipmobilesdk/rn-core';
9
9
 
10
+ import type { AuthMessageKey } from './messageKeys';
11
+
10
12
  export type AuthErrorCode =
11
13
  | 'configuration-error'
12
14
  | 'provider-cancelled'
@@ -14,14 +16,14 @@ export type AuthErrorCode =
14
16
  | 'token-acquisition-failed'
15
17
  | 'session-expired'
16
18
  | 'consent-denied'
17
- | 'entitlement-unverified'
18
19
  | 'network-unavailable';
19
20
 
20
21
  export type AuthErrorAudience = 'user' | 'developer';
21
22
 
22
23
  export interface AuthErrorArgs {
23
24
  code: AuthErrorCode;
24
- messageKey: string;
25
+ /** A key of this package's catalogue: the copy an app shows for this error. */
26
+ messageKey: AuthMessageKey;
25
27
  audience?: AuthErrorAudience;
26
28
  detail?: string | undefined;
27
29
  }
@@ -52,7 +54,7 @@ const RETRYABLE: ReadonlySet<AuthErrorCode> = new Set<AuthErrorCode>(['network-u
52
54
  /** The package's error; an `SdkError` (`@chipmobilesdk/rn-core`) with `pkg: 'rn-auth'`. */
53
55
  export class AuthError extends SdkError<AuthErrorCode> {
54
56
  readonly audience: AuthErrorAudience;
55
- readonly messageKey: string;
57
+ readonly messageKey: AuthMessageKey;
56
58
  readonly detail?: string | undefined;
57
59
 
58
60
  constructor(args: AuthErrorArgs) {
@@ -70,7 +72,7 @@ export class AuthError extends SdkError<AuthErrorCode> {
70
72
  }
71
73
 
72
74
  export class AuthConfigError extends AuthError {
73
- constructor(messageKey: string, detail?: string) {
75
+ constructor(messageKey: AuthMessageKey, detail?: string) {
74
76
  super({ code: 'configuration-error', messageKey, audience: 'developer', detail });
75
77
  this.name = 'AuthConfigError';
76
78
  }
@@ -84,7 +86,7 @@ export function isAuthError(value: unknown): value is AuthError {
84
86
  export function toAuthError(
85
87
  value: unknown,
86
88
  fallbackCode: AuthErrorCode = 'token-acquisition-failed',
87
- fallbackMessageKey = 'auth.error.token',
89
+ fallbackMessageKey: AuthMessageKey = 'auth.error.token',
88
90
  ): AuthError {
89
91
  if (isAuthError(value)) {
90
92
  return value;
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,
@@ -63,6 +69,7 @@ export { useAuthSelector } from './react/hooks';
63
69
  export type { AuthProviderProps } from './react/AuthProvider';
64
70
  export {
65
71
  useAuth,
72
+ useAuthMessages,
66
73
  useProfile,
67
74
  useAccessTier,
68
75
  useAccessToken,
@@ -116,9 +116,24 @@ function base64UrlDecode(input: string): string {
116
116
  }
117
117
  }
118
118
 
119
+ /** The endpoints discovery would have returned, when an app already knows them. */
120
+ export interface OidcServiceConfiguration {
121
+ authorizationEndpoint: string;
122
+ tokenEndpoint: string;
123
+ endSessionEndpoint?: string;
124
+ revocationEndpoint?: string;
125
+ registrationEndpoint?: string;
126
+ }
127
+
119
128
  export interface AppAuthClientOptions {
120
129
  /** When true, the OIDC discovery document is fetched from `${authority}`. */
121
130
  useDiscovery?: boolean;
131
+ /**
132
+ * Static endpoints, used instead of discovery. Every authorize, refresh and
133
+ * sign-out otherwise costs the native module one `.well-known` round-trip,
134
+ * including the refresh on the startup path (AUTH-08).
135
+ */
136
+ serviceConfiguration?: OidcServiceConfiguration;
122
137
  }
123
138
 
124
139
  interface AppAuthModule {
@@ -165,7 +180,9 @@ const AppAuth: AppAuthModule = {
165
180
  * client). Validated on device (R8).
166
181
  */
167
182
  export function createAppAuthClient(options: AppAuthClientOptions = {}): OidcAuthClient {
168
- const useDiscovery = options.useDiscovery ?? true;
183
+ const serviceConfiguration = options.serviceConfiguration;
184
+ // Discovery is pointless once the endpoints are given, whatever was asked for.
185
+ const useDiscovery = serviceConfiguration ? false : (options.useDiscovery ?? true);
169
186
 
170
187
  const baseConfig = (
171
188
  authority: string,
@@ -174,6 +191,7 @@ export function createAppAuthClient(options: AppAuthClientOptions = {}): OidcAut
174
191
  scopes: string[],
175
192
  ) => ({
176
193
  issuer: useDiscovery ? authority : undefined,
194
+ ...(serviceConfiguration ? { serviceConfiguration } : {}),
177
195
  clientId,
178
196
  redirectUrl,
179
197
  scopes,
@@ -12,10 +12,11 @@
12
12
  */
13
13
 
14
14
  import { AuthError, type AuthErrorCode } from '../diagnostics/errors';
15
+ import type { AuthMessageKey } from '../diagnostics/messageKeys';
15
16
 
16
17
  export interface ClassificationRule {
17
18
  readonly code: AuthErrorCode;
18
- readonly messageKey: string;
19
+ readonly messageKey: AuthMessageKey;
19
20
  /** Matched against the native error's `code` and `message`, case-insensitively. */
20
21
  readonly pattern: RegExp;
21
22
  }
@@ -37,7 +37,9 @@ export function resolveProvider(
37
37
 
38
38
  /** Build the OIDC additional parameters that route to the chosen IdP. */
39
39
  export function buildAdditionalParameters(provider: ProviderConfig): Record<string, string> {
40
- return { domain_hint: provider.domainHint };
40
+ // The routing hint last: a provider's own parameters cannot redirect the flow
41
+ // to a different IdP than the one the user tapped.
42
+ return { ...provider.additionalParameters, domain_hint: provider.domainHint };
41
43
  }
42
44
 
43
45
  /**
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { useExternalStore } from '@chipmobilesdk/rn-core/react';
8
- import { useCallback, useState } from 'react';
8
+ import { useCallback, useEffect, useRef, useState } from 'react';
9
9
  import { useAuthInstance } from './AuthProvider';
10
10
  import type {
11
11
  AccessTier,
@@ -18,6 +18,8 @@ import type {
18
18
  ProviderId,
19
19
  } from '../config/types';
20
20
  import { failClosedTier } from '../access/accessTier';
21
+ import type { AccessTokenRequestOptions } from '../tokens/accessToken';
22
+ import type { AuthMessageMap } from '../diagnostics/messageKeys';
21
23
 
22
24
  export interface UseAuthResult {
23
25
  state: AuthState;
@@ -25,6 +27,9 @@ export interface UseAuthResult {
25
27
  signOut(): Promise<void>;
26
28
  signOutGlobal(): Promise<GlobalSignOutResult>;
27
29
  restore(): Promise<AuthState>;
30
+ /** Whether a premium-only path is open. Changes nothing: show the denial where it happened. */
31
+ canAccessPremium(): PremiumGateResult;
32
+ /** @deprecated Use `canAccessPremium`; this moves the whole controller to `access-denied` (AUTH-12). */
28
33
  requirePremium(): PremiumGateResult;
29
34
  }
30
35
 
@@ -53,10 +58,16 @@ export function useAuth(): UseAuthResult {
53
58
  signOut: useCallback(() => auth.signOut(), [auth]),
54
59
  signOutGlobal: useCallback(() => auth.signOutGlobal(), [auth]),
55
60
  restore: useCallback(() => auth.restore(), [auth]),
61
+ canAccessPremium: useCallback(() => auth.canAccessPremium(), [auth]),
56
62
  requirePremium: useCallback(() => auth.requirePremium(), [auth]),
57
63
  };
58
64
  }
59
65
 
66
+ /** This package's copy, with the app's `messages` overrides applied. */
67
+ export function useAuthMessages(): AuthMessageMap {
68
+ return useAuthInstance().getMessages();
69
+ }
70
+
60
71
  export function useProfile(): Profile | undefined {
61
72
  return useAuthSelector(selectProfile);
62
73
  }
@@ -83,21 +94,49 @@ export type UseAccessTokenStatus =
83
94
  export interface UseAccessTokenResult {
84
95
  status: UseAccessTokenStatus;
85
96
  token?: string | undefined;
86
- request(): Promise<AccessTokenResult>;
97
+ /** `{ forceRefresh: true }` after a server refused the token this returned. */
98
+ request(options?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
87
99
  }
88
100
 
89
101
  export function useAccessToken(resourceKey: string): UseAccessTokenResult {
90
102
  const auth = useAuthInstance();
91
- const [status, setStatus] = useState<UseAccessTokenStatus>('idle');
92
- const [token, setToken] = useState<string | undefined>(undefined);
93
-
94
- const request = useCallback(async () => {
95
- setStatus('loading');
96
- const result = await auth.requestAccessToken(resourceKey);
97
- setStatus(result.status);
98
- setToken(result.token);
99
- return result;
100
- }, [auth, resourceKey]);
101
-
102
- return { status, token, request };
103
+ const [state, setState] = useState<{
104
+ status: UseAccessTokenStatus;
105
+ token?: string | undefined;
106
+ resourceKey: string;
107
+ }>({ status: 'idle', resourceKey });
108
+
109
+ // Which request may still write. A result for a resource the component has
110
+ // moved on from, or for an unmounted one, is dropped rather than applied
111
+ // (AUTH-17, standard VII).
112
+ const live = useRef(0);
113
+ useEffect(
114
+ () => () => {
115
+ live.current += 1;
116
+ },
117
+ [],
118
+ );
119
+
120
+ const request = useCallback(
121
+ async (options?: AccessTokenRequestOptions) => {
122
+ live.current += 1;
123
+ const id = live.current;
124
+ setState({ status: 'loading', resourceKey });
125
+ const result = await auth.requestAccessToken(resourceKey, options);
126
+ if (id === live.current) {
127
+ setState({ status: result.status, token: result.token, resourceKey });
128
+ }
129
+ return result;
130
+ },
131
+ [auth, resourceKey],
132
+ );
133
+
134
+ // A new resource starts from `idle`, not from the previous resource's token.
135
+ if (state.resourceKey !== resourceKey) {
136
+ live.current += 1;
137
+ setState({ status: 'idle', resourceKey });
138
+ return { status: 'idle', token: undefined, request };
139
+ }
140
+
141
+ return { status: state.status, token: state.token, request };
103
142
  }
@@ -13,7 +13,6 @@ const EXPIRY_SKEW_MS = 60_000;
13
13
  interface CachedToken {
14
14
  token: string;
15
15
  expiresAt: number;
16
- scopes: string[];
17
16
  }
18
17
 
19
18
  export interface AccessTokenManagerDeps {
@@ -37,8 +36,19 @@ export interface AccessTokenManagerDeps {
37
36
  onRefreshTokenRotated: (token: string, epoch: number) => Promise<void>;
38
37
  }
39
38
 
39
+ export interface AccessTokenRequestOptions {
40
+ /**
41
+ * Ignore the cached token and ask the provider for a new one.
42
+ *
43
+ * For the caller that just had one rejected: a token the server refused is
44
+ * still unexpired by this device's clock, so asking again without this
45
+ * returns the same token and the retry is the same request (AI-01).
46
+ */
47
+ forceRefresh?: boolean;
48
+ }
49
+
40
50
  export interface AccessTokenManager {
41
- request(resourceKey: string): Promise<AccessTokenResult>;
51
+ request(resourceKey: string, options?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
42
52
  /** Whether `request` would answer from the cache, without touching the network. */
43
53
  hasFreshToken(resourceKey: string): boolean;
44
54
  clear(): void;
@@ -94,11 +104,7 @@ export function createAccessTokenManager(deps: AccessTokenManagerDeps): AccessTo
94
104
  return { resourceKey, status: 'error', errorCode: 'token-acquisition-failed' };
95
105
  }
96
106
  const expiresAt = tokens.accessTokenExpiresAt ?? Date.now() + 3_600_000;
97
- cache.set(resourceKey, {
98
- token: tokens.accessToken,
99
- expiresAt,
100
- scopes: resource.scopes,
101
- });
107
+ cache.set(resourceKey, { token: tokens.accessToken, expiresAt });
102
108
  return { resourceKey, status: 'success', token: tokens.accessToken, expiresAt };
103
109
  } catch (error) {
104
110
  if (isAuthError(error)) {
@@ -127,13 +133,18 @@ export function createAccessTokenManager(deps: AccessTokenManagerDeps): AccessTo
127
133
  return fresh(resourceKey) !== undefined;
128
134
  },
129
135
 
130
- request(resourceKey): Promise<AccessTokenResult> {
136
+ request(resourceKey, options = {}): Promise<AccessTokenResult> {
131
137
  const resource = deps.resources.find(entry => entry.key === resourceKey);
132
138
  if (!resource) {
133
139
  return Promise.resolve({ resourceKey, status: 'error', errorCode: 'configuration-error' });
134
140
  }
135
141
 
136
- const cached = fresh(resourceKey);
142
+ if (options.forceRefresh === true) {
143
+ // Dropped, not just skipped: the token is known bad, so no later
144
+ // caller should be handed it either.
145
+ cache.delete(resourceKey);
146
+ }
147
+ const cached = options.forceRefresh === true ? undefined : fresh(resourceKey);
137
148
  if (cached) {
138
149
  return Promise.resolve({
139
150
  resourceKey,