fix(headless): secure store in memory first

This commit is contained in:
devthejo 2025-06-29 13:17:13 +02:00
parent 6a773367d4
commit 4280820e01
5 changed files with 257 additions and 8 deletions

View file

@ -7,6 +7,7 @@ import { createLogger } from "~/lib/logger";
import { SYSTEM_SCOPES } from "~/lib/logger/scopes"; import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
import { authActions, permissionWizardActions } from "~/stores"; import { authActions, permissionWizardActions } from "~/stores";
import { secureStore } from "~/lib/memorySecureStore";
import "~/lib/mapbox"; import "~/lib/mapbox";
import "~/i18n"; 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("authActions", authActions.init);
initializeStore("permissionWizard", permissionWizardActions.init); initializeStore("permissionWizard", permissionWizardActions.init);
initializeStore("storeSubscriptions", storeSubscriptions.init); initializeStore("storeSubscriptions", storeSubscriptions.init);

View file

@ -1,13 +1,49 @@
import { secureStore } from "~/lib/secureStore"; import { secureStore } from "~/lib/memorySecureStore";
import uuidGenerator from "react-native-uuid"; 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() { async function getDeviceUuid() {
let deviceUuid = await secureStore.getItemAsync("deviceUuid"); // If a UUID generation is already in progress, wait for it
if (!deviceUuid) { if (uuidGenerationPromise) {
deviceUuid = uuidGenerator.v4(); deviceLogger.debug("UUID generation already in progress, waiting...");
await secureStore.setItemAsync("deviceUuid", deviceUuid); 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 }; export { getDeviceUuid };

View file

@ -11,6 +11,7 @@ import {
usePermissionWizardState, usePermissionWizardState,
useNetworkState, useNetworkState,
} from "~/stores"; } from "~/stores";
import { secureStore } from "~/lib/memorySecureStore";
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground"; import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground"; import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";
@ -211,6 +212,16 @@ const AppLifecycleListener = () => {
); );
checkPermissions(completed); 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 // Then handle WebSocket reconnection with proper error handling
activeTimeout.current = setTimeout(() => { activeTimeout.current = setTimeout(() => {
try { try {

View file

@ -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;

View file

@ -1,4 +1,4 @@
import { secureStore } from "~/lib/secureStore"; import { secureStore } from "~/lib/memorySecureStore";
import jwtDecode from "jwt-decode"; import jwtDecode from "jwt-decode";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import { FEATURE_SCOPES } from "~/lib/logger/scopes"; import { FEATURE_SCOPES } from "~/lib/logger/scopes";