json-secure-store
Version:
Typed JSON storage wrapper for localStorage/sessionStorage with optional encryption.
100 lines (99 loc) • 3.45 kB
JavaScript
import { StorageType } from './types';
import { encryptData, decryptData } from './utils/crypto';
export class JsonStore {
constructor(options) {
this.listeners = [];
this.memoryCache = new Map();
const { storageType = StorageType.Local, encrypt = false, encryptionKey, cache = false, namespace, } = options || {};
const defaults = {
storageType: StorageType.Local,
encrypt: false,
encryptionKey: '',
cache: false,
namespace: '',
defaultTTL: 0,
};
this.options = Object.assign({}, defaults, options);
this.storage = window[this.options.storageType];
if (this.options.encrypt && !this.options.encryptionKey) {
throw new Error('Encryption key is required when encryption is enabled.');
}
this.storage = window[storageType];
this.encrypt = encrypt;
this.encryptionKey = encryptionKey;
this.cache = cache;
this.namespace = namespace;
if (this.encrypt && !this.encryptionKey) {
throw new Error('Encryption key is required when encryption is enabled.');
}
}
buildKey(key) {
return this.namespace ? `${this.namespace}:${key}` : key;
}
notifyChange(key, value) {
this.listeners.forEach((cb) => cb(key, value));
}
onChange(callback) {
this.listeners.push(callback);
}
setItem(key, value) {
const fullKey = this.buildKey(key);
const expiresAt = this.options.defaultTTL
? Date.now() + this.options.defaultTTL
: undefined;
const wrapper = { data: value, expiresAt };
let data = JSON.stringify(wrapper);
if (this.encrypt && this.encryptionKey) {
data = encryptData(data, this.encryptionKey);
}
this.storage.setItem(fullKey, data);
if (this.cache) {
this.memoryCache.set(fullKey, value);
}
}
getItem(key) {
const fullKey = this.buildKey(key);
if (this.cache && this.memoryCache.has(fullKey)) {
return this.memoryCache.get(fullKey);
}
const raw = this.storage.getItem(fullKey);
if (!raw)
return null;
try {
const data = this.encrypt && this.encryptionKey
? decryptData(raw, this.encryptionKey)
: raw;
const parsed = JSON.parse(data);
if (parsed.expiresAt && Date.now() > parsed.expiresAt) {
this.removeItem(key);
return null;
}
if (this.cache) {
this.memoryCache.set(fullKey, parsed);
}
return parsed.data;
}
catch (err) {
console.error('Failed to decrypt or parse item:', err);
return null;
}
}
removeItem(key) {
const fullKey = this.buildKey(key);
this.storage.removeItem(fullKey);
if (this.cache) {
this.memoryCache.delete(fullKey);
}
}
clear() {
const keysToRemove = Object.keys(this.storage).filter((key) => this.namespace ? key.startsWith(this.namespace + ':') : true);
keysToRemove.forEach((key) => this.storage.removeItem(key));
if (this.cache) {
this.memoryCache.clear();
}
}
raw() {
return this.storage;
}
}
export { StorageType };