@pixpilot/chrome-storage
v1.0.0
Published
A powerful and type-safe Chrome Storage API wrapper with optional built-in encryption support for Chrome extensions.
Readme
Chrome Storage
A powerful and type-safe Chrome Storage API wrapper with optional built-in encryption support for Chrome extensions.
Features
- 🎯 Type-Safe: Full TypeScript support with generics for strongly-typed storage
- 📦 Simple API: Intuitive methods for get, set, remove, and more
- 🔄 Flexible: Support for both local and sync storage areas
- 🔒 Optional Encryption: Secure AES-GCM encryption when you need it
- 🧪 Well Tested: Comprehensive test coverage
- ⚡ Easy to Use: Minimal setup for basic usage
Installation
npm install @pixpilot/chrome-storageQuick Start
Basic Usage (Type-Safe Storage)
import { ChromeStorage } from '@pixpilot/chrome-storage';
const storage = new ChromeStorage();
// Store typed data
await storage.set('username', 'john_doe');
await storage.set('settings', { theme: 'dark', notifications: true });
const initialCount = 42;
await storage.set('count', initialCount);
// Retrieve typed data
const username = await storage.get<string>('username');
const settings = await storage.get<{ theme: string; notifications: boolean }>('settings');
const count = await storage.get<number>('count');
console.log(username); // 'john_doe'
console.log(settings?.theme); // 'dark'
// Check if data exists
const hasKey = await storage.has('username');
console.log(hasKey); // true
// Remove data
await storage.remove('username');
// Set multiple items at once
await storage.setMultiple({
name: 'John',
age: 30,
email: '[email protected]',
});
// Get multiple items
const result = await storage.getMultiple<string>(['name', 'email']);
console.log(result); // { name: 'John', email: '[email protected]' }
// Get all items
const all = await storage.getAll();
console.log(all);
// Clear all storage
await storage.clear();Type-Safe Storage Schema
Define a schema for fully type-safe keys and values - TypeScript will prevent you from using invalid keys:
import { ChromeStorage } from '@pixpilot/chrome-storage';
// Define your storage schema (use type or interface)
interface MyStorageSchema {
username: string;
userAge: number;
settings: { theme: string; notifications: boolean };
}
// Or use a type
interface MyStorageSchema {
username: string;
userAge: number;
settings: { theme: string; notifications: boolean };
}
// Create storage with schema
const storage = new ChromeStorage<MyStorageSchema>();
// Now your keys and values are fully type-safe!
await storage.set('username', 'john_doe'); // ✓ Valid
await storage.set('userAge', 25); // ✓ Valid
await storage.set('username', 123); // ✗ TypeScript error: Type 'number' is not assignable to type 'string'
// Invalid keys are caught at compile time
await storage.set('invalidKey', 'value'); // ✗ TypeScript error: Argument of type '"invalidKey"' is not assignable
await storage.set('userAge', 'not-a-number'); // ✗ TypeScript error: Type 'string' is not assignable to type 'number'
// Type-safe retrieval with auto-complete
const username = await storage.get('username'); // Type: string | undefined
const age = await storage.get('userAge'); // Type: number | undefined
const settings = await storage.get('settings'); // Type: { theme: string; notifications: boolean } | undefined
// All methods enforce schema keys
await storage.remove('username'); // ✓ Valid
await storage.remove('invalidKey'); // ✗ TypeScript error
// getMultiple returns correctly typed partial object
const data = await storage.getMultiple(['username', 'userAge']);
// Type: Partial<Pick<MyStorageSchema, 'username' | 'userAge'>>
// data.username is string | undefined
// data.userAge is number | undefined
// getAll returns all schema keys
const all = await storage.getAll();
// Type: Partial<MyStorageSchema>One-Time Data (getOnce)
Get data and immediately remove it - useful for temporary tokens, one-time codes, or message passing:
// Without schema - allows any string key
const storage = new ChromeStorage();
// Store one-time token
await storage.set('oneTimeToken', 'abc123');
// Get and remove in one operation
const token = await storage.getOnce('oneTimeToken');
console.log(token); // 'abc123'
// Token is now removed
const tokenAfter = await storage.get('oneTimeToken');
console.log(tokenAfter); // undefined
// With type-safe schema
interface MyStorage {
oneTimeCode: string;
tempSession: { userId: number; expires: number };
}
const typedStorage = new ChromeStorage<MyStorage>();
await typedStorage.set('oneTimeCode', 'CODE123');
const code = await typedStorage.getOnce('oneTimeCode'); // Type: string | undefined
// typedStorage.getOnce('invalidKey'); // ✗ TypeScript error
// Works with complex data
const session = { userId: 123, expires: Date.now() + 3600000 };
await typedStorage.set('tempSession', session);
const retrievedSession = await typedStorage.getOnce('tempSession');
// Type: { userId: number; expires: number } | undefinedError Handling
All methods throw errors on failure - use try-catch for error handling:
try {
await storage.set('key', 'value');
const value = await storage.get('key');
console.log('Data:', value);
} catch (error) {
console.error('Storage error:', error.message);
}Using Sync Storage
const storage = new ChromeStorage();
// Store in sync storage instead of local
await storage.set('theme', 'dark', { area: 'sync' });
const theme = await storage.get<string>('theme', { area: 'sync' });Key Transformation for Namespacing
const storage = new ChromeStorage({
keyTransformer: (key) => `myapp_${key}`,
});
// Internally stored as "myapp_setting"
await storage.set('setting', 'value');Watching for Changes
Listen to changes for specific storage keys using the watch method:
import { ChromeStorage } from '@pixpilot/chrome-storage';
// Define your storage schema
interface MyStorage {
username: string;
settings: { theme: string; notifications: boolean };
count: number;
}
const storage = new ChromeStorage<MyStorage>();
// Watch for changes to a specific key
const unsubscribe = storage.watch('username', (change, key, area) => {
console.log(`${key} changed in ${area} storage`);
console.log('Old value:', change.oldValue);
console.log('New value:', change.newValue);
});
// Later, stop watching
unsubscribe();Watch with Storage Area Filter
// Only listen to changes in local storage
const unsubscribe = storage.watch(
'settings',
(change) => {
console.log('Settings updated:', change.newValue);
},
{ area: 'local' },
);
// This will only trigger for local storage changes, not sync storageMultiple Listeners
// You can add multiple listeners for the same key
const unsubscribe1 = storage.watch('username', (change) => {
console.log('Listener 1:', change.newValue);
});
const unsubscribe2 = storage.watch('username', (change) => {
console.log('Listener 2:', change.newValue);
});
// Both listeners will be called when username changes
// Remove listeners independently
unsubscribe1();
// unsubscribe2 still activeReact Hook Example
import { useEffect, useState } from 'react';
function useStorageValue<T>(
storage: ChromeStorage<MyStorage>,
key: keyof MyStorage,
): T | undefined {
const [value, setValue] = useState<T | undefined>();
useEffect(() => {
// Get initial value
storage.get(key).then((initialValue) => {
setValue(initialValue as T | undefined);
});
// Watch for changes
const unsubscribe = storage.watch(key, (change) => {
setValue(change.newValue as T | undefined);
});
// Cleanup on unmount
return () => {
unsubscribe();
};
}, [storage, key]);
return value;
}
// Usage
function MyComponent() {
const username = useStorageValue<string>(storage, 'username');
return <div>Username: {username}</div>;
}Handling Different Change Types
storage.watch('username', (change, key, area) => {
if (change.oldValue === undefined) {
console.log('Item created:', change.newValue);
} else if (change.newValue === undefined) {
console.log('Item deleted:', change.oldValue);
} else {
console.log('Item updated:', change.oldValue, '->', change.newValue);
}
});Using with Key Transformer
The watch method respects the keyTransformer option:
const storage = new ChromeStorage({
keyTransformer: (key) => `myapp_${key}`,
});
// Watches for changes to "myapp_username" in storage
storage.watch('username', (change) => {
console.log('Username changed:', change.newValue);
});Note: The callback receives the transformed key, not the original key.
API Reference
ChromeStorage Class
Constructor
// Constructor signature
// new ChromeStorage<TSchema>(options?: GenericChromeStorageOptions)Generic Parameters:
TSchema?- Optional schema type (interface or type) for fully type-safe keys and values. When provided, only keys from the schema are allowed (compile-time enforcement). Do not useextends Record<string, unknown>as it defeats the type safety.
Options:
keyTransformer?: (key: string) => string- Transform keys (e.g., add prefix for namespacing)encryptionProvider?: EncryptionProvider | null- Optional encryption provider (see Advanced Usage)
Methods
All methods throw errors on failure. Use try-catch for error handling. When using a schema, all methods enforce type-safe keys at compile time.
get<K>(key: K, options?)- Get a single item. ReturnsTSchema[K] | undefined. Key must be from schema.getOnce<K>(key: K, options?)- Get and remove a single item. ReturnsTSchema[K] | undefined. Key must be from schema.getMultiple<K>(keys: K[], options?)- Get multiple items. ReturnsPartial<Pick<TSchema, K>>. Keys must be from schema.getAll(options?)- Get all items from storage. ReturnsPartial<TSchema>.set<K>(key: K, value: TSchema[K], options?)- Set a single item. Key and value type must match schema.setMultiple<K>(items: Partial<Pick<TSchema, K>>, options?)- Set multiple items. Keys and values must match schema.remove(keys: keyof TSchema | (keyof TSchema)[], options?)- Remove item(s). Keys must be from schema.clear(options?)- Clear all items from storage areahas(key: keyof TSchema | string, options?)- Check if key exists (allows any string for flexibility)getBytesInUse(keys?: keyof TSchema | (keyof TSchema)[], options?)- Get storage usage in byteswatch<K>(key: K, callback: StorageChangeCallback<TSchema[K]>, options?)- Watch for changes to a specific key. Returns an unsubscribe function. Options can includeareato filter by storage area ('local' or 'sync').
Storage Change Callback
type StorageChangeCallback<T> = (
change: StorageChange<T>,
key: string,
area: 'local' | 'sync',
) => void;
interface StorageChange<T> {
oldValue?: T; // undefined if item was just created
newValue?: T; // undefined if item was just deleted
}Storage Options
interface StorageOptions {
area?: 'local' | 'sync'; // Storage area (default: 'local')
encrypted?: boolean; // Use encryption (default: false, requires encryptionProvider)
}Note: All methods throw errors on failure instead of returning success/error objects.
Advanced Usage: Encryption
For sensitive data, ChromeStorage supports optional AES-GCM encryption with user-provided keys.
⚠️ Security Notice
IMPORTANT: Encryption requires you to provide your own keys. Keys are NEVER stored in chrome.storage. This ensures that even if someone gains access to the extension's storage, they cannot decrypt the data without the key.
Secure Storage with Password-Derived Keys
import { ChromeStorage } from '@pixpilot/chrome-storage';
import { DefaultEncryptionProvider } from '@pixpilot/chrome-storage/default-encryption-provider';
import { deriveKeyFromPassword } from '@pixpilot/chrome-storage/secure-storage';
// First time: Derive key from user password
const { key, salt } = await deriveKeyFromPassword('user-password');
// Store the salt (it's safe to store, needed for key re-derivation)
await chrome.storage.local.set({ userSalt: salt });
// Create encryption provider with the key
const encryptionProvider = new DefaultEncryptionProvider({ key });
// Create storage manager with encryption
const storage = new ChromeStorage({ encryptionProvider });
// Store encrypted data (automatically serialized)
await storage.set('apiKey', 'secret-key-123', { encrypted: true });
await storage.set('credentials', { user: 'admin', pass: 'secret' }, { encrypted: true });
// Retrieve encrypted data (automatically deserialized)
const apiKey = await storage.get<string>('apiKey', { encrypted: true });
const credentials = await storage.get<{ user: string; pass: string }>('credentials', {
encrypted: true,
});
console.log(apiKey); // 'secret-key-123'
// Later: Re-derive key from password
const storedData = await chrome.storage.local.get('userSalt');
const { key: derivedKey } = await deriveKeyFromPassword(
'user-password',
storedData.userSalt,
);Low-Level Encryption Utilities
For direct encryption/decryption without the storage manager:
import {
deriveKeyFromPassword,
getEncrypted,
hasEncrypted,
removeEncrypted,
storeEncrypted,
} from '@pixpilot/chrome-storage/secure-storage';
// Derive key from password
const { key, salt } = await deriveKeyFromPassword('user-password');
await chrome.storage.local.set({ userSalt: salt });
// Store encrypted data (string only for low-level API)
await storeEncrypted('myKey', 'sensitive data', key, { area: 'local' });
// Retrieve encrypted data
const data = await getEncrypted('myKey', key);
// Check if encrypted data exists
const exists = await hasEncrypted('myKey');
// Remove encrypted data
await removeEncrypted('myKey');Custom Encryption Provider
import type { EncryptionProvider } from '@pixpilot/chrome-storage';
import { ChromeStorage } from '@pixpilot/chrome-storage';
// Implement custom encryption logic
class CustomEncryption implements EncryptionProvider {
async encrypt(key: string, value: string): Promise<void> {
// Your encryption logic
}
async decrypt(key: string): Promise<string | null> {
// Your decryption logic
}
async remove(key: string): Promise<void> {
// Cleanup logic
}
async has(key: string): Promise<boolean> {
// Check existence
}
}
const storage = new ChromeStorage({
encryptionProvider: new CustomEncryption(),
});Disable Encryption
// Explicitly disable encryption
const storage = new ChromeStorage({
encryptionProvider: null,
});
// This will now throw an error
try {
await storage.set('data', 'value', { encrypted: true });
} catch (error) {
console.error(error.message); // 'Encryption provider not configured'
}🔐 Security Best Practices (When Using Encryption)
1. Never Store Encryption Keys
❌ DON'T DO THIS:
// WRONG: Storing key defeats the purpose of encryption
const key = await generateEncryptionKey();
const exportedKey = await crypto.subtle.exportKey('raw', key);
await chrome.storage.local.set({ myKey: Array.from(new Uint8Array(exportedKey)) });✅ DO THIS:
// CORRECT: Derive key from user password, store only the salt
const { key, salt } = await deriveKeyFromPassword('user-password');
await chrome.storage.local.set({ salt }); // Salt is safe to store
// Use key for encryption/decryption, then let it be garbage collected2. Use Strong Passwords
// Enforce minimum password requirements
function isStrongPassword(password: string): boolean {
const MIN_PASSWORD_LENGTH = 12;
return (
password.length >= MIN_PASSWORD_LENGTH &&
/[A-Z]/u.test(password) &&
/[a-z]/u.test(password) &&
/\d/u.test(password) &&
/[^A-Z0-9]/iu.test(password)
);
}
const password = await promptUserForPassword();
if (!isStrongPassword(password)) {
throw new Error('Password does not meet minimum requirements');
}
const { key, salt } = await deriveKeyFromPassword(password);3. Handle Decryption Failures Gracefully
try {
const data = await storage.get('myData', { encrypted: true });
if (data === undefined) {
// Data doesn't exist
console.log('No data found');
} else {
// Data decrypted successfully
console.log('Data:', data);
}
} catch (error) {
// Wrong key or corrupted data
console.error('Decryption failed:', error.message);
// Prompt user to re-enter password or reset data
}Encryption Technical Details
When encryption is enabled:
- Algorithm: AES-GCM 256-bit encryption via Web Crypto API
- Key Derivation: PBKDF2 with 210,000 iterations (OWASP 2025 minimum), SHA-256
- Unique IVs: Each encrypted value has its own initialization vector
- Data Integrity: AES-GCM provides built-in authentication
- Serialization: Automatic JSON serialization/deserialization for type safety
Testing
The package includes comprehensive tests. Run them with:
npm testLicense
MIT
Contributing
Contributions are welcome! Please read our contributing guidelines first.
