UNPKG

@febkosq8/local-save

Version:

Lightweight wrapper around IndexedDB for secure and structured client-side data storage.

271 lines (268 loc) 10.9 kB
/** * 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 };