feat: implement core-storage package with unified promise-based interface and encrypted-at-rest support
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { EncryptionUtils } from '@repo/utils';
|
||||
import type { IStorageService } from './storage.interface';
|
||||
import { ENCRYPTED_KEYS } from './storage.key';
|
||||
|
||||
/**
|
||||
* Enterprise-grade localStorage wrapper with optional AES encryption.
|
||||
*
|
||||
* Keys listed in `ENCRYPTED_KEYS` are automatically encrypted before
|
||||
* writing and decrypted on read using `@repo/utils` `EncryptionUtils`.
|
||||
* All other keys are stored as plain JSON.
|
||||
*
|
||||
* All methods are async (returning Promises) to conform to the
|
||||
* `IStorageService` interface, ensuring consumers can swap between
|
||||
* localStorage and IndexedDB without code changes.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const storage = new LocalStorageService();
|
||||
*
|
||||
* // Encrypted at rest (ACCESS_TOKEN is in ENCRYPTED_KEYS)
|
||||
* await storage.setItem(StorageKey.ACCESS_TOKEN, 'eyJhbGci...');
|
||||
*
|
||||
* // Plain JSON (THEME is NOT in ENCRYPTED_KEYS)
|
||||
* await storage.setItem(StorageKey.THEME, 'dark');
|
||||
* ```
|
||||
*/
|
||||
export class LocalStorageService implements IStorageService {
|
||||
private readonly encryption: EncryptionUtils;
|
||||
|
||||
constructor(encryptionUtils?: EncryptionUtils) {
|
||||
this.encryption = encryptionUtils ?? EncryptionUtils.getInstance();
|
||||
}
|
||||
|
||||
/** Check if a key should be encrypted. */
|
||||
private shouldEncrypt(key: string): boolean {
|
||||
return ENCRYPTED_KEYS.has(key);
|
||||
}
|
||||
|
||||
async setItem<T>(key: string, value: T): Promise<void> {
|
||||
const serialized = JSON.stringify(value);
|
||||
|
||||
if (this.shouldEncrypt(key)) {
|
||||
const encrypted = this.encryption.encrypt(serialized);
|
||||
localStorage.setItem(key, encrypted);
|
||||
} else {
|
||||
localStorage.setItem(key, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
async getItem<T>(key: string): Promise<T | null> {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw === null) return null;
|
||||
|
||||
try {
|
||||
if (this.shouldEncrypt(key)) {
|
||||
const decrypted = this.encryption.decrypt(raw);
|
||||
if (!decrypted) return null;
|
||||
return JSON.parse(decrypted) as T;
|
||||
}
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
console.warn(`[core-storage] Failed to parse key "${key}". Removing corrupt entry.`);
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async removeItem(key: string): Promise<void> {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
localStorage.clear();
|
||||
}
|
||||
|
||||
async hasItem(key: string): Promise<boolean> {
|
||||
return localStorage.getItem(key) !== null;
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key !== null) result.push(key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user