feat(core-storage): add PouchEnvelope tests and LocalStorageService implementation

- Introduced tests for PouchEnvelope to validate envelope-aware CRUD operations using an in-memory PouchDB.
- Implemented LocalStorageService with encryption support for sensitive keys, including tests for setItem, getItem, removeItem, and clear methods.
- Defined a generic storage interface (IStorageService) to enforce type-safe serialization/deserialization across storage implementations.
This commit is contained in:
Firman Ramdhani
2026-07-06 11:38:37 +07:00
parent 7130eb3fe3
commit d1ce292e3c
17 changed files with 1568 additions and 215 deletions
@@ -0,0 +1,89 @@
import { EncryptionUtils } from '@repo/utils';
import type { IStorageService } from '../types/storage.interface';
export interface StorageOptions<TKey extends string> {
encryptedKeys?: Set<TKey>;
plainTextKeys?: Set<TKey>;
}
/**
* Enterprise-grade localStorage wrapper with optional AES encryption.
*/
export class LocalStorageService<TKey extends string> implements IStorageService<TKey> {
private readonly encryption: EncryptionUtils;
private readonly encryptedKeys: Set<TKey>;
private readonly plainTextKeys: Set<TKey>;
constructor(options?: StorageOptions<TKey>, encryptionUtils?: EncryptionUtils) {
this.encryption = encryptionUtils ?? EncryptionUtils.getInstance();
this.encryptedKeys = options?.encryptedKeys ?? new Set();
this.plainTextKeys = options?.plainTextKeys ?? new Set();
}
private validateKey(key: TKey): void {
if (!this.encryptedKeys.has(key) && !this.plainTextKeys.has(key)) {
throw new Error(`[Storage Engine] Security Exception: Key '${key}' is not registered and cannot be accessed.`);
}
}
private shouldEncrypt(key: TKey): boolean {
return this.encryptedKeys.has(key);
}
async setItem<T>(key: TKey, value: T): Promise<void> {
this.validateKey(key);
const serialized = JSON.stringify(value);
if (this.shouldEncrypt(key)) {
const encrypted = this.encryption.encrypt(serialized);
localStorage.setItem(key as string, encrypted);
} else {
localStorage.setItem(key as string, serialized);
}
}
async getItem<T>(key: TKey): Promise<T | null> {
this.validateKey(key);
const raw = localStorage.getItem(key as string);
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 as string);
return null;
}
}
async removeItem(key: TKey): Promise<void> {
this.validateKey(key);
localStorage.removeItem(key as string);
}
async clear(): Promise<void> {
localStorage.clear();
}
async hasItem(key: TKey): Promise<boolean> {
return localStorage.getItem(key as string) !== null;
}
async keys(): Promise<TKey[]> {
const result: TKey[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key !== null) result.push(key as TKey);
}
return result;
}
}
export function createLocalStorage<TKey extends string>(options?: StorageOptions<TKey>): IStorageService<TKey> {
return new LocalStorageService<TKey>(options);
}