refactor: decouple core-storage services from static keys and update i18n setup to use configurable storage adapters

This commit is contained in:
Firman Ramdhani
2026-05-28 13:27:13 +07:00
parent b4af762baa
commit d0ebceb1bb
20 changed files with 377 additions and 375 deletions
+4 -44
View File
@@ -1,48 +1,8 @@
// ─── Interfaces ─────────────────────────────────────────────────
export type { IStorageService } from './storage.interface';
// ─── Key Registry ───────────────────────────────────────────────
export { StorageKey, ENCRYPTED_KEYS } from './storage.key';
export type { StorageKeyValue } from './storage.key';
export type { StorageOptions } from './local-storage.service';
export type { IndexedDBConfig } from './indexed-db.service';
// ─── Service Classes ────────────────────────────────────────────
export { LocalStorageService } from './local-storage.service';
export { IndexedDBService } from './indexed-db.service';
// ─── Pre-configured Instances ───────────────────────────────────
import { LocalStorageService } from './local-storage.service';
import { IndexedDBService } from './indexed-db.service';
/**
* Default secure localStorage instance.
*
* Keys listed in `ENCRYPTED_KEYS` are automatically encrypted via
* `@repo/utils` `EncryptionUtils`. All other keys are plain JSON.
*
* @example
* ```ts
* import { secureStorage, StorageKey } from '@repo/core-storage';
*
* await secureStorage.setItem(StorageKey.ACCESS_TOKEN, 'eyJhbGci...');
* const token = await secureStorage.getItem<string>(StorageKey.ACCESS_TOKEN);
* ```
*/
export const secureStorage = new LocalStorageService();
/**
* Default IndexedDB instance.
*
* Uses `app_db` database with a `kv_store` object store.
* Sensitive keys are encrypted at rest using the same
* `EncryptionUtils` pipeline as `secureStorage`.
*
* @example
* ```ts
* import { secureIndexedDB } from '@repo/core-storage';
*
* await secureIndexedDB.setItem('offline_draft', { content: '...' });
* const draft = await secureIndexedDB.getItem<Draft>('offline_draft');
* ```
*/
export const secureIndexedDB = new IndexedDBService({ dbName: 'app_db', storeName: 'kv_store' });
export { LocalStorageService, createLocalStorage } from './local-storage.service';
export { IndexedDBService, createIndexedDB } from './indexed-db.service';
+35 -30
View File
@@ -1,10 +1,10 @@
import { EncryptionUtils } from '@repo/utils';
import type { IStorageService } from './storage.interface';
import { ENCRYPTED_KEYS } from './storage.key';
import type { StorageOptions } from './local-storage.service';
// ─── Types ──────────────────────────────────────────────────────
interface IndexedDBConfig {
export interface IndexedDBConfig<TKey extends string> extends StorageOptions<TKey> {
/** Database name. @default 'app_db' */
dbName?: string;
/** Object store name. @default 'kv_store' */
@@ -63,34 +63,23 @@ function withTransaction<R>(
/**
* Enterprise-grade IndexedDB wrapper with optional AES encryption.
*
* Uses a simple key-value object store pattern. Keys listed in
* `ENCRYPTED_KEYS` are automatically encrypted/decrypted using
* `@repo/utils` `EncryptionUtils`.
*
* Unlike localStorage, IndexedDB has no 5MB size limit — making
* it suitable for large payloads like cached API responses, offline
* data, or file blobs.
*
* @example
* ```ts
* const idb = new IndexedDBService({ dbName: 'my_app' });
* await idb.setItem('large_dataset', hugePayload);
* const data = await idb.getItem<HugePayload>('large_dataset');
* ```
*/
export class IndexedDBService implements IStorageService {
export class IndexedDBService<TKey extends string> implements IStorageService<TKey> {
private readonly encryption: EncryptionUtils;
private readonly dbName: string;
private readonly storeName: string;
private readonly version: number;
private readonly encryptedKeys: Set<TKey>;
private readonly plainTextKeys: Set<TKey>;
private dbPromise: Promise<IDBDatabase> | null = null;
constructor(config?: IndexedDBConfig, encryptionUtils?: EncryptionUtils) {
constructor(config?: IndexedDBConfig<TKey>, encryptionUtils?: EncryptionUtils) {
this.encryption = encryptionUtils ?? EncryptionUtils.getInstance();
this.dbName = config?.dbName ?? 'app_db';
this.storeName = config?.storeName ?? 'kv_store';
this.version = config?.version ?? 1;
this.encryptedKeys = config?.encryptedKeys ?? new Set();
this.plainTextKeys = config?.plainTextKeys ?? new Set();
}
/** Lazy-open the database connection (cached). */
@@ -101,11 +90,18 @@ export class IndexedDBService implements IStorageService {
return this.dbPromise;
}
private shouldEncrypt(key: string): boolean {
return ENCRYPTED_KEYS.has(key);
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.`);
}
}
async setItem<T>(key: string, value: T): Promise<void> {
private shouldEncrypt(key: TKey): boolean {
return this.encryptedKeys.has(key);
}
async setItem<T>(key: TKey, value: T): Promise<void> {
this.validateKey(key);
const db = await this.getDB();
const serialized = JSON.stringify(value);
const payload = this.shouldEncrypt(key)
@@ -113,18 +109,19 @@ export class IndexedDBService implements IStorageService {
: serialized;
await withTransaction(db, this.storeName, 'readwrite', (store) =>
store.put(payload, key),
store.put(payload, key as string),
);
}
async getItem<T>(key: string): Promise<T | null> {
async getItem<T>(key: TKey): Promise<T | null> {
this.validateKey(key);
const db = await this.getDB();
const raw = await withTransaction<string | undefined>(
db,
this.storeName,
'readonly',
(store) => store.get(key) as IDBRequest<string | undefined>,
(store) => store.get(key as string) as IDBRequest<string | undefined>,
);
if (raw === undefined || raw === null) return null;
@@ -143,10 +140,11 @@ export class IndexedDBService implements IStorageService {
}
}
async removeItem(key: string): Promise<void> {
async removeItem(key: TKey): Promise<void> {
this.validateKey(key);
const db = await this.getDB();
await withTransaction(db, this.storeName, 'readwrite', (store) =>
store.delete(key),
store.delete(key as string),
);
}
@@ -157,18 +155,25 @@ export class IndexedDBService implements IStorageService {
);
}
async hasItem(key: string): Promise<boolean> {
async hasItem(key: TKey): Promise<boolean> {
const value = await this.getItem(key);
return value !== null;
}
async keys(): Promise<string[]> {
async keys(): Promise<TKey[]> {
const db = await this.getDB();
return withTransaction<string[]>(
const allKeys = await withTransaction<string[]>(
db,
this.storeName,
'readonly',
(store) => store.getAllKeys() as IDBRequest<string[]>,
);
return allKeys as TKey[];
}
}
export function createIndexedDB<TKey extends string>(
config: IndexedDBConfig<TKey>
): IStorageService<TKey> {
return new IndexedDBService<TKey>(config);
}
@@ -1,54 +1,50 @@
import { EncryptionUtils } from '@repo/utils';
import type { IStorageService } from './storage.interface';
import { ENCRYPTED_KEYS } from './storage.key';
export interface StorageOptions<TKey extends string> {
encryptedKeys?: Set<TKey>;
plainTextKeys?: Set<TKey>;
}
/**
* 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 {
export class LocalStorageService<TKey extends string> implements IStorageService<TKey> {
private readonly encryption: EncryptionUtils;
private readonly encryptedKeys: Set<TKey>;
private readonly plainTextKeys: Set<TKey>;
constructor(encryptionUtils?: EncryptionUtils) {
constructor(options?: StorageOptions<TKey>, encryptionUtils?: EncryptionUtils) {
this.encryption = encryptionUtils ?? EncryptionUtils.getInstance();
this.encryptedKeys = options?.encryptedKeys ?? new Set();
this.plainTextKeys = options?.plainTextKeys ?? new Set();
}
/** Check if a key should be encrypted. */
private shouldEncrypt(key: string): boolean {
return ENCRYPTED_KEYS.has(key);
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.`);
}
}
async setItem<T>(key: string, value: T): Promise<void> {
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, encrypted);
localStorage.setItem(key as string, encrypted);
} else {
localStorage.setItem(key, serialized);
localStorage.setItem(key as string, serialized);
}
}
async getItem<T>(key: string): Promise<T | null> {
const raw = localStorage.getItem(key);
async getItem<T>(key: TKey): Promise<T | null> {
this.validateKey(key);
const raw = localStorage.getItem(key as string);
if (raw === null) return null;
try {
@@ -60,29 +56,36 @@ export class LocalStorageService implements IStorageService {
return JSON.parse(raw) as T;
} catch {
console.warn(`[core-storage] Failed to parse key "${key}". Removing corrupt entry.`);
localStorage.removeItem(key);
localStorage.removeItem(key as string);
return null;
}
}
async removeItem(key: string): Promise<void> {
localStorage.removeItem(key);
async removeItem(key: TKey): Promise<void> {
this.validateKey(key);
localStorage.removeItem(key as string);
}
async clear(): Promise<void> {
localStorage.clear();
}
async hasItem(key: string): Promise<boolean> {
return localStorage.getItem(key) !== null;
async hasItem(key: TKey): Promise<boolean> {
return localStorage.getItem(key as string) !== null;
}
async keys(): Promise<string[]> {
const result: string[] = [];
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);
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);
}
@@ -10,29 +10,29 @@
* const user = await storage.getItem<UserProfile>(StorageKey.USER_PROFILE);
* ```
*/
export interface IStorageService {
export interface IStorageService<TKey extends string = string> {
/**
* Persist a value under the given key.
* The value is JSON-serialized before storage.
* If encryption is enabled, the serialized payload is encrypted at rest.
*/
setItem<T>(key: string, value: T): Promise<void>;
setItem<T>(key: TKey, value: T): Promise<void>;
/**
* Retrieve and deserialize a value by key.
* Returns `null` if the key does not exist or decryption/parsing fails.
*/
getItem<T>(key: string): Promise<T | null>;
getItem<T>(key: TKey): Promise<T | null>;
/** Remove a single key from storage. */
removeItem(key: string): Promise<void>;
removeItem(key: TKey): Promise<void>;
/** Remove all keys managed by this storage instance. */
clear(): Promise<void>;
/** Check if a key exists in storage. */
hasItem(key: string): Promise<boolean>;
hasItem(key: TKey): Promise<boolean>;
/** Get all keys currently in storage. */
keys(): Promise<string[]>;
keys(): Promise<TKey[]>;
}
-53
View File
@@ -1,53 +0,0 @@
/**
* Centralized storage key registry.
*
* ALL keys used across the application MUST be registered here
* as string literal constants. This prevents key collisions,
* enables grep-ability, and provides a single source of truth
* for what data is persisted in the browser.
*
* Convention: `SCREAMING_SNAKE_CASE` for the constant,
* `kebab-case` or `snake_case` for the actual string value.
*
* @example
* ```ts
* await secureStorage.setItem(StorageKey.ACCESS_TOKEN, token);
* ```
*/
export const StorageKey = {
// ── Auth ─────────────────────────────────────────────────────
ACCESS_TOKEN: 'access_token',
REFRESH_TOKEN: 'refresh_token',
USER_PROFILE: 'user_profile',
USER_PERMISSIONS: 'user_permissions',
// ── App Preferences ──────────────────────────────────────────
THEME: 'app_theme',
LOCALE: 'app_locale',
SIDEBAR_COLLAPSED: 'sidebar_collapsed',
// ── Session ──────────────────────────────────────────────────
FARO_SESSION: 'faroSession',
LAST_ACTIVE_ROUTE: 'last_active_route',
// ── Feature Flags / Cache ────────────────────────────────────
FEATURE_FLAGS: 'feature_flags',
CACHE_VERSION: 'cache_version',
} as const;
/** Union type of all registered storage key values. */
export type StorageKeyValue = (typeof StorageKey)[keyof typeof StorageKey];
/**
* Keys that require encryption at rest.
*
* Any key listed here will be automatically encrypted before
* writing to storage and decrypted on read. All other keys
* are stored as plain JSON.
*/
export const ENCRYPTED_KEYS: ReadonlySet<string> = new Set<string>([
StorageKey.ACCESS_TOKEN,
StorageKey.REFRESH_TOKEN,
StorageKey.USER_PROFILE,
StorageKey.USER_PERMISSIONS,
]);
+72 -39
View File
@@ -1,6 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { LocalStorageService } from './local-storage.service';
import { StorageKey, ENCRYPTED_KEYS } from './storage.key';
// ─── Mock @repo/utils EncryptionUtils ───────────────────────────
@@ -37,6 +36,27 @@ Object.defineProperty(globalThis, 'localStorage', {
// ─── Test Data ──────────────────────────────────────────────────
const TestStorageKey = {
THEME: 'theme',
LOCALE: 'locale',
ACCESS_TOKEN: 'access_token',
REFRESH_TOKEN: 'refresh_token',
USER_PROFILE: 'user_profile',
} as const;
type TestStorageKeyValue = (typeof TestStorageKey)[keyof typeof TestStorageKey];
const ENCRYPTED_KEYS = new Set<TestStorageKeyValue>([
TestStorageKey.ACCESS_TOKEN,
TestStorageKey.REFRESH_TOKEN,
TestStorageKey.USER_PROFILE,
]);
const PLAIN_KEYS = new Set<TestStorageKeyValue>([
TestStorageKey.THEME,
TestStorageKey.LOCALE,
]);
interface TestUser {
id: number;
name: string;
@@ -48,95 +68,108 @@ const testUser: TestUser = { id: 1, name: 'Firman', role: 'admin' };
// ─── Tests ──────────────────────────────────────────────────────
describe('LocalStorageService', () => {
let storage: LocalStorageService;
let storage: LocalStorageService<TestStorageKeyValue>;
beforeEach(() => {
vi.clearAllMocks();
for (const key of Object.keys(store)) delete store[key];
// Pass mock encryption utils to avoid importing real crypto-js
storage = new LocalStorageService(mockEncryptionUtils as never);
storage = new LocalStorageService<TestStorageKeyValue>(
{ encryptedKeys: ENCRYPTED_KEYS, plainTextKeys: PLAIN_KEYS },
mockEncryptionUtils as never
);
});
describe('Runtime Validation', () => {
it('throws an error if the key is not in encryptedKeys or plainTextKeys', async () => {
// Cast a rogue key to bypass TS for the runtime check test
const rogueKey = 'unregistered_key' as TestStorageKeyValue;
await expect(storage.setItem(rogueKey, 'data')).rejects.toThrowError(
"[Storage Engine] Security Exception: Key 'unregistered_key' is not registered and cannot be accessed."
);
});
});
// ── setItem / getItem ─────────────────────────────────────────
describe('setItem / getItem', () => {
it('stores and retrieves a plain object (non-encrypted key)', async () => {
await storage.setItem(StorageKey.THEME, 'dark');
await storage.setItem(TestStorageKey.THEME, 'dark');
const result = await storage.getItem<string>(StorageKey.THEME);
const result = await storage.getItem<string>(TestStorageKey.THEME);
expect(result).toBe('dark');
});
it('stores plain JSON without encryption for non-sensitive keys', async () => {
await storage.setItem(StorageKey.LOCALE, 'en-US');
await storage.setItem(TestStorageKey.LOCALE, 'en-US');
expect(mockEncrypt).not.toHaveBeenCalled();
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
StorageKey.LOCALE,
TestStorageKey.LOCALE,
'"en-US"',
);
});
it('encrypts sensitive keys (ACCESS_TOKEN)', async () => {
const token = 'eyJhbGciOiJIUzI1NiJ9.test';
await storage.setItem(StorageKey.ACCESS_TOKEN, token);
await storage.setItem(TestStorageKey.ACCESS_TOKEN, token);
expect(mockEncrypt).toHaveBeenCalledWith(JSON.stringify(token));
// The stored value should be the encrypted payload
expect(store[StorageKey.ACCESS_TOKEN]).toBe(`ENC[${JSON.stringify(token)}]`);
expect(store[TestStorageKey.ACCESS_TOKEN]).toBe(`ENC[${JSON.stringify(token)}]`);
});
it('decrypts sensitive keys on read', async () => {
const token = 'secret_token_123';
await storage.setItem(StorageKey.ACCESS_TOKEN, token);
await storage.setItem(TestStorageKey.ACCESS_TOKEN, token);
const result = await storage.getItem<string>(StorageKey.ACCESS_TOKEN);
const result = await storage.getItem<string>(TestStorageKey.ACCESS_TOKEN);
expect(mockDecrypt).toHaveBeenCalled();
expect(result).toBe(token);
});
it('stores and retrieves complex objects with generics', async () => {
await storage.setItem(StorageKey.THEME, testUser);
await storage.setItem(TestStorageKey.THEME, testUser);
const result = await storage.getItem<TestUser>(StorageKey.THEME);
const result = await storage.getItem<TestUser>(TestStorageKey.THEME);
expect(result).toEqual(testUser);
});
it('stores complex objects encrypted for sensitive keys', async () => {
await storage.setItem(StorageKey.USER_PROFILE, testUser);
await storage.setItem(TestStorageKey.USER_PROFILE, testUser);
expect(mockEncrypt).toHaveBeenCalledWith(JSON.stringify(testUser));
const result = await storage.getItem<TestUser>(StorageKey.USER_PROFILE);
const result = await storage.getItem<TestUser>(TestStorageKey.USER_PROFILE);
expect(result).toEqual(testUser);
});
it('returns null for non-existent keys', async () => {
const result = await storage.getItem<string>('nonexistent');
const result = await storage.getItem<string>('nonexistent' as TestStorageKeyValue);
expect(result).toBeNull();
});
it('handles corrupt/invalid JSON gracefully', async () => {
store[StorageKey.THEME] = '{invalid json';
store[TestStorageKey.THEME] = '{invalid json';
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const result = await storage.getItem<string>(StorageKey.THEME);
const result = await storage.getItem<string>(TestStorageKey.THEME);
expect(result).toBeNull();
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed to parse key'),
);
// Corrupt entry should be cleaned up
expect(store[StorageKey.THEME]).toBeUndefined();
expect(store[TestStorageKey.THEME]).toBeUndefined();
warnSpy.mockRestore();
});
it('handles failed decryption gracefully', async () => {
// Write raw garbage to an encrypted key
store[StorageKey.ACCESS_TOKEN] = 'not-encrypted-data';
store[TestStorageKey.ACCESS_TOKEN] = 'not-encrypted-data';
mockDecrypt.mockReturnValueOnce('');
const result = await storage.getItem<string>(StorageKey.ACCESS_TOKEN);
const result = await storage.getItem<string>(TestStorageKey.ACCESS_TOKEN);
expect(result).toBeNull();
});
});
@@ -145,11 +178,11 @@ describe('LocalStorageService', () => {
describe('removeItem', () => {
it('removes a key from storage', async () => {
await storage.setItem(StorageKey.THEME, 'dark');
await storage.removeItem(StorageKey.THEME);
await storage.setItem(TestStorageKey.THEME, 'dark');
await storage.removeItem(TestStorageKey.THEME);
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(StorageKey.THEME);
const result = await storage.getItem<string>(StorageKey.THEME);
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(TestStorageKey.THEME);
const result = await storage.getItem<string>(TestStorageKey.THEME);
expect(result).toBeNull();
});
});
@@ -158,8 +191,8 @@ describe('LocalStorageService', () => {
describe('clear', () => {
it('clears all keys from storage', async () => {
await storage.setItem(StorageKey.THEME, 'dark');
await storage.setItem(StorageKey.LOCALE, 'en');
await storage.setItem(TestStorageKey.THEME, 'dark');
await storage.setItem(TestStorageKey.LOCALE, 'en');
await storage.clear();
expect(mockLocalStorage.clear).toHaveBeenCalled();
@@ -171,12 +204,12 @@ describe('LocalStorageService', () => {
describe('hasItem', () => {
it('returns true for existing keys', async () => {
await storage.setItem(StorageKey.THEME, 'dark');
expect(await storage.hasItem(StorageKey.THEME)).toBe(true);
await storage.setItem(TestStorageKey.THEME, 'dark');
expect(await storage.hasItem(TestStorageKey.THEME)).toBe(true);
});
it('returns false for non-existent keys', async () => {
expect(await storage.hasItem('ghost_key')).toBe(false);
expect(await storage.hasItem('ghost_key' as TestStorageKeyValue)).toBe(false);
});
});
@@ -184,12 +217,12 @@ describe('LocalStorageService', () => {
describe('keys', () => {
it('returns all stored keys', async () => {
await storage.setItem(StorageKey.THEME, 'dark');
await storage.setItem(StorageKey.LOCALE, 'en');
await storage.setItem(TestStorageKey.THEME, 'dark');
await storage.setItem(TestStorageKey.LOCALE, 'en');
const allKeys = await storage.keys();
expect(allKeys).toContain(StorageKey.THEME);
expect(allKeys).toContain(StorageKey.LOCALE);
expect(allKeys).toContain(TestStorageKey.THEME);
expect(allKeys).toContain(TestStorageKey.LOCALE);
expect(allKeys).toHaveLength(2);
});
});
@@ -198,23 +231,23 @@ describe('LocalStorageService', () => {
describe('encryption key classification', () => {
it('ACCESS_TOKEN is in ENCRYPTED_KEYS', () => {
expect(ENCRYPTED_KEYS.has(StorageKey.ACCESS_TOKEN)).toBe(true);
expect(ENCRYPTED_KEYS.has(TestStorageKey.ACCESS_TOKEN)).toBe(true);
});
it('REFRESH_TOKEN is in ENCRYPTED_KEYS', () => {
expect(ENCRYPTED_KEYS.has(StorageKey.REFRESH_TOKEN)).toBe(true);
expect(ENCRYPTED_KEYS.has(TestStorageKey.REFRESH_TOKEN)).toBe(true);
});
it('USER_PROFILE is in ENCRYPTED_KEYS', () => {
expect(ENCRYPTED_KEYS.has(StorageKey.USER_PROFILE)).toBe(true);
expect(ENCRYPTED_KEYS.has(TestStorageKey.USER_PROFILE)).toBe(true);
});
it('THEME is NOT in ENCRYPTED_KEYS', () => {
expect(ENCRYPTED_KEYS.has(StorageKey.THEME)).toBe(false);
expect(ENCRYPTED_KEYS.has(TestStorageKey.THEME)).toBe(false);
});
it('LOCALE is NOT in ENCRYPTED_KEYS', () => {
expect(ENCRYPTED_KEYS.has(StorageKey.LOCALE)).toBe(false);
expect(ENCRYPTED_KEYS.has(TestStorageKey.LOCALE)).toBe(false);
});
});
});