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