@febkosq8/local-save
Version:
Lightweight wrapper around IndexedDB for secure and structured client-side data storage.
271 lines (268 loc) • 10.9 kB
TypeScript
/**
* IndexedDB database name.
*/
type DBName = string;
/**
* Raw encryption key string used to derive an AES-GCM key.
*/
type EncryptionKey = string;
/**
* Object store name used as a logical category.
*/
type Category = string;
/**
* Number expected to be positive by runtime validation.
*/
type PositiveNumber = number;
/**
* Canonical record structure stored by LocalSave before encryption.
*/
interface DBItem {
/** Unix timestamp in milliseconds when the item was written. */
timestamp: number;
/** User payload associated with the key. */
data: unknown;
}
/**
* Base64 payload containing IV + encrypted record bytes.
*/
type DBItemEncryptedBase64 = string;
/**
* Configuration options for constructing LocalSave.
*/
interface Config {
/**
* The name of the database to use for local save
*
* @default "LocalSave"
*/
dbName?: DBName;
/**
* The key to use for encrypting and decrypting data
* Not providing this will store data in plain text
* Should be a string without spaces of length 16, 24, or 32 characters
*
* @default undefined
*/
encryptionKey?: EncryptionKey;
/**
* The categories to use for storing data
* You can use these to separate different types of data
*
* @default ["userData"]
*/
categories?: Category[];
/**
* The threshold in milliseconds for expiring data.
*
* Example day-to-ms conversion: days * 24 * 60 * 60 * 1000
*
* Default is 30 days - 30 * 24 * 60 * 60 * 1000
* @default 2592000000
*/
expiryThreshold?: PositiveNumber;
/**
* The time in milliseconds to wait before failing blocked IndexedDB open/delete requests.
*
* @default 10000
*/
blockedTimeoutThreshold?: PositiveNumber;
/**
* Whether to clear all data for a category if an error occurs while decrypting data
* Most likely reason of error is due to an incorrect encryption key
*
* @default true
*/
clearOnDecryptError?: boolean;
/**
* Whether to print logs
* Includes debug and errors logs
*
* @default false
*/
printLogs?: boolean;
}
/**
* LocalSave provides a small IndexedDB-backed key-value API with optional AES-GCM encryption,
* category-based separation, and utility operations like key listing, expiration, and teardown.
*/
declare class LocalSave {
dbName: DBName;
encryptionKey?: EncryptionKey;
private crypto;
categories: Category[];
expiryThreshold: PositiveNumber;
blockedTimeoutThreshold: PositiveNumber;
clearOnDecryptError: boolean;
printLogs: boolean;
/**
* Creates a LocalSave instance and applies configuration defaults.
*
* - Persists constructor options into instance fields used by all operations.
* - Validates encryption key format when provided.
* - Validates numeric thresholds to fail fast for invalid runtime behavior.
*
* @param config Optional runtime configuration object.
* @param config.dbName Optional IndexedDB database name override.
* @param config.encryptionKey Optional AES-GCM key (no whitespace, length 16, 24, or 32).
* @param config.categories Optional list of object store categories to use.
* @param config.expiryThreshold Optional default expiration window in milliseconds.
* @param config.blockedTimeoutThreshold Optional timeout in milliseconds for blocked open/delete requests.
* @param config.clearOnDecryptError Optional flag to clear a category when decrypt fails.
* @param config.printLogs Optional flag to enable debug/error logging.
*
* @throws {LocalSaveConfigError} If encryption key contains whitespace or its length is invalid.
* @throws {LocalSaveConfigError} If expiryThreshold is not a positive finite number.
* @throws {LocalSaveConfigError} If blockedTimeoutThreshold is not a positive finite number.
*/
constructor(config?: Config);
/**
* Opens a connection to the IndexedDB database.
* It handles the database versioning and ensures that the required object stores are created if they do not exist.
*
* @internal
*
* @param version - The version of the database to open. Optional.
*
* @returns {Promise<IDBDatabase>} A promise that resolves to the opened IDBDatabase instance.
*/
private openDB;
/**
* Lists all object stores currently available in the configured database.
*
* @internal
*
* @returns {Promise<Category[]>} A promise that resolves to an array of object store names.
*/
private listStores;
/**
* Retrieves an object store from the IndexedDB database.
* It handles the transaction mode and ensures that the requested object store is returned.
*
* If the object store does not exist in the database and the category is valid, it will create a new version of the database with the object store.
*
* @internal
*
* @param category - The name of the object store to retrieve.
* @param mode - The mode for the transaction (default is "readonly").
*
* @returns {Promise<IDBObjectStore>} A promise that resolves to the requested object store.
*
* @throws {LocalSaveError} Will throw an error if the object store does not exist in the database and the category is invalid
*/
private getStore;
/**
* Encrypts a DBItem using the configured encryption key.
* Delegates to the extracted crypto helper.
*
* @internal
*
* @param data The item payload to encrypt.
*
* @returns {Promise<DBItemEncryptedBase64>} A promise that resolves to a base64 payload containing IV + ciphertext.
*
* @throws {LocalSaveEncryptionKeyError} If the encryption key is not configured or invalid.
* @throws {LocalSaveError} If encryption fails.
*/
private encryptData;
/**
* Decrypts data using the configured encryption key.
*
* @param encryptedBase64Data The data to decrypt, as a string.
* @returns {Promise<DBItem>} A promise that resolves to the decrypted data object.
*
* @throws {LocalSaveError} If decryption fails.
*/
decryptData(encryptedBase64Data: string): Promise<DBItem>;
/**
* Stores data in the specified category with the given item key.
* If encryption key is configured, the data is encrypted first before being stored.
*
* @param category The category under which the data should be stored.
* @param itemKey The key to identify the stored data.
* @param data The data to be stored.
*
* @returns {Promise<true>} A promise that resolves to `true` if the operation was successful.
*
* @throws {LocalSaveError} Will reject the promise if an error occurs during the saving process.
*/
set(category: Category, itemKey: string, data: unknown): Promise<true>;
/**
* Retrieves an item from the specified category in the IndexedDB.
* If the item is not found, the promise resolves to 'null'.
* If an encryption key is configured, the data is decrypted before being returned.
*
* @param category The category from which to retrieve the item.
* @param itemKey The key of the item to retrieve.
*
* @returns {Promise<DBItem | null>} A promise that resolves to the retrieved item or null if not found.
*
* @throws {LocalSaveError} Will reject the promise if an error occurs while decrypting the data. Depending on the 'clearOnDecryptError' configuration, all data for the category can be cleared.
* @throws {LocalSaveError} Will reject the promise if an error occurs during the retrieval process.
*/
get(category: Category, itemKey: string): Promise<DBItem | null>;
/**
* Lists all categories (object stores) currently available in the database.
*
* @returns {Promise<Category[]>} A promise that resolves to an array of category names.
*/
listCategories(): Promise<Category[]>;
/**
* Lists all item keys stored under the specified category.
*
* @param category The category from which item keys should be listed.
*
* @returns {Promise<string[]>} A promise that resolves to an array of item keys.
*
* @throws {LocalSaveError} Will reject the promise if an error occurs while listing keys.
*/
listKeys(category: Category): Promise<string[]>;
/**
* Removes an entry from the specified category and the specific itemKey in the IndexedDB store.
*
* @param category The category from which the item should be removed.
* @param itemKey The key of the item to be removed.
*
* @returns {Promise<true>} A promise that resolves to `true` if the operation was successful.
*
* @throws {LocalSaveError} Will reject the promise if an error occurs during the removal process.
*/
remove(category: Category, itemKey: string): Promise<true>;
/**
* Clears all entries in the specified category.
*
* @param category - The category to clear.
*
* @returns {Promise<true>} A promise that resolves to `true` if the operation was successful.
*
* @throws {LocalSaveError} Will reject the promise if an error occurs during the clearing process.
*/
clear(category: Category): Promise<true>;
/**
* Expires data older than the specified threshold in milliseconds.
*
* For each category, this method performs a readonly cursor scan to identify candidate
* records, decrypts encrypted entries to inspect their timestamps, and then deletes the
* expired keys in a separate readwrite transaction.
*
* If decryption fails during expiration and `clearOnDecryptError` is enabled, the category
* is cleared before the error is rethrown.
*
* @param {number} [thresholdMs=this.expiryThreshold] The threshold in milliseconds to use for expiring data.
* Defaults to expiryThreshold from config if not provided.
*
* @returns {Promise<true>} A promise that resolves to `true` if the operation was successful.
*
* @throws {LocalSaveError} - Throws an error if there is an issue scanning entries, decrypting data, or removing expired items.
*/
expire(thresholdMs?: number): Promise<true>;
/**
* Asynchronously destroys the database by deleting it from IndexedDB.
*
* @returns {Promise<true>} A promise that resolves to `true` if the operation was successful.
*
* @throws {LocalSaveError} Will reject the promise if an error occurs during the deletion process.
*/
destroy(): Promise<true>;
}
export { type Category, type Config, type DBItem, type DBItemEncryptedBase64, type DBName, type EncryptionKey, type PositiveNumber, LocalSave as default };