npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

Quick 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 } | undefined

Error 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 storage

Multiple 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 active

React 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 use extends 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. Returns TSchema[K] | undefined. Key must be from schema.
  • getOnce<K>(key: K, options?) - Get and remove a single item. Returns TSchema[K] | undefined. Key must be from schema.
  • getMultiple<K>(keys: K[], options?) - Get multiple items. Returns Partial<Pick<TSchema, K>>. Keys must be from schema.
  • getAll(options?) - Get all items from storage. Returns Partial<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 area
  • has(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 bytes
  • watch<K>(key: K, callback: StorageChangeCallback<TSchema[K]>, options?) - Watch for changes to a specific key. Returns an unsubscribe function. Options can include area to 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 collected

2. 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 test

License

MIT

Contributing

Contributions are welcome! Please read our contributing guidelines first.