@zyphr-dev/auth-react-native 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  ZyphrAuthProvider: () => ZyphrAuthProvider,
24
24
  asyncStorageAdapter: () => asyncStorageAdapter,
25
25
  createZyphrTransport: () => createZyphrTransport,
26
+ keychainAdapter: () => keychainAdapter,
26
27
  secureStoreAdapter: () => secureStoreAdapter,
27
28
  useAuth: () => useAuth,
28
29
  useSession: () => useSession
@@ -62,6 +63,7 @@ var ZyphrAuthContext = (0, import_react.createContext)(null);
62
63
  function ZyphrAuthProvider({
63
64
  client,
64
65
  storage,
66
+ keyPrefix,
65
67
  refreshSkewMs,
66
68
  refreshOnForeground = true,
67
69
  children
@@ -69,7 +71,11 @@ function ZyphrAuthProvider({
69
71
  const [manager] = (0, import_react.useState)(
70
72
  () => new import_auth_core2.SessionManager({
71
73
  transport: createZyphrTransport(client),
72
- storage: storage ?? new import_auth_core2.MemoryTokenStorage(),
74
+ // Pass storage through as-is: auth-core applies the in-memory default
75
+ // AND emits the dev-mode "no storage" warning when it's omitted. Don't
76
+ // default it here or that warning would never fire for RN consumers.
77
+ storage,
78
+ keyPrefix,
73
79
  refreshSkewMs
74
80
  })
75
81
  );
@@ -140,11 +146,23 @@ function asyncStorageAdapter(asyncStorage) {
140
146
  removeItem: (key) => asyncStorage.removeItem(key)
141
147
  };
142
148
  }
149
+ var KEYCHAIN_USER = "zyphr";
150
+ function keychainAdapter(keychain) {
151
+ return {
152
+ getItem: async (key) => {
153
+ const result = await keychain.getGenericPassword({ service: key });
154
+ return result ? result.password : null;
155
+ },
156
+ setItem: (key, value) => keychain.setGenericPassword(KEYCHAIN_USER, value, { service: key }).then(() => void 0),
157
+ removeItem: (key) => keychain.resetGenericPassword({ service: key }).then(() => void 0)
158
+ };
159
+ }
143
160
  // Annotate the CommonJS export names for ESM import in node:
144
161
  0 && (module.exports = {
145
162
  ZyphrAuthProvider,
146
163
  asyncStorageAdapter,
147
164
  createZyphrTransport,
165
+ keychainAdapter,
148
166
  secureStoreAdapter,
149
167
  useAuth,
150
168
  useSession
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/transport.ts","../src/storage.ts"],"sourcesContent":["/**\n * @zyphr-dev/auth-react-native\n *\n * React Native bindings for Zyphr auth. Wrap your app in <ZyphrAuthProvider> and\n * read the session with useSession() / act with useAuth(). Same session engine as\n * the web binding (@zyphr-dev/auth-core), plus a secure-storage adapter and\n * refresh-on-foreground via React Native's AppState.\n */\n\nexport { ZyphrAuthProvider, useSession, useAuth } from './provider.js';\nexport type { ZyphrAuthProviderProps, UseAuthResult } from './provider.js';\n\nexport { createZyphrTransport } from './transport.js';\nexport { secureStoreAdapter, asyncStorageAdapter } from './storage.js';\n\nexport type { SessionState, SessionStatus, LoginParams, TokenStorage } from '@zyphr-dev/auth-core';\n","import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport { AppState, type AppStateStatus } from 'react-native';\nimport type { ZyphrClient } from '@zyphr-dev/node-sdk';\nimport {\n SessionManager,\n MemoryTokenStorage,\n type LoginParams,\n type SessionState,\n type TokenStorage,\n} from '@zyphr-dev/auth-core';\nimport { createZyphrTransport } from './transport.js';\n\nexport interface ZyphrAuthProviderProps {\n /** A client-safe ZyphrClient (constructed with your publishable application key). */\n client: ZyphrClient;\n /**\n * Where tokens persist. Strongly recommended in production — pass a secure\n * store via `secureStoreAdapter(SecureStore)` (expo-secure-store) or\n * `asyncStorageAdapter(AsyncStorage)`. Defaults to in-memory (tokens are lost\n * on app restart), which is fine only for prototyping.\n */\n storage?: TokenStorage;\n /** Refresh this many ms before the access token expires. Default 60_000. */\n refreshSkewMs?: number;\n /**\n * Refresh the session when the app returns to the foreground (default true).\n * A backgrounded app's scheduled refresh timer may not fire reliably, so we\n * also refresh on foreground to keep the token fresh.\n */\n refreshOnForeground?: boolean;\n children: ReactNode;\n}\n\ninterface ZyphrAuthContextValue {\n manager: SessionManager;\n}\n\nconst ZyphrAuthContext = createContext<ZyphrAuthContextValue | null>(null);\n\n/**\n * Provides a reactive Zyphr auth session to a React Native app. Same session\n * engine as the web binding (@zyphr-dev/auth-core), plus refresh-on-foreground\n * via React Native's AppState. Read state with {@link useSession}, act with\n * {@link useAuth}.\n */\nexport function ZyphrAuthProvider({\n client,\n storage,\n refreshSkewMs,\n refreshOnForeground = true,\n children,\n}: ZyphrAuthProviderProps): ReactElement {\n const [manager] = useState(\n () =>\n new SessionManager({\n transport: createZyphrTransport(client),\n storage: storage ?? new MemoryTokenStorage(),\n refreshSkewMs,\n }),\n );\n\n const initialized = useRef(false);\n useEffect(() => {\n if (!initialized.current) {\n initialized.current = true;\n void manager.initialize();\n }\n return () => {\n manager.destroy();\n };\n }, [manager]);\n\n // Refresh when the app returns to the foreground.\n useEffect(() => {\n if (!refreshOnForeground || !AppState?.addEventListener) return;\n const onChange = (next: AppStateStatus) => {\n if (next === 'active' && manager.getState().isAuthenticated) {\n void manager.refresh();\n }\n };\n const sub = AppState.addEventListener('change', onChange);\n return () => {\n // RN >= 0.65 returns a subscription with .remove(); older returns void.\n (sub as { remove?: () => void } | undefined)?.remove?.();\n };\n }, [manager, refreshOnForeground]);\n\n const value = useMemo(() => ({ manager }), [manager]);\n return <ZyphrAuthContext.Provider value={value}>{children}</ZyphrAuthContext.Provider>;\n}\n\nfunction useManager(): SessionManager {\n const ctx = useContext(ZyphrAuthContext);\n if (!ctx) {\n throw new Error('useSession/useAuth must be used within a <ZyphrAuthProvider>.');\n }\n return ctx.manager;\n}\n\n/** Reactive session state — re-renders on login, logout, refresh. */\nexport function useSession(): SessionState {\n const manager = useManager();\n return useSyncExternalStore(\n (onChange) => manager.subscribe(() => onChange()),\n () => manager.getState(),\n () => manager.getState(),\n );\n}\n\nexport interface UseAuthResult {\n login: (params: LoginParams) => Promise<void>;\n logout: () => Promise<void>;\n refresh: () => Promise<void>;\n}\n\n/** Auth actions bound to the provider's session manager. */\nexport function useAuth(): UseAuthResult {\n const manager = useManager();\n return useMemo(\n () => ({\n login: (params) => manager.login(params),\n logout: () => manager.logout(),\n refresh: () => manager.refresh(),\n }),\n [manager],\n );\n}\n","import type { ZyphrClient } from '@zyphr-dev/node-sdk';\nimport { type AuthTransport, type LoginParams, type SessionTokens, toSessionTokens } from '@zyphr-dev/auth-core';\n\n/**\n * An {@link AuthTransport} over the client-safe `ZyphrClient` (publishable\n * application key — no secret in the app bundle). Same transport the web binding\n * uses; re-implemented here so the React Native package doesn't depend on\n * `@zyphr-dev/auth-react` (which pulls web-React types).\n */\nexport function createZyphrTransport(client: ZyphrClient, now: () => number = Date.now): AuthTransport {\n const extract = (tokens?: { accessToken?: string; refreshToken?: string; expiresIn?: number }): SessionTokens =>\n toSessionTokens(\n { accessToken: tokens?.accessToken, refreshToken: tokens?.refreshToken, expiresIn: tokens?.expiresIn },\n now(),\n );\n\n return {\n async login({ email, password }: LoginParams): Promise<SessionTokens> {\n const res = await client.auth.login.loginEndUser({ email, password });\n return extract(res.data?.tokens);\n },\n async refresh(refreshToken: string): Promise<SessionTokens> {\n const res = await client.auth.sessions.refreshEndUserToken({ refreshToken });\n return extract(res.data?.tokens);\n },\n async logout(refreshToken: string): Promise<void> {\n await client.auth.sessions.revokeEndUserSession({ refreshToken });\n },\n };\n}\n","import type { TokenStorage } from '@zyphr-dev/auth-core';\n\n/**\n * The minimal async key/value shape common to React Native secure stores —\n * `expo-secure-store` (getItemAsync/setItemAsync/deleteItemAsync) and\n * `@react-native-async-storage/async-storage` (getItem/setItem/removeItem) both\n * fit one of these once wrapped. We keep the dependency OPTIONAL: the app passes\n * whichever it uses, so we don't force a native module on every consumer.\n */\n\n/** Adapt `expo-secure-store` (the recommended secure default). */\nexport function secureStoreAdapter(secureStore: {\n getItemAsync(key: string): Promise<string | null>;\n setItemAsync(key: string, value: string): Promise<void>;\n deleteItemAsync(key: string): Promise<void>;\n}): TokenStorage {\n return {\n getItem: (key) => secureStore.getItemAsync(key),\n setItem: (key, value) => secureStore.setItemAsync(key, value),\n removeItem: (key) => secureStore.deleteItemAsync(key),\n };\n}\n\n/**\n * Adapt `@react-native-async-storage/async-storage`. NOTE: AsyncStorage is NOT\n * encrypted — prefer {@link secureStoreAdapter} for tokens. Provided for apps\n * that already standardize on AsyncStorage and accept the tradeoff.\n */\nexport function asyncStorageAdapter(asyncStorage: {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}): TokenStorage {\n return {\n getItem: (key) => asyncStorage.getItem(key),\n setItem: (key, value) => asyncStorage.setItem(key, value),\n removeItem: (key) => asyncStorage.removeItem(key),\n };\n}\n"],"mappings":"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","names":["import_auth_core"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/transport.ts","../src/storage.ts"],"sourcesContent":["/**\n * @zyphr-dev/auth-react-native\n *\n * React Native bindings for Zyphr auth. Wrap your app in <ZyphrAuthProvider> and\n * read the session with useSession() / act with useAuth(). Same session engine as\n * the web binding (@zyphr-dev/auth-core), plus a secure-storage adapter and\n * refresh-on-foreground via React Native's AppState.\n */\n\nexport { ZyphrAuthProvider, useSession, useAuth } from './provider.js';\nexport type { ZyphrAuthProviderProps, UseAuthResult } from './provider.js';\n\nexport { createZyphrTransport } from './transport.js';\nexport { secureStoreAdapter, asyncStorageAdapter, keychainAdapter } from './storage.js';\nexport type { KeychainModule } from './storage.js';\n\nexport type { SessionState, SessionStatus, LoginParams, TokenStorage } from '@zyphr-dev/auth-core';\n","import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport { AppState, type AppStateStatus } from 'react-native';\nimport type { ZyphrClient } from '@zyphr-dev/node-sdk';\nimport {\n SessionManager,\n type LoginParams,\n type SessionState,\n type TokenStorage,\n} from '@zyphr-dev/auth-core';\nimport { createZyphrTransport } from './transport.js';\n\nexport interface ZyphrAuthProviderProps {\n /** A client-safe ZyphrClient (constructed with your publishable application key). */\n client: ZyphrClient;\n /**\n * Where tokens persist. Strongly recommended in production — pass a secure\n * store via `secureStoreAdapter(SecureStore)` (expo-secure-store) or\n * `asyncStorageAdapter(AsyncStorage)`. Defaults to in-memory (tokens are lost\n * on app restart), which is fine only for prototyping.\n */\n storage?: TokenStorage;\n /**\n * Namespace the storage key so multiple providers can share one storage\n * adapter without colliding (e.g. one session per account for account\n * switching). The effective key becomes `${keyPrefix}zyphr.session`. Default `''`.\n */\n keyPrefix?: string;\n /** Refresh this many ms before the access token expires. Default 60_000. */\n refreshSkewMs?: number;\n /**\n * Refresh the session when the app returns to the foreground (default true).\n * A backgrounded app's scheduled refresh timer may not fire reliably, so we\n * also refresh on foreground to keep the token fresh.\n */\n refreshOnForeground?: boolean;\n children: ReactNode;\n}\n\ninterface ZyphrAuthContextValue {\n manager: SessionManager;\n}\n\nconst ZyphrAuthContext = createContext<ZyphrAuthContextValue | null>(null);\n\n/**\n * Provides a reactive Zyphr auth session to a React Native app. Same session\n * engine as the web binding (@zyphr-dev/auth-core), plus refresh-on-foreground\n * via React Native's AppState. Read state with {@link useSession}, act with\n * {@link useAuth}.\n */\nexport function ZyphrAuthProvider({\n client,\n storage,\n keyPrefix,\n refreshSkewMs,\n refreshOnForeground = true,\n children,\n}: ZyphrAuthProviderProps): ReactElement {\n const [manager] = useState(\n () =>\n new SessionManager({\n transport: createZyphrTransport(client),\n // Pass storage through as-is: auth-core applies the in-memory default\n // AND emits the dev-mode \"no storage\" warning when it's omitted. Don't\n // default it here or that warning would never fire for RN consumers.\n storage,\n keyPrefix,\n refreshSkewMs,\n }),\n );\n\n const initialized = useRef(false);\n useEffect(() => {\n if (!initialized.current) {\n initialized.current = true;\n void manager.initialize();\n }\n return () => {\n manager.destroy();\n };\n }, [manager]);\n\n // Refresh when the app returns to the foreground.\n useEffect(() => {\n if (!refreshOnForeground || !AppState?.addEventListener) return;\n const onChange = (next: AppStateStatus) => {\n if (next === 'active' && manager.getState().isAuthenticated) {\n void manager.refresh();\n }\n };\n const sub = AppState.addEventListener('change', onChange);\n return () => {\n // RN >= 0.65 returns a subscription with .remove(); older returns void.\n (sub as { remove?: () => void } | undefined)?.remove?.();\n };\n }, [manager, refreshOnForeground]);\n\n const value = useMemo(() => ({ manager }), [manager]);\n return <ZyphrAuthContext.Provider value={value}>{children}</ZyphrAuthContext.Provider>;\n}\n\nfunction useManager(): SessionManager {\n const ctx = useContext(ZyphrAuthContext);\n if (!ctx) {\n throw new Error('useSession/useAuth must be used within a <ZyphrAuthProvider>.');\n }\n return ctx.manager;\n}\n\n/** Reactive session state — re-renders on login, logout, refresh. */\nexport function useSession(): SessionState {\n const manager = useManager();\n return useSyncExternalStore(\n (onChange) => manager.subscribe(() => onChange()),\n () => manager.getState(),\n () => manager.getState(),\n );\n}\n\nexport interface UseAuthResult {\n login: (params: LoginParams) => Promise<void>;\n logout: () => Promise<void>;\n refresh: () => Promise<void>;\n}\n\n/** Auth actions bound to the provider's session manager. */\nexport function useAuth(): UseAuthResult {\n const manager = useManager();\n return useMemo(\n () => ({\n login: (params) => manager.login(params),\n logout: () => manager.logout(),\n refresh: () => manager.refresh(),\n }),\n [manager],\n );\n}\n","import type { ZyphrClient } from '@zyphr-dev/node-sdk';\nimport { type AuthTransport, type LoginParams, type SessionTokens, toSessionTokens } from '@zyphr-dev/auth-core';\n\n/**\n * An {@link AuthTransport} over the client-safe `ZyphrClient` (publishable\n * application key — no secret in the app bundle). Same transport the web binding\n * uses; re-implemented here so the React Native package doesn't depend on\n * `@zyphr-dev/auth-react` (which pulls web-React types).\n */\nexport function createZyphrTransport(client: ZyphrClient, now: () => number = Date.now): AuthTransport {\n const extract = (tokens?: { accessToken?: string; refreshToken?: string; expiresIn?: number }): SessionTokens =>\n toSessionTokens(\n { accessToken: tokens?.accessToken, refreshToken: tokens?.refreshToken, expiresIn: tokens?.expiresIn },\n now(),\n );\n\n return {\n async login({ email, password }: LoginParams): Promise<SessionTokens> {\n const res = await client.auth.login.loginEndUser({ email, password });\n return extract(res.data?.tokens);\n },\n async refresh(refreshToken: string): Promise<SessionTokens> {\n const res = await client.auth.sessions.refreshEndUserToken({ refreshToken });\n return extract(res.data?.tokens);\n },\n async logout(refreshToken: string): Promise<void> {\n await client.auth.sessions.revokeEndUserSession({ refreshToken });\n },\n };\n}\n","import type { TokenStorage } from '@zyphr-dev/auth-core';\n\n/**\n * The minimal async key/value shape common to React Native secure stores —\n * `expo-secure-store` (getItemAsync/setItemAsync/deleteItemAsync) and\n * `@react-native-async-storage/async-storage` (getItem/setItem/removeItem) both\n * fit one of these once wrapped. We keep the dependency OPTIONAL: the app passes\n * whichever it uses, so we don't force a native module on every consumer.\n */\n\n/** Adapt `expo-secure-store` (the recommended secure default). */\nexport function secureStoreAdapter(secureStore: {\n getItemAsync(key: string): Promise<string | null>;\n setItemAsync(key: string, value: string): Promise<void>;\n deleteItemAsync(key: string): Promise<void>;\n}): TokenStorage {\n return {\n getItem: (key) => secureStore.getItemAsync(key),\n setItem: (key, value) => secureStore.setItemAsync(key, value),\n removeItem: (key) => secureStore.deleteItemAsync(key),\n };\n}\n\n/**\n * Adapt `@react-native-async-storage/async-storage`. NOTE: AsyncStorage is NOT\n * encrypted — prefer {@link secureStoreAdapter} for tokens. Provided for apps\n * that already standardize on AsyncStorage and accept the tradeoff.\n */\nexport function asyncStorageAdapter(asyncStorage: {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}): TokenStorage {\n return {\n getItem: (key) => asyncStorage.getItem(key),\n setItem: (key, value) => asyncStorage.setItem(key, value),\n removeItem: (key) => asyncStorage.removeItem(key),\n };\n}\n\n/** The subset of `react-native-keychain` this adapter uses. */\nexport interface KeychainModule {\n setGenericPassword(\n username: string,\n password: string,\n options?: { service?: string },\n ): Promise<unknown>;\n getGenericPassword(\n options?: { service?: string },\n ): Promise<false | { username: string; password: string }>;\n resetGenericPassword(options?: { service?: string }): Promise<boolean>;\n}\n\n/** Fixed username — the session lives in the `password` field; only one entry per service. */\nconst KEYCHAIN_USER = 'zyphr';\n\n/**\n * Adapt `react-native-keychain` (iOS Keychain / Android Keystore) — the\n * recommended secure store for **bare** React Native (no Expo). Encrypted at the\n * OS level, appropriate for long-lived refresh tokens.\n *\n * Keychain's API is service-based rather than key/value, so each storage `key`\n * maps to its own keychain **service** (`setGenericPassword(user, value, { service: key })`).\n * The core only ever uses a single key (`zyphr.session`, or `${keyPrefix}zyphr.session`\n * for per-account isolation), so each session is one service entry.\n *\n * ```ts\n * import * as Keychain from 'react-native-keychain';\n * <ZyphrAuthProvider client={client} storage={keychainAdapter(Keychain)} />\n * ```\n *\n * `react-native-keychain` is an OPTIONAL peer dependency — install it only if\n * you use this adapter.\n */\nexport function keychainAdapter(keychain: KeychainModule): TokenStorage {\n return {\n getItem: async (key) => {\n const result = await keychain.getGenericPassword({ service: key });\n return result ? result.password : null;\n },\n setItem: (key, value) =>\n keychain.setGenericPassword(KEYCHAIN_USER, value, { service: key }).then(() => undefined),\n removeItem: (key) => keychain.resetGenericPassword({ service: key }).then(() => undefined),\n };\n}\n"],"mappings":"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","names":["import_auth_core"]}
package/dist/index.d.cts CHANGED
@@ -13,6 +13,12 @@ interface ZyphrAuthProviderProps {
13
13
  * on app restart), which is fine only for prototyping.
14
14
  */
15
15
  storage?: TokenStorage;
16
+ /**
17
+ * Namespace the storage key so multiple providers can share one storage
18
+ * adapter without colliding (e.g. one session per account for account
19
+ * switching). The effective key becomes `${keyPrefix}zyphr.session`. Default `''`.
20
+ */
21
+ keyPrefix?: string;
16
22
  /** Refresh this many ms before the access token expires. Default 60_000. */
17
23
  refreshSkewMs?: number;
18
24
  /**
@@ -29,7 +35,7 @@ interface ZyphrAuthProviderProps {
29
35
  * via React Native's AppState. Read state with {@link useSession}, act with
30
36
  * {@link useAuth}.
31
37
  */
32
- declare function ZyphrAuthProvider({ client, storage, refreshSkewMs, refreshOnForeground, children, }: ZyphrAuthProviderProps): ReactElement;
38
+ declare function ZyphrAuthProvider({ client, storage, keyPrefix, refreshSkewMs, refreshOnForeground, children, }: ZyphrAuthProviderProps): ReactElement;
33
39
  /** Reactive session state — re-renders on login, logout, refresh. */
34
40
  declare function useSession(): SessionState;
35
41
  interface UseAuthResult {
@@ -71,5 +77,39 @@ declare function asyncStorageAdapter(asyncStorage: {
71
77
  setItem(key: string, value: string): Promise<void>;
72
78
  removeItem(key: string): Promise<void>;
73
79
  }): TokenStorage;
80
+ /** The subset of `react-native-keychain` this adapter uses. */
81
+ interface KeychainModule {
82
+ setGenericPassword(username: string, password: string, options?: {
83
+ service?: string;
84
+ }): Promise<unknown>;
85
+ getGenericPassword(options?: {
86
+ service?: string;
87
+ }): Promise<false | {
88
+ username: string;
89
+ password: string;
90
+ }>;
91
+ resetGenericPassword(options?: {
92
+ service?: string;
93
+ }): Promise<boolean>;
94
+ }
95
+ /**
96
+ * Adapt `react-native-keychain` (iOS Keychain / Android Keystore) — the
97
+ * recommended secure store for **bare** React Native (no Expo). Encrypted at the
98
+ * OS level, appropriate for long-lived refresh tokens.
99
+ *
100
+ * Keychain's API is service-based rather than key/value, so each storage `key`
101
+ * maps to its own keychain **service** (`setGenericPassword(user, value, { service: key })`).
102
+ * The core only ever uses a single key (`zyphr.session`, or `${keyPrefix}zyphr.session`
103
+ * for per-account isolation), so each session is one service entry.
104
+ *
105
+ * ```ts
106
+ * import * as Keychain from 'react-native-keychain';
107
+ * <ZyphrAuthProvider client={client} storage={keychainAdapter(Keychain)} />
108
+ * ```
109
+ *
110
+ * `react-native-keychain` is an OPTIONAL peer dependency — install it only if
111
+ * you use this adapter.
112
+ */
113
+ declare function keychainAdapter(keychain: KeychainModule): TokenStorage;
74
114
 
75
- export { type UseAuthResult, ZyphrAuthProvider, type ZyphrAuthProviderProps, asyncStorageAdapter, createZyphrTransport, secureStoreAdapter, useAuth, useSession };
115
+ export { type KeychainModule, type UseAuthResult, ZyphrAuthProvider, type ZyphrAuthProviderProps, asyncStorageAdapter, createZyphrTransport, keychainAdapter, secureStoreAdapter, useAuth, useSession };
package/dist/index.d.ts CHANGED
@@ -13,6 +13,12 @@ interface ZyphrAuthProviderProps {
13
13
  * on app restart), which is fine only for prototyping.
14
14
  */
15
15
  storage?: TokenStorage;
16
+ /**
17
+ * Namespace the storage key so multiple providers can share one storage
18
+ * adapter without colliding (e.g. one session per account for account
19
+ * switching). The effective key becomes `${keyPrefix}zyphr.session`. Default `''`.
20
+ */
21
+ keyPrefix?: string;
16
22
  /** Refresh this many ms before the access token expires. Default 60_000. */
17
23
  refreshSkewMs?: number;
18
24
  /**
@@ -29,7 +35,7 @@ interface ZyphrAuthProviderProps {
29
35
  * via React Native's AppState. Read state with {@link useSession}, act with
30
36
  * {@link useAuth}.
31
37
  */
32
- declare function ZyphrAuthProvider({ client, storage, refreshSkewMs, refreshOnForeground, children, }: ZyphrAuthProviderProps): ReactElement;
38
+ declare function ZyphrAuthProvider({ client, storage, keyPrefix, refreshSkewMs, refreshOnForeground, children, }: ZyphrAuthProviderProps): ReactElement;
33
39
  /** Reactive session state — re-renders on login, logout, refresh. */
34
40
  declare function useSession(): SessionState;
35
41
  interface UseAuthResult {
@@ -71,5 +77,39 @@ declare function asyncStorageAdapter(asyncStorage: {
71
77
  setItem(key: string, value: string): Promise<void>;
72
78
  removeItem(key: string): Promise<void>;
73
79
  }): TokenStorage;
80
+ /** The subset of `react-native-keychain` this adapter uses. */
81
+ interface KeychainModule {
82
+ setGenericPassword(username: string, password: string, options?: {
83
+ service?: string;
84
+ }): Promise<unknown>;
85
+ getGenericPassword(options?: {
86
+ service?: string;
87
+ }): Promise<false | {
88
+ username: string;
89
+ password: string;
90
+ }>;
91
+ resetGenericPassword(options?: {
92
+ service?: string;
93
+ }): Promise<boolean>;
94
+ }
95
+ /**
96
+ * Adapt `react-native-keychain` (iOS Keychain / Android Keystore) — the
97
+ * recommended secure store for **bare** React Native (no Expo). Encrypted at the
98
+ * OS level, appropriate for long-lived refresh tokens.
99
+ *
100
+ * Keychain's API is service-based rather than key/value, so each storage `key`
101
+ * maps to its own keychain **service** (`setGenericPassword(user, value, { service: key })`).
102
+ * The core only ever uses a single key (`zyphr.session`, or `${keyPrefix}zyphr.session`
103
+ * for per-account isolation), so each session is one service entry.
104
+ *
105
+ * ```ts
106
+ * import * as Keychain from 'react-native-keychain';
107
+ * <ZyphrAuthProvider client={client} storage={keychainAdapter(Keychain)} />
108
+ * ```
109
+ *
110
+ * `react-native-keychain` is an OPTIONAL peer dependency — install it only if
111
+ * you use this adapter.
112
+ */
113
+ declare function keychainAdapter(keychain: KeychainModule): TokenStorage;
74
114
 
75
- export { type UseAuthResult, ZyphrAuthProvider, type ZyphrAuthProviderProps, asyncStorageAdapter, createZyphrTransport, secureStoreAdapter, useAuth, useSession };
115
+ export { type KeychainModule, type UseAuthResult, ZyphrAuthProvider, type ZyphrAuthProviderProps, asyncStorageAdapter, createZyphrTransport, keychainAdapter, secureStoreAdapter, useAuth, useSession };
package/dist/index.js CHANGED
@@ -10,8 +10,7 @@ import {
10
10
  } from "react";
11
11
  import { AppState } from "react-native";
12
12
  import {
13
- SessionManager,
14
- MemoryTokenStorage
13
+ SessionManager
15
14
  } from "@zyphr-dev/auth-core";
16
15
 
17
16
  // src/transport.ts
@@ -42,6 +41,7 @@ var ZyphrAuthContext = createContext(null);
42
41
  function ZyphrAuthProvider({
43
42
  client,
44
43
  storage,
44
+ keyPrefix,
45
45
  refreshSkewMs,
46
46
  refreshOnForeground = true,
47
47
  children
@@ -49,7 +49,11 @@ function ZyphrAuthProvider({
49
49
  const [manager] = useState(
50
50
  () => new SessionManager({
51
51
  transport: createZyphrTransport(client),
52
- storage: storage ?? new MemoryTokenStorage(),
52
+ // Pass storage through as-is: auth-core applies the in-memory default
53
+ // AND emits the dev-mode "no storage" warning when it's omitted. Don't
54
+ // default it here or that warning would never fire for RN consumers.
55
+ storage,
56
+ keyPrefix,
53
57
  refreshSkewMs
54
58
  })
55
59
  );
@@ -120,10 +124,22 @@ function asyncStorageAdapter(asyncStorage) {
120
124
  removeItem: (key) => asyncStorage.removeItem(key)
121
125
  };
122
126
  }
127
+ var KEYCHAIN_USER = "zyphr";
128
+ function keychainAdapter(keychain) {
129
+ return {
130
+ getItem: async (key) => {
131
+ const result = await keychain.getGenericPassword({ service: key });
132
+ return result ? result.password : null;
133
+ },
134
+ setItem: (key, value) => keychain.setGenericPassword(KEYCHAIN_USER, value, { service: key }).then(() => void 0),
135
+ removeItem: (key) => keychain.resetGenericPassword({ service: key }).then(() => void 0)
136
+ };
137
+ }
123
138
  export {
124
139
  ZyphrAuthProvider,
125
140
  asyncStorageAdapter,
126
141
  createZyphrTransport,
142
+ keychainAdapter,
127
143
  secureStoreAdapter,
128
144
  useAuth,
129
145
  useSession
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider.tsx","../src/transport.ts","../src/storage.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport { AppState, type AppStateStatus } from 'react-native';\nimport type { ZyphrClient } from '@zyphr-dev/node-sdk';\nimport {\n SessionManager,\n MemoryTokenStorage,\n type LoginParams,\n type SessionState,\n type TokenStorage,\n} from '@zyphr-dev/auth-core';\nimport { createZyphrTransport } from './transport.js';\n\nexport interface ZyphrAuthProviderProps {\n /** A client-safe ZyphrClient (constructed with your publishable application key). */\n client: ZyphrClient;\n /**\n * Where tokens persist. Strongly recommended in production — pass a secure\n * store via `secureStoreAdapter(SecureStore)` (expo-secure-store) or\n * `asyncStorageAdapter(AsyncStorage)`. Defaults to in-memory (tokens are lost\n * on app restart), which is fine only for prototyping.\n */\n storage?: TokenStorage;\n /** Refresh this many ms before the access token expires. Default 60_000. */\n refreshSkewMs?: number;\n /**\n * Refresh the session when the app returns to the foreground (default true).\n * A backgrounded app's scheduled refresh timer may not fire reliably, so we\n * also refresh on foreground to keep the token fresh.\n */\n refreshOnForeground?: boolean;\n children: ReactNode;\n}\n\ninterface ZyphrAuthContextValue {\n manager: SessionManager;\n}\n\nconst ZyphrAuthContext = createContext<ZyphrAuthContextValue | null>(null);\n\n/**\n * Provides a reactive Zyphr auth session to a React Native app. Same session\n * engine as the web binding (@zyphr-dev/auth-core), plus refresh-on-foreground\n * via React Native's AppState. Read state with {@link useSession}, act with\n * {@link useAuth}.\n */\nexport function ZyphrAuthProvider({\n client,\n storage,\n refreshSkewMs,\n refreshOnForeground = true,\n children,\n}: ZyphrAuthProviderProps): ReactElement {\n const [manager] = useState(\n () =>\n new SessionManager({\n transport: createZyphrTransport(client),\n storage: storage ?? new MemoryTokenStorage(),\n refreshSkewMs,\n }),\n );\n\n const initialized = useRef(false);\n useEffect(() => {\n if (!initialized.current) {\n initialized.current = true;\n void manager.initialize();\n }\n return () => {\n manager.destroy();\n };\n }, [manager]);\n\n // Refresh when the app returns to the foreground.\n useEffect(() => {\n if (!refreshOnForeground || !AppState?.addEventListener) return;\n const onChange = (next: AppStateStatus) => {\n if (next === 'active' && manager.getState().isAuthenticated) {\n void manager.refresh();\n }\n };\n const sub = AppState.addEventListener('change', onChange);\n return () => {\n // RN >= 0.65 returns a subscription with .remove(); older returns void.\n (sub as { remove?: () => void } | undefined)?.remove?.();\n };\n }, [manager, refreshOnForeground]);\n\n const value = useMemo(() => ({ manager }), [manager]);\n return <ZyphrAuthContext.Provider value={value}>{children}</ZyphrAuthContext.Provider>;\n}\n\nfunction useManager(): SessionManager {\n const ctx = useContext(ZyphrAuthContext);\n if (!ctx) {\n throw new Error('useSession/useAuth must be used within a <ZyphrAuthProvider>.');\n }\n return ctx.manager;\n}\n\n/** Reactive session state — re-renders on login, logout, refresh. */\nexport function useSession(): SessionState {\n const manager = useManager();\n return useSyncExternalStore(\n (onChange) => manager.subscribe(() => onChange()),\n () => manager.getState(),\n () => manager.getState(),\n );\n}\n\nexport interface UseAuthResult {\n login: (params: LoginParams) => Promise<void>;\n logout: () => Promise<void>;\n refresh: () => Promise<void>;\n}\n\n/** Auth actions bound to the provider's session manager. */\nexport function useAuth(): UseAuthResult {\n const manager = useManager();\n return useMemo(\n () => ({\n login: (params) => manager.login(params),\n logout: () => manager.logout(),\n refresh: () => manager.refresh(),\n }),\n [manager],\n );\n}\n","import type { ZyphrClient } from '@zyphr-dev/node-sdk';\nimport { type AuthTransport, type LoginParams, type SessionTokens, toSessionTokens } from '@zyphr-dev/auth-core';\n\n/**\n * An {@link AuthTransport} over the client-safe `ZyphrClient` (publishable\n * application key — no secret in the app bundle). Same transport the web binding\n * uses; re-implemented here so the React Native package doesn't depend on\n * `@zyphr-dev/auth-react` (which pulls web-React types).\n */\nexport function createZyphrTransport(client: ZyphrClient, now: () => number = Date.now): AuthTransport {\n const extract = (tokens?: { accessToken?: string; refreshToken?: string; expiresIn?: number }): SessionTokens =>\n toSessionTokens(\n { accessToken: tokens?.accessToken, refreshToken: tokens?.refreshToken, expiresIn: tokens?.expiresIn },\n now(),\n );\n\n return {\n async login({ email, password }: LoginParams): Promise<SessionTokens> {\n const res = await client.auth.login.loginEndUser({ email, password });\n return extract(res.data?.tokens);\n },\n async refresh(refreshToken: string): Promise<SessionTokens> {\n const res = await client.auth.sessions.refreshEndUserToken({ refreshToken });\n return extract(res.data?.tokens);\n },\n async logout(refreshToken: string): Promise<void> {\n await client.auth.sessions.revokeEndUserSession({ refreshToken });\n },\n };\n}\n","import type { TokenStorage } from '@zyphr-dev/auth-core';\n\n/**\n * The minimal async key/value shape common to React Native secure stores —\n * `expo-secure-store` (getItemAsync/setItemAsync/deleteItemAsync) and\n * `@react-native-async-storage/async-storage` (getItem/setItem/removeItem) both\n * fit one of these once wrapped. We keep the dependency OPTIONAL: the app passes\n * whichever it uses, so we don't force a native module on every consumer.\n */\n\n/** Adapt `expo-secure-store` (the recommended secure default). */\nexport function secureStoreAdapter(secureStore: {\n getItemAsync(key: string): Promise<string | null>;\n setItemAsync(key: string, value: string): Promise<void>;\n deleteItemAsync(key: string): Promise<void>;\n}): TokenStorage {\n return {\n getItem: (key) => secureStore.getItemAsync(key),\n setItem: (key, value) => secureStore.setItemAsync(key, value),\n removeItem: (key) => secureStore.deleteItemAsync(key),\n };\n}\n\n/**\n * Adapt `@react-native-async-storage/async-storage`. NOTE: AsyncStorage is NOT\n * encrypted — prefer {@link secureStoreAdapter} for tokens. Provided for apps\n * that already standardize on AsyncStorage and accept the tradeoff.\n */\nexport function asyncStorageAdapter(asyncStorage: {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}): TokenStorage {\n return {\n getItem: (key) => asyncStorage.getItem(key),\n setItem: (key, value) => asyncStorage.setItem(key, value),\n removeItem: (key) => asyncStorage.removeItem(key),\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/provider.tsx","../src/transport.ts","../src/storage.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport { AppState, type AppStateStatus } from 'react-native';\nimport type { ZyphrClient } from '@zyphr-dev/node-sdk';\nimport {\n SessionManager,\n type LoginParams,\n type SessionState,\n type TokenStorage,\n} from '@zyphr-dev/auth-core';\nimport { createZyphrTransport } from './transport.js';\n\nexport interface ZyphrAuthProviderProps {\n /** A client-safe ZyphrClient (constructed with your publishable application key). */\n client: ZyphrClient;\n /**\n * Where tokens persist. Strongly recommended in production — pass a secure\n * store via `secureStoreAdapter(SecureStore)` (expo-secure-store) or\n * `asyncStorageAdapter(AsyncStorage)`. Defaults to in-memory (tokens are lost\n * on app restart), which is fine only for prototyping.\n */\n storage?: TokenStorage;\n /**\n * Namespace the storage key so multiple providers can share one storage\n * adapter without colliding (e.g. one session per account for account\n * switching). The effective key becomes `${keyPrefix}zyphr.session`. Default `''`.\n */\n keyPrefix?: string;\n /** Refresh this many ms before the access token expires. Default 60_000. */\n refreshSkewMs?: number;\n /**\n * Refresh the session when the app returns to the foreground (default true).\n * A backgrounded app's scheduled refresh timer may not fire reliably, so we\n * also refresh on foreground to keep the token fresh.\n */\n refreshOnForeground?: boolean;\n children: ReactNode;\n}\n\ninterface ZyphrAuthContextValue {\n manager: SessionManager;\n}\n\nconst ZyphrAuthContext = createContext<ZyphrAuthContextValue | null>(null);\n\n/**\n * Provides a reactive Zyphr auth session to a React Native app. Same session\n * engine as the web binding (@zyphr-dev/auth-core), plus refresh-on-foreground\n * via React Native's AppState. Read state with {@link useSession}, act with\n * {@link useAuth}.\n */\nexport function ZyphrAuthProvider({\n client,\n storage,\n keyPrefix,\n refreshSkewMs,\n refreshOnForeground = true,\n children,\n}: ZyphrAuthProviderProps): ReactElement {\n const [manager] = useState(\n () =>\n new SessionManager({\n transport: createZyphrTransport(client),\n // Pass storage through as-is: auth-core applies the in-memory default\n // AND emits the dev-mode \"no storage\" warning when it's omitted. Don't\n // default it here or that warning would never fire for RN consumers.\n storage,\n keyPrefix,\n refreshSkewMs,\n }),\n );\n\n const initialized = useRef(false);\n useEffect(() => {\n if (!initialized.current) {\n initialized.current = true;\n void manager.initialize();\n }\n return () => {\n manager.destroy();\n };\n }, [manager]);\n\n // Refresh when the app returns to the foreground.\n useEffect(() => {\n if (!refreshOnForeground || !AppState?.addEventListener) return;\n const onChange = (next: AppStateStatus) => {\n if (next === 'active' && manager.getState().isAuthenticated) {\n void manager.refresh();\n }\n };\n const sub = AppState.addEventListener('change', onChange);\n return () => {\n // RN >= 0.65 returns a subscription with .remove(); older returns void.\n (sub as { remove?: () => void } | undefined)?.remove?.();\n };\n }, [manager, refreshOnForeground]);\n\n const value = useMemo(() => ({ manager }), [manager]);\n return <ZyphrAuthContext.Provider value={value}>{children}</ZyphrAuthContext.Provider>;\n}\n\nfunction useManager(): SessionManager {\n const ctx = useContext(ZyphrAuthContext);\n if (!ctx) {\n throw new Error('useSession/useAuth must be used within a <ZyphrAuthProvider>.');\n }\n return ctx.manager;\n}\n\n/** Reactive session state — re-renders on login, logout, refresh. */\nexport function useSession(): SessionState {\n const manager = useManager();\n return useSyncExternalStore(\n (onChange) => manager.subscribe(() => onChange()),\n () => manager.getState(),\n () => manager.getState(),\n );\n}\n\nexport interface UseAuthResult {\n login: (params: LoginParams) => Promise<void>;\n logout: () => Promise<void>;\n refresh: () => Promise<void>;\n}\n\n/** Auth actions bound to the provider's session manager. */\nexport function useAuth(): UseAuthResult {\n const manager = useManager();\n return useMemo(\n () => ({\n login: (params) => manager.login(params),\n logout: () => manager.logout(),\n refresh: () => manager.refresh(),\n }),\n [manager],\n );\n}\n","import type { ZyphrClient } from '@zyphr-dev/node-sdk';\nimport { type AuthTransport, type LoginParams, type SessionTokens, toSessionTokens } from '@zyphr-dev/auth-core';\n\n/**\n * An {@link AuthTransport} over the client-safe `ZyphrClient` (publishable\n * application key — no secret in the app bundle). Same transport the web binding\n * uses; re-implemented here so the React Native package doesn't depend on\n * `@zyphr-dev/auth-react` (which pulls web-React types).\n */\nexport function createZyphrTransport(client: ZyphrClient, now: () => number = Date.now): AuthTransport {\n const extract = (tokens?: { accessToken?: string; refreshToken?: string; expiresIn?: number }): SessionTokens =>\n toSessionTokens(\n { accessToken: tokens?.accessToken, refreshToken: tokens?.refreshToken, expiresIn: tokens?.expiresIn },\n now(),\n );\n\n return {\n async login({ email, password }: LoginParams): Promise<SessionTokens> {\n const res = await client.auth.login.loginEndUser({ email, password });\n return extract(res.data?.tokens);\n },\n async refresh(refreshToken: string): Promise<SessionTokens> {\n const res = await client.auth.sessions.refreshEndUserToken({ refreshToken });\n return extract(res.data?.tokens);\n },\n async logout(refreshToken: string): Promise<void> {\n await client.auth.sessions.revokeEndUserSession({ refreshToken });\n },\n };\n}\n","import type { TokenStorage } from '@zyphr-dev/auth-core';\n\n/**\n * The minimal async key/value shape common to React Native secure stores —\n * `expo-secure-store` (getItemAsync/setItemAsync/deleteItemAsync) and\n * `@react-native-async-storage/async-storage` (getItem/setItem/removeItem) both\n * fit one of these once wrapped. We keep the dependency OPTIONAL: the app passes\n * whichever it uses, so we don't force a native module on every consumer.\n */\n\n/** Adapt `expo-secure-store` (the recommended secure default). */\nexport function secureStoreAdapter(secureStore: {\n getItemAsync(key: string): Promise<string | null>;\n setItemAsync(key: string, value: string): Promise<void>;\n deleteItemAsync(key: string): Promise<void>;\n}): TokenStorage {\n return {\n getItem: (key) => secureStore.getItemAsync(key),\n setItem: (key, value) => secureStore.setItemAsync(key, value),\n removeItem: (key) => secureStore.deleteItemAsync(key),\n };\n}\n\n/**\n * Adapt `@react-native-async-storage/async-storage`. NOTE: AsyncStorage is NOT\n * encrypted — prefer {@link secureStoreAdapter} for tokens. Provided for apps\n * that already standardize on AsyncStorage and accept the tradeoff.\n */\nexport function asyncStorageAdapter(asyncStorage: {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}): TokenStorage {\n return {\n getItem: (key) => asyncStorage.getItem(key),\n setItem: (key, value) => asyncStorage.setItem(key, value),\n removeItem: (key) => asyncStorage.removeItem(key),\n };\n}\n\n/** The subset of `react-native-keychain` this adapter uses. */\nexport interface KeychainModule {\n setGenericPassword(\n username: string,\n password: string,\n options?: { service?: string },\n ): Promise<unknown>;\n getGenericPassword(\n options?: { service?: string },\n ): Promise<false | { username: string; password: string }>;\n resetGenericPassword(options?: { service?: string }): Promise<boolean>;\n}\n\n/** Fixed username — the session lives in the `password` field; only one entry per service. */\nconst KEYCHAIN_USER = 'zyphr';\n\n/**\n * Adapt `react-native-keychain` (iOS Keychain / Android Keystore) — the\n * recommended secure store for **bare** React Native (no Expo). Encrypted at the\n * OS level, appropriate for long-lived refresh tokens.\n *\n * Keychain's API is service-based rather than key/value, so each storage `key`\n * maps to its own keychain **service** (`setGenericPassword(user, value, { service: key })`).\n * The core only ever uses a single key (`zyphr.session`, or `${keyPrefix}zyphr.session`\n * for per-account isolation), so each session is one service entry.\n *\n * ```ts\n * import * as Keychain from 'react-native-keychain';\n * <ZyphrAuthProvider client={client} storage={keychainAdapter(Keychain)} />\n * ```\n *\n * `react-native-keychain` is an OPTIONAL peer dependency — install it only if\n * you use this adapter.\n */\nexport function keychainAdapter(keychain: KeychainModule): TokenStorage {\n return {\n getItem: async (key) => {\n const result = await keychain.getGenericPassword({ service: key });\n return result ? result.password : null;\n },\n setItem: (key, value) =>\n keychain.setGenericPassword(KEYCHAIN_USER, value, { service: key }).then(() => undefined),\n removeItem: (key) => keychain.resetGenericPassword({ service: key }).then(() => undefined),\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zyphr-dev/auth-react-native",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "React Native bindings for Zyphr auth — <ZyphrAuthProvider>, useSession, useAuth, with a secure-storage adapter and refresh-on-foreground. Built on @zyphr-dev/auth-core.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -40,6 +40,9 @@
40
40
  },
41
41
  "expo-secure-store": {
42
42
  "optional": true
43
+ },
44
+ "react-native-keychain": {
45
+ "optional": true
43
46
  }
44
47
  },
45
48
  "dependencies": {
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export { ZyphrAuthProvider, useSession, useAuth } from './provider.js';
11
11
  export type { ZyphrAuthProviderProps, UseAuthResult } from './provider.js';
12
12
 
13
13
  export { createZyphrTransport } from './transport.js';
14
- export { secureStoreAdapter, asyncStorageAdapter } from './storage.js';
14
+ export { secureStoreAdapter, asyncStorageAdapter, keychainAdapter } from './storage.js';
15
+ export type { KeychainModule } from './storage.js';
15
16
 
16
17
  export type { SessionState, SessionStatus, LoginParams, TokenStorage } from '@zyphr-dev/auth-core';
package/src/provider.tsx CHANGED
@@ -13,7 +13,6 @@ import { AppState, type AppStateStatus } from 'react-native';
13
13
  import type { ZyphrClient } from '@zyphr-dev/node-sdk';
14
14
  import {
15
15
  SessionManager,
16
- MemoryTokenStorage,
17
16
  type LoginParams,
18
17
  type SessionState,
19
18
  type TokenStorage,
@@ -30,6 +29,12 @@ export interface ZyphrAuthProviderProps {
30
29
  * on app restart), which is fine only for prototyping.
31
30
  */
32
31
  storage?: TokenStorage;
32
+ /**
33
+ * Namespace the storage key so multiple providers can share one storage
34
+ * adapter without colliding (e.g. one session per account for account
35
+ * switching). The effective key becomes `${keyPrefix}zyphr.session`. Default `''`.
36
+ */
37
+ keyPrefix?: string;
33
38
  /** Refresh this many ms before the access token expires. Default 60_000. */
34
39
  refreshSkewMs?: number;
35
40
  /**
@@ -56,6 +61,7 @@ const ZyphrAuthContext = createContext<ZyphrAuthContextValue | null>(null);
56
61
  export function ZyphrAuthProvider({
57
62
  client,
58
63
  storage,
64
+ keyPrefix,
59
65
  refreshSkewMs,
60
66
  refreshOnForeground = true,
61
67
  children,
@@ -64,7 +70,11 @@ export function ZyphrAuthProvider({
64
70
  () =>
65
71
  new SessionManager({
66
72
  transport: createZyphrTransport(client),
67
- storage: storage ?? new MemoryTokenStorage(),
73
+ // Pass storage through as-is: auth-core applies the in-memory default
74
+ // AND emits the dev-mode "no storage" warning when it's omitted. Don't
75
+ // default it here or that warning would never fire for RN consumers.
76
+ storage,
77
+ keyPrefix,
68
78
  refreshSkewMs,
69
79
  }),
70
80
  );
package/src/storage.ts CHANGED
@@ -37,3 +37,49 @@ export function asyncStorageAdapter(asyncStorage: {
37
37
  removeItem: (key) => asyncStorage.removeItem(key),
38
38
  };
39
39
  }
40
+
41
+ /** The subset of `react-native-keychain` this adapter uses. */
42
+ export interface KeychainModule {
43
+ setGenericPassword(
44
+ username: string,
45
+ password: string,
46
+ options?: { service?: string },
47
+ ): Promise<unknown>;
48
+ getGenericPassword(
49
+ options?: { service?: string },
50
+ ): Promise<false | { username: string; password: string }>;
51
+ resetGenericPassword(options?: { service?: string }): Promise<boolean>;
52
+ }
53
+
54
+ /** Fixed username — the session lives in the `password` field; only one entry per service. */
55
+ const KEYCHAIN_USER = 'zyphr';
56
+
57
+ /**
58
+ * Adapt `react-native-keychain` (iOS Keychain / Android Keystore) — the
59
+ * recommended secure store for **bare** React Native (no Expo). Encrypted at the
60
+ * OS level, appropriate for long-lived refresh tokens.
61
+ *
62
+ * Keychain's API is service-based rather than key/value, so each storage `key`
63
+ * maps to its own keychain **service** (`setGenericPassword(user, value, { service: key })`).
64
+ * The core only ever uses a single key (`zyphr.session`, or `${keyPrefix}zyphr.session`
65
+ * for per-account isolation), so each session is one service entry.
66
+ *
67
+ * ```ts
68
+ * import * as Keychain from 'react-native-keychain';
69
+ * <ZyphrAuthProvider client={client} storage={keychainAdapter(Keychain)} />
70
+ * ```
71
+ *
72
+ * `react-native-keychain` is an OPTIONAL peer dependency — install it only if
73
+ * you use this adapter.
74
+ */
75
+ export function keychainAdapter(keychain: KeychainModule): TokenStorage {
76
+ return {
77
+ getItem: async (key) => {
78
+ const result = await keychain.getGenericPassword({ service: key });
79
+ return result ? result.password : null;
80
+ },
81
+ setItem: (key, value) =>
82
+ keychain.setGenericPassword(KEYCHAIN_USER, value, { service: key }).then(() => undefined),
83
+ removeItem: (key) => keychain.resetGenericPassword({ service: key }).then(() => undefined),
84
+ };
85
+ }