115 lines
4.2 KiB
Markdown
115 lines
4.2 KiB
Markdown
# @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 `secureStorage` (or your app's configured instance) for small payloads. If the key is in `ENCRYPTED_KEYS`, it will be encrypted at rest.
|
|
|
|
```typescript
|
|
import { secureStorage, 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 secureStorage.setItem(StorageKey.USER_PROFILE, {
|
|
id: 1,
|
|
name: 'Firman',
|
|
role: 'admin'
|
|
});
|
|
|
|
// READ
|
|
const profile = await secureStorage.getItem<UserProfile>(StorageKey.USER_PROFILE);
|
|
if (profile) {
|
|
console.log('Welcome back,', profile.name);
|
|
}
|
|
|
|
// DELETE
|
|
await secureStorage.removeItem(StorageKey.USER_PROFILE);
|
|
```
|
|
|
|
### 2. IndexedDB (Offline Data, Large Payloads)
|
|
|
|
Use the pre-configured `secureIndexedDB` (or instantiate your own) for large, asynchronous data. It uses the exact same API and encryption pipeline.
|
|
|
|
```typescript
|
|
import { secureIndexedDB } from '@repo/core-storage';
|
|
|
|
interface DraftData {
|
|
id: string;
|
|
content: string;
|
|
lastModified: number;
|
|
}
|
|
|
|
// Save a large draft offline
|
|
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');
|
|
```
|
|
|
|
---
|
|
|
|
## 🔑 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.
|