@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
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Public hooks (LIB-002). Each reads the controller's state through
3
+ * `useSyncExternalStore` (rn-core's `useExternalStore`), selecting only what it
4
+ * returns, so `useProfile` does not re-render when only the status changes.
5
+ */
6
+ import type { AccessTier, AccessTokenResult, AuthState, GlobalSignOutResult, PremiumGateResult, Profile, ProviderConfig, ProviderId } from '../config/types.js';
7
+ import type { AccessTokenRequestOptions } from '../tokens/accessToken.js';
8
+ import type { AuthMessageMap } from '../diagnostics/messageKeys.js';
9
+ export interface UseAuthResult {
10
+ state: AuthState;
11
+ signIn(providerId: ProviderId): Promise<AuthState>;
12
+ signOut(): Promise<void>;
13
+ signOutGlobal(): Promise<GlobalSignOutResult>;
14
+ restore(): Promise<AuthState>;
15
+ /** Whether a premium-only path is open. Changes nothing: show the denial where it happened. */
16
+ canAccessPremium(): PremiumGateResult;
17
+ /** @deprecated Use `canAccessPremium`; this moves the whole controller to `access-denied` (AUTH-12). */
18
+ requirePremium(): PremiumGateResult;
19
+ }
20
+ /**
21
+ * Any slice of the auth state; the component re-renders only when the slice
22
+ * changes by `isEqual` (default `Object.is`).
23
+ */
24
+ export declare function useAuthSelector<Selected>(select: (state: AuthState) => Selected, isEqual?: (a: Selected, b: Selected) => boolean): Selected;
25
+ export declare function useAuth(): UseAuthResult;
26
+ /** This package's copy, with the app's `messages` overrides applied. */
27
+ export declare function useAuthMessages(): AuthMessageMap;
28
+ export declare function useProfile(): Profile | undefined;
29
+ export declare function useAccessTier(): AccessTier;
30
+ /** The first provider to present on the current platform (UX-004), or undefined. */
31
+ export declare function useSignInProvider(): ProviderConfig | undefined;
32
+ /** All available providers for the current platform, in configuration order (FR-003/FR-004). */
33
+ export declare function useSignInProviders(): ProviderConfig[];
34
+ export type UseAccessTokenStatus = 'idle' | 'loading' | 'success' | 'denied' | 'unavailable' | 'error';
35
+ export interface UseAccessTokenResult {
36
+ status: UseAccessTokenStatus;
37
+ token?: string | undefined;
38
+ /** `{ forceRefresh: true }` after a server refused the token this returned. */
39
+ request(options?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
40
+ }
41
+ export declare function useAccessToken(resourceKey: string): UseAccessTokenResult;
42
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../../src/react/hooks.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EACX,UAAU,EACV,iBAAiB,EACjB,SAAS,EACT,mBAAmB,EACnB,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,UAAU,EACV,MAAM,oBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,0BAAuB,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA4B,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACnD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,aAAa,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC9C,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC;IAC9B,+FAA+F;IAC/F,gBAAgB,IAAI,iBAAiB,CAAC;IACtC,wGAAwG;IACxG,cAAc,IAAI,iBAAiB,CAAC;CACpC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EACvC,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,QAAQ,EACtC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,KAAK,OAAO,GAC7C,QAAQ,CAGV;AAMD,wBAAgB,OAAO,IAAI,aAAa,CAYvC;AAED,wEAAwE;AACxE,wBAAgB,eAAe,IAAI,cAAc,CAEhD;AAED,wBAAgB,UAAU,IAAI,OAAO,GAAG,SAAS,CAEhD;AAED,wBAAgB,aAAa,IAAI,UAAU,CAE1C;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,SAAS,CAG9D;AAED,gGAAgG;AAChG,wBAAgB,kBAAkB,IAAI,cAAc,EAAE,CAGrD;AAED,MAAM,MAAM,oBAAoB,GAC/B,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,GAAG,OAAO,CAAC;AAErE,MAAM,WAAW,oBAAoB;IACpC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,+EAA+E;IAC/E,OAAO,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACzE;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,oBAAoB,CAyCxE"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * What any `SecureTokenStore` and `OidcAuthClient` must do (IOS-12, AUTH-13).
3
+ *
4
+ * The same checks run against the in-memory doubles and against the keychain and
5
+ * app-auth adapters (native modules mocked in Jest, or for real from a demo
6
+ * screen on a device), so a test that drives a double proves something about the
7
+ * device path. Each check builds its own subject through a harness.
8
+ */
9
+ import { type Check, type ConformanceReport } from '@chipmobilesdk/rn-core/testing';
10
+ import type { OidcAuthClient } from '../oidc/authClient.js';
11
+ import type { SecureTokenStore } from '../tokens/secureStore.js';
12
+ export interface SecureStoreHarness {
13
+ /** An empty store. */
14
+ create(): SecureTokenStore;
15
+ }
16
+ export interface OidcSubject {
17
+ readonly client: OidcAuthClient;
18
+ /** The identity provider answers sign-in and refresh with tokens for these claims. */
19
+ issues(claims: Record<string, unknown>): void;
20
+ /** The user closes the sign-in sheet. */
21
+ userCancels(): void;
22
+ /** The provider no longer accepts the refresh token. */
23
+ rejectsRefresh(): void;
24
+ }
25
+ export interface OidcHarness {
26
+ create(): OidcSubject;
27
+ }
28
+ export declare const secureTokenStoreChecks: readonly Check<SecureStoreHarness>[];
29
+ export declare const oidcAuthClientChecks: readonly Check<OidcHarness>[];
30
+ export declare function runSecureTokenStoreConformance(harness: SecureStoreHarness): Promise<ConformanceReport>;
31
+ export declare function runOidcAuthClientConformance(harness: OidcHarness): Promise<ConformanceReport>;
32
+ //# sourceMappingURL=conformance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../../../../src/testing/conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAGN,KAAK,KAAK,EACV,KAAK,iBAAiB,EACtB,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAAuB,CAAC;AAE9D,MAAM,WAAW,kBAAkB;IAClC,sBAAsB;IACtB,MAAM,IAAI,gBAAgB,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,sFAAsF;IACtF,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9C,yCAAyC;IACzC,WAAW,IAAI,IAAI,CAAC;IACpB,wDAAwD;IACxD,cAAc,IAAI,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC3B,MAAM,IAAI,WAAW,CAAC;CACtB;AAKD,eAAO,MAAM,sBAAsB,EAAE,SAAS,KAAK,CAAC,kBAAkB,CAAC,EA+CtE,CAAC;AAaF,eAAO,MAAM,oBAAoB,EAAE,SAAS,KAAK,CAAC,WAAW,CAAC,EAkE7D,CAAC;AAEF,wBAAgB,8BAA8B,CAC7C,OAAO,EAAE,kBAAkB,GACzB,OAAO,CAAC,iBAAiB,CAAC,CAE5B;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE7F"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `@chipmobilesdk/rn-auth/testing`: test doubles and the port conformance suites.
3
+ *
4
+ * A separate entrypoint so neither can reach a production bundle.
5
+ */
6
+ import type { SecureTokenStore } from '../tokens/secureStore.js';
7
+ /** A `SecureTokenStore` held in memory, for tests and for the conformance suite. */
8
+ export declare function createMemorySecureStore(seed?: Record<string, string>): SecureTokenStore;
9
+ export { oidcAuthClientChecks, runOidcAuthClientConformance, runSecureTokenStoreConformance, secureTokenStoreChecks, } from './conformance.js';
10
+ export type { OidcHarness, OidcSubject, SecureStoreHarness } from './conformance.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAAuB,CAAC;AAE9D,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,gBAAgB,CAa3F;AAED,OAAO,EACN,oBAAoB,EACpB,4BAA4B,EAC5B,8BAA8B,EAC9B,sBAAsB,GACtB,MAAM,kBAAe,CAAC;AACvB,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,kBAAe,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Per-resource delegated access-token acquisition with an in-memory cache and
3
+ * silent refresh (FR-007, PERF-003, R4). Access tokens are never persisted to
4
+ * disk (FR-021) — only kept in memory keyed by resource until expiry.
5
+ */
6
+ import type { AccessTokenResult, ResourceConfig } from '../config/types.js';
7
+ import type { OidcAuthClient } from '../oidc/authClient.js';
8
+ export interface AccessTokenManagerDeps {
9
+ authority: string;
10
+ clientId: string;
11
+ redirectUrl: string;
12
+ resources: ResourceConfig[];
13
+ authClient: OidcAuthClient;
14
+ getRefreshToken: () => string | null;
15
+ /**
16
+ * The session generation, bumped by every sign-in, sign-out and restore. A
17
+ * refresh that started under one generation and finished under another
18
+ * belongs to a session that no longer exists, so its result is discarded.
19
+ */
20
+ sessionEpoch: () => number;
21
+ /**
22
+ * The provider rotated the refresh token. Resolves once the new token is
23
+ * live, and persisted where it can be: the old one may already be dead, so a
24
+ * restart must not come back with it.
25
+ */
26
+ onRefreshTokenRotated: (token: string, epoch: number) => Promise<void>;
27
+ }
28
+ export interface AccessTokenRequestOptions {
29
+ /**
30
+ * Ignore the cached token and ask the provider for a new one.
31
+ *
32
+ * For the caller that just had one rejected: a token the server refused is
33
+ * still unexpired by this device's clock, so asking again without this
34
+ * returns the same token and the retry is the same request (AI-01).
35
+ */
36
+ forceRefresh?: boolean;
37
+ }
38
+ export interface AccessTokenManager {
39
+ request(resourceKey: string, options?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
40
+ /** Whether `request` would answer from the cache, without touching the network. */
41
+ hasFreshToken(resourceKey: string): boolean;
42
+ clear(): void;
43
+ }
44
+ export declare function createAccessTokenManager(deps: AccessTokenManagerDeps): AccessTokenManager;
45
+ //# sourceMappingURL=accessToken.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessToken.d.ts","sourceRoot":"","sources":["../../../../src/tokens/accessToken.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,oBAAiB,CAAC;AACzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAoB,CAAC;AAUzD,MAAM,WAAW,sBAAsB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,UAAU,EAAE,cAAc,CAAC;IAC3B,eAAe,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,YAAY,EAAE,MAAM,MAAM,CAAC;IAC3B;;;;OAIG;IACH,qBAAqB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,yBAAyB;IACzC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9F,mFAAmF;IACnF,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5C,KAAK,IAAI,IAAI,CAAC;CACd;AAOD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,sBAAsB,GAAG,kBAAkB,CA4GzF"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * SecureTokenStore port + default react-native-keychain adapter (FR-021, R2).
3
+ *
4
+ * Refresh tokens and the session-restore artifact persist ONLY through this
5
+ * port, backed by platform secure storage (iOS Keychain / Android Keystore).
6
+ * Access tokens are never written here — they stay in memory (see accessToken).
7
+ * The default adapter loads the native module on its first read or write, so
8
+ * creating it, and unit tests that inject a fake, load no native code.
9
+ */
10
+ /** Single key under which the persisted session artifact is stored. */
11
+ export declare const AUTH_STORAGE_KEY = "@chipmobilesdk/rn-auth/session";
12
+ export interface SecureTokenStore {
13
+ getItem(key: string): Promise<string | null>;
14
+ setItem(key: string, value: string): Promise<void>;
15
+ removeItem(key: string): Promise<void>;
16
+ }
17
+ export interface KeychainSecureStoreOptions {
18
+ /** Optional Keychain access-control accessibility constant. */
19
+ accessible?: string;
20
+ }
21
+ /**
22
+ * Default secure store backed by react-native-keychain. Each logical key maps to
23
+ * a Keychain "service" so multiple keys can coexist. Validated on device.
24
+ */
25
+ export declare function createKeychainSecureStore(options?: KeychainSecureStoreOptions): SecureTokenStore;
26
+ //# sourceMappingURL=secureStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secureStore.d.ts","sourceRoot":"","sources":["../../../../src/tokens/secureStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,mCAAmC,CAAC;AAEjE,MAAM,WAAW,gBAAgB;IAChC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,0BAA0B;IAC1C,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACxC,OAAO,GAAE,0BAA+B,GACtC,gBAAgB,CAoBlB"}
package/package.json CHANGED
@@ -1,30 +1,56 @@
1
1
  {
2
2
  "name": "@chipmobilesdk/rn-auth",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
- "main": "src/index.ts",
7
- "types": "src/index.ts",
6
+ "source": "./src/index.ts",
7
+ "main": "./lib/commonjs/index.js",
8
+ "module": "./lib/module/index.js",
9
+ "types": "./lib/typescript/commonjs/index.d.ts",
8
10
  "exports": {
9
11
  ".": {
10
- "types": "./src/index.ts",
11
- "react-native": "./src/index.ts",
12
- "default": "./src/index.ts"
12
+ "chipmobilesdk-source": "./src/index.ts",
13
+ "import": {
14
+ "types": "./lib/typescript/module/index.d.ts",
15
+ "default": "./lib/module/index.js"
16
+ },
17
+ "require": {
18
+ "types": "./lib/typescript/commonjs/index.d.ts",
19
+ "default": "./lib/commonjs/index.js"
20
+ }
13
21
  },
14
22
  "./app-auth": {
15
- "types": "./src/app-auth.ts",
16
- "react-native": "./src/app-auth.ts",
17
- "default": "./src/app-auth.ts"
23
+ "chipmobilesdk-source": "./src/app-auth.ts",
24
+ "import": {
25
+ "types": "./lib/typescript/module/app-auth.d.ts",
26
+ "default": "./lib/module/app-auth.js"
27
+ },
28
+ "require": {
29
+ "types": "./lib/typescript/commonjs/app-auth.d.ts",
30
+ "default": "./lib/commonjs/app-auth.js"
31
+ }
18
32
  },
19
33
  "./keychain": {
20
- "types": "./src/keychain.ts",
21
- "react-native": "./src/keychain.ts",
22
- "default": "./src/keychain.ts"
34
+ "chipmobilesdk-source": "./src/keychain.ts",
35
+ "import": {
36
+ "types": "./lib/typescript/module/keychain.d.ts",
37
+ "default": "./lib/module/keychain.js"
38
+ },
39
+ "require": {
40
+ "types": "./lib/typescript/commonjs/keychain.d.ts",
41
+ "default": "./lib/commonjs/keychain.js"
42
+ }
23
43
  },
24
44
  "./testing": {
25
- "types": "./src/testing/index.ts",
26
- "react-native": "./src/testing/index.ts",
27
- "default": "./src/testing/index.ts"
45
+ "chipmobilesdk-source": "./src/testing/index.ts",
46
+ "import": {
47
+ "types": "./lib/typescript/module/testing/index.d.ts",
48
+ "default": "./lib/module/testing/index.js"
49
+ },
50
+ "require": {
51
+ "types": "./lib/typescript/commonjs/testing/index.d.ts",
52
+ "default": "./lib/commonjs/testing/index.js"
53
+ }
28
54
  }
29
55
  },
30
56
  "files": [
@@ -41,21 +67,23 @@
41
67
  "src/testing",
42
68
  "skills",
43
69
  "README.md",
44
- "CHANGELOG.md"
70
+ "CHANGELOG.md",
71
+ "lib"
45
72
  ],
46
73
  "sideEffects": false,
47
74
  "scripts": {
75
+ "build": "bob build",
48
76
  "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
49
- "pack:check": "npm pack --dry-run"
77
+ "pack:check": "npm pack --dry-run",
78
+ "prepack": "npm run build"
50
79
  },
51
80
  "dependencies": {
52
- "@chipmobilesdk/rn-core": "^0.1.0"
81
+ "@chipmobilesdk/rn-core": "^0.2.0"
53
82
  },
54
83
  "peerDependencies": {
55
84
  "react": ">=19",
56
- "react-native": ">=0.85",
57
- "react-native-app-auth": ">=8",
58
- "react-native-keychain": ">=10"
85
+ "react-native-app-auth": "^8.0.0",
86
+ "react-native-keychain": "^10.0.0"
59
87
  },
60
88
  "peerDependenciesMeta": {
61
89
  "react-native-app-auth": {
@@ -68,5 +96,24 @@
68
96
  "publishConfig": {
69
97
  "access": "public",
70
98
  "registry": "https://registry.npmjs.org/"
99
+ },
100
+ "react-native-builder-bob": {
101
+ "source": "src",
102
+ "output": "lib",
103
+ "targets": [
104
+ [
105
+ "commonjs",
106
+ {
107
+ "esm": true
108
+ }
109
+ ],
110
+ [
111
+ "module",
112
+ {
113
+ "esm": true
114
+ }
115
+ ],
116
+ "typescript"
117
+ ]
71
118
  }
72
119
  }
@@ -35,12 +35,27 @@ value.
35
35
 
36
36
  ## Public API
37
37
 
38
+ `createAuth(config)` is the entry point: one controller, one active account, created at
39
+ startup without loading any native module. Wrap the app in `<AuthProvider auth={auth}>`
40
+ and read state through the hooks — never re-implement the orchestration.
41
+
38
42
  Runtime:
39
43
 
40
- - `createAppAuthClient(...)` — the OIDC client
44
+ - `createAuth` — the controller: `getState`, `subscribe`, `restore`, `signIn(providerId)`,
45
+ `signOut`, `signOutGlobal`, `requestAccessToken(resourceKey)`, `getAccessTier`,
46
+ `canAccessPremium`, `getPlatformProvider(s)`, `getMessages`
47
+ - Hooks: `useAuth`, `useProfile`, `useAccessTier`, `useAccessToken`, `useAuthMessages`,
48
+ `useSignInProvider`, `useSignInProviders`, `useAuthSelector`
49
+ - `AuthProvider`
50
+ - `createAppAuthClient(...)` — the OIDC client; pass `serviceConfiguration` to skip a
51
+ `.well-known` fetch on every call
41
52
  - `createKeychainSecureStore(...)` — token persistence; `AUTH_STORAGE_KEY`
42
- - `evaluateAccessTier(...)`, `failClosedTier` — feature gating
53
+ - `evaluateAccessTier(...)`, `failClosedTier` — feature gating, denying when unknown
54
+ - `mapClaimsToProfile`, `describeProviderIdentity` — claims to a profile, and whether an
55
+ address is direct, Apple's private relay, or absent
43
56
  - `AuthProvider` — React context provider
57
+ - `AuthError`, `AuthConfigError`, `isAuthError`, `redact`, `authMessages`,
58
+ `resolveMessages`, `DEFAULT_MESSAGES`
44
59
 
45
60
  Testing (`@chipmobilesdk/rn-auth/testing`): `createMemorySecureStore`,
46
61
  `runSecureTokenStoreConformance`, `runOidcAuthClientConformance` — README `## Testing`.
@@ -51,6 +66,24 @@ Types: `SecureTokenStore`, `AuthProviderProps`, `AuthErrorCode`, `AuthErrorAudie
51
66
  `AuthErrorAudience` distinguishes what may be shown to a user from what is diagnostic
52
67
  only — never render a diagnostic-audience message in the UI.
53
68
 
69
+ ## Three things that bite
70
+
71
+ - **`signOutGlobal()` can report `{ global: 'failed' }`.** Local state is cleared either
72
+ way, but the tenant session may still be alive, so the next sign-in can complete with
73
+ no password prompt. Say so in the UI rather than claiming the user is fully signed out.
74
+ - **`requestAccessToken(resourceKey)` answers with a status, not a throw.** Handle
75
+ `denied` and `unavailable` as states; only `success` carries a token. `useAccessToken`
76
+ starts from `idle` again when the resource key changes.
77
+ - **iOS needs the native redirect set up** (URL scheme matching `redirectUrl`, and the
78
+ Apple provider configured in the tenant). A `redirect-mismatch` error is that, not a
79
+ code fault — see the prerequisite checklist.
80
+
81
+ ## A premium gate is a question
82
+
83
+ Use `canAccessPremium()` and render the denial where it happened.
84
+ `requirePremium()` is deprecated: it also moves the whole controller to
85
+ `access-denied`, which every screen reading `state.status` then sees.
86
+
54
87
  ## Where the detail lives
55
88
 
56
89
  Read one section of `node_modules/@chipmobilesdk/rn-auth/README.md`:
@@ -3,23 +3,44 @@
3
3
  * specs/003-mobile-entra-auth/data-model.md.
4
4
  */
5
5
 
6
+ import type { AccessTokenRequestOptions } from '../tokens/accessToken';
6
7
  import type { SecureTokenStore } from '../tokens/secureStore';
7
8
  import type { OidcAuthClient } from '../oidc/authClient';
8
9
  import type { AuthError, AuthErrorCode } from '../diagnostics/errors';
9
10
  import type { AuthMessageKey, AuthMessageMap } from '../diagnostics/messageKeys';
10
11
 
11
- export type ProviderId = 'google' | 'apple' | 'facebook';
12
+ /** The providers this package has copy for out of the box. */
13
+ export type KnownProviderId = 'google' | 'apple' | 'facebook';
14
+
15
+ /**
16
+ * A provider id. Open on purpose: routing is `domain_hint` and nothing else, so
17
+ * adding an IdP the tenant federates — Microsoft account, an email one-time
18
+ * code — is a change to an app's config, not a release of this package
19
+ * (AUTH-14). `KnownProviderId` keeps the three familiar ones in autocomplete.
20
+ */
21
+ export type ProviderId = KnownProviderId | (string & {});
12
22
  export type DevicePlatform = 'ios' | 'android';
13
23
 
24
+ export const DEVICE_PLATFORMS: readonly DevicePlatform[] = ['ios', 'android'];
25
+
14
26
  /** Provider Setup Configuration — non-secret public values only (FR-016/FR-017). */
15
27
  export interface ProviderConfig {
16
28
  id: ProviderId;
17
29
  platforms: DevicePlatform[];
18
30
  /** External ID `domain_hint` routing the user flow to this IdP (R3). */
19
31
  domainHint: string;
20
- /** Message key for the button label (Continue with Google / Apple). */
21
- labelKey: AuthMessageKey;
32
+ /**
33
+ * Message key for the button label (Continue with Google / Apple). A key of
34
+ * this package's catalogue, or one of the app's own for a provider it added.
35
+ */
36
+ labelKey: AuthMessageKey | (string & {});
22
37
  available?: boolean;
38
+ /**
39
+ * Extra OIDC authorize parameters for this provider, merged after
40
+ * `domain_hint` — `prompt`, `login_hint`, whatever the tenant's user flow
41
+ * expects. `domain_hint` itself cannot be overridden here.
42
+ */
43
+ additionalParameters?: Record<string, string>;
23
44
  }
24
45
 
25
46
  /** One configured protected resource for delegated token requests (FR-007). */
@@ -55,6 +76,11 @@ export interface AuthConfig {
55
76
  accessTierMapping: AccessTierMapping;
56
77
  secureStore: SecureTokenStore;
57
78
  authClient?: OidcAuthClient;
79
+ /**
80
+ * Copy overrides for this package's own message keys, resolved over the
81
+ * English defaults and readable through `auth.getMessages()` and
82
+ * `useAuthMessages()`.
83
+ */
58
84
  messages?: Partial<AuthMessageMap>;
59
85
  }
60
86
 
@@ -81,6 +107,11 @@ export interface Profile {
81
107
  displayName?: string | undefined;
82
108
  email?: string | undefined;
83
109
  provider: string;
110
+ /**
111
+ * The id-token claims this profile was mapped from. Empty after a restore
112
+ * that could not reach the provider: the stored artifact holds the identity,
113
+ * not the token (AUTH-23). Read `restore` on the state to tell the two apart.
114
+ */
84
115
  claims: Record<string, unknown>;
85
116
  }
86
117
 
@@ -143,10 +174,27 @@ export interface Auth {
143
174
  signIn(providerId: ProviderId): Promise<AuthState>;
144
175
  signOut(): Promise<void>;
145
176
  signOutGlobal(): Promise<GlobalSignOutResult>;
146
- requestAccessToken(resourceKey: string): Promise<AccessTokenResult>;
177
+ /**
178
+ * A delegated token for one configured resource. Answers from the in-memory
179
+ * cache while the token is unexpired; `forceRefresh` skips and drops that
180
+ * cache, which is what a caller whose token was just rejected needs.
181
+ */
182
+ requestAccessToken(
183
+ resourceKey: string,
184
+ options?: AccessTokenRequestOptions,
185
+ ): Promise<AccessTokenResult>;
147
186
  getAccessTier(): AccessTier;
148
- /** Gate a premium-only path; enters `access-denied` when unverified (FR-011). */
187
+ /** Whether a premium-only path is open, and the tier behind the answer. Reads nothing else and changes nothing (FR-011). */
188
+ canAccessPremium(): PremiumGateResult;
189
+ /**
190
+ * @deprecated Use `canAccessPremium` and show the denial where it happened.
191
+ * This also moves the whole controller to `access-denied`, which every screen
192
+ * reading `status` then sees, and only a `restore` or a sign-in leaves
193
+ * (AUTH-12). Kept for 0.4.x.
194
+ */
149
195
  requirePremium(): PremiumGateResult;
196
+ /** This package's copy: the English defaults with the app's `messages` over them. */
197
+ getMessages(): AuthMessageMap;
150
198
  /** The first available provider to present on the current platform (UX-004), or undefined. */
151
199
  getPlatformProvider(): ProviderConfig | undefined;
152
200
  /** All available providers for the current platform, in configuration order (FR-003/FR-004). */
@@ -4,13 +4,21 @@
4
4
  * Covers the auth-config.md Section A–F rules.
5
5
  */
6
6
 
7
- import type { AuthConfig } from './types';
7
+ import { DEVICE_PLATFORMS, type AuthConfig } from './types';
8
8
  import { AuthConfigError } from '../diagnostics/errors';
9
9
 
10
10
  function hasScheme(url: string): boolean {
11
11
  return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url);
12
12
  }
13
13
 
14
+ /** https, or http on a development machine. */
15
+ function isSecureAuthority(url: string): boolean {
16
+ if (/^https:/i.test(url)) {
17
+ return true;
18
+ }
19
+ return /^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?(\/|$)/i.test(url);
20
+ }
21
+
14
22
  export function validateConfig(config: AuthConfig): void {
15
23
  // Section A — tenant/client settings
16
24
  if (!config.authority || !config.authority.trim()) {
@@ -19,6 +27,14 @@ export function validateConfig(config: AuthConfig): void {
19
27
  if (!hasScheme(config.authority)) {
20
28
  throw new AuthConfigError('auth.error.configuration', '"authority" must be an absolute URL');
21
29
  }
30
+ // Tokens travel over this. A tenant reachable over plaintext http is a
31
+ // configuration mistake, not a deployment choice — except on a dev machine.
32
+ if (!isSecureAuthority(config.authority)) {
33
+ throw new AuthConfigError(
34
+ 'auth.error.configuration',
35
+ '"authority" must use https (http is accepted only for localhost)',
36
+ );
37
+ }
22
38
  if (!config.clientId || !config.clientId.trim()) {
23
39
  throw new AuthConfigError('auth.error.configuration', 'Missing "clientId"');
24
40
  }
@@ -66,6 +82,21 @@ export function validateConfig(config: AuthConfig): void {
66
82
  `Provider "${provider.id}" must declare at least one platform`,
67
83
  );
68
84
  }
85
+ for (const platform of provider.platforms) {
86
+ if (!DEVICE_PLATFORMS.includes(platform)) {
87
+ // A typo here hides the provider on every device rather than failing.
88
+ throw new AuthConfigError(
89
+ 'auth.error.configuration',
90
+ `Provider "${provider.id}" declares unknown platform "${String(platform)}"`,
91
+ );
92
+ }
93
+ }
94
+ if (typeof provider.labelKey !== 'string' || !provider.labelKey.trim()) {
95
+ throw new AuthConfigError(
96
+ 'auth.error.configuration',
97
+ `Provider "${provider.id}" is missing a labelKey`,
98
+ );
99
+ }
69
100
  }
70
101
 
71
102
  // Section D — resources
@@ -16,6 +16,8 @@ import {
16
16
  shallowEqual,
17
17
  } from '@chipmobilesdk/rn-core';
18
18
 
19
+ import type { AuthMessageMap } from '../diagnostics/messageKeys';
20
+ import { resolveMessages } from '../diagnostics/messageKeys';
19
21
  import type {
20
22
  Auth,
21
23
  AuthConfig,
@@ -37,7 +39,7 @@ import {
37
39
  } from '../oidc/socialRouting';
38
40
  import { mapClaimsToProfile } from './profile';
39
41
  import { evaluateAccessTier, failClosedTier } from '../access/accessTier';
40
- import { restoreSession, type PersistedSession } from './session';
42
+ import { restoreSession, sameSession, storedProfileFrom, type PersistedSession } from './session';
41
43
  import { AUTH_STORAGE_KEY } from '../tokens/secureStore';
42
44
  import { createAccessTokenManager } from '../tokens/accessToken';
43
45
  import { AuthError, toAuthError, type AuthErrorCode } from '../diagnostics/errors';
@@ -62,6 +64,8 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
62
64
  const secureStore = config.secureStore;
63
65
  const authClient = config.authClient ?? createAppAuthClient();
64
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);
65
69
 
66
70
  let state: AuthState = Object.freeze({ status: 'loading', restore: 'pending' });
67
71
  let refreshToken: string | null = null;
@@ -84,12 +88,29 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
84
88
  return next;
85
89
  };
86
90
 
87
- 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> =>
88
98
  enqueueStoreOp(async () => {
89
99
  if (epoch !== sessionEpoch) {
90
100
  return; // The session this belonged to has already ended.
91
101
  }
92
- 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;
93
114
  });
94
115
 
95
116
  const tokenManager = createAccessTokenManager({
@@ -155,6 +176,7 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
155
176
  sessionEpoch += 1;
156
177
  refreshToken = null;
157
178
  idToken = null;
179
+ storedSession = null;
158
180
  tokenManager.clear();
159
181
  await enqueueStoreOp(() => secureStore.removeItem(AUTH_STORAGE_KEY));
160
182
  }
@@ -193,6 +215,8 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
193
215
  return getState();
194
216
  }
195
217
 
218
+ storedSession = result.persisted ?? null;
219
+
196
220
  if (result.outcome === 'restored' && result.profile && result.refreshToken) {
197
221
  refreshToken = result.refreshToken;
198
222
  idToken = result.idToken ?? null;
@@ -334,16 +358,17 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
334
358
  return result;
335
359
  },
336
360
 
337
- async requestAccessToken(resourceKey) {
361
+ async requestAccessToken(resourceKey, tokenOptions) {
338
362
  const epoch = sessionEpoch;
339
363
  // Only a real network refresh is a `token-refreshing` phase; a cache hit
340
364
  // would otherwise flash every subscriber twice for nothing.
341
365
  const refreshing =
342
- state.status === 'authenticated' && !tokenManager.hasFreshToken(resourceKey);
366
+ state.status === 'authenticated' &&
367
+ (tokenOptions?.forceRefresh === true || !tokenManager.hasFreshToken(resourceKey));
343
368
  if (refreshing) {
344
369
  setState({ status: 'token-refreshing' });
345
370
  }
346
- const result = await tokenManager.request(resourceKey);
371
+ const result = await tokenManager.request(resourceKey, tokenOptions);
347
372
  // Restored only if nothing else moved the session meanwhile: a sign-out
348
373
  // during the refresh used to be overwritten back to `authenticated`.
349
374
  if (refreshing && epoch === sessionEpoch && state.status === 'token-refreshing') {
@@ -356,6 +381,11 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
356
381
  return state.accessTier ?? failClosedTier();
357
382
  },
358
383
 
384
+ canAccessPremium(): PremiumGateResult {
385
+ const tier = state.accessTier ?? failClosedTier();
386
+ return { allowed: tier.tier === 'premium' && tier.verification === 'verified', tier };
387
+ },
388
+
359
389
  requirePremium(): PremiumGateResult {
360
390
  const tier = state.accessTier ?? failClosedTier();
361
391
  const allowed = tier.tier === 'premium' && tier.verification === 'verified';
@@ -365,6 +395,10 @@ export function createAuthMachine(config: AuthConfig, options: CreateAuthOptions
365
395
  return { allowed, tier };
366
396
  },
367
397
 
398
+ getMessages(): AuthMessageMap {
399
+ return messages;
400
+ },
401
+
368
402
  getPlatformProvider() {
369
403
  return defaultProviderForPlatform(config, platform);
370
404
  },