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
+88 -101
View File
@@ -4,7 +4,7 @@
The **Enterprise-grade storage engine** for the monorepo.
This package provides a unified, Promise-based interface for interacting with browser storage (`localStorage` and `IndexedDB`). It enforces strict type safety, prevents key collisions, and automatically provides **AES encryption at rest** for sensitive payloads (like access tokens) using `@repo/utils`.
This package provides a unified, Factory-based interface for interacting with browser storage (`localStorage` and `IndexedDB`). It enforces strict type safety, **Runtime Validation**, App Autonomy (Inversion of Control), and automatically provides **AES encryption at rest** for sensitive payloads (like access tokens) using `@repo/utils`.
---
@@ -12,13 +12,16 @@ This package provides a unified, Promise-based interface for interacting with br
```mermaid
graph TD
subgraph Apps ["apps/* (Consumers)"]
subgraph Apps ["apps/* (App Autonomy)"]
REG[[AppStorageKey & App Registries]]
UI[React Components / API Interceptors]
INST{{Storage Instances}}
end
subgraph Core ["@repo/core-storage (Engine)"]
subgraph Core ["@repo/core-storage (Engine Factories)"]
API[IStorageService API]
REG[[StorageKey & ENCRYPTED_KEYS Registry]]
FAC[createLocalStorage / createIndexedDB]
VAL{Runtime Gatekeeper}
ENC{{AES Encryption Pipeline}}
LOCAL[LocalStorage Adapter]
IDB[IndexedDB Adapter]
@@ -29,153 +32,137 @@ graph TD
B_IDB[(IndexedDB)]
end
UI -->|getItem / setItem| API
API --> REG
REG -.->|Sensitive Key?| ENC
ENC -.-> LOCAL & IDB
REG -.->|Plain-text Key| LOCAL & IDB
REG -.->|Injects Keys & Config| FAC
FAC --> INST
UI -->|getItem / setItem| INST
INST --> API
API --> VAL
VAL -.->|Valid Key?| ENC
VAL -.->|Invalid Key!| ERR[Throws Security Exception]
ENC -.->|Sensitive Key| LOCAL & IDB
VAL -.->|Plain-text Key| LOCAL & IDB
LOCAL <--> B_LOCAL
IDB <--> B_IDB
%% Styling Subgraphs (Backgrounds)
%% Styling Subgraphs
style Apps fill:#e7f5ff,stroke:#74c0fc,stroke-width:2px,color:#1864ab
style Core fill:#f8f9fa,stroke:#ced4da,stroke-width:2px,color:#495057
style Browser fill:#f1f3f5,stroke:#ced4da,stroke-width:2px,color:#495057
%% Styling App Nodes (Blue)
%% Styling Nodes
style UI fill:#339af0,stroke:#1864ab,color:#fff
%% Styling Core Nodes (Purple Engine, Green Registry, Gold Encryption)
style REG fill:#1864ab,stroke:#1864ab,color:#fff
style INST fill:#339af0,stroke:#1864ab,color:#fff
style API fill:#845ef7,stroke:#5f3dc4,color:#fff
style FAC fill:#845ef7,stroke:#5f3dc4,color:#fff
%% Gatekeeper is GREEN (Security Checkpoint), Error is RED
style VAL fill:#20c997,stroke:#089981,color:#fff
style ERR fill:#fa5252,stroke:#c92a2a,color:#fff
style LOCAL fill:#845ef7,stroke:#5f3dc4,color:#fff
style IDB fill:#845ef7,stroke:#5f3dc4,color:#fff
style REG fill:#20c997,stroke:#089981,color:#fff
style ENC fill:#fab005,stroke:#e67700,color:#fff
%% Styling Browser Nodes (Neutral Gray)
style B_LOCAL fill:#868e96,stroke:#495057,color:#fff
style B_IDB fill:#868e96,stroke:#495057,color:#fff
```
---
## 🎯 Primary Goals & Separation of Concerns
## 🎯 Primary Goals & Architectural Principles
* **Separation from `@repo/core-api`**: Storage is a fundamental primitive. While the API client uses storage (to retrieve tokens), storage itself does not need to know about HTTP requests.
* **App Autonomy (Inversion of Control)**: The core storage engine does not know about your application's keys. Consuming applications define their own keys, their own `encryptedKeys` sets, and their own `plainTextKeys` sets, injecting them into the factory upon instantiation.
* **Runtime Gatekeeper (Defensive Programming)**: The engine validates every `setItem`, `getItem`, and `removeItem` operation. If an app attempts to access a key that wasn't explicitly registered in `encryptedKeys` or `plainTextKeys`, the engine will immediately throw a Security Exception to prevent rogue data access/injection.
* **Dual Backend Strategy**:
* `secureStorage` (localStorage): Ideal for small, synchronous-like data (tokens, user preferences, settings).
* `secureIndexedDB` (IndexedDB): Built for large, asynchronous data (offline drafts, cached API responses, large arrays/blobs) bypassing the 5MB localStorage limit.
* **Security by Default**: Developers don't need to manually encrypt/decrypt data. If a key is marked as sensitive, the library handles AES encryption transparently.
* `createLocalStorage`: Ideal for small, synchronous-like data (tokens, user preferences, settings).
* `createIndexedDB`: Built for large, asynchronous data (offline drafts, cached API responses, large arrays/blobs) bypassing the 5MB localStorage limit.
* **Security by Default**: Developers don't need to manually encrypt/decrypt data. If a key is passed in the `encryptedKeys` configuration, the engine handles AES encryption transparently.
* **Corrupt Data Resilience**: If parsing or decryption fails (e.g., tampered data or changed encryption keys), the corrupt entry is safely removed and returns `null`, preventing the app from crashing.
---
## 🚀 Usage Examples
## 🚀 App-Level Setup & Usage
Because this package is framework-agnostic, these instances can be imported anywhere: React components, Redux/Zustand stores, or Axios interceptors.
### 1. Define App Keys and Instantiate (Inversion of Control)
### 1. Secure Local Storage (Tokens, Profile)
Use `secureStorage` for small payloads. If the key is in `ENCRYPTED_KEYS`, it will be encrypted at rest automatically.
In your consuming application (e.g., `apps/web/src/core/storage/index.ts`), define your keys and use the factories to create your instances.
```typescript
import { secureStorage, StorageKey } from '@repo/core-storage';
// apps/web/src/core/storage/index.ts
import { createLocalStorage, createIndexedDB } from '@repo/core-storage';
// 1. Define Keys
export const AppStorageKey = {
USER_PROFILE: 'user_profile',
ACCESS_TOKEN: 'access_token',
LOCALE: 'app_locale',
} as const;
export type AppStorageKeyValue = (typeof AppStorageKey)[keyof typeof AppStorageKey];
// 2. Classify Keys
export const ENCRYPTED_KEYS = new Set<AppStorageKeyValue>([
AppStorageKey.USER_PROFILE,
AppStorageKey.ACCESS_TOKEN,
]);
export const PLAIN_KEYS = new Set<AppStorageKeyValue>([
AppStorageKey.LOCALE,
]);
// 3. Instantiate Factories
export const secureStorage = createLocalStorage<AppStorageKeyValue>({
encryptedKeys: ENCRYPTED_KEYS,
plainTextKeys: PLAIN_KEYS
});
export const secureIndexedDB = createIndexedDB<AppStorageKeyValue>({
dbName: 'eigen_erp_db',
storeName: 'web_store',
encryptedKeys: ENCRYPTED_KEYS,
plainTextKeys: PLAIN_KEYS
});
```
### 2. Usage in App Components
Now, you can import your locally-created instances anywhere in your app.
```typescript
import { secureStorage, AppStorageKey } from '@/core/storage';
import type { UserProfile } from '@/types';
// CREATE / UPDATE
// If StorageKey.USER_PROFILE is in ENCRYPTED_KEYS, this is AES-encrypted automatically.
await secureStorage.setItem(StorageKey.USER_PROFILE, {
// Since USER_PROFILE is in ENCRYPTED_KEYS, it is AES-encrypted automatically.
await secureStorage.setItem(AppStorageKey.USER_PROFILE, {
id: 1,
name: 'Firman',
role: 'admin'
});
// READ (Returns null if not found or if decryption fails)
const profile = await secureStorage.getItem<UserProfile>(StorageKey.USER_PROFILE);
const profile = await secureStorage.getItem<UserProfile>(AppStorageKey.USER_PROFILE);
if (profile) {
console.log('Welcome back,', profile.name);
}
// DELETE
await secureStorage.removeItem(StorageKey.USER_PROFILE);
await secureStorage.removeItem(AppStorageKey.USER_PROFILE);
```
### 2. IndexedDB (Offline Data, Large Payloads)
### 3. The Runtime Gatekeeper
Use the pre-configured `secureIndexedDB` for large, asynchronous data. It uses the exact same `IStorageService` interface and encryption pipeline as local storage.
If you try to access an unregistered key, the engine protects the app by throwing an error at runtime:
```typescript
import { secureIndexedDB } from '@repo/core-storage';
interface DraftData {
id: string;
content: string;
lastModified: number;
}
// Save a large draft offline (No 5MB limit)
await secureIndexedDB.setItem('offline_draft_123', {
id: '123',
content: 'Huge text content...',
lastModified: Date.now()
});
// Retrieve the draft
const draft = await secureIndexedDB.getItem<DraftData>('offline_draft_123');
// Throws Error: "[Storage Engine] Security Exception: Key 'rogue_key' is not registered..."
await secureStorage.setItem('rogue_key' as any, 'hacked');
```
---
## 🔑 Adding New Keys (Current Registry Pattern)
To maintain type safety and avoid collisions across the monorepo, **all** global storage keys must currently be registered in `packages/core-storage/src/storage.key.ts`.
### 1. Register the Key
Add your key to the `StorageKey` object:
```typescript
export const StorageKey = {
// ... existing keys
MY_NEW_FEATURE: 'my_new_feature_key',
} as const;
```
### 2. Define Encryption (If Needed)
If the data stored under this key is sensitive (e.g., PII, tokens, financials), add it to the `ENCRYPTED_KEYS` set.
```typescript
export const ENCRYPTED_KEYS: ReadonlySet<string> = new Set<string>([
StorageKey.ACCESS_TOKEN,
StorageKey.REFRESH_TOKEN,
StorageKey.USER_PROFILE,
StorageKey.MY_NEW_FEATURE, // <--- Now encrypted at rest!
]);
```
> [!WARNING]
> **Migration Hazard**: If you add an existing plain-text key to `ENCRYPTED_KEYS`, existing users will experience a parsing failure on their next session (because the library expects encrypted data but finds plain text). The resilient parser will catch this and gracefully clear the key, which may effectively log them out or reset their local preference.
---
## 🚧 Planned Updates: Inversion of Control (IoC) Refactor
**Current Limitation:** Currently, the `StorageKey` and `ENCRYPTED_KEYS` registries live inside `@repo/core-storage`. This violates the strict **App Autonomy (IoC)** principle established in other core packages (like `@repo/core-events`), as consuming applications must modify the core package to register their app-specific keys.
**Future Architecture Roadmap:** In a future major update, this package will be refactored into a pure Factory/Engine pattern to fully decouple it from application business logic.
1. The core will export a generic `StorageEngine` class or `createStorage()` factory.
2. Apps (`apps/web`, `apps/landing`) will instantiate their own storage engines and define their own key registries and encryption rules autonomously.
*Proposed Future API:*
```typescript
// apps/web/src/lib/storage.ts
import { StorageEngine } from '@repo/core-storage';
export const webStorage = new StorageEngine({
prefix: 'web_erp_',
encryptedKeys: ['access_token', 'user_profile'],
});
```
> **Migration Hazard**: If you move an existing key from `plainTextKeys` to `encryptedKeys` (or vice versa), existing users will experience a parsing failure on their next session (because the library expects encrypted data but finds plain text). The resilient parser will catch this and gracefully clear the key, which may effectively log them out or reset their local preference.