fix(headless): secure store in memory first
This commit is contained in:
parent
6a773367d4
commit
4280820e01
5 changed files with 257 additions and 8 deletions
|
@ -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);
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
198
src/lib/memorySecureStore.js
Normal file
198
src/lib/memorySecureStore.js
Normal 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;
|
|
@ -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";
|
||||||
|
|
Loading…
Add table
Reference in a new issue