@jvsoft/utils 1.0.0-alpha.11 → 1.0.0-alpha.12
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/fesm2022/jvsoft-utils-functions.mjs +19 -14
- package/fesm2022/jvsoft-utils-functions.mjs.map +1 -1
- package/fesm2022/jvsoft-utils-services.mjs +128 -2
- package/fesm2022/jvsoft-utils-services.mjs.map +1 -1
- package/fesm2022/jvsoft-utils.mjs +220 -28
- package/fesm2022/jvsoft-utils.mjs.map +1 -1
- package/functions/string.d.ts +7 -0
- package/interceptors/encryption.interceptor.d.ts +7 -0
- package/interceptors/public-api.d.ts +1 -0
- package/package.json +9 -10
- package/public-api.d.ts +1 -0
- package/services/encryption.service.d.ts +40 -0
- package/services/public-api.d.ts +1 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, computed, Injectable } from '@angular/core';
|
|
2
|
+
import { signal, computed, Injectable, InjectionToken, inject } from '@angular/core';
|
|
3
3
|
import { format } from 'date-fns';
|
|
4
|
+
import { HttpClient } from '@angular/common/http';
|
|
5
|
+
import { firstValueFrom } from 'rxjs';
|
|
6
|
+
import { map, catchError } from 'rxjs/operators';
|
|
7
|
+
import * as CryptoJS from 'crypto-js';
|
|
8
|
+
import JSEncrypt from 'jsencrypt';
|
|
4
9
|
|
|
5
10
|
class RelojService {
|
|
6
11
|
// El offset es la diferencia: ServerTime - LocalTime (performance.now())
|
|
@@ -93,6 +98,127 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
|
|
|
93
98
|
}]
|
|
94
99
|
}], ctorParameters: () => [] });
|
|
95
100
|
|
|
101
|
+
const ENCRYPTION_CONFIG = new InjectionToken('ENCRYPTION_CONFIG');
|
|
102
|
+
class EncryptionService {
|
|
103
|
+
http = inject(HttpClient);
|
|
104
|
+
reloj = inject(RelojService);
|
|
105
|
+
config = inject(ENCRYPTION_CONFIG, { optional: true });
|
|
106
|
+
PUBLIC_KEY_URL = this.config?.publicKeyUrl ?? '/api/auth/public-key';
|
|
107
|
+
CACHE_KEY = 'jvs_rsa_public_key';
|
|
108
|
+
publicKey = signal(null);
|
|
109
|
+
pendingFetch;
|
|
110
|
+
constructor() {
|
|
111
|
+
this.loadCachedPublicKey();
|
|
112
|
+
this.escucharCambiosOtrasPestanas();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Obtiene la llave pública del servidor.
|
|
116
|
+
* Utiliza la Signal (RAM) primariamente, luego localStorage.
|
|
117
|
+
* Maneja peticiones concurrentes de forma que solo se dispare una descarga.
|
|
118
|
+
*/
|
|
119
|
+
async getPublicKey() {
|
|
120
|
+
const current = this.publicKey();
|
|
121
|
+
if (current) {
|
|
122
|
+
return current;
|
|
123
|
+
}
|
|
124
|
+
if (this.pendingFetch) {
|
|
125
|
+
return this.pendingFetch;
|
|
126
|
+
}
|
|
127
|
+
this.pendingFetch = firstValueFrom(this.http.get(this.PUBLIC_KEY_URL).pipe(map(res => {
|
|
128
|
+
const key = res.publicKey || res.public_key;
|
|
129
|
+
if (!key)
|
|
130
|
+
throw new Error('Respuesta inválida del servidor (Falta publicKey/public_key).');
|
|
131
|
+
this.setPublicKey(key);
|
|
132
|
+
return key;
|
|
133
|
+
}), catchError(err => {
|
|
134
|
+
this.pendingFetch = undefined;
|
|
135
|
+
console.error('[EncryptionService] Error al obtener llave pública:', err);
|
|
136
|
+
throw err;
|
|
137
|
+
})));
|
|
138
|
+
try {
|
|
139
|
+
const key = await this.pendingFetch;
|
|
140
|
+
this.pendingFetch = undefined;
|
|
141
|
+
return key;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
this.pendingFetch = undefined;
|
|
145
|
+
throw new Error('No se pudo establecer el canal seguro (RSA Key Fallback).');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Fuerza la limpieza del caché y descarga una nueva llave.
|
|
150
|
+
* Útil para auto-saneamiento cuando el backend reporta error de desencriptación.
|
|
151
|
+
*/
|
|
152
|
+
async forceRefreshPublicKey() {
|
|
153
|
+
console.info('[EncryptionService] Forzando refresco de llave RSA por solicitud del interceptor.');
|
|
154
|
+
this.publicKey.set(null);
|
|
155
|
+
localStorage.removeItem(this.CACHE_KEY);
|
|
156
|
+
return this.getPublicKey();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Encriptación Híbrida (RSA + AES-256-CBC)
|
|
160
|
+
*/
|
|
161
|
+
async encryptHybrid(data) {
|
|
162
|
+
const pubKey = await this.getPublicKey();
|
|
163
|
+
// 1. Generar Key (32 bytes) e IV (16 bytes)
|
|
164
|
+
const aesKey = CryptoJS.lib.WordArray.random(32);
|
|
165
|
+
const aesIv = CryptoJS.lib.WordArray.random(16);
|
|
166
|
+
// 2. Preparar payload con timestamp (Unix Epoch)
|
|
167
|
+
const timestamp = Math.floor((this.reloj.timestamp() || Date.now()) / 1000);
|
|
168
|
+
const bodyToEncrypt = typeof data === 'object' && data !== null ? { ...data, timestamp } : { data, timestamp };
|
|
169
|
+
const plainData = JSON.stringify(bodyToEncrypt);
|
|
170
|
+
// 3. Cifrar con AES
|
|
171
|
+
const encryptedPayload = CryptoJS.AES.encrypt(plainData, aesKey, {
|
|
172
|
+
iv: aesIv,
|
|
173
|
+
mode: CryptoJS.mode.CBC,
|
|
174
|
+
padding: CryptoJS.pad.Pkcs7
|
|
175
|
+
});
|
|
176
|
+
// 4. Concatenar Key + IV (48 bytes) y cifrar con RSA
|
|
177
|
+
const combinedBytes = aesKey.concat(aesIv);
|
|
178
|
+
const combinedLatin1 = combinedBytes.toString(CryptoJS.enc.Latin1);
|
|
179
|
+
const rsa = new JSEncrypt();
|
|
180
|
+
rsa.setPublicKey(pubKey);
|
|
181
|
+
const encryptedKeys = rsa.encrypt(combinedLatin1);
|
|
182
|
+
if (!encryptedKeys) {
|
|
183
|
+
throw new Error('Fallo en cifrado RSA (Llave posiblemente inválida).');
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
payload: encryptedPayload.toString(),
|
|
187
|
+
keys: encryptedKeys
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
setPublicKey(key) {
|
|
191
|
+
this.publicKey.set(key);
|
|
192
|
+
localStorage.setItem(this.CACHE_KEY, key);
|
|
193
|
+
}
|
|
194
|
+
loadCachedPublicKey() {
|
|
195
|
+
const cached = localStorage.getItem(this.CACHE_KEY);
|
|
196
|
+
if (cached) {
|
|
197
|
+
this.publicKey.set(cached);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
escucharCambiosOtrasPestanas() {
|
|
201
|
+
window.addEventListener('storage', (event) => {
|
|
202
|
+
if (event.key === this.CACHE_KEY) {
|
|
203
|
+
if (event.newValue) {
|
|
204
|
+
this.publicKey.set(event.newValue);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
this.publicKey.set(null);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EncryptionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
213
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EncryptionService, providedIn: 'root' });
|
|
214
|
+
}
|
|
215
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EncryptionService, decorators: [{
|
|
216
|
+
type: Injectable,
|
|
217
|
+
args: [{
|
|
218
|
+
providedIn: 'root'
|
|
219
|
+
}]
|
|
220
|
+
}], ctorParameters: () => [] });
|
|
221
|
+
|
|
96
222
|
/*
|
|
97
223
|
* Public API Surface of utils services
|
|
98
224
|
*/
|
|
@@ -101,5 +227,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
|
|
|
101
227
|
* Generated bundle index. Do not edit.
|
|
102
228
|
*/
|
|
103
229
|
|
|
104
|
-
export { RelojService };
|
|
230
|
+
export { ENCRYPTION_CONFIG, EncryptionService, RelojService };
|
|
105
231
|
//# sourceMappingURL=jvsoft-utils-services.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jvsoft-utils-services.mjs","sources":["../../../projects/utils/services/reloj.service.ts","../../../projects/utils/services/public-api.ts","../../../projects/utils/services/jvsoft-utils-services.ts"],"sourcesContent":["import { computed, Injectable, signal } from '@angular/core';\nimport { format } from 'date-fns';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class RelojService {\n // El offset es la diferencia: ServerTime - LocalTime (performance.now())\n private readonly offset = signal<number>(0);\n private readonly STORAGE_KEY = 'srv_clock_offset';\n \n // Signal que cambia cada segundo para forzar la reactividad del reloj\n private readonly ticker = signal<number>(0);\n\n // Hora actual calculada basándose en performance.now() para evitar drift y suspensión\n readonly timestamp = computed(() => {\n this.ticker(); // Dependencia reactiva para que el computed se ejecute cada segundo\n // performance.now() es monotónico, Date.now() es manipulable por el usuario\n // Usamos Date.now() inicialmente pero ajustado por el offset calculado\n return Date.now() + this.offset();\n });\n\n readonly date = computed(() => new Date(this.timestamp()));\n\n readonly dateTime = computed(() => {\n return format(this.date(), \"yyyy-MM-dd'T'HH:mm\");\n });\n\n constructor() {\n this.cargarOffsetInicial();\n this.escucharCambiosOtrasPestanas();\n this.iniciarVigilanteWarp();\n \n // Iniciar el ticker que mantiene el reloj vivo\n setInterval(() => {\n this.ticker.update(n => n + 1);\n }, 1000);\n }\n\n /**\n * Sincroniza el reloj interno con una hora oficial del servidor.\n * Calculamos el offset respecto al tiempo local actual.\n */\n sincronizar(serverTime: number | string | Date) {\n const srvTime = new Date(serverTime).getTime();\n const lclTime = Date.now();\n const nuevoOffset = srvTime - lclTime;\n\n this.offset.set(nuevoOffset);\n localStorage.setItem(this.STORAGE_KEY, nuevoOffset.toString());\n this.estaSincronizado.set(true);\n }\n\n /**\n * Fuerza que la próxima petición HTTP pida la hora al servidor.\n * Útil tras reconexiones de socket o detecciones de warp.\n */\n forzarSincronizacion() {\n this.estaSincronizado.set(false);\n }\n\n private estaSincronizado = signal<boolean>(false);\n get sincronizado() { return this.estaSincronizado(); }\n\n private cargarOffsetInicial() {\n const guardado = localStorage.getItem(this.STORAGE_KEY);\n if (guardado) {\n this.offset.set(parseInt(guardado, 10));\n }\n }\n\n private escucharCambiosOtrasPestanas() {\n window.addEventListener('storage', (event) => {\n if (event.key === this.STORAGE_KEY && event.newValue) {\n this.offset.set(parseInt(event.newValue, 10));\n }\n });\n }\n\n /**\n * Monitoriza saltos bruscos en el tiempo (suspensión o cambio manual de hora).\n * Si detectamos un \"warp\", podríamos pedir una sincronización forzada.\n */\n private iniciarVigilanteWarp() {\n let lastPerformance = performance.now();\n let lastDate = Date.now();\n\n setInterval(() => {\n const currentPerformance = performance.now();\n const currentDate = Date.now();\n\n const perfDelta = currentPerformance - lastPerformance;\n const dateDelta = currentDate - lastDate;\n\n // Si la diferencia entre deltas es mayor a 2s (ajustable), hubo un warp\n if (Math.abs(perfDelta - dateDelta) > 2000) {\n console.warn('[RelojService] Salto temporal detectado (Warp). Se recomienda sincronizar.');\n this.forzarSincronizacion();\n }\n\n lastPerformance = currentPerformance;\n lastDate = currentDate;\n }, 5000);\n }\n}\n","/*\n * Public API Surface of utils services\n */\nexport * from './reloj.service';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"jvsoft-utils-services.mjs","sources":["../../../projects/utils/services/reloj.service.ts","../../../projects/utils/services/encryption.service.ts","../../../projects/utils/services/public-api.ts","../../../projects/utils/services/jvsoft-utils-services.ts"],"sourcesContent":["import { computed, Injectable, signal } from '@angular/core';\nimport { format } from 'date-fns';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class RelojService {\n // El offset es la diferencia: ServerTime - LocalTime (performance.now())\n private readonly offset = signal<number>(0);\n private readonly STORAGE_KEY = 'srv_clock_offset';\n \n // Signal que cambia cada segundo para forzar la reactividad del reloj\n private readonly ticker = signal<number>(0);\n\n // Hora actual calculada basándose en performance.now() para evitar drift y suspensión\n readonly timestamp = computed(() => {\n this.ticker(); // Dependencia reactiva para que el computed se ejecute cada segundo\n // performance.now() es monotónico, Date.now() es manipulable por el usuario\n // Usamos Date.now() inicialmente pero ajustado por el offset calculado\n return Date.now() + this.offset();\n });\n\n readonly date = computed(() => new Date(this.timestamp()));\n\n readonly dateTime = computed(() => {\n return format(this.date(), \"yyyy-MM-dd'T'HH:mm\");\n });\n\n constructor() {\n this.cargarOffsetInicial();\n this.escucharCambiosOtrasPestanas();\n this.iniciarVigilanteWarp();\n \n // Iniciar el ticker que mantiene el reloj vivo\n setInterval(() => {\n this.ticker.update(n => n + 1);\n }, 1000);\n }\n\n /**\n * Sincroniza el reloj interno con una hora oficial del servidor.\n * Calculamos el offset respecto al tiempo local actual.\n */\n sincronizar(serverTime: number | string | Date) {\n const srvTime = new Date(serverTime).getTime();\n const lclTime = Date.now();\n const nuevoOffset = srvTime - lclTime;\n\n this.offset.set(nuevoOffset);\n localStorage.setItem(this.STORAGE_KEY, nuevoOffset.toString());\n this.estaSincronizado.set(true);\n }\n\n /**\n * Fuerza que la próxima petición HTTP pida la hora al servidor.\n * Útil tras reconexiones de socket o detecciones de warp.\n */\n forzarSincronizacion() {\n this.estaSincronizado.set(false);\n }\n\n private estaSincronizado = signal<boolean>(false);\n get sincronizado() { return this.estaSincronizado(); }\n\n private cargarOffsetInicial() {\n const guardado = localStorage.getItem(this.STORAGE_KEY);\n if (guardado) {\n this.offset.set(parseInt(guardado, 10));\n }\n }\n\n private escucharCambiosOtrasPestanas() {\n window.addEventListener('storage', (event) => {\n if (event.key === this.STORAGE_KEY && event.newValue) {\n this.offset.set(parseInt(event.newValue, 10));\n }\n });\n }\n\n /**\n * Monitoriza saltos bruscos en el tiempo (suspensión o cambio manual de hora).\n * Si detectamos un \"warp\", podríamos pedir una sincronización forzada.\n */\n private iniciarVigilanteWarp() {\n let lastPerformance = performance.now();\n let lastDate = Date.now();\n\n setInterval(() => {\n const currentPerformance = performance.now();\n const currentDate = Date.now();\n\n const perfDelta = currentPerformance - lastPerformance;\n const dateDelta = currentDate - lastDate;\n\n // Si la diferencia entre deltas es mayor a 2s (ajustable), hubo un warp\n if (Math.abs(perfDelta - dateDelta) > 2000) {\n console.warn('[RelojService] Salto temporal detectado (Warp). Se recomienda sincronizar.');\n this.forzarSincronizacion();\n }\n\n lastPerformance = currentPerformance;\n lastDate = currentDate;\n }, 5000);\n }\n}\n","import { Injectable, inject, signal, InjectionToken } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { catchError, map } from 'rxjs/operators';\nimport * as CryptoJS from 'crypto-js';\nimport JSEncrypt from 'jsencrypt';\nimport { RelojService } from './reloj.service';\n\nexport interface EncryptedPayload {\n payload: string;\n keys: string;\n}\n\nexport interface EncryptionConfig {\n publicKeyUrl?: string;\n}\n\nexport const ENCRYPTION_CONFIG = new InjectionToken<EncryptionConfig>('ENCRYPTION_CONFIG');\n\n@Injectable({\n providedIn: 'root'\n})\nexport class EncryptionService {\n private readonly http = inject(HttpClient);\n private readonly reloj = inject(RelojService);\n private readonly config = inject(ENCRYPTION_CONFIG, { optional: true });\n \n private readonly PUBLIC_KEY_URL = this.config?.publicKeyUrl ?? '/api/auth/public-key';\n private readonly CACHE_KEY = 'jvs_rsa_public_key';\n \n private publicKey = signal<string | null>(null);\n private pendingFetch?: Promise<string>;\n\n constructor() {\n this.loadCachedPublicKey();\n this.escucharCambiosOtrasPestanas();\n }\n\n /**\n * Obtiene la llave pública del servidor.\n * Utiliza la Signal (RAM) primariamente, luego localStorage.\n * Maneja peticiones concurrentes de forma que solo se dispare una descarga.\n */\n async getPublicKey(): Promise<string> {\n const current = this.publicKey();\n if (current) {\n return current;\n }\n\n if (this.pendingFetch) {\n return this.pendingFetch;\n }\n\n this.pendingFetch = firstValueFrom(\n this.http.get<{ publicKey?: string; public_key?: string }>(this.PUBLIC_KEY_URL).pipe(\n map(res => {\n const key = res.publicKey || res.public_key;\n if (!key) throw new Error('Respuesta inválida del servidor (Falta publicKey/public_key).');\n this.setPublicKey(key);\n return key;\n }),\n catchError(err => {\n this.pendingFetch = undefined;\n console.error('[EncryptionService] Error al obtener llave pública:', err);\n throw err;\n })\n )\n );\n\n try {\n const key = await this.pendingFetch;\n this.pendingFetch = undefined;\n return key;\n } catch {\n this.pendingFetch = undefined;\n throw new Error('No se pudo establecer el canal seguro (RSA Key Fallback).');\n }\n }\n\n /**\n * Fuerza la limpieza del caché y descarga una nueva llave.\n * Útil para auto-saneamiento cuando el backend reporta error de desencriptación.\n */\n async forceRefreshPublicKey(): Promise<string> {\n console.info('[EncryptionService] Forzando refresco de llave RSA por solicitud del interceptor.');\n this.publicKey.set(null);\n localStorage.removeItem(this.CACHE_KEY);\n return this.getPublicKey();\n }\n\n /**\n * Encriptación Híbrida (RSA + AES-256-CBC)\n */\n async encryptHybrid(data: any): Promise<EncryptedPayload> {\n const pubKey = await this.getPublicKey();\n\n // 1. Generar Key (32 bytes) e IV (16 bytes)\n const aesKey = CryptoJS.lib.WordArray.random(32);\n const aesIv = CryptoJS.lib.WordArray.random(16);\n\n // 2. Preparar payload con timestamp (Unix Epoch)\n const timestamp = Math.floor((this.reloj.timestamp() || Date.now()) / 1000);\n const bodyToEncrypt = typeof data === 'object' && data !== null ? { ...data, timestamp } : { data, timestamp };\n const plainData = JSON.stringify(bodyToEncrypt);\n\n // 3. Cifrar con AES\n const encryptedPayload = CryptoJS.AES.encrypt(plainData, aesKey, {\n iv: aesIv,\n mode: CryptoJS.mode.CBC,\n padding: CryptoJS.pad.Pkcs7\n });\n\n // 4. Concatenar Key + IV (48 bytes) y cifrar con RSA\n const combinedBytes = aesKey.concat(aesIv);\n const combinedLatin1 = combinedBytes.toString(CryptoJS.enc.Latin1);\n\n const rsa = new JSEncrypt();\n rsa.setPublicKey(pubKey);\n const encryptedKeys = rsa.encrypt(combinedLatin1);\n\n if (!encryptedKeys) {\n throw new Error('Fallo en cifrado RSA (Llave posiblemente inválida).');\n }\n\n return {\n payload: encryptedPayload.toString(),\n keys: encryptedKeys\n };\n }\n\n private setPublicKey(key: string) {\n this.publicKey.set(key);\n localStorage.setItem(this.CACHE_KEY, key);\n }\n\n private loadCachedPublicKey() {\n const cached = localStorage.getItem(this.CACHE_KEY);\n if (cached) {\n this.publicKey.set(cached);\n }\n }\n\n private escucharCambiosOtrasPestanas() {\n window.addEventListener('storage', (event) => {\n if (event.key === this.CACHE_KEY) {\n if (event.newValue) {\n this.publicKey.set(event.newValue);\n } else {\n this.publicKey.set(null);\n }\n }\n });\n }\n}\n","/*\n * Public API Surface of utils services\n */\nexport * from './reloj.service';\nexport * from './encryption.service';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { Validators, FormGroup, FormControl, FormArray, NG_VALIDATORS, NgControl, FormControlName } from '@angular/forms';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { inject, DestroyRef, Pipe, input, forwardRef, Directive, computed, Component, TemplateRef, Injectable, ElementRef, ViewContainerRef, HostListener, output, effect, untracked, signal } from '@angular/core';
|
|
3
|
+
import { inject, DestroyRef, Pipe, input, forwardRef, Directive, computed, Component, TemplateRef, Injectable, ElementRef, ViewContainerRef, HostListener, output, effect, untracked, signal, InjectionToken } from '@angular/core';
|
|
4
4
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
|
-
import { tap, startWith, debounceTime, isObservable, switchMap, of, finalize, Subject, merge, distinctUntilChanged, map as map$1 } from 'rxjs';
|
|
6
|
-
import { map } from 'rxjs/operators';
|
|
5
|
+
import { tap, startWith, debounceTime, isObservable, switchMap, of, finalize, Subject, merge, distinctUntilChanged, map as map$1, firstValueFrom, from, throwError, catchError as catchError$1 } from 'rxjs';
|
|
6
|
+
import { map, catchError } from 'rxjs/operators';
|
|
7
7
|
import { Buffer } from 'buffer';
|
|
8
|
-
import CryptoJS from 'crypto-js';
|
|
8
|
+
import * as CryptoJS from 'crypto-js';
|
|
9
|
+
import CryptoJS__default from 'crypto-js';
|
|
9
10
|
import * as i1$1 from '@angular/common';
|
|
10
11
|
import { formatDate, CommonModule } from '@angular/common';
|
|
11
12
|
import { saveAs } from 'file-saver';
|
|
12
|
-
import {
|
|
13
|
-
import moment from 'moment';
|
|
13
|
+
import { format, differenceInSeconds } from 'date-fns';
|
|
14
14
|
import swal from 'sweetalert2';
|
|
15
15
|
import { jwtDecode } from 'jwt-decode';
|
|
16
16
|
import * as i1 from '@angular/platform-browser';
|
|
17
17
|
import { Overlay, OverlayPositionBuilder } from '@angular/cdk/overlay';
|
|
18
18
|
import { ComponentPortal } from '@angular/cdk/portal';
|
|
19
19
|
import { ToastrService } from 'ngx-toastr';
|
|
20
|
-
import {
|
|
20
|
+
import { HttpClient } from '@angular/common/http';
|
|
21
|
+
import JSEncrypt from 'jsencrypt';
|
|
21
22
|
|
|
22
23
|
function deepMerge(source, target) {
|
|
23
24
|
// Crea un clon profundo sin usar JSON.parse(JSON.stringify)
|
|
@@ -759,25 +760,25 @@ function getBrowserName() {
|
|
|
759
760
|
// import * as CryptoJS from 'crypto-js';
|
|
760
761
|
// var CryptoJS = require("crypto-js");
|
|
761
762
|
// Clave secreta (debe ser de 16, 24 o 32 caracteres)
|
|
762
|
-
const secretKey =
|
|
763
|
-
const iv =
|
|
763
|
+
const secretKey = CryptoJS__default.enc.Utf8.parse('JVSoftSecret@20615178350');
|
|
764
|
+
const iv = CryptoJS__default.enc.Utf8.parse('AnSalHuaJVSoft07'); // Debe ser de 16 bytes
|
|
764
765
|
// Función para encriptar texto
|
|
765
766
|
function encriptar(text) {
|
|
766
|
-
const encrypted =
|
|
767
|
+
const encrypted = CryptoJS__default.AES.encrypt(text, secretKey, {
|
|
767
768
|
iv: iv,
|
|
768
|
-
mode:
|
|
769
|
-
padding:
|
|
769
|
+
mode: CryptoJS__default.mode.CBC,
|
|
770
|
+
padding: CryptoJS__default.pad.Pkcs7,
|
|
770
771
|
});
|
|
771
772
|
return encrypted.toString(); // Texto cifrado en Base64
|
|
772
773
|
}
|
|
773
774
|
// Función para desencriptar texto
|
|
774
775
|
function desencriptar(ciphertext) {
|
|
775
|
-
const decrypted =
|
|
776
|
+
const decrypted = CryptoJS__default.AES.decrypt(ciphertext, secretKey, {
|
|
776
777
|
iv: iv,
|
|
777
|
-
mode:
|
|
778
|
-
padding:
|
|
778
|
+
mode: CryptoJS__default.mode.CBC,
|
|
779
|
+
padding: CryptoJS__default.pad.Pkcs7,
|
|
779
780
|
});
|
|
780
|
-
return decrypted.toString(
|
|
781
|
+
return decrypted.toString(CryptoJS__default.enc.Utf8);
|
|
781
782
|
}
|
|
782
783
|
|
|
783
784
|
function formatearFechaFormato(val, format = 'dd/MM/yyyy') {
|
|
@@ -1121,15 +1122,6 @@ function getFormValidationErrors(form) {
|
|
|
1121
1122
|
function mensajesErrorFormControl(control) {
|
|
1122
1123
|
if (!control || !control.errors || !control.touched)
|
|
1123
1124
|
return '';
|
|
1124
|
-
ReactiveFormConfig.set({
|
|
1125
|
-
// RxwebValidators
|
|
1126
|
-
validationMessage: {
|
|
1127
|
-
required: 'Es requerido',
|
|
1128
|
-
numeric: 'Debe ser numérico valido',
|
|
1129
|
-
// minLength: 'minimum length is {{1}}',
|
|
1130
|
-
// maxLength: 'allowed max length is {{1}}',
|
|
1131
|
-
},
|
|
1132
|
-
});
|
|
1133
1125
|
const errorMessages = {
|
|
1134
1126
|
required: 'Es requerido',
|
|
1135
1127
|
numeric: 'Debe ser numérico válido',
|
|
@@ -1245,7 +1237,7 @@ function transformarFechasPorNombreDeCampo(formValue) {
|
|
|
1245
1237
|
}
|
|
1246
1238
|
if (value instanceof Date) {
|
|
1247
1239
|
if (/^d[A-Za-z]+/.test(key)) {
|
|
1248
|
-
retData[key] =
|
|
1240
|
+
retData[key] = format(value, 'yyyy-MM-dd');
|
|
1249
1241
|
}
|
|
1250
1242
|
else {
|
|
1251
1243
|
retData[key] = new Date(Date.UTC(value.getFullYear(), value.getMonth(), value.getDate(), value.getHours(), value.getMinutes(), value.getSeconds()));
|
|
@@ -1525,7 +1517,7 @@ function jwtTokenExpiracion(key = dataSessionStorageKey.tokenStringKey) {
|
|
|
1525
1517
|
}
|
|
1526
1518
|
}
|
|
1527
1519
|
function jwtTokenExpiracionFaltante() {
|
|
1528
|
-
return Math.abs(
|
|
1520
|
+
return Math.abs(differenceInSeconds(new Date(), jwtTokenExpiracion()));
|
|
1529
1521
|
}
|
|
1530
1522
|
function setCambiarPwd(accion = true) {
|
|
1531
1523
|
localStorage.setItem(dataSessionStorageKey.changePasswordKey, (accion ? 1 : 0).toString());
|
|
@@ -1723,6 +1715,21 @@ function obtenerHostDesdeUrl(url, options) {
|
|
|
1723
1715
|
}
|
|
1724
1716
|
/** @deprecated Alias compatible (deprecated) — preferir usar `obtenerHostDesdeUrl`. */
|
|
1725
1717
|
const extraerDominio = (url, options) => obtenerHostDesdeUrl(url, options);
|
|
1718
|
+
/**
|
|
1719
|
+
* Genera un hash numérico simple a partir de una cadena (versión ligera de shorthash2).
|
|
1720
|
+
* Útil para generar identificadores únicos deterministas basados en contenido.
|
|
1721
|
+
* @param str Cadena a hashear
|
|
1722
|
+
* @returns Hash alfanumérico en base 36
|
|
1723
|
+
*/
|
|
1724
|
+
function simpleHash(str) {
|
|
1725
|
+
let hash = 0;
|
|
1726
|
+
for (let i = 0; i < str.length; i++) {
|
|
1727
|
+
const char = str.charCodeAt(i);
|
|
1728
|
+
hash = ((hash << 5) - hash) + char;
|
|
1729
|
+
hash = hash & hash; // Convertir a entero de 32 bits
|
|
1730
|
+
}
|
|
1731
|
+
return Math.abs(hash).toString(36);
|
|
1732
|
+
}
|
|
1726
1733
|
|
|
1727
1734
|
function verificarRUC(ruc) {
|
|
1728
1735
|
const f = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
|
|
@@ -2738,10 +2745,195 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
|
|
|
2738
2745
|
}]
|
|
2739
2746
|
}], ctorParameters: () => [] });
|
|
2740
2747
|
|
|
2748
|
+
const ENCRYPTION_CONFIG = new InjectionToken('ENCRYPTION_CONFIG');
|
|
2749
|
+
class EncryptionService {
|
|
2750
|
+
http = inject(HttpClient);
|
|
2751
|
+
reloj = inject(RelojService);
|
|
2752
|
+
config = inject(ENCRYPTION_CONFIG, { optional: true });
|
|
2753
|
+
PUBLIC_KEY_URL = this.config?.publicKeyUrl ?? '/api/auth/public-key';
|
|
2754
|
+
CACHE_KEY = 'jvs_rsa_public_key';
|
|
2755
|
+
publicKey = signal(null);
|
|
2756
|
+
pendingFetch;
|
|
2757
|
+
constructor() {
|
|
2758
|
+
this.loadCachedPublicKey();
|
|
2759
|
+
this.escucharCambiosOtrasPestanas();
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Obtiene la llave pública del servidor.
|
|
2763
|
+
* Utiliza la Signal (RAM) primariamente, luego localStorage.
|
|
2764
|
+
* Maneja peticiones concurrentes de forma que solo se dispare una descarga.
|
|
2765
|
+
*/
|
|
2766
|
+
async getPublicKey() {
|
|
2767
|
+
const current = this.publicKey();
|
|
2768
|
+
if (current) {
|
|
2769
|
+
return current;
|
|
2770
|
+
}
|
|
2771
|
+
if (this.pendingFetch) {
|
|
2772
|
+
return this.pendingFetch;
|
|
2773
|
+
}
|
|
2774
|
+
this.pendingFetch = firstValueFrom(this.http.get(this.PUBLIC_KEY_URL).pipe(map(res => {
|
|
2775
|
+
const key = res.publicKey || res.public_key;
|
|
2776
|
+
if (!key)
|
|
2777
|
+
throw new Error('Respuesta inválida del servidor (Falta publicKey/public_key).');
|
|
2778
|
+
this.setPublicKey(key);
|
|
2779
|
+
return key;
|
|
2780
|
+
}), catchError(err => {
|
|
2781
|
+
this.pendingFetch = undefined;
|
|
2782
|
+
console.error('[EncryptionService] Error al obtener llave pública:', err);
|
|
2783
|
+
throw err;
|
|
2784
|
+
})));
|
|
2785
|
+
try {
|
|
2786
|
+
const key = await this.pendingFetch;
|
|
2787
|
+
this.pendingFetch = undefined;
|
|
2788
|
+
return key;
|
|
2789
|
+
}
|
|
2790
|
+
catch {
|
|
2791
|
+
this.pendingFetch = undefined;
|
|
2792
|
+
throw new Error('No se pudo establecer el canal seguro (RSA Key Fallback).');
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Fuerza la limpieza del caché y descarga una nueva llave.
|
|
2797
|
+
* Útil para auto-saneamiento cuando el backend reporta error de desencriptación.
|
|
2798
|
+
*/
|
|
2799
|
+
async forceRefreshPublicKey() {
|
|
2800
|
+
console.info('[EncryptionService] Forzando refresco de llave RSA por solicitud del interceptor.');
|
|
2801
|
+
this.publicKey.set(null);
|
|
2802
|
+
localStorage.removeItem(this.CACHE_KEY);
|
|
2803
|
+
return this.getPublicKey();
|
|
2804
|
+
}
|
|
2805
|
+
/**
|
|
2806
|
+
* Encriptación Híbrida (RSA + AES-256-CBC)
|
|
2807
|
+
*/
|
|
2808
|
+
async encryptHybrid(data) {
|
|
2809
|
+
const pubKey = await this.getPublicKey();
|
|
2810
|
+
// 1. Generar Key (32 bytes) e IV (16 bytes)
|
|
2811
|
+
const aesKey = CryptoJS.lib.WordArray.random(32);
|
|
2812
|
+
const aesIv = CryptoJS.lib.WordArray.random(16);
|
|
2813
|
+
// 2. Preparar payload con timestamp (Unix Epoch)
|
|
2814
|
+
const timestamp = Math.floor((this.reloj.timestamp() || Date.now()) / 1000);
|
|
2815
|
+
const bodyToEncrypt = typeof data === 'object' && data !== null ? { ...data, timestamp } : { data, timestamp };
|
|
2816
|
+
const plainData = JSON.stringify(bodyToEncrypt);
|
|
2817
|
+
// 3. Cifrar con AES
|
|
2818
|
+
const encryptedPayload = CryptoJS.AES.encrypt(plainData, aesKey, {
|
|
2819
|
+
iv: aesIv,
|
|
2820
|
+
mode: CryptoJS.mode.CBC,
|
|
2821
|
+
padding: CryptoJS.pad.Pkcs7
|
|
2822
|
+
});
|
|
2823
|
+
// 4. Concatenar Key + IV (48 bytes) y cifrar con RSA
|
|
2824
|
+
const combinedBytes = aesKey.concat(aesIv);
|
|
2825
|
+
const combinedLatin1 = combinedBytes.toString(CryptoJS.enc.Latin1);
|
|
2826
|
+
const rsa = new JSEncrypt();
|
|
2827
|
+
rsa.setPublicKey(pubKey);
|
|
2828
|
+
const encryptedKeys = rsa.encrypt(combinedLatin1);
|
|
2829
|
+
if (!encryptedKeys) {
|
|
2830
|
+
throw new Error('Fallo en cifrado RSA (Llave posiblemente inválida).');
|
|
2831
|
+
}
|
|
2832
|
+
return {
|
|
2833
|
+
payload: encryptedPayload.toString(),
|
|
2834
|
+
keys: encryptedKeys
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
setPublicKey(key) {
|
|
2838
|
+
this.publicKey.set(key);
|
|
2839
|
+
localStorage.setItem(this.CACHE_KEY, key);
|
|
2840
|
+
}
|
|
2841
|
+
loadCachedPublicKey() {
|
|
2842
|
+
const cached = localStorage.getItem(this.CACHE_KEY);
|
|
2843
|
+
if (cached) {
|
|
2844
|
+
this.publicKey.set(cached);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
escucharCambiosOtrasPestanas() {
|
|
2848
|
+
window.addEventListener('storage', (event) => {
|
|
2849
|
+
if (event.key === this.CACHE_KEY) {
|
|
2850
|
+
if (event.newValue) {
|
|
2851
|
+
this.publicKey.set(event.newValue);
|
|
2852
|
+
}
|
|
2853
|
+
else {
|
|
2854
|
+
this.publicKey.set(null);
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EncryptionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2860
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EncryptionService, providedIn: 'root' });
|
|
2861
|
+
}
|
|
2862
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EncryptionService, decorators: [{
|
|
2863
|
+
type: Injectable,
|
|
2864
|
+
args: [{
|
|
2865
|
+
providedIn: 'root'
|
|
2866
|
+
}]
|
|
2867
|
+
}], ctorParameters: () => [] });
|
|
2868
|
+
|
|
2741
2869
|
/*
|
|
2742
2870
|
* Public API Surface of utils services
|
|
2743
2871
|
*/
|
|
2744
2872
|
|
|
2873
|
+
/**
|
|
2874
|
+
* Interceptor de Encriptación Híbrida.
|
|
2875
|
+
* Detecta el header 'X-Secure-Request: true'.
|
|
2876
|
+
* Autogestiona la descarga de llaves y reintenta ante errores de desencriptación.
|
|
2877
|
+
*/
|
|
2878
|
+
const encryptionInterceptor = (req, next) => {
|
|
2879
|
+
const encryptionService = inject(EncryptionService);
|
|
2880
|
+
return executeEncryptionLogic(req, next, encryptionService);
|
|
2881
|
+
};
|
|
2882
|
+
/**
|
|
2883
|
+
* Lógica interna del interceptor para permitir recursividad/reintentos seguros.
|
|
2884
|
+
*/
|
|
2885
|
+
function executeEncryptionLogic(req, next, encryptionService) {
|
|
2886
|
+
const isSecure = req.headers.has('X-Secure-Request');
|
|
2887
|
+
const isRaw = req.headers.has('X-Secure-Raw');
|
|
2888
|
+
const isRetry = req.headers.has('X-Encryption-Retry');
|
|
2889
|
+
/**
|
|
2890
|
+
* Capturador de errores de túnel seguro.
|
|
2891
|
+
* Si el servidor reporta 412, refrescamos la llave y reintentamos la petición original.
|
|
2892
|
+
*/
|
|
2893
|
+
const handleEncryptionError = (error, originalReq) => {
|
|
2894
|
+
const isDecryptionError = error.status === 412 ||
|
|
2895
|
+
(error.status === 400 && (error.error?.error === 'INVALID_SECURE_TUNNEL' ||
|
|
2896
|
+
error.error?.error === 'Error de desencriptación'));
|
|
2897
|
+
if (isDecryptionError && !isRetry) {
|
|
2898
|
+
console.warn(`[EncryptionInterceptor] Detectado fallo de llave RSA (Status: ${error.status}). Reintentando auto-saneamiento...`);
|
|
2899
|
+
return from(encryptionService.forceRefreshPublicKey()).pipe(switchMap(() => {
|
|
2900
|
+
const retryReq = originalReq.clone({
|
|
2901
|
+
headers: originalReq.headers.set('X-Encryption-Retry', 'true')
|
|
2902
|
+
});
|
|
2903
|
+
// Llamada recursiva pasando el servicio ya inyectado
|
|
2904
|
+
return executeEncryptionLogic(retryReq, next, encryptionService);
|
|
2905
|
+
}));
|
|
2906
|
+
}
|
|
2907
|
+
return throwError(() => error);
|
|
2908
|
+
};
|
|
2909
|
+
if (!isSecure || !req.body) {
|
|
2910
|
+
return next(req).pipe(catchError$1(error => handleEncryptionError(error, req)));
|
|
2911
|
+
}
|
|
2912
|
+
// Limpiamos los headers de control antes de enviar
|
|
2913
|
+
const cleanHeaders = req.headers
|
|
2914
|
+
.delete('X-Secure-Request')
|
|
2915
|
+
.delete('X-Secure-Raw');
|
|
2916
|
+
// Flujo de encriptación y envío
|
|
2917
|
+
return from(encryptionService.encryptHybrid(req.body)).pipe(switchMap(encryptResult => {
|
|
2918
|
+
let finalReq;
|
|
2919
|
+
if (isRaw) {
|
|
2920
|
+
// MODO HEADER (Raw): Llaves en header jvsenc-hybrid, body es solo el payload string
|
|
2921
|
+
finalReq = req.clone({
|
|
2922
|
+
body: encryptResult.payload,
|
|
2923
|
+
headers: cleanHeaders.set('jvsenc-hybrid', encryptResult.keys)
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
else {
|
|
2927
|
+
// MODO BODY (Standard): Body es { payload, keys }
|
|
2928
|
+
finalReq = req.clone({
|
|
2929
|
+
body: encryptResult,
|
|
2930
|
+
headers: cleanHeaders
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
return next(finalReq).pipe(catchError$1(error => handleEncryptionError(error, req)));
|
|
2934
|
+
}));
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2745
2937
|
/*
|
|
2746
2938
|
* Public API Surface of utils
|
|
2747
2939
|
*/
|
|
@@ -2750,5 +2942,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
|
|
|
2750
2942
|
* Generated bundle index. Do not edit.
|
|
2751
2943
|
*/
|
|
2752
2944
|
|
|
2753
|
-
export { AutoSelectFirstDirective, AutocompleteMatchValidatorDirective, DataEnListaPipe, DataModel, DateDiffStringPipe, FiltroPipe, FormControlIsRequiredPipe, JsonParsePipe, JvsAutocompleteDirective, JvsDisplayWithPipe, JvsPopoverDirective, JvsPopoverListenerDirective, JvsPopoverPanelComponent, JvsPopoverService, NoSanitizePipe, RelojService, TipoValorFuncionPipe, ZeroFillPipe, b64Decode, b64Encode, buscarPorCampo, changeSelect, changeSelectApi, changeSelectData, changeSelectDataApi, changeSelectReformateado, convertirBytes, dataEnLista, dateDiffString, decodeBase64Object, decodeBase64String, decodeBase64ToBlob, deepClone, deepMerge, delLocalStorage, desencriptar, devError, devGroup, devGroupEnd, devLog, devTable, devTime, devTimeEnd, devWarn, downLoadFileStream, eliminarColumnaPorIndex, eliminarDuplicados, eliminarElementos, encodeBase64File, encodeBase64Object, encodeBase64String, encriptar, esNumero, esPromise, establecerQuitarRequired, extraerDominio, filtrarDatosLocal, formControlIsRequired, formatearFecha, formatearFechaCadena, formatearFechaFormato, generateRandomString, getBrowserName, getCambiarPwd, getDataArchivoFromPath, getFormValidationErrors, getLocalStorage, getUniqueValues, getUniqueValuesByProperty, getValueByPath, groupBy, inicializarVariablesSessionStorage, isEmail, isProduction, jwtToken, jwtTokenData, jwtTokenExpiracion, jwtTokenExpiracionFaltante, localStorageKeys, markAsTouchedWithoutEmitEvent, maskEmail, mensajeAlerta, mensajeConfirmacion, mensajeTimer, mensajeToast, mensajesDeError, mensajesErrorFormControl, mostrarValorEnBusqueda, nestGroupsBy, numberToWords, objectPropertiesBoolean, objectPropertiesToType, obtenerHostDesdeUrl, obtenerMimeType, obtenerUltimoOrden, ordenarArray, ordenarPorPropiedad, ordenarPorPropiedades, roundToDecimal, sanitizarNombreArchivo, seleccionarTextoInput, setCambiarPwd, setControlDesdeLista, setJwtTokenData, setLocalStorage, setProductionMode, sumarObjetos, sumarPropiedades, tipoValorFuncion, toFormData, transformarFechasPorNombreDeCampo, verificarRUC, zeroFill };
|
|
2945
|
+
export { AutoSelectFirstDirective, AutocompleteMatchValidatorDirective, DataEnListaPipe, DataModel, DateDiffStringPipe, ENCRYPTION_CONFIG, EncryptionService, FiltroPipe, FormControlIsRequiredPipe, JsonParsePipe, JvsAutocompleteDirective, JvsDisplayWithPipe, JvsPopoverDirective, JvsPopoverListenerDirective, JvsPopoverPanelComponent, JvsPopoverService, NoSanitizePipe, RelojService, TipoValorFuncionPipe, ZeroFillPipe, b64Decode, b64Encode, buscarPorCampo, changeSelect, changeSelectApi, changeSelectData, changeSelectDataApi, changeSelectReformateado, convertirBytes, dataEnLista, dateDiffString, decodeBase64Object, decodeBase64String, decodeBase64ToBlob, deepClone, deepMerge, delLocalStorage, desencriptar, devError, devGroup, devGroupEnd, devLog, devTable, devTime, devTimeEnd, devWarn, downLoadFileStream, eliminarColumnaPorIndex, eliminarDuplicados, eliminarElementos, encodeBase64File, encodeBase64Object, encodeBase64String, encriptar, encryptionInterceptor, esNumero, esPromise, establecerQuitarRequired, extraerDominio, filtrarDatosLocal, formControlIsRequired, formatearFecha, formatearFechaCadena, formatearFechaFormato, generateRandomString, getBrowserName, getCambiarPwd, getDataArchivoFromPath, getFormValidationErrors, getLocalStorage, getUniqueValues, getUniqueValuesByProperty, getValueByPath, groupBy, inicializarVariablesSessionStorage, isEmail, isProduction, jwtToken, jwtTokenData, jwtTokenExpiracion, jwtTokenExpiracionFaltante, localStorageKeys, markAsTouchedWithoutEmitEvent, maskEmail, mensajeAlerta, mensajeConfirmacion, mensajeTimer, mensajeToast, mensajesDeError, mensajesErrorFormControl, mostrarValorEnBusqueda, nestGroupsBy, numberToWords, objectPropertiesBoolean, objectPropertiesToType, obtenerHostDesdeUrl, obtenerMimeType, obtenerUltimoOrden, ordenarArray, ordenarPorPropiedad, ordenarPorPropiedades, roundToDecimal, sanitizarNombreArchivo, seleccionarTextoInput, setCambiarPwd, setControlDesdeLista, setJwtTokenData, setLocalStorage, setProductionMode, simpleHash, sumarObjetos, sumarPropiedades, tipoValorFuncion, toFormData, transformarFechasPorNombreDeCampo, verificarRUC, zeroFill };
|
|
2754
2946
|
//# sourceMappingURL=jvsoft-utils.mjs.map
|