@bantis/local-cipher 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.esm.js CHANGED
@@ -19,6 +19,9 @@ const DEFAULT_CONFIG = {
19
19
  compressionThreshold: 1024,
20
20
  autoCleanup: true,
21
21
  cleanupInterval: 60000,
22
+ enableCache: true,
23
+ verifyIntegrityOnRead: false,
24
+ storageEngine: typeof window !== 'undefined' ? window.localStorage : {},
22
25
  },
23
26
  debug: {
24
27
  enabled: false,
@@ -46,6 +49,8 @@ class EncryptionHelper {
46
49
  this.baseKey = '';
47
50
  this.baseKeyPromise = null;
48
51
  this.keyVersion = 1;
52
+ // Cache para optimización de rendimiento
53
+ this.keyNameCache = new Map();
49
54
  this.config = { ...DEFAULT_CONFIG.encryption, ...config };
50
55
  // Load key version from storage
51
56
  const storedVersion = localStorage.getItem(EncryptionHelper.KEY_VERSION_KEY);
@@ -108,7 +113,7 @@ class EncryptionHelper {
108
113
  // Derivar la clave AES-GCM
109
114
  return crypto.subtle.deriveKey({
110
115
  name: 'PBKDF2',
111
- salt,
116
+ salt: salt,
112
117
  iterations: this.config.iterations,
113
118
  hash: EncryptionHelper.HASH_ALGORITHM,
114
119
  }, keyMaterial, {
@@ -215,10 +220,16 @@ class EncryptionHelper {
215
220
  * @returns Nombre encriptado con prefijo __enc_
216
221
  */
217
222
  async encryptKey(keyName) {
223
+ // Optimización: devolver desde cache si existe
224
+ if (this.keyNameCache.has(keyName)) {
225
+ return this.keyNameCache.get(keyName);
226
+ }
218
227
  const baseKey = await this.generateBaseKey();
219
228
  const combined = keyName + baseKey;
220
229
  const hash = await this.hashString(combined);
221
- return `__enc_${hash.substring(0, 16)}`;
230
+ const encryptedKey = `__enc_${hash.substring(0, 16)}`;
231
+ this.keyNameCache.set(keyName, encryptedKey);
232
+ return encryptedKey;
222
233
  }
223
234
  /**
224
235
  * Limpia todos los datos encriptados del localStorage
@@ -236,10 +247,11 @@ class EncryptionHelper {
236
247
  keysToRemove.forEach(key => localStorage.removeItem(key));
237
248
  // Eliminar salt
238
249
  localStorage.removeItem(EncryptionHelper.SALT_STORAGE_KEY);
239
- // Resetear clave en memoria
250
+ // Resetear clave en memoria y cache
240
251
  this.key = null;
241
252
  this.baseKey = '';
242
253
  this.baseKeyPromise = null;
254
+ this.keyNameCache.clear();
243
255
  }
244
256
  /**
245
257
  * Verifica si el navegador soporta Web Crypto API
@@ -390,7 +402,7 @@ class Logger {
390
402
  return false;
391
403
  return Logger.LOG_LEVELS[level] <= Logger.LOG_LEVELS[this.logLevel];
392
404
  }
393
- formatMessage(level, message, ...args) {
405
+ formatMessage(level, message) {
394
406
  const timestamp = new Date().toISOString();
395
407
  return `[${this.prefix}] [${level.toUpperCase()}] ${timestamp} - ${message}`;
396
408
  }
@@ -543,17 +555,37 @@ class NamespacedStorage {
543
555
  this.storage = storage;
544
556
  this.prefix = `__ns_${namespace}__`;
545
557
  }
558
+ async getIndex() {
559
+ const indexValue = await this.storage.getItem(`${this.prefix}__index__`);
560
+ return indexValue ? JSON.parse(indexValue) : [];
561
+ }
562
+ async saveToIndex(key) {
563
+ const index = await this.getIndex();
564
+ if (!index.includes(key)) {
565
+ index.push(key);
566
+ await this.storage.setItem(`${this.prefix}__index__`, JSON.stringify(index));
567
+ }
568
+ }
569
+ async removeFromIndex(key) {
570
+ const index = await this.getIndex();
571
+ const newIndex = index.filter(k => k !== key);
572
+ if (newIndex.length !== index.length) {
573
+ await this.storage.setItem(`${this.prefix}__index__`, JSON.stringify(newIndex));
574
+ }
575
+ }
546
576
  /**
547
577
  * Set item in this namespace
548
578
  */
549
579
  async setItem(key, value) {
550
- return this.storage.setItem(`${this.prefix}${key}`, value);
580
+ await this.storage.setItem(`${this.prefix}${key}`, value);
581
+ await this.saveToIndex(key);
551
582
  }
552
583
  /**
553
584
  * Set item with expiry in this namespace
554
585
  */
555
586
  async setItemWithExpiry(key, value, options) {
556
- return this.storage.setItemWithExpiry(`${this.prefix}${key}`, value, options);
587
+ await this.storage.setItemWithExpiry(`${this.prefix}${key}`, value, options);
588
+ await this.saveToIndex(key);
557
589
  }
558
590
  /**
559
591
  * Get item from this namespace
@@ -565,7 +597,8 @@ class NamespacedStorage {
565
597
  * Remove item from this namespace
566
598
  */
567
599
  async removeItem(key) {
568
- return this.storage.removeItem(`${this.prefix}${key}`);
600
+ await this.storage.removeItem(`${this.prefix}${key}`);
601
+ await this.removeFromIndex(key);
569
602
  }
570
603
  /**
571
604
  * Check if item exists in this namespace
@@ -577,29 +610,17 @@ class NamespacedStorage {
577
610
  * Clear all items in this namespace
578
611
  */
579
612
  async clearNamespace() {
580
- const keysToRemove = [];
581
- for (let i = 0; i < localStorage.length; i++) {
582
- const key = localStorage.key(i);
583
- if (key && key.includes(this.prefix)) {
584
- keysToRemove.push(key.replace(this.prefix, ''));
585
- }
586
- }
613
+ const keysToRemove = await this.getIndex();
587
614
  for (const key of keysToRemove) {
588
- await this.removeItem(key);
615
+ await this.storage.removeItem(`${this.prefix}${key}`);
589
616
  }
617
+ await this.storage.removeItem(`${this.prefix}__index__`);
590
618
  }
591
619
  /**
592
620
  * Get all keys in this namespace
593
621
  */
594
622
  async keys() {
595
- const keys = [];
596
- for (let i = 0; i < localStorage.length; i++) {
597
- const key = localStorage.key(i);
598
- if (key && key.includes(this.prefix)) {
599
- keys.push(key.replace(this.prefix, ''));
600
- }
601
- }
602
- return keys;
623
+ return this.getIndex();
603
624
  }
604
625
  }
605
626
 
@@ -622,7 +643,6 @@ async function compress(data) {
622
643
  return encoder.encode(data);
623
644
  }
624
645
  try {
625
- const encoder = new TextEncoder();
626
646
  const stream = new Blob([data]).stream();
627
647
  const compressedStream = stream.pipeThrough(new CompressionStream('gzip'));
628
648
  const compressedBlob = await new Response(compressedStream).blob();
@@ -671,6 +691,7 @@ function shouldCompress(data, threshold = 1024) {
671
691
  class SecureStorage {
672
692
  constructor(config) {
673
693
  this.cleanupInterval = null;
694
+ this.memoryCache = new Map();
674
695
  // Merge config with defaults
675
696
  this.config = {
676
697
  encryption: { ...DEFAULT_CONFIG.encryption, ...config?.encryption },
@@ -752,8 +773,12 @@ class SecureStorage {
752
773
  const encryptedKey = await this.encryptionHelper.encryptKey(key);
753
774
  // Encrypt the value
754
775
  const encryptedValue = await this.encryptionHelper.encrypt(serialized);
755
- // Store in localStorage
756
- localStorage.setItem(encryptedKey, encryptedValue);
776
+ // Store in storage
777
+ this.config.storage.storageEngine.setItem(encryptedKey, encryptedValue);
778
+ // Update cache
779
+ if (this.config.storage.enableCache) {
780
+ this.memoryCache.set(key, { value });
781
+ }
757
782
  this.logger.verbose(`Stored key: ${key}, compressed: ${compressed}, size: ${encryptedValue.length}`);
758
783
  this.eventEmitter.emit('encrypted', { key, metadata: { compressed, size: encryptedValue.length } });
759
784
  this.logger.timeEnd(`setItem:${key}`);
@@ -761,7 +786,7 @@ class SecureStorage {
761
786
  catch (error) {
762
787
  this.logger.error(`Error al guardar dato encriptado para ${key}:`, error);
763
788
  this.eventEmitter.emit('error', { key, error: error });
764
- localStorage.setItem(key, value);
789
+ this.config.storage.storageEngine.setItem(key, value);
765
790
  }
766
791
  }
767
792
  /**
@@ -814,8 +839,12 @@ class SecureStorage {
814
839
  const encryptedKey = await this.encryptionHelper.encryptKey(key);
815
840
  // Encrypt the value
816
841
  const encryptedValue = await this.encryptionHelper.encrypt(serialized);
817
- // Store in localStorage
818
- localStorage.setItem(encryptedKey, encryptedValue);
842
+ // Store in storage
843
+ this.config.storage.storageEngine.setItem(encryptedKey, encryptedValue);
844
+ // Update cache
845
+ if (this.config.storage.enableCache) {
846
+ this.memoryCache.set(key, { value, expiresAt });
847
+ }
819
848
  this.logger.verbose(`Stored key with expiry: ${key}, expiresAt: ${expiresAt}`);
820
849
  this.eventEmitter.emit('encrypted', { key, metadata: { compressed, expiresAt } });
821
850
  this.logger.timeEnd(`setItemWithExpiry:${key}`);
@@ -832,16 +861,31 @@ class SecureStorage {
832
861
  async getItem(key) {
833
862
  this.logger.time(`getItem:${key}`);
834
863
  if (!EncryptionHelper.isSupported()) {
835
- return localStorage.getItem(key);
864
+ return this.config.storage.storageEngine.getItem(key);
836
865
  }
837
866
  try {
867
+ // Check memory cache first
868
+ if (this.config.storage.enableCache && this.memoryCache.has(key)) {
869
+ const cached = this.memoryCache.get(key);
870
+ if (cached.expiresAt && cached.expiresAt < Date.now()) {
871
+ this.logger.info(`Key expired in cache: ${key}`);
872
+ await this.removeItem(key);
873
+ this.eventEmitter.emit('expired', { key });
874
+ this.logger.timeEnd(`getItem:${key}`);
875
+ return null;
876
+ }
877
+ this.logger.debug(`Retrieved from cache: ${key}`);
878
+ this.eventEmitter.emit('decrypted', { key });
879
+ this.logger.timeEnd(`getItem:${key}`);
880
+ return cached.value;
881
+ }
838
882
  // Encrypt the key
839
883
  const encryptedKey = await this.encryptionHelper.encryptKey(key);
840
884
  // Get encrypted value
841
- let encryptedValue = localStorage.getItem(encryptedKey);
885
+ let encryptedValue = this.config.storage.storageEngine.getItem(encryptedKey);
842
886
  // Backward compatibility: try with plain key
843
887
  if (!encryptedValue) {
844
- encryptedValue = localStorage.getItem(key);
888
+ encryptedValue = this.config.storage.storageEngine.getItem(key);
845
889
  if (!encryptedValue) {
846
890
  this.logger.timeEnd(`getItem:${key}`);
847
891
  return null;
@@ -877,8 +921,8 @@ class SecureStorage {
877
921
  this.logger.timeEnd(`getItem:${key}`);
878
922
  return null;
879
923
  }
880
- // Verify integrity if checksum exists
881
- if (storedValue.checksum) {
924
+ // Verify integrity if checksum exists and configured
925
+ if (storedValue.checksum && this.config.storage.verifyIntegrityOnRead) {
882
926
  const calculatedChecksum = await this.calculateChecksum(storedValue.value);
883
927
  if (calculatedChecksum !== storedValue.checksum) {
884
928
  this.logger.warn(`Integrity check failed for key: ${key}`);
@@ -896,6 +940,9 @@ class SecureStorage {
896
940
  finalValue = await decompress(new Uint8Array(compressedData));
897
941
  this.eventEmitter.emit('decompressed', { key });
898
942
  }
943
+ if (this.config.storage.enableCache) {
944
+ this.memoryCache.set(key, { value: finalValue, expiresAt: storedValue.expiresAt });
945
+ }
899
946
  this.eventEmitter.emit('decrypted', { key });
900
947
  this.logger.timeEnd(`getItem:${key}`);
901
948
  return finalValue;
@@ -904,7 +951,7 @@ class SecureStorage {
904
951
  this.logger.error(`Error al recuperar dato encriptado para ${key}:`, error);
905
952
  this.eventEmitter.emit('error', { key, error: error });
906
953
  // Fallback: try plain key
907
- const fallback = localStorage.getItem(key);
954
+ const fallback = this.config.storage.storageEngine.getItem(key);
908
955
  this.logger.timeEnd(`getItem:${key}`);
909
956
  return fallback;
910
957
  }
@@ -914,19 +961,20 @@ class SecureStorage {
914
961
  */
915
962
  async removeItem(key) {
916
963
  if (!EncryptionHelper.isSupported()) {
917
- localStorage.removeItem(key);
964
+ this.config.storage.storageEngine.removeItem(key);
918
965
  return;
919
966
  }
920
967
  try {
921
968
  const encryptedKey = await this.encryptionHelper.encryptKey(key);
922
- localStorage.removeItem(encryptedKey);
923
- localStorage.removeItem(key); // Remove both versions
969
+ this.config.storage.storageEngine.removeItem(encryptedKey);
970
+ this.config.storage.storageEngine.removeItem(key); // Remove both versions
971
+ this.memoryCache.delete(key);
924
972
  this.eventEmitter.emit('deleted', { key });
925
973
  this.logger.info(`Removed key: ${key}`);
926
974
  }
927
975
  catch (error) {
928
976
  this.logger.error(`Error al eliminar dato para ${key}:`, error);
929
- localStorage.removeItem(key);
977
+ this.config.storage.storageEngine.removeItem(key);
930
978
  }
931
979
  }
932
980
  /**
@@ -941,6 +989,7 @@ class SecureStorage {
941
989
  */
942
990
  clear() {
943
991
  this.encryptionHelper.clearEncryptedData();
992
+ this.memoryCache.clear();
944
993
  this.eventEmitter.emit('cleared', {});
945
994
  this.logger.info('All encrypted data cleared');
946
995
  }
@@ -951,21 +1000,28 @@ class SecureStorage {
951
1000
  this.logger.info('Starting cleanup of expired items...');
952
1001
  let cleanedCount = 0;
953
1002
  const keysToCheck = [];
954
- for (let i = 0; i < localStorage.length; i++) {
955
- const key = localStorage.key(i);
1003
+ for (let i = 0; i < this.config.storage.storageEngine.length; i++) {
1004
+ const key = this.config.storage.storageEngine.key(i);
956
1005
  if (key && key.startsWith('__enc_')) {
957
1006
  keysToCheck.push(key);
958
1007
  }
959
1008
  }
960
1009
  for (const encryptedKey of keysToCheck) {
961
1010
  try {
962
- const encryptedValue = localStorage.getItem(encryptedKey);
1011
+ const encryptedValue = this.config.storage.storageEngine.getItem(encryptedKey);
963
1012
  if (!encryptedValue)
964
1013
  continue;
965
1014
  const decrypted = await this.encryptionHelper.decrypt(encryptedValue);
966
1015
  const storedValue = JSON.parse(decrypted);
967
1016
  if (storedValue.expiresAt && storedValue.expiresAt < Date.now()) {
968
- localStorage.removeItem(encryptedKey);
1017
+ this.config.storage.storageEngine.removeItem(encryptedKey);
1018
+ // Encontrar la clave original en el cache y eliminarla
1019
+ for (const [cacheKey] of Array.from(this.memoryCache.entries())) {
1020
+ this.encryptionHelper.encryptKey(cacheKey).then(enc => {
1021
+ if (enc === encryptedKey)
1022
+ this.memoryCache.delete(cacheKey);
1023
+ });
1024
+ }
969
1025
  cleanedCount++;
970
1026
  this.eventEmitter.emit('expired', { key: encryptedKey });
971
1027
  }
@@ -983,7 +1039,7 @@ class SecureStorage {
983
1039
  async verifyIntegrity(key) {
984
1040
  try {
985
1041
  const encryptedKey = await this.encryptionHelper.encryptKey(key);
986
- const encryptedValue = localStorage.getItem(encryptedKey);
1042
+ const encryptedValue = this.config.storage.storageEngine.getItem(encryptedKey);
987
1043
  if (!encryptedValue)
988
1044
  return false;
989
1045
  const decrypted = await this.encryptionHelper.decrypt(encryptedValue);
@@ -1006,7 +1062,7 @@ class SecureStorage {
1006
1062
  async getIntegrityInfo(key) {
1007
1063
  try {
1008
1064
  const encryptedKey = await this.encryptionHelper.encryptKey(key);
1009
- const encryptedValue = localStorage.getItem(encryptedKey);
1065
+ const encryptedValue = this.config.storage.storageEngine.getItem(encryptedKey);
1010
1066
  if (!encryptedValue)
1011
1067
  return null;
1012
1068
  const decrypted = await this.encryptionHelper.decrypt(encryptedValue);
@@ -1089,7 +1145,7 @@ class SecureStorage {
1089
1145
  this.logger.info(`Iniciando migración de ${keys.length} claves...`);
1090
1146
  for (const key of keys) {
1091
1147
  try {
1092
- const value = localStorage.getItem(key);
1148
+ const value = this.config.storage.storageEngine.getItem(key);
1093
1149
  if (value === null)
1094
1150
  continue;
1095
1151
  // Try to decrypt to check if already encrypted
@@ -1102,7 +1158,7 @@ class SecureStorage {
1102
1158
  // Not encrypted, proceed with migration
1103
1159
  }
1104
1160
  await this.setItem(key, value);
1105
- localStorage.removeItem(key);
1161
+ this.config.storage.storageEngine.removeItem(key);
1106
1162
  this.logger.info(`✓ ${key} migrado exitosamente`);
1107
1163
  }
1108
1164
  catch (error) {
@@ -1116,8 +1172,8 @@ class SecureStorage {
1116
1172
  */
1117
1173
  getDebugInfo() {
1118
1174
  const allKeys = [];
1119
- for (let i = 0; i < localStorage.length; i++) {
1120
- const key = localStorage.key(i);
1175
+ for (let i = 0; i < this.config.storage.storageEngine.length; i++) {
1176
+ const key = this.config.storage.storageEngine.key(i);
1121
1177
  if (key)
1122
1178
  allKeys.push(key);
1123
1179
  }
@@ -1151,10 +1207,199 @@ class SecureStorage {
1151
1207
  }
1152
1208
  this.removeAllListeners();
1153
1209
  this.logger.info('SecureStorage destroyed');
1210
+ SecureStorage.instance = null;
1154
1211
  }
1155
1212
  }
1156
1213
  SecureStorage.instance = null;
1157
1214
 
1215
+ /**
1216
+ * SecureCookie - API de alto nivel para almacenamiento en cookies cifradas
1217
+ * Soporta opciones de cookies incluyendo domininios/subdominios y compresión.
1218
+ */
1219
+ class SecureCookie {
1220
+ constructor(config) {
1221
+ this.config = {
1222
+ encryption: { ...DEFAULT_CONFIG.encryption, ...config?.encryption },
1223
+ cookieOptions: { path: '/', ...config?.cookieOptions },
1224
+ compression: config?.compression ?? true,
1225
+ debug: { ...DEFAULT_CONFIG.debug, ...config?.debug }
1226
+ };
1227
+ this.logger = new Logger(this.config.debug);
1228
+ this.encryptionHelper = new EncryptionHelper(this.config.encryption);
1229
+ this.logger.info('SecureCookie initialized', this.config);
1230
+ }
1231
+ /**
1232
+ * Obtiene la instancia singleton de SecureCookie
1233
+ */
1234
+ static getInstance(config) {
1235
+ if (!SecureCookie.instance) {
1236
+ SecureCookie.instance = new SecureCookie(config);
1237
+ }
1238
+ return SecureCookie.instance;
1239
+ }
1240
+ /**
1241
+ * Serializa las opciones de cookie en un string
1242
+ */
1243
+ serializeOptions(options) {
1244
+ let cookieString = '';
1245
+ if (options.expires) {
1246
+ cookieString += `; expires=${options.expires.toUTCString()}`;
1247
+ }
1248
+ if (options.maxAge) {
1249
+ cookieString += `; max-age=${options.maxAge}`;
1250
+ }
1251
+ if (options.domain) {
1252
+ cookieString += `; domain=${options.domain}`;
1253
+ }
1254
+ if (options.path) {
1255
+ cookieString += `; path=${options.path}`;
1256
+ }
1257
+ if (options.secure) {
1258
+ cookieString += `; secure`;
1259
+ }
1260
+ if (options.sameSite) {
1261
+ cookieString += `; samesite=${options.sameSite}`;
1262
+ }
1263
+ return cookieString;
1264
+ }
1265
+ /**
1266
+ * Guarda un valor encriptado en una cookie
1267
+ */
1268
+ async set(name, value, options) {
1269
+ this.logger.time(`cookie:set:${name}`);
1270
+ if (!EncryptionHelper.isSupported()) {
1271
+ this.logger.warn('Web Crypto API no soportada, guardando cookie sin cifrar');
1272
+ const mergedOptions = { ...this.config.cookieOptions, ...options };
1273
+ document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}${this.serializeOptions(mergedOptions)}`;
1274
+ return;
1275
+ }
1276
+ try {
1277
+ // Check compression (aggressively compress cookies to save space since max is ~4KB)
1278
+ const shouldCompressData = this.config.compression && shouldCompress(value, 200);
1279
+ let processedValue = value;
1280
+ let metadataPrefix = 'P_'; // P_ = Plain
1281
+ if (shouldCompressData) {
1282
+ this.logger.debug(`Compressing cookie value for: ${name}`);
1283
+ const compressedData = await compress(value);
1284
+ processedValue = this.encryptionHelper['arrayBufferToBase64'](compressedData.buffer);
1285
+ metadataPrefix = 'C_'; // C_ = Compressed
1286
+ }
1287
+ // Encrypt key name
1288
+ const encryptedKey = await this.encryptionHelper.encryptKey(name);
1289
+ // Serialize and Encrypt Value
1290
+ // Adding metadata prefix to avoid bloating value with massive StoredValue JSON headers
1291
+ const encryptedData = await this.encryptionHelper.encrypt(processedValue);
1292
+ const finalValue = metadataPrefix + encryptedData;
1293
+ // Prepare Cookie string
1294
+ const mergedOptions = { ...this.config.cookieOptions, ...options };
1295
+ const cookieOptionsStr = this.serializeOptions(mergedOptions);
1296
+ const cookieString = `${encodeURIComponent(encryptedKey)}=${encodeURIComponent(finalValue)}${cookieOptionsStr}`;
1297
+ // Verificación de tamaño de cookie
1298
+ if (cookieString.length > 4096) {
1299
+ this.logger.warn(`Cookie '${name}' es muy grande (${cookieString.length} bytes). Puede ser rechazada por el navegador.`);
1300
+ }
1301
+ document.cookie = cookieString;
1302
+ this.logger.verbose(`Stored cookie: ${name}`);
1303
+ this.logger.timeEnd(`cookie:set:${name}`);
1304
+ }
1305
+ catch (error) {
1306
+ this.logger.error(`Error al guardar cookie encriptada para ${name}:`, error);
1307
+ // Fallback sin cifrar como último recurso
1308
+ const mergedOptions = { ...this.config.cookieOptions, ...options };
1309
+ document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}${this.serializeOptions(mergedOptions)}`;
1310
+ }
1311
+ }
1312
+ /**
1313
+ * Pide una cookie por nombre
1314
+ * Retorna el string o null si no existe
1315
+ */
1316
+ getRawCookie(name) {
1317
+ const nameEQ = encodeURIComponent(name) + '=';
1318
+ const ca = document.cookie.split(';');
1319
+ for (let i = 0; i < ca.length; i++) {
1320
+ let c = ca[i];
1321
+ while (c.charAt(0) === ' ')
1322
+ c = c.substring(1, c.length);
1323
+ if (c.indexOf(nameEQ) === 0)
1324
+ return decodeURIComponent(c.substring(nameEQ.length, c.length));
1325
+ }
1326
+ return null;
1327
+ }
1328
+ /**
1329
+ * Recupera y desencripta un valor de cookie
1330
+ */
1331
+ async get(name) {
1332
+ this.logger.time(`cookie:get:${name}`);
1333
+ if (!EncryptionHelper.isSupported()) {
1334
+ return this.getRawCookie(name);
1335
+ }
1336
+ try {
1337
+ // Find encrypted key
1338
+ const encryptedKey = await this.encryptionHelper.encryptKey(name);
1339
+ let rawValue = this.getRawCookie(encryptedKey);
1340
+ if (!rawValue) {
1341
+ // Backward compatibility just in case fallback was used
1342
+ rawValue = this.getRawCookie(name);
1343
+ if (!rawValue) {
1344
+ this.logger.timeEnd(`cookie:get:${name}`);
1345
+ return null;
1346
+ }
1347
+ }
1348
+ // Check if it has our metadata prefixes
1349
+ if (!rawValue.startsWith('P_') && !rawValue.startsWith('C_')) {
1350
+ // Podría ser un fallback plano
1351
+ this.logger.timeEnd(`cookie:get:${name}`);
1352
+ return rawValue; // Asumimos que es plano
1353
+ }
1354
+ const isCompressed = rawValue.startsWith('C_');
1355
+ const encryptedData = rawValue.substring(2);
1356
+ // Decrypt
1357
+ const decryptedString = await this.encryptionHelper.decrypt(encryptedData);
1358
+ // Decompress if needed
1359
+ let finalValue = decryptedString;
1360
+ if (isCompressed) {
1361
+ const compressedBuffer = this.encryptionHelper['base64ToArrayBuffer'](decryptedString);
1362
+ finalValue = await decompress(new Uint8Array(compressedBuffer));
1363
+ }
1364
+ this.logger.timeEnd(`cookie:get:${name}`);
1365
+ return finalValue;
1366
+ }
1367
+ catch (error) {
1368
+ this.logger.error(`Error al recuperar cookie encriptada para ${name}:`, error);
1369
+ // Fallback
1370
+ return this.getRawCookie(name);
1371
+ }
1372
+ }
1373
+ /**
1374
+ * Elimina una cookie
1375
+ */
1376
+ async remove(name, options) {
1377
+ const mergedOptions = { ...this.config.cookieOptions, ...options };
1378
+ // Expirar la cookie configurándole una fecha del pasado
1379
+ const expireOptions = {
1380
+ ...mergedOptions,
1381
+ expires: new Date(0),
1382
+ maxAge: 0
1383
+ };
1384
+ if (!EncryptionHelper.isSupported()) {
1385
+ document.cookie = `${encodeURIComponent(name)}=${this.serializeOptions(expireOptions)}`;
1386
+ return;
1387
+ }
1388
+ const encryptedKey = await this.encryptionHelper.encryptKey(name);
1389
+ // Remove encrypted cookie
1390
+ document.cookie = `${encodeURIComponent(encryptedKey)}=${this.serializeOptions(expireOptions)}`;
1391
+ // Also remove plain version just in case
1392
+ document.cookie = `${encodeURIComponent(name)}=${this.serializeOptions(expireOptions)}`;
1393
+ }
1394
+ /**
1395
+ * Limpia la instancia actual (útil para testing o refresco)
1396
+ */
1397
+ destroy() {
1398
+ SecureCookie.instance = null;
1399
+ }
1400
+ }
1401
+ SecureCookie.instance = null;
1402
+
1158
1403
  const secureStorage$2 = SecureStorage.getInstance();
1159
1404
  /**
1160
1405
  * Función de debug para verificar el estado del sistema de encriptación
@@ -1207,6 +1452,202 @@ async function forceMigration(customKeys) {
1207
1452
  await debugEncryptionState();
1208
1453
  }
1209
1454
 
1455
+ /**
1456
+ * PlainStorage - Una interfaz consistente con SecureStorage pero sin cifrado.
1457
+ * Envuelve localStorage o sessionStorage con serialización JSON.
1458
+ */
1459
+ class PlainStorage {
1460
+ constructor(storageEngine = typeof window !== 'undefined' ? window.localStorage : {}) {
1461
+ this.storage = storageEngine;
1462
+ this.logger = new Logger({ prefix: 'PlainStorage' });
1463
+ }
1464
+ async set(key, value) {
1465
+ try {
1466
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
1467
+ this.storage.setItem(key, serialized);
1468
+ }
1469
+ catch (error) {
1470
+ this.logger.error(`Error saving unencrypted item ${key}:`, error);
1471
+ }
1472
+ }
1473
+ async get(key) {
1474
+ try {
1475
+ const value = this.storage.getItem(key);
1476
+ if (value === null)
1477
+ return null;
1478
+ try {
1479
+ return JSON.parse(value);
1480
+ }
1481
+ catch {
1482
+ return value; // Fallback for plain strings
1483
+ }
1484
+ }
1485
+ catch (error) {
1486
+ this.logger.error(`Error reading unencrypted item ${key}:`, error);
1487
+ return null;
1488
+ }
1489
+ }
1490
+ async remove(key) {
1491
+ this.storage.removeItem(key);
1492
+ }
1493
+ clear() {
1494
+ this.storage.clear();
1495
+ }
1496
+ }
1497
+
1498
+ /**
1499
+ * Crea una instancia de almacenamiento encriptado.
1500
+ * @param engine 'localStorage' o 'sessionStorage'
1501
+ * @param secretKey Llave secreta opcional para derivación de claves
1502
+ * @param config Configuración adicional opcional
1503
+ * @returns Instancia de SecureStorage
1504
+ */
1505
+ function createEncryptedStorage(engine = 'localStorage', secretKey, config) {
1506
+ const defaultEngine = typeof window !== 'undefined'
1507
+ ? (engine === 'sessionStorage' ? window.sessionStorage : window.localStorage)
1508
+ : {};
1509
+ const mergedConfig = {
1510
+ ...config,
1511
+ encryption: {
1512
+ ...config?.encryption,
1513
+ ...(secretKey ? { appIdentifier: secretKey } : {})
1514
+ },
1515
+ storage: {
1516
+ ...config?.storage,
1517
+ storageEngine: defaultEngine
1518
+ }
1519
+ };
1520
+ return SecureStorage.getInstance(mergedConfig);
1521
+ }
1522
+ /**
1523
+ * Crea una instancia de almacenamiento plano (sin cifrar).
1524
+ * @param engine 'localStorage' o 'sessionStorage'
1525
+ * @returns Instancia de PlainStorage
1526
+ */
1527
+ function createPlainStorage(engine = 'localStorage') {
1528
+ const defaultEngine = typeof window !== 'undefined'
1529
+ ? (engine === 'sessionStorage' ? window.sessionStorage : window.localStorage)
1530
+ : {};
1531
+ return new PlainStorage(defaultEngine);
1532
+ }
1533
+
1534
+ /**
1535
+ * PlainCookie - Una interfaz consistente con SecureCookie pero sin cifrado.
1536
+ */
1537
+ class PlainCookie {
1538
+ constructor(defaultOptions) {
1539
+ this.logger = new Logger({ prefix: 'PlainCookie' });
1540
+ this.defaultOptions = { path: '/', ...defaultOptions };
1541
+ }
1542
+ serializeOptions(options) {
1543
+ let cookieString = '';
1544
+ if (options.expires)
1545
+ cookieString += `; expires=${options.expires.toUTCString()}`;
1546
+ if (options.maxAge)
1547
+ cookieString += `; max-age=${options.maxAge}`;
1548
+ if (options.domain)
1549
+ cookieString += `; domain=${options.domain}`;
1550
+ if (options.path)
1551
+ cookieString += `; path=${options.path}`;
1552
+ if (options.secure)
1553
+ cookieString += `; secure`;
1554
+ if (options.sameSite)
1555
+ cookieString += `; samesite=${options.sameSite}`;
1556
+ return cookieString;
1557
+ }
1558
+ async set(name, value, options) {
1559
+ try {
1560
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
1561
+ const mergedOptions = { ...this.defaultOptions, ...options };
1562
+ document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(serialized)}${this.serializeOptions(mergedOptions)}`;
1563
+ }
1564
+ catch (error) {
1565
+ this.logger.error(`Error saving unencrypted cookie ${name}:`, error);
1566
+ }
1567
+ }
1568
+ async get(name) {
1569
+ try {
1570
+ const nameEQ = encodeURIComponent(name) + '=';
1571
+ const ca = document.cookie.split(';');
1572
+ for (let i = 0; i < ca.length; i++) {
1573
+ let c = ca[i];
1574
+ while (c.charAt(0) === ' ')
1575
+ c = c.substring(1, c.length);
1576
+ if (c.indexOf(nameEQ) === 0) {
1577
+ const decoded = decodeURIComponent(c.substring(nameEQ.length, c.length));
1578
+ try {
1579
+ return JSON.parse(decoded);
1580
+ }
1581
+ catch {
1582
+ return decoded;
1583
+ }
1584
+ }
1585
+ }
1586
+ return null;
1587
+ }
1588
+ catch (error) {
1589
+ this.logger.error(`Error reading unencrypted cookie ${name}:`, error);
1590
+ return null;
1591
+ }
1592
+ }
1593
+ async remove(name, options) {
1594
+ const mergedOptions = { ...this.defaultOptions, ...options };
1595
+ document.cookie = `${encodeURIComponent(name)}=${this.serializeOptions({ ...mergedOptions, expires: new Date(0), maxAge: 0 })}`;
1596
+ }
1597
+ clearAll() {
1598
+ const cookies = document.cookie.split(';');
1599
+ for (let i = 0; i < cookies.length; i++) {
1600
+ const cookie = cookies[i];
1601
+ const eqPos = cookie.indexOf('=');
1602
+ const name = eqPos > -1 ? cookie.substring(0, eqPos).trim() : cookie.trim();
1603
+ this.remove(decodeURIComponent(name));
1604
+ }
1605
+ }
1606
+ }
1607
+
1608
+ /**
1609
+ * Crea una instancia de manejador de cookies encriptadas.
1610
+ * @param secretKey Llave secreta opcional para derivación de claves
1611
+ * @param domain Dominio opcional (incluir punto inicial para subdominios ej. '.dominio.com')
1612
+ * @param config Configuración adicional opcional
1613
+ * @returns Instancia de SecureCookie
1614
+ */
1615
+ function createEncryptedCookieManager(secretKey, domain, config) {
1616
+ const mergedConfig = {
1617
+ ...config,
1618
+ encryption: {
1619
+ ...config?.encryption,
1620
+ ...(secretKey ? { appIdentifier: secretKey } : {})
1621
+ },
1622
+ cookieOptions: {
1623
+ ...config?.cookieOptions,
1624
+ ...(domain ? { domain } : {})
1625
+ }
1626
+ };
1627
+ return SecureCookie.getInstance(mergedConfig);
1628
+ }
1629
+ /**
1630
+ * Crea una instancia de manejador de cookies plano (sin cifrar).
1631
+ * @param defaultOptions Opciones por defecto para las cookies (ej. domain)
1632
+ * @returns Instancia de PlainCookie
1633
+ */
1634
+ function createPlainCookieManager(defaultOptions) {
1635
+ return new PlainCookie(defaultOptions);
1636
+ }
1637
+
1638
+ /**
1639
+ * Instancia predeterminada de almacenamiento local (sin cifrar).
1640
+ */
1641
+ const localStore = createPlainStorage('localStorage');
1642
+ /**
1643
+ * Instancia predeterminada de almacenamiento de sesión (sin cifrar).
1644
+ */
1645
+ const sessionStore = createPlainStorage('sessionStorage');
1646
+ /**
1647
+ * Instancia predeterminada de gestor de cookies (sin cifrar).
1648
+ */
1649
+ const cookies = createPlainCookieManager();
1650
+
1210
1651
  /**
1211
1652
  * @bantis/local-cipher - Core Module
1212
1653
  * Framework-agnostic client-side encryption for browser storage
@@ -1216,8 +1657,9 @@ async function forceMigration(customKeys) {
1216
1657
  */
1217
1658
  // Core classes
1218
1659
  const secureStorage$1 = SecureStorage.getInstance();
1660
+ const secureCookie = SecureCookie.getInstance();
1219
1661
  // Version
1220
- const VERSION = '2.1.0';
1662
+ const VERSION = '2.2.0';
1221
1663
 
1222
1664
  /**
1223
1665
  * React Hooks for @bantis/local-cipher
@@ -1397,18 +1839,35 @@ function useSecureStorageDebug(storage) {
1397
1839
  }, [secureStorage]);
1398
1840
  return debugInfo;
1399
1841
  }
1400
- /**
1401
- * Hook para usar un namespace de SecureStorage
1402
- */
1403
- function useNamespace(namespace, storage) {
1404
- const secureStorage = storage || getDefaultStorage();
1405
- const [namespacedStorage] = useState(() => secureStorage.namespace(namespace));
1406
- return namespacedStorage;
1407
- }
1408
1842
  /**
1409
1843
  * Exportar la instancia de SecureStorage para uso directo
1410
1844
  */
1411
1845
  const secureStorage = getDefaultStorage();
1846
+ /**
1847
+ * Hook para sincronizar el estado de React con el Storage Manager (por defecto localStore en texto plano)
1848
+ */
1849
+ function useLocalStore(key, initialValue, storageManager = localStore) {
1850
+ const [value, setValue] = useState(initialValue);
1851
+ useEffect(() => {
1852
+ let mounted = true;
1853
+ storageManager.get(key).then((saved) => {
1854
+ if (!mounted)
1855
+ return;
1856
+ if (saved !== null) {
1857
+ setValue(saved);
1858
+ }
1859
+ else {
1860
+ storageManager.set(key, initialValue);
1861
+ }
1862
+ });
1863
+ return () => { mounted = false; };
1864
+ }, [key, storageManager]);
1865
+ const setNewValue = useCallback(async (newValue) => {
1866
+ setValue(newValue);
1867
+ await storageManager.set(key, newValue);
1868
+ }, [key, storageManager]);
1869
+ return [value, setNewValue];
1870
+ }
1412
1871
 
1413
- export { DEFAULT_CONFIG, EncryptionHelper, EventEmitter, KeyRotation, LIBRARY_VERSION, Logger, NamespacedStorage, STORAGE_VERSION, SecureStorage, VERSION, compress, debugEncryptionState, decompress, forceMigration, initializeSecureStorage, isCompressionSupported, secureStorage as reactSecureStorage, secureStorage$1 as secureStorage, shouldCompress, useNamespace, useSecureStorage, useSecureStorageDebug, useSecureStorageEvents, useSecureStorageItem, useSecureStorageWithExpiry };
1872
+ export { DEFAULT_CONFIG, EncryptionHelper, EventEmitter, KeyRotation, LIBRARY_VERSION, Logger, NamespacedStorage, PlainCookie, PlainStorage, STORAGE_VERSION, SecureCookie, SecureStorage, VERSION, compress, cookies, createEncryptedCookieManager, createEncryptedStorage, createPlainCookieManager, createPlainStorage, debugEncryptionState, decompress, forceMigration, initializeSecureStorage, isCompressionSupported, localStore, secureStorage as reactSecureStorage, secureCookie, secureStorage$1 as secureStorage, sessionStore, shouldCompress, useLocalStore, useSecureStorage, useSecureStorageDebug, useSecureStorageEvents, useSecureStorageItem, useSecureStorageWithExpiry };
1414
1873
  //# sourceMappingURL=react.esm.js.map