From 4280820e0169c4f9e36793d3f0ad460e9bde558f Mon Sep 17 00:00:00 2001 From: devthejo Date: Sun, 29 Jun 2025 13:17:13 +0200 Subject: [PATCH] fix(headless): secure store in memory first --- src/app/index.js | 6 +- src/auth/deviceUuid.js | 48 +++++- src/containers/AppLifecycleListener.js | 11 ++ src/lib/memorySecureStore.js | 198 +++++++++++++++++++++++++ src/stores/auth.js | 2 +- 5 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 src/lib/memorySecureStore.js diff --git a/src/app/index.js b/src/app/index.js index d2d4eda..5384190 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -7,6 +7,7 @@ import { createLogger } from "~/lib/logger"; import { SYSTEM_SCOPES } from "~/lib/logger/scopes"; import { authActions, permissionWizardActions } from "~/stores"; +import { secureStore } from "~/lib/memorySecureStore"; import "~/lib/mapbox"; import "~/i18n"; @@ -53,7 +54,10 @@ const initializeStores = () => { } }; - // Initialize stores sequentially to maintain order + // Initialize memory secure store first + initializeStore("memorySecureStore", secureStore.init); + + // Then initialize other stores sequentially initializeStore("authActions", authActions.init); initializeStore("permissionWizard", permissionWizardActions.init); initializeStore("storeSubscriptions", storeSubscriptions.init); diff --git a/src/auth/deviceUuid.js b/src/auth/deviceUuid.js index bcf203b..bc41e2e 100644 --- a/src/auth/deviceUuid.js +++ b/src/auth/deviceUuid.js @@ -1,13 +1,49 @@ -import { secureStore } from "~/lib/secureStore"; +import { secureStore } from "~/lib/memorySecureStore"; import uuidGenerator from "react-native-uuid"; +import { createLogger } from "~/lib/logger"; +import { FEATURE_SCOPES } from "~/lib/logger/scopes"; + +const deviceLogger = createLogger({ + module: FEATURE_SCOPES.AUTH, + feature: "device-uuid", +}); + +// Mutex lock for atomic UUID generation +let uuidGenerationPromise = null; async function getDeviceUuid() { - let deviceUuid = await secureStore.getItemAsync("deviceUuid"); - if (!deviceUuid) { - deviceUuid = uuidGenerator.v4(); - await secureStore.setItemAsync("deviceUuid", deviceUuid); + // If a UUID generation is already in progress, wait for it + if (uuidGenerationPromise) { + deviceLogger.debug("UUID generation already in progress, waiting..."); + return await uuidGenerationPromise; } - return deviceUuid; + + // Create a new promise for this generation attempt + uuidGenerationPromise = (async () => { + try { + let deviceUuid = await secureStore.getItemAsync("deviceUuid"); + + if (!deviceUuid) { + deviceLogger.info("No device UUID found, generating new one"); + deviceUuid = uuidGenerator.v4(); + await secureStore.setItemAsync("deviceUuid", deviceUuid); + deviceLogger.info("New device UUID generated and stored", { + uuid: deviceUuid.substring(0, 8) + "...", + }); + } else { + deviceLogger.debug("Device UUID retrieved", { + uuid: deviceUuid.substring(0, 8) + "...", + }); + } + + return deviceUuid; + } finally { + // Clear the promise so future calls can proceed + uuidGenerationPromise = null; + } + })(); + + return await uuidGenerationPromise; } export { getDeviceUuid }; diff --git a/src/containers/AppLifecycleListener.js b/src/containers/AppLifecycleListener.js index 4d620dc..53cbc67 100644 --- a/src/containers/AppLifecycleListener.js +++ b/src/containers/AppLifecycleListener.js @@ -11,6 +11,7 @@ import { usePermissionWizardState, useNetworkState, } from "~/stores"; +import { secureStore } from "~/lib/memorySecureStore"; import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground"; import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground"; @@ -211,6 +212,16 @@ const AppLifecycleListener = () => { ); checkPermissions(completed); + // Sync memory secure store back to persistent storage + lifecycleLogger.info( + "Syncing memory secure store to persistent storage", + ); + secureStore.syncToSecureStore().catch((error) => { + lifecycleLogger.error("Failed to sync memory secure store", { + error: error.message, + }); + }); + // Then handle WebSocket reconnection with proper error handling activeTimeout.current = setTimeout(() => { try { diff --git a/src/lib/memorySecureStore.js b/src/lib/memorySecureStore.js new file mode 100644 index 0000000..c98e0be --- /dev/null +++ b/src/lib/memorySecureStore.js @@ -0,0 +1,198 @@ +import { secureStore as originalSecureStore } from "./secureStore"; +import { createLogger } from "~/lib/logger"; +import { SYSTEM_SCOPES } from "~/lib/logger/scopes"; + +const storageLogger = createLogger({ + module: SYSTEM_SCOPES.STORAGE, + feature: "memory-secure-store", +}); + +// In-memory cache for secure store values +const memoryCache = new Map(); + +// Track if we've loaded from secure store +let isInitialized = false; +const initPromise = new Promise((resolve) => { + global.__memorySecureStoreInitResolve = resolve; +}); + +/** + * Memory-first secure store wrapper that maintains an in-memory cache + * for headless/background mode access when secure store is unavailable + */ +export const memorySecureStore = { + /** + * Initialize the memory cache by loading all known keys from secure store + */ + async init() { + if (isInitialized) return; + + storageLogger.info("Initializing memory secure store"); + + // List of known keys that need to be cached + const knownKeys = [ + "deviceUuid", + "authToken", + "userToken", + "dev.authToken", + "dev.userToken", + "anon.authToken", + "anon.userToken", + ]; + + // Load all known keys into memory + for (const key of knownKeys) { + try { + const value = await originalSecureStore.getItemAsync(key); + if (value !== null) { + memoryCache.set(key, value); + storageLogger.debug("Loaded key into memory", { + key, + hasValue: true, + }); + } + } catch (error) { + storageLogger.warn("Failed to load key from secure store", { + key, + error: error.message, + }); + } + } + + isInitialized = true; + if (global.__memorySecureStoreInitResolve) { + global.__memorySecureStoreInitResolve(); + delete global.__memorySecureStoreInitResolve; + } + + storageLogger.info("Memory secure store initialized", { + cachedKeys: Array.from(memoryCache.keys()), + }); + }, + + /** + * Ensure initialization is complete before operations + */ + async ensureInitialized() { + if (!isInitialized) { + await initPromise; + } + }, + + /** + * Get item from memory first, fallback to secure store + */ + async getItemAsync(key) { + await this.ensureInitialized(); + + // Try memory first + if (memoryCache.has(key)) { + const value = memoryCache.get(key); + storageLogger.debug("Retrieved from memory cache", { + key, + hasValue: !!value, + }); + return value; + } + + // Fallback to secure store + try { + const value = await originalSecureStore.getItemAsync(key); + if (value !== null) { + // Cache for future use + memoryCache.set(key, value); + storageLogger.debug("Retrieved from secure store and cached", { key }); + } + return value; + } catch (error) { + storageLogger.warn( + "Failed to retrieve from secure store, returning null", + { + key, + error: error.message, + }, + ); + // In headless mode, secure store might not be accessible + return null; + } + }, + + /** + * Set item in both memory and secure store + */ + async setItemAsync(key, value) { + await this.ensureInitialized(); + + // Always set in memory first + memoryCache.set(key, value); + storageLogger.debug("Set in memory cache", { key }); + + // Try to persist to secure store + try { + await originalSecureStore.setItemAsync(key, value); + storageLogger.debug("Persisted to secure store", { key }); + } catch (error) { + storageLogger.warn( + "Failed to persist to secure store, kept in memory only", + { + key, + error: error.message, + }, + ); + // Continue - value is at least in memory + } + }, + + /** + * Delete item from both memory and secure store + */ + async deleteItemAsync(key) { + await this.ensureInitialized(); + + // Delete from memory + memoryCache.delete(key); + storageLogger.debug("Deleted from memory cache", { key }); + + // Try to delete from secure store + try { + await originalSecureStore.deleteItemAsync(key); + storageLogger.debug("Deleted from secure store", { key }); + } catch (error) { + storageLogger.warn("Failed to delete from secure store", { + key, + error: error.message, + }); + // Continue - at least removed from memory + } + }, + + /** + * Sync memory cache back to secure store (useful when returning from background) + */ + async syncToSecureStore() { + storageLogger.info("Syncing memory cache to secure store"); + + const syncResults = { + success: 0, + failed: 0, + }; + + for (const [key, value] of memoryCache.entries()) { + try { + await originalSecureStore.setItemAsync(key, value); + syncResults.success++; + } catch (error) { + syncResults.failed++; + storageLogger.warn("Failed to sync key to secure store", { + key, + error: error.message, + }); + } + } + + storageLogger.info("Memory cache sync completed", syncResults); + }, +}; + +// Export as default to match the original secureStore interface +export const secureStore = memorySecureStore; diff --git a/src/stores/auth.js b/src/stores/auth.js index e654598..ca46553 100644 --- a/src/stores/auth.js +++ b/src/stores/auth.js @@ -1,4 +1,4 @@ -import { secureStore } from "~/lib/secureStore"; +import { secureStore } from "~/lib/memorySecureStore"; import jwtDecode from "jwt-decode"; import { createLogger } from "~/lib/logger"; import { FEATURE_SCOPES } from "~/lib/logger/scopes";