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
+114
View File
@@ -0,0 +1,114 @@
# @repo/core-storage
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`.
---
## 🎯 Primary Goals & Separation of Concerns
* **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.
* **Dual Backend Strategy**:
* `secureStorage` (localStorage): Ideal for small, synchronous-like data (tokens, user preferences).
* `IndexedDBService`: Built for large, asynchronous data (offline drafts, cached API responses, blobs) without the 5MB size 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.
---
## ✨ Key Features
| Feature | Description |
|---|---|
| 🔒 **Selective Encryption** | Uses `@repo/utils` `EncryptionUtils` to automatically AES-encrypt payloads whose keys are listed in `ENCRYPTED_KEYS`. |
| 🛡️ **Type-Safe Keys** | All keys must be registered in `storage.key.ts`. Prevents typos and key collisions across the monorepo. |
| 🔄 **Unified Promise API** | Both `localStorage` and `IndexedDB` implement the same async `IStorageService` interface. |
| 🧬 **Strict Generics** | Read and write operations enforce payload types via generics (e.g., `getItem<UserProfile>('user_profile')`). |
| 🩹 **Corrupt Data Resilience** | If parsing or decryption fails (e.g., tampered data), the corrupt entry is safely removed and returns `null`. |
---
## 🚀 Usage Examples
### 1. Secure Local Storage (Tokens, Profile)
Use `demoSecureStorage` (or your app's configured instance) for small payloads. If the key is in `ENCRYPTED_KEYS`, it will be encrypted at rest.
```typescript
import { demoSecureStorage, StorageKey } from '@repo/core-storage';
import type { UserProfile } from '@/types';
// CREATE / UPDATE
// If StorageKey.USER_PROFILE is in ENCRYPTED_KEYS, this is AES-encrypted automatically.
await demoSecureStorage.setItem(StorageKey.USER_PROFILE, {
id: 1,
name: 'Firman',
role: 'admin'
});
// READ
const profile = await demoSecureStorage.getItem<UserProfile>(StorageKey.USER_PROFILE);
if (profile) {
console.log('Welcome back,', profile.name);
}
// DELETE
await demoSecureStorage.removeItem(StorageKey.USER_PROFILE);
```
### 2. IndexedDB (Offline Data, Large Payloads)
Use the pre-configured `demoIndexedDB` (or instantiate your own) for large, asynchronous data. It uses the exact same API and encryption pipeline.
```typescript
import { demoIndexedDB } from '@repo/core-storage';
interface DraftData {
id: string;
content: string;
lastModified: number;
}
// Save a large draft offline
await demoIndexedDB.setItem('offline_draft_123', {
id: '123',
content: 'Huge text content...',
lastModified: Date.now()
});
// Retrieve the draft
const draft = await demoIndexedDB.getItem<DraftData>('offline_draft_123');
```
---
## 🔑 Adding New Keys
To maintain type safety and avoid collisions, **all** `localStorage` keys must 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]
> 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 clear the key, effectively logging them out or resetting the preference.