Files
trackgo-fe/packages/core-storage/README.md
T

4.2 KiB

@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.

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.

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:

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.

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.