UNPKG

@febkosq8/local-save

Version:

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

8 lines 8.3 kB
'use strict';var u=class extends Error{constructor(e,t){super(e,t),this.name="LocalSaveEncryptionKeyError";}};var p=class extends Error{constructor(e,t){super(e,t),this.name="LocalSaveError";}};var n=class{constructor(){throw new Error("This class cannot be instantiated.")}static log(e,...t){console.log(`[LocalSave | LOG] ${e} `,...t);}static warn(e,...t){console.warn(`[LocalSave | WARN] ${e} `,...t);}static error(e,...t){console.error(`[LocalSave | ERROR] ${e} `,...t);}static info(e,...t){console.info(`[LocalSave | INFO] ${e} `,...t);}static debug(e,...t){console.debug(`[LocalSave | DEBUG] ${e} `,...t);}};function g(d){return !!d&&d!=""&&d.length>0}function y(d){return d.length>0&&[16,24,32].includes(d.length)}function f(d){let e="",t=new Uint8Array(d),r=t.byteLength;for(let o=0;o<r;o++)e+=String.fromCharCode(t[o]);return window.btoa(e)}function b(d){let e=window.atob(d),t=e.length,r=new Uint8Array(t);for(let o=0;o<t;o++)r[o]=e.charCodeAt(o);return r.buffer}var l=class{dbName="LocalSave";encryptionKey;categories=["userData"];expiryThreshold=30;clearOnDecryptError=!0;printLogs=!1;constructor(e){this.dbName=e?.dbName??this.dbName,this.encryptionKey=e?.encryptionKey,this.categories=e?.categories??this.categories,this.expiryThreshold=e?.expiryThreshold??this.expiryThreshold,this.clearOnDecryptError=e?.clearOnDecryptError??this.clearOnDecryptError,this.printLogs=e?.printLogs??this.printLogs,e?.encryptionKey&&!y(e?.encryptionKey)&&n.warn("Encryption key should be of length 16, 24, or 32 characters",{keyLength:e.encryptionKey.length});}openDB(e){return new Promise((t,r)=>{let o=indexedDB.open(this.dbName,e);o.onupgradeneeded=()=>{let i=o.result;this.printLogs&&n.debug("Database upgrade triggered",{dbName:this.dbName,version:i.version});for(let s of this.categories)i.objectStoreNames.contains(s)||(this.printLogs&&n.debug("Creating object store",{category:s}),i.createObjectStore(s));},o.onsuccess=()=>(this.printLogs&&n.debug("Database opened successfully",{dbName:this.dbName,version:o.result.version}),t(o.result)),o.onerror=()=>(this.printLogs&&n.error(`LocalSaveError opening database [dbName:${this.dbName}]`,o.error),r(new p(o.error?.message??"Error opening database")));})}async getStore(e,t="readonly"){let r=await this.openDB();if(!r.objectStoreNames.contains(e)&&this.categories.includes(e)){this.printLogs&&n.debug(`Requested object store not found in current database version. Triggering database upgrade to create object store.`,{category:e,dbName:this.dbName,version:r.version});let s=r.version;r.close(),r=await this.openDB(s+1);}else if(!r.objectStoreNames.contains(e))throw new p(`Requested object store not found in current database version [category:${e} / dbName:${this.dbName} / version:${r.version}].`);let i=r.transaction(e,t).objectStore(e);return this.printLogs&&n.debug("Object store retrieved from database",{category:e,mode:t,dbName:this.dbName,version:r.version}),i}async getEncryptKey(){if(!g(this.encryptionKey))throw new u("Encryption key is not configured");if(this.encryptionKey&&!y(this.encryptionKey))throw new u("Encryption key should be of length 16, 24, or 32 characters");let t=new TextEncoder().encode(this.encryptionKey),r=await crypto.subtle.importKey("raw",t,{name:"AES-GCM"},!1,["encrypt","decrypt"]);return this.printLogs&&n.debug("Encryption key retrieved successfully",{keyLength:this.encryptionKey?.length,keyBytesLength:t.length}),r}async encryptData(e){try{if(!g(this.encryptionKey))throw new u("Encryption key is not configured");let t=window.crypto.getRandomValues(new Uint8Array(12)),r=await this.getEncryptKey(),o=new TextEncoder().encode(JSON.stringify(e)),i=await window.crypto.subtle.encrypt({name:"AES-GCM",iv:t},r,o),s=new Uint8Array(t),a=new Uint8Array(i),c=new Uint8Array(s.byteLength+a.byteLength);c.set(s,0),c.set(a,s.byteLength);let h=f(c.buffer);return this.printLogs&&n.debug("Data encrypted successfully",{base64DataLength:h.length}),h}catch(t){throw this.printLogs&&n.error("Data encryption failed",t),t}}async decryptData(e){try{if(!g(this.encryptionKey))throw new u("Encryption key is not configured");let t=b(e),r=new Uint8Array(t,0,12),o=await this.getEncryptKey(),i=new Uint8Array(t,12),s=await window.crypto.subtle.decrypt({name:"AES-GCM",iv:r},o,i),a=JSON.parse(new TextDecoder().decode(s));return this.printLogs&&n.debug("Data decrypted successfully",{timestamp:a.timestamp}),a}catch(t){throw this.printLogs&&n.error("Data decryption failed",t),new p("Data decryption failed")}}async set(e,t,r){this.printLogs&&n.debug("set() called to store data with following props",{category:e,itemKey:t});let o={timestamp:Date.now(),data:r};try{this.encryptionKey&&(o=await this.encryptData(o));let i=await this.getStore(e,"readwrite");return new Promise((s,a)=>{let c=i.put(o,t);c.onsuccess=()=>{this.printLogs&&n.debug("Data stored successfully",{category:e,itemKey:t}),s(!0);},c.onerror=()=>{this.printLogs&&n.error(`LocalSaveError storing data [category:${e} / key:${t}]`,c.error),a(new p(c.error?.message??"Error storing data"));};})}catch(i){throw this.printLogs&&n.error("Data storing failed",i),i}}async get(e,t){this.printLogs&&n.debug("get() called to retrieve data with following props",{category:e,itemKey:t});let r=await this.getStore(e);return new Promise((o,i)=>{let s=r.get(t);s.onsuccess=async()=>{let a=s.result;if(!a)return this.printLogs&&n.debug("No data was found",{category:e,itemKey:t}),o(null);if(typeof a=="string")try{a=await this.decryptData(a);}catch(c){return this.printLogs&&n.error("Failed to get data",c),this.clearOnDecryptError&&(this.printLogs&&n.error("Triggering clear for all data for category since decryption failed"),this.clear(e)),i(new p(c instanceof Error?c.message:"Failed to decrypt data"))}return this.printLogs&&n.debug("Data retrieved successfully",{category:e,itemKey:t,timestamp:a.timestamp}),o(a)},s.onerror=()=>i(new p(s.error?.message??"Error getting data"));})}async remove(e,t){this.printLogs&&n.debug("remove() called to remove data with following props",{category:e,itemKey:t});let r=await this.getStore(e,"readwrite");return new Promise((o,i)=>{let s=r.delete(t);s.onsuccess=()=>(this.printLogs&&n.debug("Data removed successfully",{category:e,itemKey:t}),o(!0)),s.onerror=()=>(this.printLogs&&n.error(`Failed to remove data from [category:${e} / key:${t}]`,s.error),i(new p(s.error?.message??"Error removing data")));})}async clear(e){this.printLogs&&n.debug(`clear() called to store all data under '${e}' category`);let t=await this.getStore(e,"readwrite");return new Promise((r,o)=>{let i=t.clear();i.onsuccess=()=>(this.printLogs&&n.debug("Data cleared successfully",{category:e,dbName:this.dbName,version:t.transaction.db.version}),r(!0)),i.onerror=()=>(this.printLogs&&n.error(`LocalSaveError clearing data [category:${e} / dbName:${this.dbName} / version:${t.transaction.db.version}]`,i.error),o(new p(i.error?.message??"Error clearing data")));})}async expire(e=this.expiryThreshold){this.printLogs&&n.debug(`expire() called to expire data older than ${e} days`);let t=Date.now()-e*864e5;for(let r of this.categories){let o=await this.getStore(r);try{let i=await new Promise((s,a)=>{let c=o.getAllKeys();c.onsuccess=()=>{this.printLogs&&n.debug("Keys retrieved successfully for expiring data",{category:r,keys:c.result}),s(c.result);},c.onerror=()=>{this.printLogs&&n.error(`LocalSaveError getting keys for expiring data [category:${r}]`,c.error),a(new p(c.error?.message??"Error getting keys"));};});for(let s of i){if(typeof s!="string")continue;let a=await this.get(r,s);a&&a.timestamp<t&&(this.printLogs&&n.debug("Removing expired data",{category:r,key:s,timestamp:a.timestamp}),await this.remove(r,s));}}catch(i){throw this.printLogs&&n.error(`Expiring data older than '${e}' days failed`,i),i}}return !0}async destroy(){return this.printLogs&&n.debug("destroy() called to wipe all data under all categories"),new Promise((e,t)=>{let r=indexedDB.deleteDatabase(this.dbName);r.onsuccess=()=>{this.printLogs&&n.debug("Database deleted successfully",{dbName:this.dbName,version:r.result}),e(!0);},r.onerror=()=>{this.printLogs&&n.error(`Error deleting database [dbName:${this.dbName}]`),t(new p(r.error?.message??"Error deleting database"));};})}},x=l;module.exports=x;//# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map