feat: implement core-storage package with unified promise-based interface and encrypted-at-rest support

This commit is contained in:
Firman Ramdhani
2026-05-22 22:23:02 +07:00
parent 9b265b8f46
commit 255704f867
15 changed files with 1091 additions and 4 deletions
@@ -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;
}
}