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