// RN persistence — a key-value store with the security split M0 needs. // // The web client persists its cache/outbox via `window.localStorage`, which does // not exist in RN. This adapter provides the RN equivalent, with a deliberate // split (see plans/open/mobile/03-offline-first-and-sync.md): // - SECRETS (session tokens) → `expo-secure-store` (Keychain / Keystore). // - the query cache / outbox → `@react-native-async-storage/async-storage`. // Putting a session token in AsyncStorage is a security bug; secure-store is its // home. // // Two exports, and the split between them is the point: // - `kv` is the APP's own store for things it manages itself, tokens included. // - `storeStorage` backs `defineStore({ persist })` and is AsyncStorage ONLY. // // A persisted store is app STATE — a draft, a filter, a wizard step — and none of // that belongs in the Keychain, which cannot be enumerated and which the OS may // gate behind a biometric prompt. A secret does not belong in a persisted store // either; put it through `kv` with `secretKey()`. Routing store keys by prefix // would blur a rule worth keeping sharp. import AsyncStorage from '@react-native-async-storage/async-storage' import * as SecureStore from 'expo-secure-store' /** Keys carrying secrets — routed to the Keychain/Keystore, never AsyncStorage. */ const SECRET_PREFIX = 'secret:' const isSecret = (key: string): boolean => key.startsWith(SECRET_PREFIX) export const kv = { get: async (key: string): Promise => isSecret(key) ? SecureStore.getItemAsync(key) : AsyncStorage.getItem(key), set: async (key: string, value: string): Promise => { if (isSecret(key)) await SecureStore.setItemAsync(key, value) else await AsyncStorage.setItem(key, value) }, remove: async (key: string): Promise => { if (isSecret(key)) await SecureStore.deleteItemAsync(key) else await AsyncStorage.removeItem(key) }, } /** Prefix a key so it lands in secure storage. */ export const secretKey = (name: string): string => `${SECRET_PREFIX}${name}` /** * The backing store for `defineStore({ persist })`, handed to * `createAsyncStoragePersistence()` in `src/client.ts`. * * Deliberately AsyncStorage alone — see the header. `getAllKeys` is what lets * the adapter hydrate without being told which keys exist; SecureStore has no * equivalent, which is the second reason secrets do not live here. */ export const storeStorage = { getItem: (key: string): Promise => AsyncStorage.getItem(key), setItem: (key: string, value: string): Promise => AsyncStorage.setItem(key, value), removeItem: (key: string): Promise => AsyncStorage.removeItem(key), getAllKeys: async (): Promise> => AsyncStorage.getAllKeys(), }