refactor: decouple core-storage services from static keys and update i18n setup to use configurable storage adapters
This commit is contained in:
+88
-101
@@ -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.
|
||||
Reference in New Issue
Block a user