@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/README.md +85 -2
- package/dist/angular/SecureStorageService.d.ts +1 -1
- package/dist/angular/StorageService.d.ts +22 -0
- package/dist/angular.d.ts +1 -0
- package/dist/angular.esm.js +539 -53
- package/dist/angular.esm.js.map +1 -1
- package/dist/angular.js +550 -52
- package/dist/angular.js.map +1 -1
- package/dist/core/EncryptionHelper.d.ts +1 -0
- package/dist/core/NamespacedStorage.d.ts +3 -0
- package/dist/core/SecureCookie.d.ts +41 -0
- package/dist/core/SecureStorage.d.ts +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.esm.js +493 -51
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +503 -50
- package/dist/index.js.map +1 -1
- package/dist/managers/CookieManager.d.ts +17 -0
- package/dist/managers/PlainCookie.d.ts +14 -0
- package/dist/managers/PlainStorage.d.ts +13 -0
- package/dist/managers/StorageManager.d.ts +17 -0
- package/dist/managers/index.d.ts +17 -0
- package/dist/react/hooks.d.ts +20 -2
- package/dist/react.d.ts +1 -1
- package/dist/react.esm.js +518 -59
- package/dist/react.esm.js.map +1 -1
- package/dist/react.js +529 -59
- package/dist/react.js.map +1 -1
- package/dist/types/index.d.ts +36 -0
- package/package.json +8 -4
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
754
|
-
|
|
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
|
-
|
|
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
|
|
816
|
-
|
|
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
|
|
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 =
|
|
883
|
+
let encryptedValue = this.config.storage.storageEngine.getItem(encryptedKey);
|
|
840
884
|
// Backward compatibility: try with plain key
|
|
841
885
|
if (!encryptedValue) {
|
|
842
|
-
encryptedValue =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
921
|
-
|
|
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
|
-
|
|
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 <
|
|
953
|
-
const key =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 <
|
|
1118
|
-
const key =
|
|
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.
|
|
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
|